checkpoint/backup-fail-server
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user