checkpoint/backup-fail-server

This commit is contained in:
hreyes
2026-02-05 12:35:03 -06:00
parent 8e53293e62
commit 4cc613d1df
7 changed files with 234 additions and 75 deletions

View File

@@ -32,7 +32,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Doda>[] {
},
{
id: 'actions',
Headers: 'Acciones',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,

View File

@@ -0,0 +1,121 @@
<script lang="ts" generics="TData, TValue">
import { type ColumnDef, getCoreRowModel, type RowSelectionState } from '@tanstack/table-core';
import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import { Button } from '$lib/components/ui/button';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
selectedId?: number | null;
onRowClick?: (row: TData) => void;
};
let {
data,
columns,
pageCount,
totalItems,
selectedId = null,
onRowClick
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row: any) => row.id?.toString(),
state: {
get rowSelection() {
return selectedId ? { [selectedId]: true } : {};
}
},
enableRowSelection: true,
enableMultiRowSelection: false,
manualPagination: true,
pageCount: pageCount
});
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url);
}
</script>
<div class="space-y-4">
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row
data-state={row.getIsSelected() && 'selected'}
onclick={() => onRowClick?.(row.original)}
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected()
? 'bg-primary/10'
: ''}"
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="whitespace-nowrap">
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<!-- Paginación -->
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems}
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>
</div>
</div>

View File

@@ -25,9 +25,12 @@
'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]):not([role="tab"])'
);
// Find the first one that is actually visible
// Find the first one that is actually visible AND not in the sidebar
const firstVisible = Array.from(focusables).find((el) => {
const htmlEl = el as HTMLElement;
// Exclude sidebar elements
if (htmlEl.closest('[data-sidebar="sidebar"]')) return false;
return !!(htmlEl.offsetWidth || htmlEl.offsetHeight || htmlEl.getClientRects().length);
}) as HTMLElement;
@@ -60,6 +63,9 @@
const elements = document.querySelectorAll('button, [role="tab"], a');
const target = Array.from(elements).find((el) => {
const text = el.textContent?.trim() || '';
// Exclude sidebar
if (el.closest('[data-sidebar="sidebar"]')) return false;
return text.toLowerCase().includes(keyword.toLowerCase());
}) as HTMLElement;
@@ -276,6 +282,9 @@
const target = event.target as HTMLElement;
if (!target) return;
// Exclude sidebar from any magic scrolling
if (target.closest('[data-sidebar="sidebar"]')) return;
// Check if it's an interactive element we care about
const isInteractive =
['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'].includes(target.tagName) ||

View File

@@ -156,7 +156,7 @@ export async function authenticatedFetch(
}
// Construir URL completa
const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`;
const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`;
// Crear AbortController para timeout
const controller = new AbortController();
@@ -171,7 +171,7 @@ export async function authenticatedFetch(
const headers = isFormData
? { 'Authorization': `Bearer ${accessToken}`, ...(options.headers as Record<string, string> || {}) }
: createAuthHeaders(accessToken, options.headers as Record<string, string>);
let response = await fetch(url, {
...options,
headers,
@@ -202,7 +202,7 @@ export async function authenticatedFetch(
const newHeaders = isFormData
? { 'Authorization': `Bearer ${newToken}`, ...(options.headers as Record<string, string> || {}) }
: createAuthHeaders(newToken, options.headers as Record<string, string>);
response = await fetch(url, {
...options,
headers: newHeaders,
@@ -263,7 +263,7 @@ export async function validateAuth(
}
const keycloakData = await response.json();
// Obtener perfil adicional del usuario (avatar, bio, etc.)
try {
const profileResponse = await authenticatedFetch(
@@ -272,7 +272,7 @@ export async function validateAuth(
cookies,
fetch
);
if (profileResponse.ok) {
const profileData = await profileResponse.json();
// Combinar datos de Keycloak con datos del perfil
@@ -287,7 +287,7 @@ export async function validateAuth(
} catch (profileError) {
console.warn('⚠️ [API] No se pudo cargar el perfil del usuario, usando solo datos de Keycloak');
}
return keycloakData;
} catch (error) {
// Si es un redirect, re-lanzarlo
@@ -365,14 +365,14 @@ export async function handleApiResponse<T = any>(
if (response.status === 204) {
return { data: null as T };
}
const data = await response.json();
return { data };
}
// Manejar errores
const errorData = await response.json().catch(() => ({ detail: 'Error desconocido' }));
const error = {
detail: errorData.detail || errorData.message || 'Error en la petición',
status: response.status,

View File

@@ -15,15 +15,14 @@
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let dialogOpen = $state(false);
// Atajos
useShortcuts(
'Lista Conceptos',
obtenerAtajosListaConceptos({
manejarNuevo: () => (createDialogOpen = true),
manejarActualizar: () => goto($page.url, { invalidateAll: true }),
manejarEditar: () => console.log('Editar concepto'),
manejarEliminar: () => console.log('Eliminar concepto')
manejarActualizar: () => goto($page.url, { invalidateAll: true })
})
);
@@ -88,5 +87,7 @@
/>
</div>
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
{#if dialogOpen}
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
{/if}
</div>

View File

@@ -12,6 +12,7 @@
import { invalidateAll } from '$app/navigation';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaDoda } from '$lib/config/shortcuts/dashboard/general_catalogs/doda/list';
import type { Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
let { data } = $props();
let dialogOpen = $state(false);
@@ -20,20 +21,39 @@
let searchIntegration = $state($page.url.searchParams.get('integration_number') || '');
let timeout: ReturnType<typeof setTimeout>;
// Selection
let selectedId = $state<number | null>(null);
// Funciones de acción
function handleCreateClick() {
goto('/dashboard/general_catalogs/doda/edit');
}
function handleEditSelected() {
if (selectedId) {
goto(`/dashboard/general_catalogs/doda/edit/${selectedId}`);
} else {
console.log('Seleccione un registro para editar (Alt+Shift+E)');
}
}
function handleDelete() {
if (selectedId) {
// Implement delete logic or dialog
console.log('Eliminar no implementado en esta vista. Use el menú de acciones.');
} else {
console.log('Seleccione un registro para eliminar (Alt+Shift+D)');
}
}
// Atajos
useShortcuts(
'Lista DODA',
obtenerAtajosListaDoda({
manejarNuevo: () => goto('/dashboard/general_catalogs/doda/edit'),
manejarNuevo: handleCreateClick,
manejarActualizar: handleSuccess,
manejarEditar: () => {
// Nota: La edición requiere un ID, pero la lista actual no rastrea selección.
// Por ahora apuntamos a la página de nuevo como placeholder o documentamos que requiere selección.
toast.info('Seleccione un registro para editar (Alt+Shift+E)');
},
manejarEliminar: () => {
toast.info('Seleccione un registro para eliminar (Alt+Shift+D)');
}
manejarEditar: handleEditSelected,
manejarEliminar: handleDelete
})
);
@@ -53,6 +73,7 @@
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
selectedId = null;
}
</script>
@@ -85,8 +106,12 @@
columns={createColumns(handleSuccess)}
pageCount={data.dodas?.pages || 0}
totalItems={data.dodas?.total || 0}
{selectedId}
onRowClick={handleRowClick}
/>
</div>
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
{#if dialogOpen}
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
{/if}
</div>

View File

@@ -1,57 +1,60 @@
<script lang="ts">
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/packages/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/packages/columns';
import CreateEditDialog from '$lib/components/dashboard/packages/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import * as m from "$lib/paraglide/messages.js";
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/packages/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/packages/columns';
import CreateEditDialog from '$lib/components/dashboard/packages/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import * as m from '$lib/paraglide/messages.js';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaBultos } from '$lib/config/shortcuts/dashboard/general_catalogs/packages/list';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaBultos } from '$lib/config/shortcuts/dashboard/general_catalogs/packages/list';
let { data } = $props();
let { data } = $props();
let createDialogOpen = $state(false);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchName = $state($page.url.searchParams.get('name') || '');
let timeout: ReturnType<typeof setTimeout>;
let createDialogOpen = $state(false);
// Atajos
useShortcuts(
'Lista Bultos',
obtenerAtajosListaBultos({
manejarNuevo: () => createDialogOpen = true,
manejarActualizar: handleSuccess,
manejarEditar: () => console.log('Editar bulto'),
manejarEliminar: () => console.log('Eliminar bulto')
}));
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchName = $state($page.url.searchParams.get('name') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
else url.searchParams.delete('key');
if (searchDesc) url.searchParams.set('description_es', searchDesc);
else url.searchParams.delete('description_es');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
// Atajos
useShortcuts(
'Lista Bultos',
obtenerAtajosListaBultos({
manejarNuevo: () => (createDialogOpen = true),
manejarActualizar: handleSuccess,
manejarEditar: () => console.log('Editar bulto'),
manejarEliminar: () => console.log('Eliminar bulto')
})
);
function handleSuccess() {
// Recargar datos
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchName) url.searchParams.set('name', searchName);
else url.searchParams.delete('name');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
// Recargar datos
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
@@ -60,7 +63,7 @@
<h1 class="text-2xl font-bold tracking-tight">Bultos y Embalajes</h1>
<p class="text-muted-foreground">Gestión del catálogo de bultos y embalajes</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Button onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Bulto
</Button>
@@ -68,12 +71,12 @@
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por clave..." bind:value={searchKey} oninput={handleSearch} />
<Input placeholder="Buscar por clave..." bind:value={searchCode} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
bind:value={searchName}
oninput={handleSearch}
/>
</div>
@@ -88,5 +91,5 @@
/>
</div>
<CreateEditDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
</div>