- 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.
361 lines
11 KiB
TypeScript
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(); |