Files
plantillas-proyectos/frontend/src/lib/components/dashboard/invoices/columns.ts
AlexeerCT cf56ca3a68 Enhance invoice processing and validation logic
- Updated `InvoiceService` to accurately reflect the number of line items in `party_count` for invoices.
- Modified weight calculations in the create and update validators to ensure consistent handling of weight types, now using lowercase comparison for "KGS".
- Adjusted invoice data structure in the frontend to include `total_items` and refined logistics handling.
- Improved table rendering in the invoice edit component for better sticky header behavior and item visibility.

These changes aim to improve data integrity and user experience in invoice management.
2026-03-19 14:22:21 -05:00

361 lines
11 KiB
TypeScript

import type { ColumnDef } from "@tanstack/table-core";
import { renderSnippet } from "$lib/components/ui/data-table/index.js";
import { createRawSnippet } from "svelte";
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import { getInvoiceTypeColor } from "$lib/utils";
function formatDate(date?: string | null): string {
if (!date) return '-';
// Avoid timezone shifts (e.g. showing one day earlier) by parsing as local date.
const raw = String(date);
const ymd = raw.includes('T') ? raw.split('T')[0] : raw;
const parts = ymd.split('-').map(Number);
if (parts.length === 3 && parts.every((n) => Number.isFinite(n))) {
const [year, month, day] = parts;
return new Date(year, month - 1, day).toLocaleDateString('es-MX', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
return new Date(raw).toLocaleDateString('es-MX', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
export function createColumns(
onSuccess?: () => void
): ColumnDef<Invoice>[] {
return [
// Checkbox visual
{
id: "select",
header: ({ table }) => {
const isAllSelected = table.getIsAllPageRowsSelected();
const isSomeSelected = table.getIsSomePageRowsSelected();
const selectAllSnippet = createRawSnippet<[
{ checked: boolean; indeterminate: boolean; onchange: (e: Event) => void }
]>((getProps) => {
const { checked, indeterminate, onchange } = getProps();
return {
render: () => `<div class="w-4">
<input
type="checkbox"
class="h-4 w-4 cursor-pointer"
${checked ? 'checked' : ''}
${indeterminate ? 'indeterminate="true"' : ''}
/>
</div>`,
setup: (node) => {
const input = node.querySelector('input') as HTMLInputElement;
if (input) {
input.indeterminate = indeterminate;
input.addEventListener('change', onchange);
}
}
};
});
return renderSnippet(selectAllSnippet, {
checked: isAllSelected,
indeterminate: isSomeSelected && !isAllSelected,
onchange: (e: Event) => {
table.toggleAllPageRowsSelected(!!(e.target as HTMLInputElement).checked);
}
});
},
cell: ({ row }) => {
const isSelected = row.getIsSelected();
const checkboxSnippet = createRawSnippet<[
{ selected: boolean; onchange: (e: Event) => void }
]>((getProps) => {
const { selected, onchange } = getProps();
return {
render: () => `<div class="flex items-center justify-center">
<input type="checkbox" class="h-4 w-4 cursor-pointer" ${selected ? 'checked' : ''} />
</div>`,
setup: (node) => {
const input = node.querySelector('input') as HTMLInputElement;
if (input) {
input.addEventListener('change', onchange);
}
}
};
});
return renderSnippet(checkboxSnippet, {
selected: isSelected,
onchange: (e: Event) => {
e.stopPropagation(); // Evitar que el clic en el checkbox dispare el rowClick
row.toggleSelected(!!(e.target as HTMLInputElement).checked);
}
});
},
enableSorting: false,
enableHiding: false,
},
{
id: "processed",
header: "Procesada",
cell: ({ row }) => {
const s = row.original.status;
const isProcessed = s === "processed" || s === true;
const processedCheckSnippet = createRawSnippet<[{ checked: boolean }]>((getProps) => {
const { checked } = getProps();
return {
render: () => `<div class="flex items-center justify-center">
<input type="checkbox" class="h-4 w-4 cursor-default" ${checked ? 'checked' : ''} disabled title="${checked ? 'Procesada' : 'Pendiente'}" />
</div>`
};
});
return renderSnippet(processedCheckSnippet, { checked: isProcessed });
},
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "operation_type",
header: "Operación",
cell: ({ row }) => {
const operationType = row.original.operation_type;
const operationSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
const { type } = getType();
const isImport = type === 'imp';
const colorClass = isImport ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
const label = isImport ? 'Importación' : type === 'exp' ? 'Exportación' : '-';
return {
render: () =>
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
${label}
</span>`
};
});
return renderSnippet(operationSnippet, { type: operationType });
}
},
{
accessorKey: "invoice_type",
header: "Tipo Factura",
cell: ({ row }) => {
const invoiceType = row.original.invoice_type;
const colorClass = getInvoiceTypeColor(invoiceType);
const typeSnippet = createRawSnippet<[{ type?: string | null; colorClass: string }]>((getProps) => {
const { type, colorClass } = getProps();
return {
render: () =>
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
${type || '-'}
</span>`
};
});
return renderSnippet(typeSnippet, { type: invoiceType, colorClass });
}
},
{
accessorKey: "invoice_number",
header: "Núm. Factura",
cell: ({ row }) => {
const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getNumber) => {
const { number } = getNumber();
return {
render: () =>
`<code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">${number || 'N/A'}</code>`
};
});
return renderSnippet(numberSnippet, { number: row.original.invoice_number });
}
},
{
accessorKey: "compliance_mx.pedimento",
header: "Pedimento 18",
cell: ({ row }) => {
const pedimento = row.original.compliance_mx?.pedimento;
const fullNumber = pedimento
? `${pedimento.year || ''}-${pedimento.customs_office || ''}-${pedimento.license || ''}-${pedimento.pedimento_number || ''}`
: null;
const pedimentoSnippet = createRawSnippet<[{ number?: string | null }]>((getPedimento) => {
const { number } = getPedimento();
return {
render: () =>
`<code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">${number || 'N/A'}</code>`
};
});
return renderSnippet(pedimentoSnippet, { number: fullNumber });
}
},
{
accessorKey: "compliance_mx.remesa",
header: "Remesa",
cell: ({ row }) => {
const remesa = row.original.compliance_mx?.remesa;
const remesaSnippet = createRawSnippet<[{ remesa?: number | null }]>((getRemesa) => {
const { remesa } = getRemesa();
return {
render: () =>
`<div class="text-sm">${remesa || '-'}</div>`
};
});
return renderSnippet(remesaSnippet, { remesa });
}
},
{
accessorKey: "invoice_date",
header: "Fecha Factura",
cell: ({ row }) => {
const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => {
const { date } = getDate();
return {
render: () =>
`<div class="text-sm text-muted-foreground">${date}</div>`
};
});
return renderSnippet(dateSnippet, { date: formatDate(row.original.invoice_date) });
}
},
{
accessorKey: "compliance_mx.pedimento.pedimento_code",
header: "Clave Ped.",
cell: ({ row }) => {
const pedimentoCode = row.original.compliance_mx?.pedimento?.pedimento_code;
const claveSnippet = createRawSnippet<[{ code?: string }]>((getClave) => {
const { code } = getClave();
return {
render: () =>
`<div class="text-sm font-medium">${code || '-'}</div>`
};
});
return renderSnippet(claveSnippet, { code: pedimentoCode });
}
},
{
accessorKey: "document_type",
header: "Tipo Doc.",
cell: ({ row }) => {
const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
const { type } = getType();
return {
render: () =>
`<div class="text-sm capitalize">${type || '-'}</div>`
};
});
return renderSnippet(typeSnippet, { type: row.original.document_type });
}
},
{
accessorKey: "total_items",
header: "Total Partidas",
cell: ({ row }) => {
const totalItems = (row.original as Invoice & { total_items?: number | null }).total_items
?? row.original.party_count
?? row.original.details?.length
?? 0;
const itemsSnippet = createRawSnippet<[{ total: number }]>((getTotal) => {
const { total } = getTotal();
return {
render: () =>
`<div class="text-sm text-center">${total}</div>`
};
});
return renderSnippet(itemsSnippet, { total: totalItems });
}
},
{
accessorKey: "financials.currency",
header: "Moneda",
cell: ({ row }) => {
const currency = row.original.financials?.currency;
const currencySnippet = createRawSnippet<[{ currency?: string | null }]>((getCurrency) => {
const { currency } = getCurrency();
return {
render: () =>
`<div class="text-sm font-medium">${currency?.toLocaleUpperCase() || '-'}</div>`
};
});
return renderSnippet(currencySnippet, { currency });
}
},
{
accessorKey: "financials.currency_type",
header: "Tipo Moneda",
cell: ({ row }) => {
const currencyType = row.original.financials?.currency_type;
const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
const { type } = getType();
return {
render: () =>
`<div class="text-sm">${type || '-'}</div>`
};
});
return renderSnippet(typeSnippet, { type: currencyType });
}
},
{
accessorKey: "logistics.weight_type",
header: "Tipo Peso",
cell: ({ row }) => {
const weightType = row.original.logistics?.weight_type;
const weightSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
const { type } = getType();
return {
render: () =>
`<div class="text-sm">${type?.toUpperCase() || '-'}</div>`
};
});
return renderSnippet(weightSnippet, { type: weightType });
}
},
{
accessorKey: "compliance_mx.is_mixed",
header: "Mixto",
cell: ({ row }) => {
const isMixed = row.original.compliance_mx?.is_mixed;
const mixedSnippet = createRawSnippet<[{ isMixed?: boolean | null }]>((getMixed) => {
const { isMixed } = getMixed();
const colorClass = isMixed ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800';
const label = isMixed ? 'Sí' : 'No';
return {
render: () =>
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
${label}
</span>`
};
});
return renderSnippet(mixedSnippet, { isMixed });
}
},
{
accessorKey: "related_doc_id",
header: "Doc. Relacionado",
cell: ({ row }) => {
const relDoc = row.original.related_doc_id;
const relDocSnippet = createRawSnippet<[{ relDoc?: number | null }]>((getRelDoc) => {
const { relDoc } = getRelDoc();
return {
render: () =>
`<div class="text-sm">${relDoc || '-'}</div>`
};
});
return renderSnippet(relDocSnippet, { relDoc });
}
},
];
}
export const columns = createColumns();