Merge pull request 'fix/table-pedimentos' (#72) from fix/table-pedimentos into development
Reviewed-on: ADUANASOFT/anexo76#72
This commit is contained in:
@@ -118,6 +118,6 @@
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground overflow-x-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,5 +324,5 @@ export const pedimentosApi = {
|
||||
* @param id - ID del pedimento a eliminar
|
||||
* @param companyId - ID de la compañía (por defecto 1)
|
||||
*/
|
||||
delete: (id: number, companyId = 1) => api.delete(`/v1/a76/pedimentos/${id}/?company_id=${companyId}`)
|
||||
delete: (id: number, companyId = 1) => api.delete(`/v1/a76/pedimentos/${id}?company_id=${companyId}`)
|
||||
};
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { ColumnDef } from "@tanstack/table-core";
|
||||
import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js";
|
||||
import { createRawSnippet } from "svelte";
|
||||
import DataTableActions from "./data-table-actions.svelte";
|
||||
import type { Pedimento } from "$lib/api/dashboard/a76/pedimentos";
|
||||
|
||||
// Extender el tipo ColumnMeta para incluir className
|
||||
declare module "@tanstack/table-core" {
|
||||
interface ColumnMeta<TData extends unknown, TValue> {
|
||||
className?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea un número como moneda
|
||||
*/
|
||||
@@ -86,6 +92,33 @@ function getStatusColor(status?: string | null): string {
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
return [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return renderSnippet(
|
||||
createRawSnippet(() => ({
|
||||
render: () => `<div class="w-4"></div>`
|
||||
}))
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isSelected = row.getIsSelected();
|
||||
|
||||
const checkboxSnippet = createRawSnippet<[{ selected: boolean }]>((getProps) => {
|
||||
const { selected } = getProps();
|
||||
return {
|
||||
render: () => `<div class="flex items-center justify-center">
|
||||
<input type="checkbox" class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer" ${selected ? 'checked' : ''} />
|
||||
</div>`
|
||||
};
|
||||
});
|
||||
|
||||
return renderSnippet(checkboxSnippet, { selected: isSelected });
|
||||
},
|
||||
size: 40,
|
||||
enableSorting: false,
|
||||
enableHiding: false
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
@@ -120,6 +153,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "pedimento_type",
|
||||
header: "Tipo",
|
||||
meta: { className: "hidden md:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
|
||||
const { type } = getType();
|
||||
@@ -134,6 +168,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "pedimento_code",
|
||||
header: "Clave",
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const codeSnippet = createRawSnippet<[{ code?: string | null }]>((getCode) => {
|
||||
const { code } = getCode();
|
||||
@@ -148,6 +183,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "regime",
|
||||
header: "Régimen",
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const regimeSnippet = createRawSnippet<[{ regime?: string | null }]>((getRegime) => {
|
||||
const { regime } = getRegime();
|
||||
@@ -162,6 +198,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "pedimento_dates.start_date",
|
||||
header: "Fecha Inicio",
|
||||
meta: { className: "hidden xl:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => {
|
||||
const { date } = getDate();
|
||||
@@ -176,6 +213,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "pedimento_dates.end_date",
|
||||
header: "Fecha Final",
|
||||
meta: { className: "hidden xl:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => {
|
||||
const { date } = getDate();
|
||||
@@ -190,6 +228,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "pedimento_dates.payment_date",
|
||||
header: "Fecha de Pago",
|
||||
meta: { className: "hidden xl:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => {
|
||||
const { date } = getDate();
|
||||
@@ -201,9 +240,10 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
return renderSnippet(dateSnippet, { date: formatDate(row.original.pedimento_dates?.payment_date) });
|
||||
}
|
||||
},
|
||||
{
|
||||
/*{
|
||||
accessorKey: "pedimento_config_update_rectification.pediment_rectifed_18",
|
||||
header: "Pedimento 18",
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const ped18Snippet = createRawSnippet<[{ value?: string | null }]>((getValue) => {
|
||||
const { value } = getValue();
|
||||
@@ -214,10 +254,11 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
});
|
||||
return renderSnippet(ped18Snippet, { value: row.original.pedimento_config_update_rectification?.pediment_rectifed_18 });
|
||||
}
|
||||
},
|
||||
{
|
||||
},*/
|
||||
/*{
|
||||
accessorKey: "pedimento_config_update_rectification.r1",
|
||||
header: "Pedimento R1",
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const r1Snippet = createRawSnippet<[{ value?: string | null }]>((getValue) => {
|
||||
const { value } = getValue();
|
||||
@@ -228,10 +269,11 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
});
|
||||
return renderSnippet(r1Snippet, { value: row.original.pedimento_config_update_rectification?.r1 });
|
||||
}
|
||||
},
|
||||
},*/
|
||||
{
|
||||
accessorKey: "pedimento_validation.electronic_signature",
|
||||
header: "Acuse Electrónico",
|
||||
meta: { className: "hidden xl:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const ackSnippet = createRawSnippet<[{ value?: string | null }]>((getValue) => {
|
||||
const { value } = getValue();
|
||||
@@ -249,6 +291,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "pedimento_payments.total_contributions",
|
||||
header: "¿Se pagó el impuesto?",
|
||||
meta: { className: "hidden xl:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const paidSnippet = createRawSnippet<[{ value?: string | null }]>((getValue) => {
|
||||
const { value } = getValue();
|
||||
@@ -266,6 +309,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "client_id",
|
||||
header: "Cliente",
|
||||
meta: { className: "hidden md:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const clientSnippet = createRawSnippet<[{ clientId?: number | null }]>((getClient) => {
|
||||
const { clientId } = getClient();
|
||||
@@ -309,6 +353,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
});
|
||||
return renderSnippet(headerSnippet, {});
|
||||
},
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => {
|
||||
const { value } = getValue();
|
||||
@@ -330,6 +375,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
});
|
||||
return renderSnippet(headerSnippet, {});
|
||||
},
|
||||
meta: { className: "hidden xl:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const priceSnippet = createRawSnippet<[{ price: string }]>((getPrice) => {
|
||||
const { price } = getPrice();
|
||||
@@ -351,6 +397,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
});
|
||||
return renderSnippet(headerSnippet, {});
|
||||
},
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const weightSnippet = createRawSnippet<[{ weight: string }]>((getWeight) => {
|
||||
const { weight } = getWeight();
|
||||
@@ -365,6 +412,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: "Fecha de Creación",
|
||||
meta: { className: "hidden 2xl:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => {
|
||||
const { date } = getDate();
|
||||
@@ -375,13 +423,8 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
});
|
||||
return renderSnippet(dateSnippet, { date: formatDate(row.original.created_at) });
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, { item: row.original, onSuccess });
|
||||
}
|
||||
}
|
||||
// Columna de acciones eliminada - ahora usamos botones en el footer
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel
|
||||
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 { Edit } from "lucide-svelte";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
@@ -13,6 +16,8 @@
|
||||
loading: boolean;
|
||||
hasMore: boolean;
|
||||
loadMore: () => void;
|
||||
selectedId?: number | null;
|
||||
onRowClick?: (row: TData) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -20,7 +25,9 @@
|
||||
columns,
|
||||
loading,
|
||||
hasMore,
|
||||
loadMore
|
||||
loadMore,
|
||||
selectedId = null,
|
||||
onRowClick
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
@@ -28,12 +35,28 @@
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (row: any) => row.id?.toString(),
|
||||
state: {
|
||||
get rowSelection() {
|
||||
return selectedId ? { [selectedId]: true } : {};
|
||||
}
|
||||
},
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: false
|
||||
});
|
||||
|
||||
let scrollContainer = $state<HTMLDivElement>();
|
||||
let loadingTrigger = $state<HTMLDivElement>();
|
||||
|
||||
// Función para manejar doble clic en una fila
|
||||
function handleRowDoubleClick(row: any) {
|
||||
const pedimento = row.original;
|
||||
if (pedimento?.id) {
|
||||
window.location.href = `/dashboard/pedimentos/edit/${pedimento.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Intersection Observer para detectar cuando el usuario llega al final
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
@@ -60,13 +83,13 @@
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
|
||||
<Table.Root>
|
||||
<Table.Header class="bg-background">
|
||||
<div class="rounded-md border max-h-[600px] overflow-auto" bind:this={scrollContainer}>
|
||||
<Table.Root class="w-full">
|
||||
<Table.Header class="bg-background sticky top-0 z-10">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
<Table.Head class="whitespace-nowrap {header.column.columnDef.meta?.className || ''}">
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
@@ -80,9 +103,18 @@
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
<Table.Row
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
onclick={() => {
|
||||
if (onRowClick) {
|
||||
onRowClick(row.original);
|
||||
}
|
||||
}}
|
||||
ondblclick={() => handleRowDoubleClick(row)}
|
||||
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected() ? 'bg-primary/10' : ''}"
|
||||
>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<Table.Cell class="whitespace-nowrap {cell.column.columnDef.meta?.className || ''}">
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<Sidebar.Provider>
|
||||
<AppSidebar />
|
||||
<Sidebar.Inset>
|
||||
<Sidebar.Inset class="overflow-x-hidden">
|
||||
<header
|
||||
class="group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear"
|
||||
>
|
||||
@@ -55,7 +55,7 @@
|
||||
-->
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<div class="flex flex-1 flex-col gap-4 p-4 pt-0 overflow-x-hidden">
|
||||
<!-- Contenido de cada página -->
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte';
|
||||
import { Edit } from 'lucide-svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -72,6 +74,59 @@
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Estado para selección de filas
|
||||
let selectedId = $state<number | null>(null);
|
||||
let hasSelection = $derived(selectedId !== null);
|
||||
let showDeleteDialog = $state(false);
|
||||
|
||||
function handleRowClick(pedimento: Pedimento) {
|
||||
// Toggle: si ya está seleccionado, deseleccionar; si no, seleccionar
|
||||
selectedId = selectedId === pedimento.id ? null : pedimento.id;
|
||||
console.log('🔘 Row clicked, pedimento.id:', pedimento.id, 'selectedId:', selectedId, 'hasSelection:', hasSelection);
|
||||
}
|
||||
|
||||
function handleEditSelected() {
|
||||
if (selectedId) {
|
||||
window.location.href = `/dashboard/pedimentos/edit/${selectedId}`;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (!selectedId) {
|
||||
return;
|
||||
}
|
||||
showDeleteDialog = true;
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!selectedId) return;
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await pedimentosApi.delete(selectedId, companyId);
|
||||
|
||||
if (response.error) {
|
||||
console.error('🗑️ [Pedimentos] Error al eliminar:', response.error);
|
||||
error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
// Recargar datos
|
||||
await reloadData();
|
||||
|
||||
selectedId = null;
|
||||
showDeleteDialog = false;
|
||||
} catch (e) {
|
||||
console.error('🗑️ [Pedimentos] Error deleting:', e);
|
||||
error = 'Error al eliminar el pedimento';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
@@ -343,7 +398,50 @@
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
{selectedId}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Footer fijo con botones de acción -->
|
||||
<div class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5]">
|
||||
<div class="px-4 py-4 max-w-[1400px] mx-auto">
|
||||
<!-- Botones de acción -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button size="sm" onclick={handleCreateClick}>
|
||||
<Plus class="h-4 w-4 mr-1" />
|
||||
Insertar
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={handleEditSelected} disabled={!hasSelection}>
|
||||
<Edit size={16} class="mr-1" />
|
||||
Editar
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={handleDelete} disabled={!hasSelection}>
|
||||
<Trash2 size={16} class="mr-1" />
|
||||
Borrar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diálogo de confirmación para borrar -->
|
||||
<Dialog.Root bind:open={showDeleteDialog}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>¿Eliminar pedimento?</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Esta acción no se puede deshacer. El pedimento será eliminado permanentemente.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="flex justify-end gap-3 mt-4">
|
||||
<Button variant="outline" onclick={() => showDeleteDialog = false}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button variant="destructive" onclick={confirmDelete}>
|
||||
Eliminar
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
Reference in New Issue
Block a user