feat: add Pedimento relationship and update invoice handling for improved data management
This commit is contained in:
@@ -15,6 +15,14 @@ export interface InvoiceComplianceMx {
|
||||
pedimento_r1?: number | null;
|
||||
pedimento_k1?: number | null;
|
||||
remesa?: number | null;
|
||||
// Pedimento relationship data (when joined)
|
||||
pedimento?: {
|
||||
pedimento_number?: string;
|
||||
pedimento_code?: string;
|
||||
customs_office?: string;
|
||||
license?: string;
|
||||
year?: string;
|
||||
} | null;
|
||||
aduana?: string | null;
|
||||
port_of_entry?: string | null;
|
||||
destination?: string | null;
|
||||
@@ -282,6 +290,7 @@ export interface CreateInvoiceData {
|
||||
}
|
||||
|
||||
export interface UpdateInvoiceData {
|
||||
id: number; // Required by backend
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
document_type?: string | null;
|
||||
|
||||
@@ -4,32 +4,6 @@ import { createRawSnippet } from "svelte";
|
||||
import DataTableActions from "./data-table-actions.svelte";
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
|
||||
/**
|
||||
* Formatea un número como moneda MXN
|
||||
*/
|
||||
function formatCurrencyMXN(value?: number | null): string {
|
||||
if (value === null || value === undefined) return '-';
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency: 'MXN',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea un número como moneda USD
|
||||
*/
|
||||
function formatCurrencyUSD(value?: number | null): string {
|
||||
if (value === null || value === undefined) return '-';
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea una fecha
|
||||
*/
|
||||
@@ -42,81 +16,11 @@ function formatDate(date?: string | null): string {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el color del badge según el tipo de operación
|
||||
*/
|
||||
function getOperationTypeColor(type?: string | null): string {
|
||||
if (!type) return 'bg-gray-100 text-gray-800';
|
||||
return type === 'imp' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800';
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el color del badge según el semáforo fiscal
|
||||
*/
|
||||
function getTrafficLightColor(status?: string | null): string {
|
||||
if (!status) return 'bg-gray-100 text-gray-800';
|
||||
|
||||
const statusLower = status.toLowerCase();
|
||||
if (statusLower.includes('verde') || statusLower === 'green') return 'bg-green-100 text-green-800';
|
||||
if (statusLower.includes('amarillo') || statusLower === 'yellow') return 'bg-yellow-100 text-yellow-800';
|
||||
if (statusLower.includes('rojo') || statusLower === 'red') return 'bg-red-100 text-red-800';
|
||||
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Invoice>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
const idSnippet = createRawSnippet<[{ id: number }]>((getId) => {
|
||||
const { id } = getId();
|
||||
return {
|
||||
render: () =>
|
||||
`<div class="font-medium">#${id}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(idSnippet, { id: row.original.id });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "operation_type",
|
||||
header: "Operación",
|
||||
cell: ({ row }) => {
|
||||
const type = row.original.operation_type;
|
||||
const colorClass = getOperationTypeColor(type);
|
||||
const label = type === 'imp' ? 'IMP' : type === 'exp' ? 'EXP' : 'N/A';
|
||||
|
||||
const typeSnippet = createRawSnippet<[{ label: string; colorClass: string }]>((getType) => {
|
||||
const { label, colorClass } = getType();
|
||||
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(typeSnippet, { label, colorClass });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "invoice_type",
|
||||
header: "Tipo",
|
||||
cell: ({ row }) => {
|
||||
const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
|
||||
const { type } = getType();
|
||||
return {
|
||||
render: () =>
|
||||
`<div class="capitalize">${type || '-'}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(typeSnippet, { type: row.original.invoice_type });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "invoice_number",
|
||||
header: "Número de Factura",
|
||||
header: "Núm. Factura",
|
||||
cell: ({ row }) => {
|
||||
const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getNumber) => {
|
||||
const { number } = getNumber();
|
||||
@@ -127,100 +31,40 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Invoice>[] {
|
||||
});
|
||||
return renderSnippet(numberSnippet, { number: row.original.invoice_number });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "project_number",
|
||||
header: "Proyecto",
|
||||
cell: ({ row }) => {
|
||||
const projectSnippet = createRawSnippet<[{ project?: string | null }]>((getProject) => {
|
||||
const { project } = getProject();
|
||||
return {
|
||||
render: () =>
|
||||
`<div>${project || '-'}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(projectSnippet, { project: row.original.project_number });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "compliance_mx.pedimento",
|
||||
header: "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<[{ pedimento?: string | null }]>((getPedimento) => {
|
||||
const { pedimento } = getPedimento();
|
||||
const pedimentoSnippet = createRawSnippet<[{ number?: string | null }]>((getPedimento) => {
|
||||
const { number } = getPedimento();
|
||||
return {
|
||||
render: () =>
|
||||
`<div class="text-sm">${pedimento || '-'}</div>`
|
||||
`<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, { pedimento });
|
||||
return renderSnippet(pedimentoSnippet, { number: fullNumber });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "financials.value_mn",
|
||||
header: () => {
|
||||
const headerSnippet = createRawSnippet(() => {
|
||||
return {
|
||||
render: () => `<div class="text-right">Valor MN</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(headerSnippet, {});
|
||||
},
|
||||
accessorKey: "compliance_mx.remesa",
|
||||
header: "Remesa",
|
||||
cell: ({ row }) => {
|
||||
const valueMN = row.original.financials?.value_mn;
|
||||
const remesa = row.original.compliance_mx?.remesa;
|
||||
|
||||
const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => {
|
||||
const { value } = getValue();
|
||||
const remesaSnippet = createRawSnippet<[{ remesa?: number | null }]>((getRemesa) => {
|
||||
const { remesa } = getRemesa();
|
||||
return {
|
||||
render: () =>
|
||||
`<div class="text-right font-medium">${value}</div>`
|
||||
`<div class="text-sm">${remesa || '-'}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(valueSnippet, { value: formatCurrencyMXN(valueMN) });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "financials.value_me",
|
||||
header: () => {
|
||||
const headerSnippet = createRawSnippet(() => {
|
||||
return {
|
||||
render: () => `<div class="text-right">Valor ME</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(headerSnippet, {});
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const valueME = row.original.financials?.value_me;
|
||||
|
||||
const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => {
|
||||
const { value } = getValue();
|
||||
return {
|
||||
render: () =>
|
||||
`<div class="text-right font-medium">${value}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(valueSnippet, { value: formatCurrencyUSD(valueME) });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "traffic_light_status",
|
||||
header: "Semáforo",
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.traffic_light_status;
|
||||
const colorClass = getTrafficLightColor(status);
|
||||
|
||||
const statusSnippet = createRawSnippet<[{ status?: string | null; colorClass: string }]>((getStatus) => {
|
||||
const { status, colorClass } = getStatus();
|
||||
return {
|
||||
render: () =>
|
||||
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
|
||||
${status || '-'}
|
||||
</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(statusSnippet, { status, colorClass });
|
||||
return renderSnippet(remesaSnippet, { remesa });
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -237,6 +81,158 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Invoice>[] {
|
||||
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 Items",
|
||||
cell: ({ row }) => {
|
||||
// El total de items viene del conteo de details
|
||||
const totalItems = 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 || '-'}</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 }) => {
|
||||
// weight_type está en logistics que es un array, tomamos el primer elemento
|
||||
const weightType = row.original.logistics?.[0]?.weight_type;
|
||||
|
||||
const weightSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
|
||||
const { type } = getType();
|
||||
return {
|
||||
render: () =>
|
||||
`<div class="text-sm">${type || '-'}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(weightSnippet, { type: weightType });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "is_updated",
|
||||
header: "Actualizado",
|
||||
cell: ({ row }) => {
|
||||
const isUpdated = row.original.is_updated;
|
||||
|
||||
const updatedSnippet = createRawSnippet<[{ isUpdated?: boolean | null }]>((getUpdated) => {
|
||||
const { isUpdated } = getUpdated();
|
||||
const colorClass = isUpdated ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
const label = isUpdated ? '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(updatedSnippet, { isUpdated });
|
||||
}
|
||||
},
|
||||
{
|
||||
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 });
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
|
||||
@@ -89,7 +89,8 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<SaveInvo
|
||||
await goto(`/dashboard/invoices/edit/${newInvoiceId}`);
|
||||
} else {
|
||||
// Actualizar factura existente con todos sus sub-recursos
|
||||
const response = await invoicesApi.update(invoiceId!, companyId, payload as UpdateInvoiceData);
|
||||
const updatePayload = { ...payload, id: invoiceId } as UpdateInvoiceData;
|
||||
const response = await invoicesApi.update(invoiceId!, companyId, updatePayload);
|
||||
console.log('Update response:', response);
|
||||
if (response.error) {
|
||||
const error: any = new Error(response.error);
|
||||
@@ -112,7 +113,7 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<SaveInvo
|
||||
function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateInvoiceData {
|
||||
const { InvoiceTopFieldsFormData, generalFormData, observationFormData, itemsFormData, othersFormData, continuationFormData } = formData;
|
||||
|
||||
const payload: CreateInvoiceData | UpdateInvoiceData = {
|
||||
const payload: any = {
|
||||
// Datos generales desde InvoiceTopFieldsFormData
|
||||
system: 'fixed_asset',
|
||||
operation_type: InvoiceTopFieldsFormData?.operation_type !== null && InvoiceTopFieldsFormData?.operation_type !== undefined
|
||||
@@ -178,7 +179,7 @@ function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: a
|
||||
return {
|
||||
// Pedimento fields - desde InvoiceTopFieldsFormData
|
||||
pedimento_id: InvoiceTopFieldsFormData?.pedimento_id ? Number(InvoiceTopFieldsFormData.pedimento_id) : null,
|
||||
remesa: Number(InvoiceTopFieldsFormData?.remesa || null),
|
||||
remesa: InvoiceTopFieldsFormData?.remesa ? Number(InvoiceTopFieldsFormData.remesa) : null,
|
||||
is_pedimento_pending: Boolean(InvoiceTopFieldsFormData?.is_pedimento_pending || false),
|
||||
// Fields from generalFormData
|
||||
aduana: generalFormData?.aduana || null,
|
||||
@@ -188,8 +189,8 @@ function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: a
|
||||
sold_to_id: generalFormData?.sold_to_id || null,
|
||||
shipped_to_header: generalFormData?.shipped_to_header || '',
|
||||
shipped_to_id: generalFormData?.shipped_to_id || null,
|
||||
customs_broker_id: Number(generalFormData?.customs_broker_id || null),
|
||||
customs_broker_us_id: Number(generalFormData?.customs_broker_us_id || null),
|
||||
customs_broker_id: generalFormData?.customs_broker_id ? Number(generalFormData.customs_broker_id) : null,
|
||||
customs_broker_us_id: generalFormData?.customs_broker_us_id ? Number(generalFormData.customs_broker_us_id) : null,
|
||||
// Fields from observationFormData
|
||||
movement_type: observationFormData?.movement_type || null,
|
||||
enclosure: observationFormData?.enclosure || null,
|
||||
|
||||
Reference in New Issue
Block a user