Merge branch 'feature/facturas' into development

This commit is contained in:
AlexeerCT
2025-12-24 15:07:10 -06:00
49 changed files with 4899 additions and 6443 deletions

View File

@@ -13,8 +13,12 @@ export interface InvoiceComplianceMx {
invoice_id?: number;
pedimento?: string | null;
pedimento_code?: string | null;
pedimento_k1?: string | null;
remesa?: number | null;
aduana?: string | null;
aduana?: string | null;
port_of_entry?: string | null;
destination?: string | null;
manifest_number?: string | null;
provider_header?: string | null;
provider_id?: string | null;
sold_to_header?: string | null;
@@ -24,11 +28,42 @@ export interface InvoiceComplianceMx {
shipped_by_header?: string | null;
shipped_by_id?: string | null;
customs_broker_id?: string | null;
customs_broker_us_id?: string | null;
broker_invoice_num?: string | null;
broker_invoice_date?: string | null;
is_mixed?: boolean | null;
waste_type?: string | null;
scrap_type?: string | null;
appendix_17?: number | null;
is_regime_change?: string | null;
which_exchange_rate?: string | null;
value_method?: string | null;
act_value?: string | null;
is_pedimento_pending?: boolean | null;
is_owner_of_goods?: string | null;
generate_balances?: string | null;
was_reviewed_by_company?: boolean | null;
edocument?: string | null;
code_signature?: string | null;
electronic_signature?: string | null;
certificate_number?: string | null;
niu_number?: string | null;
bill_of_lading_count?: string | null;
addendum_vu?: string | null;
origin_destination_cove?: string | null;
vucem_operation_num?: string | null;
customs_person_line?: number | null;
contingency_mode?: boolean | null;
enclosure?: string | null;
guide_type_to_identify?: string | null;
location?: string | null;
dot_code?: string | null;
subdivision?: string | null;
acts_as?: string | null;
movement_type?: string | null;
office_document?: string | null;
reason_export?: string | null;
signature_key?: string | null;
sem_id?: number | null;
}
@@ -38,33 +73,83 @@ export interface InvoiceFinancials {
currency?: string | null;
currency_type?: string | null;
exchange_rate?: number | null;
exchange_rate_mm?: number | null;
value_mn?: number | null;
value_me?: number | null;
value_mc?: number | null;
customs_value_mn?: number | null;
customs_value_me?: number | null;
raw_material_value_mn?: number | null;
raw_material_value_me?: number | null;
aggregate_value_mn?: number | null;
aggregate_value_me?: number | null;
aggregate_value_mc?: number | null;
mexican_value_mn?: number | null;
mexican_value_me?: number | null;
mexican_value_mc?: number | null;
national_packaging_mn?: number | null;
national_packaging_me?: number | null;
national_packaging_mc?: number | null;
freight?: number | null;
insurance?: number | null;
insurance_value?: number | null;
packaging?: number | null;
other_increments?: number | null;
total_increments_mn?: number | null;
total_increments_me?: number | null;
iva_mn?: number | null;
iva_factor?: number | null;
iva_me?: number | null;
iva_mc?: number | null;
iva_factor?: string | null;
tax_value_me?: number | null;
seal_value_2500?: boolean | null;
total_quantity?: number | null;
gross_weight?: number | null;
net_weight?: number | null;
bundle_count?: number | null;
weight_factor?: number | null;
}
export interface InvoiceLogistics {
id?: number;
invoice_id?: number;
carrier_id?: string | null;
transport_id?: string | null;
transport_us_id?: string | null;
transport_type?: TransportType | null;
transport_num?: string | null;
transport_mode?: string | null;
driver_name?: string | null;
is_rail?: string | null;
rail_id?: string | null;
vehicle_num?: string | null;
license_plate?: string | null;
license_plate_complete?: string | null;
trailer_num?: string | null;
seal_number?: string | null;
guide_number?: string | null;
bill_number?: string | null;
reference_number?: string | null;
shipment_number?: string | null;
incoterm?: string | null;
identifier_1?: string | null;
complement_1?: string | null;
identifier_2?: string | null;
complement_2?: string | null;
weight_type?: string | null;
container_types?: string | null;
vehicle_data?: string | null;
origin_location?: string | null;
destination_location?: string | null;
transport_itinerary?: string | null;
destination_goods?: string | null;
entry_exit_date?: string | null;
delivery_date?: string | null;
delivered_status?: string | null;
received_by?: string | null;
payment_date?: string | null;
payment_receipt_num?: string | null;
is_ctm_process?: string | null;
}
export interface InvoiceSalesDetails {
@@ -91,25 +176,47 @@ export interface Invoice {
id: number;
tenant_id: number;
company_id: number;
system?: string | null;
operation_type?: OperationType | null;
invoice_type?: string | null;
invoice_number?: string | null;
project_number?: string | null;
purchase_order?: string | null;
related_doc_id?: number | null;
alternate_invoice?: string | null;
invoice_ref?: string | null;
proforma_number?: string | null;
invoice_date?: string | null;
capture_date: string;
emission_date?: string | null;
is_updated?: boolean | null;
updated_date?: string | null;
who_updated?: string | null;
capture_user?: string | null;
traffic_light_status?: string | null;
process_log?: string | null;
status_rec?: number | null;
status_rep?: string | null;
observation_es?: string | null;
observation_en?: string | null;
comments_status?: string | null;
vu_observations?: string | null;
cfdi_uuid?: string | null;
path_pdf?: string | null;
path_xml?: string | null;
subcompany?: string | null;
party_count?: number | null;
generate_id?: string | null;
generate_desc_parties?: string | null;
apply_manual_discount?: string | null;
is_bulk?: boolean | null;
download_substance?: boolean | null;
download_class?: boolean | null;
download_def?: boolean | null;
payment_terms?: string | null;
handling_fees?: number | null;
option_iv18?: string | null;
enajenation_goods?: boolean | null;
compliance_mx?: InvoiceComplianceMx | null;
financials?: InvoiceFinancials | null;
logistics?: InvoiceLogistics[];
@@ -124,22 +231,47 @@ export interface InvoiceListResponse {
page_size: number;
}
export interface InvoiceData {
export interface CreateInvoiceData {
system?: string | null;
operation_type?: OperationType | null;
invoice_type?: string | null;
invoice_number?: string | null;
project_number?: string | null;
purchase_order?: string | null;
related_doc_id?: number | null;
alternate_invoice?: string | null;
invoice_ref?: string | null;
proforma_number?: string | null;
invoice_date?: string | null;
emission_date?: string | null;
is_updated?: boolean | null;
updated_date?: string | null;
who_updated?: string | null;
capture_user?: string | null;
traffic_light_status?: string | null;
process_log?: string | null;
status_rec?: number | null;
status_rep?: string | null;
observation_es?: string | null;
observation_en?: string | null;
comments_status?: string | null;
vu_observations?: string | null;
cfdi_uuid?: string | null;
path_pdf?: string | null;
path_xml?: string | null;
subcompany?: string | null;
party_count?: number | null;
generate_id?: string | null;
generate_desc_parties?: string | null;
apply_manual_discount?: string | null;
is_bulk?: boolean | null;
download_substance?: boolean | null;
download_class?: boolean | null;
download_def?: boolean | null;
payment_terms?: string | null;
handling_fees?: number | null;
option_iv18?: string | null;
enajenation_goods?: boolean | null;
compliance_mx?: Omit<InvoiceComplianceMx, 'invoice_id'> | null;
financials?: Omit<InvoiceFinancials, 'id' | 'invoice_id'> | null;
logistics?: Omit<InvoiceLogistics, 'id' | 'invoice_id'>[] | null;

View File

@@ -1,13 +1 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="techGradient" x1="16" y1="16" x2="48" y2="48" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#00F2FE" /> <stop offset="100%" stop-color="#4FACFE" /> </linearGradient>
</defs>
<rect width="64" height="64" rx="18" fill="#0F172A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32 14L46 26V40L32 52L18 40V26L32 14ZM32 20.5L23 28.2V35.8L32 43.5L41 35.8V28.2L32 20.5Z" fill="url(#techGradient)"/>
<path d="M32 20.5V30M32 34V43.5" stroke="#0F172A" stroke-width="2" stroke-linecap="round"/>
<path d="M23 35.8L32 30M41 35.8L32 30" stroke="#0F172A" stroke-width="2" stroke-linecap="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64"><defs><linearGradient id="techGradient" x1="16" x2="48" y1="16" y2="48" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#00f2fe"/><stop offset="100%" stop-color="#4facfe"/></linearGradient></defs><rect width="64" height="64" fill="#0f172a" rx="18"/><path fill="url(#techGradient)" fill-rule="evenodd" d="m32 14 14 12v14L32 52 18 40V26zm0 6.5-9 7.7v7.6l9 7.7 9-7.7v-7.6z" clip-rule="evenodd"/><path stroke="#0f172a" stroke-linecap="round" stroke-width="2" d="M32 20.5V30m0 4v9.5m-9-7.7 9-5.8m9 5.8L32 30"/></svg>

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 619 B

View File

@@ -1,89 +1,250 @@
/**
* Definición de columnas para la tabla de facturas
*/
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 { Invoice } from '$lib/api/dashboard/a76/invoices';
import DataTableActions from './data-table-actions.svelte';
export function createColumns() {
/**
* 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
*/
function formatDate(date?: string | null): string {
if (!date) return '-';
return new Date(date).toLocaleDateString('es-MX', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
/**
* 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: (info: any) => info.getValue(),
enableSorting: true
},
{
accessorKey: 'operation_type',
header: 'Tipo',
cell: (info: any) => {
const type = info.getValue();
return type === 'imp' ? 'Importación' : type === 'exp' ? 'Exportación' : '-';
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: 'invoice_number',
header: 'Número de Factura',
cell: (info: any) => info.getValue() || '-'
},
{
accessorKey: 'invoice_type',
header: 'Tipo Factura',
cell: (info: any) => info.getValue() || '-'
},
{
accessorKey: 'project_number',
header: 'Proyecto',
cell: (info: any) => info.getValue() || '-'
},
{
accessorKey: 'compliance_mx.pedimento',
header: 'Pedimento',
cell: (info: any) => {
const row = info.row.original;
return row.compliance_mx?.pedimento || '-';
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_date',
header: 'Fecha Factura',
cell: (info: any) => {
const date = info.getValue();
if (!date) return '-';
return new Date(date).toLocaleDateString('es-MX');
accessorKey: "invoice_number",
header: "Número de 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: 'financials.value_mn',
header: 'Valor MN',
cell: (info: any) => {
const row = info.row.original;
const value = row.financials?.value_mn;
if (value === null || value === undefined) return '-';
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN'
}).format(value);
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: 'traffic_light_status',
header: 'Semáforo',
cell: (info: any) => info.getValue() || '-'
},
{
accessorKey: 'capture_date',
header: 'Fecha Captura',
cell: (info: any) => {
const date = info.getValue();
if (!date) return '-';
return new Date(date).toLocaleDateString('es-MX');
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 });
}
},
{
id: 'actions',
header: 'Acciones',
cell: (info: any) => DataTableActions,
enableSorting: false
accessorKey: "compliance_mx.pedimento",
header: "Pedimento",
cell: ({ row }) => {
const pedimento = row.original.compliance_mx?.pedimento;
const pedimentoSnippet = createRawSnippet<[{ pedimento?: string | null }]>((getPedimento) => {
const { pedimento } = getPedimento();
return {
render: () =>
`<div class="text-sm">${pedimento || '-'}</div>`
};
});
return renderSnippet(pedimentoSnippet, { pedimento });
}
},
{
accessorKey: "financials.value_mn",
header: () => {
const headerSnippet = createRawSnippet(() => {
return {
render: () => `<div class="text-right">Valor MN</div>`
};
});
return renderSnippet(headerSnippet, {});
},
cell: ({ row }) => {
const valueMN = row.original.financials?.value_mn;
const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => {
const { value } = getValue();
return {
render: () =>
`<div class="text-right font-medium">${value}</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 });
}
},
{
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) });
}
},
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { invoice: row.original, onSuccess });
}
}
];
}
// Mantener compatibilidad hacia atrás
export const columns = createColumns();

View File

@@ -3,26 +3,24 @@
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { Ellipsis, Eye, Pencil, Trash2 } from 'lucide-svelte';
import { goto } from '$app/navigation';
import DetailsDialog from './details-dialog.svelte';
import DeleteDialog from './delete-dialog.svelte';
interface Props {
invoice: Invoice;
onSuccess?: () => void;
}
let { invoice }: Props = $props();
let { invoice, onSuccess }: Props = $props();
let showDetails = $state(false);
let showDelete = $state(false);
function dispatchView() {
window.dispatchEvent(new CustomEvent('invoiceView', { detail: invoice }));
function handleEdit() {
// Redirigir a la página de edición
window.location.href = `/dashboard/invoices/edit/${invoice.id}`;
}
function dispatchDelete() {
window.dispatchEvent(new CustomEvent('invoiceDelete', { detail: invoice }));
}
function dispatchEdit() {
window.dispatchEvent(new CustomEvent('invoiceEdit', { detail: invoice }));
}
</script>
<DropdownMenu.Root>
@@ -38,20 +36,36 @@
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={dispatchView}>
<DropdownMenu.Item onclick={() => showDetails = true}>
<Eye class="mr-2 h-4 w-4" />
Ver Detalles
</DropdownMenu.Item>
<DropdownMenu.Item onclick={dispatchEdit}>
<DropdownMenu.Item onclick={handleEdit}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={dispatchDelete} class="text-destructive">
<DropdownMenu.Item onclick={() => showDelete = true} class="text-destructive">
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</DropdownMenu.Root>
<!-- Diálogos -->
{#if showDetails}
<DetailsDialog
invoice={invoice}
onClose={() => showDetails = false}
/>
{/if}
{#if showDelete}
<DeleteDialog
invoice={invoice}
onClose={() => showDelete = false}
{onSuccess}
/>
{/if}

View File

@@ -83,18 +83,10 @@
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
{#if cell.column.id === 'actions'}
{@const cellDef = cell.column.columnDef.cell}
{#if cellDef && typeof cellDef === 'function'}
{@const Component = cellDef(cell.getContext())}
<Component invoice={cell.row.original} />
{/if}
{:else}
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
{/if}
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>

View File

@@ -5,27 +5,26 @@
import { companyStore } from "$lib/stores/company.svelte";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
item,
onSuccess
}: {
open: boolean;
item: Invoice | null;
interface Props {
invoice: Invoice;
onClose: () => void;
onSuccess?: () => void;
} = $props();
}
let { invoice, onClose, onSuccess }: Props = $props();
let open = $state(true);
let loading = $state(false);
let error = $state<string | null>(null);
async function handleDelete() {
if (!item || !companyStore.activeCompany) return;
if (!invoice || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await invoicesApi.delete(item.id, companyStore.activeCompany.id);
const response = await invoicesApi.delete(invoice.id, companyStore.activeCompany.id);
if (response.error) {
error = response.error;
@@ -33,7 +32,7 @@
}
// Éxito
open = false;
onClose();
if (onSuccess) {
onSuccess();
}
@@ -48,8 +47,10 @@
function handleOpenChange(newOpen: boolean) {
if (!newOpen) {
error = null;
onClose();
} else {
open = newOpen;
}
open = newOpen;
}
</script>
@@ -59,30 +60,30 @@
<AlertDialog.Title>¿Estás seguro?</AlertDialog.Title>
<AlertDialog.Description class="space-y-2">
<p>Esta acción no se puede deshacer. Se eliminará permanentemente esta factura:</p>
{#if item}
{#if invoice}
<div class="mt-2 rounded-lg bg-muted p-3 space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="font-medium">ID:</span>
<span class="font-semibold">{item.id}</span>
<span class="font-semibold">{invoice.id}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="font-medium">Número de Factura:</span>
<code class="font-mono font-semibold">{item.invoice_number || 'N/A'}</code>
<code class="font-mono font-semibold">{invoice.invoice_number || 'N/A'}</code>
</div>
<div class="flex items-center justify-between text-sm">
<span class="font-medium">Tipo:</span>
<span class="text-xs">
{item.operation_type === 'imp' ? 'Importación' :
item.operation_type === 'exp' ? 'Exportación' : 'N/A'}
{invoice.operation_type === 'imp' ? 'Importación' :
invoice.operation_type === 'exp' ? 'Exportación' : 'N/A'}
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="font-medium">Proyecto:</span>
<span class="text-xs">{item.project_number || 'N/A'}</span>
<span class="text-xs">{invoice.project_number || 'N/A'}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="font-medium">Pedimento:</span>
<span class="text-xs">{item.compliance_mx?.pedimento || 'N/A'}</span>
<span class="text-xs">{invoice.compliance_mx?.pedimento || 'N/A'}</span>
</div>
</div>
{/if}

View File

@@ -5,13 +5,13 @@
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
let {
open = $bindable(false),
invoice
}: {
open: boolean;
invoice: Invoice | null;
} = $props();
interface Props {
invoice: Invoice;
onClose: () => void;
}
let { invoice, onClose }: Props = $props();
let open = $state(true);
function formatDate(dateString: string | null | undefined): string {
if (!dateString) return '-';
@@ -32,7 +32,7 @@
}
</script>
<Dialog.Root {open} onOpenChange={(v) => (open = v)}>
<Dialog.Root {open} onOpenChange={(v) => { open = v; if (!v) onClose(); }}>
<Dialog.Content class="max-w-5xl max-h-[90vh] overflow-y-auto">
<Dialog.Header>
<Dialog.Title>Detalles de Factura #{invoice?.id}</Dialog.Title>

View File

@@ -0,0 +1,298 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group';
import { Button } from '$lib/components/ui/button';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
let {
invoice,
formData = $bindable(),
exists = $bindable()
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
} = $props();
if (!formData && invoice?.financials) {
formData = {
// Currency & Exchange
currency: invoice.financials.currency || '',
currency_type: invoice.financials.currency_type || '',
exchange_rate: invoice.financials.exchange_rate || null,
exchange_rate_mm: invoice.financials.exchange_rate_mm || null,
// Values
value_mn: invoice.financials.value_mn || null,
value_me: invoice.financials.value_me || null,
value_mc: invoice.financials.value_mc || null,
customs_value_mn: invoice.financials.customs_value_mn || null,
customs_value_me: invoice.financials.customs_value_me || null,
// Raw materials
raw_material_value_mn: invoice.financials.raw_material_value_mn || null,
raw_material_value_me: invoice.financials.raw_material_value_me || null,
// Aggregate values
aggregate_value_mn: invoice.financials.aggregate_value_mn || null,
aggregate_value_me: invoice.financials.aggregate_value_me || null,
aggregate_value_mc: invoice.financials.aggregate_value_mc || null,
// Mexican values
mexican_value_mn: invoice.financials.mexican_value_mn || null,
mexican_value_me: invoice.financials.mexican_value_me || null,
mexican_value_mc: invoice.financials.mexican_value_mc || null,
// National packaging
national_packaging_mn: invoice.financials.national_packaging_mn || null,
national_packaging_me: invoice.financials.national_packaging_me || null,
national_packaging_mc: invoice.financials.national_packaging_mc || null,
// Costs & increments
freight: invoice.financials.freight || null,
insurance: invoice.financials.insurance || null,
insurance_value: invoice.financials.insurance_value || null,
packaging: invoice.financials.packaging || null,
other_increments: invoice.financials.other_increments || null,
total_increments_mn: invoice.financials.total_increments_mn || null,
total_increments_me: invoice.financials.total_increments_me || null,
// Taxes
iva_mn: invoice.financials.iva_mn || null,
iva_me: invoice.financials.iva_me || null,
iva_mc: invoice.financials.iva_mc || null,
iva_factor: invoice.financials.iva_factor || '',
tax_value_me: invoice.financials.tax_value_me || null,
seal_value_2500: invoice.financials.seal_value_2500 || false,
// Weights & quantities
total_quantity: invoice.financials.total_quantity || null,
gross_weight: invoice.financials.gross_weight || null,
net_weight: invoice.financials.net_weight || null,
bundle_count: invoice.financials.bundle_count || null,
weight_factor: invoice.financials.weight_factor || null,
// Additional fields not in backend
numero_tipo_transporte: '',
es_ferrocarril: 'no',
numero_bl: '',
cantidad_guias_embarque: null,
destino_origen: '',
puerto_entrada: '',
// Checkboxes
fue_revisado_equipo: false,
sub_division: false,
funge_como_cd: false,
llego_pedimento: false,
// Errores
errores_facturacion: [],
// Semáforos
semaforo_verde_aduana_mexicana: false,
semaforo_verde_aduana_americana: false,
semaforo_rojo_aduana_mexicana: false,
semaforo_rojo_aduana_americana: false
};
exists = true;
} else if (!formData) {
formData = {
// Currency & Exchange
currency: '',
currency_type: '',
exchange_rate: null,
exchange_rate_mm: null,
// Values
value_mn: null,
value_me: null,
value_mc: null,
customs_value_mn: null,
customs_value_me: null,
// Raw materials
raw_material_value_mn: null,
raw_material_value_me: null,
// Aggregate values
aggregate_value_mn: null,
aggregate_value_me: null,
aggregate_value_mc: null,
// Mexican values
mexican_value_mn: null,
mexican_value_me: null,
mexican_value_mc: null,
// National packaging
national_packaging_mn: null,
national_packaging_me: null,
national_packaging_mc: null,
// Costs & increments
freight: null,
insurance: null,
insurance_value: null,
packaging: null,
other_increments: null,
total_increments_mn: null,
total_increments_me: null,
// Taxes
iva_mn: null,
iva_me: null,
iva_mc: null,
iva_factor: '',
tax_value_me: null,
seal_value_2500: false,
// Weights & quantities
total_quantity: null,
gross_weight: null,
net_weight: null,
bundle_count: null,
weight_factor: null,
// Additional fields not in backend
numero_tipo_transporte: '',
es_ferrocarril: 'no',
numero_bl: '',
cantidad_guias_embarque: null,
destino_origen: '',
puerto_entrada: '',
// Checkboxes
fue_revisado_equipo: false,
sub_division: false,
funge_como_cd: false,
llego_pedimento: false,
// Errores
errores_facturacion: [],
// Semáforos
semaforo_verde_aduana_mexicana: false,
semaforo_verde_aduana_americana: false,
semaforo_rojo_aduana_mexicana: false,
semaforo_rojo_aduana_americana: false
};
exists = false;
}
</script>
<!-- Layout de 2 columnas compacto -->
<div class="grid grid-cols-2 gap-3">
<!-- Columna Izquierda -->
<div class="border rounded-md p-3 space-y-3">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Información General</h4>
<!-- NÚMERO/TIPO DE TRANSPORTE -->
<div class="space-y-1.5">
<Label for="numero_tipo_transporte" class="text-xs">Número/Tipo de Transporte:</Label>
<Input id="numero_tipo_transporte" bind:value={formData.numero_tipo_transporte} class="h-7 text-xs" />
</div>
<!-- DATOS VEHÍCULO -->
<div class="border rounded p-2 space-y-2 bg-muted/30">
<Label class="text-xs font-semibold">Datos Vehículo:</Label>
<div class="space-y-1.5">
<Label class="text-xs">Es Ferrocarril?</Label>
<RadioGroup bind:value={formData.es_ferrocarril} class="flex gap-4">
<div class="flex items-center gap-2">
<RadioGroupItem value="si" id="ferrocarril_si" />
<Label for="ferrocarril_si" class="text-xs">SI</Label>
</div>
<div class="flex items-center gap-2">
<RadioGroupItem value="no" id="ferrocarril_no" />
<Label for="ferrocarril_no" class="text-xs">NO</Label>
</div>
</RadioGroup>
</div>
<div class="space-y-1.5">
<Label for="numero_bl" class="text-xs">Número BL:</Label>
<Input id="numero_bl" bind:value={formData.numero_bl} class="h-7 text-xs" />
</div>
<div class="space-y-1.5">
<Label for="cantidad_guias_embarque" class="text-xs">Cantidad de Guías de Embarque (BL):</Label>
<Input id="cantidad_guias_embarque" type="number" bind:value={formData.cantidad_guias_embarque} class="h-7 text-xs" />
</div>
</div>
<!-- DESTINO/ORIGEN Y PUERTO ENTRADA -->
<div class="space-y-1.5">
<Label for="destino_origen" class="text-xs">Destino/Origen:</Label>
<Input id="destino_origen" bind:value={formData.destino_origen} placeholder="FRANJA FRONT." class="h-7 text-xs" />
</div>
<div class="space-y-1.5">
<Label for="puerto_entrada" class="text-xs">Puerto Entrada:</Label>
<Input id="puerto_entrada" bind:value={formData.puerto_entrada} class="h-7 text-xs" />
</div>
<!-- CHECKBOXES -->
<div class="grid grid-cols-2 gap-2">
<div class="flex items-center gap-2">
<Checkbox id="fue_revisado" bind:checked={formData.fue_revisado_equipo} />
<Label for="fue_revisado" class="text-xs">Fue Revisado el Equipo</Label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="sub_division" bind:checked={formData.sub_division} />
<Label for="sub_division" class="text-xs">Sub División</Label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="funge_como_cd" bind:checked={formData.funge_como_cd} />
<Label for="funge_como_cd" class="text-xs">Funge Como CD</Label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="llego_pedimento" bind:checked={formData.llego_pedimento} />
<Label for="llego_pedimento" class="text-xs">Llegó el Pedimento</Label>
</div>
</div>
</div>
<!-- Columna Derecha - Errores de Facturación -->
<div class="border rounded-md p-3 space-y-3">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Errores de Facturación</h4>
<div class="border rounded">
<table class="w-full text-xs">
<thead class="bg-muted">
<tr>
<th class="border px-2 py-1 text-left">Línea</th>
<th class="border px-2 py-1 text-left">Clave</th>
<th class="border px-2 py-1 text-left">Descripción</th>
</tr>
</thead>
<tbody>
{#if formData.errores_facturacion?.length}
{#each formData.errores_facturacion as error}
<tr>
<td class="border px-2 py-1">{error.linea}</td>
<td class="border px-2 py-1">{error.clave}</td>
<td class="border px-2 py-1">{error.descripcion}</td>
</tr>
{/each}
{:else}
<tr>
<td colspan="3" class="border px-2 py-12 text-center text-muted-foreground">
Sin errores registrados
</td>
</tr>
{/if}
</tbody>
</table>
</div>
<div class="flex justify-end gap-2">
<Button variant="outline" size="sm" class="h-7 text-xs">Insertar</Button>
<Button variant="outline" size="sm" class="h-7 text-xs">Editar</Button>
<Button variant="outline" size="sm" class="h-7 text-xs">Borrar</Button>
</div>
<!-- SEMÁFORO -->
<div class="border rounded p-2 space-y-2 bg-muted/30">
<Label class="text-xs font-semibold">Semáforo</Label>
<div class="grid grid-cols-2 gap-2">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<Checkbox id="verde_mex" bind:checked={formData.semaforo_verde_aduana_mexicana} />
<Label for="verde_mex" class="text-xs">Verde MX</Label>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-green-600"></div>
<Checkbox id="verde_usa" bind:checked={formData.semaforo_verde_aduana_americana} />
<Label for="verde_usa" class="text-xs">Verde USA</Label>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<Checkbox id="rojo_mex" bind:checked={formData.semaforo_rojo_aduana_mexicana} />
<Label for="rojo_mex" class="text-xs">Rojo MX</Label>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-red-600"></div>
<Checkbox id="rojo_usa" bind:checked={formData.semaforo_rojo_aduana_americana} />
<Label for="rojo_usa" class="text-xs">Rojo USA</Label>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,574 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import * as RadioGroup from '$lib/components/ui/radio-group';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
let {
invoice,
formData = $bindable(),
invoiceTypes = [],
customsBrokers = [],
clients = [],
providers = [],
currencyTypes = [],
transportTypes = [],
defaultOperationType = undefined,
defaultInvoiceType = undefined
}: {
invoice: Invoice | null;
formData?: any;
invoiceTypes?: InvoiceType[];
customsBrokers?: CustomsBroker[];
clients?: ClientProvider[];
providers?: any[];
currencyTypes?: any[];
transportTypes?: any[];
defaultOperationType?: number | null;
defaultInvoiceType?: string | null;
} = $props();
if (!formData) {
if (invoice) {
// Editando una factura existente
let operationType: number | null = null;
if (invoice.operation_type) {
operationType = invoice.operation_type === 'exp' ? 1 : 2;
}
formData = {
// TOP fields
is_pedimento_pending: false,
pedimento: invoice.compliance_mx?.pedimento || '',
remesa: invoice.compliance_mx?.remesa || '',
invoice_number: invoice.invoice_number || '',
invoice_date: invoice.invoice_date || '',
emission_date: '',
// Extra fields
operation_type: operationType,
// RANGO DE FECHAS fields
fecha_pedimento_del: '',
fecha_pedimento_al: '',
clave_pedimento: '',
regimen_pedimento: '',
// LEFT fields
provider_header: invoice.compliance_mx?.provider_header || '',
provider_id: invoice.compliance_mx?.provider_id || null,
sold_to_header: invoice.compliance_mx?.sold_to_header || '',
sold_to_id: invoice.compliance_mx?.sold_to_id || null,
shipped_to_header: invoice.compliance_mx?.shipped_to_header || '',
shipped_to_id: invoice.compliance_mx?.shipped_to_id || null,
customs_broker_id: invoice.compliance_mx?.customs_broker_id || null,
customs_broker_us_id: null,
// RIGHT fields
currency_type: invoice.financials?.currency_type || '',
currency_mode: 'extranjera', // extranjera, nacional, captura
weight_type: '',
iva_factor: invoice.financials?.iva_factor || null,
carrier_id: invoice.logistics?.[0]?.carrier_id || null,
transport_id: '',
driver_name: invoice.logistics?.[0]?.driver_name || '',
transport_type: invoice.logistics?.[0]?.transport_type || '',
transport_num: invoice.logistics?.[0]?.vehicle_num || '',
aduana: invoice.compliance_mx?.aduana || '',
invoice_type: invoice.invoice_type || '',
clave_regimen_aduanero: '',
};
} else {
// Creando una nueva factura
formData = {
// TOP fields
is_pedimento_pending: false,
pedimento: '',
remesa: '',
invoice_number: '',
invoice_date: '',
emission_date: '',
// Extra fields
operation_type: defaultOperationType ?? null,
// RANGO DE FECHAS fields
fecha_pedimento_del: '',
fecha_pedimento_al: '',
clave_pedimento: '',
regimen_pedimento: '',
// LEFT fields
provider_header: '',
provider_id: null,
sold_to_header: '',
sold_to_id: null,
shipped_to_header: '',
shipped_to_id: null,
customs_broker_id: null,
customs_broker_us_id: null,
// RIGHT fields
currency_type: '',
currency_mode: 'extranjera', // extranjera, nacional, captura
weight_type: '',
iva_factor: null,
carrier_id: null,
transport_id: '',
driver_name: '',
transport_type: '',
transport_num: '',
aduana: '',
invoice_type: defaultInvoiceType ?? '',
clave_regimen_aduanero: '',
};
}
} else {
// Si formData ya existe, asegurar que tiene currency_mode
if (formData.currency_mode === undefined) {
formData.currency_mode = 'extranjera';
}
}
// Opciones de tipo de peso
const weightTypeOptions = [
{ value: 'kg', label: 'Kilogramos (kg)' },
{ value: 'lb', label: 'Libras (lb)' }
];
// Opciones de encabezados
const providerHeaderOptions = [
{ value: 'proveedor', label: 'Proveedor' },
{ value: 'exportador', label: 'Exportador' }
];
const soldToHeaderOptions = $derived([
{ value: 'consignado_a', label: 'Consignado a' },
{ value: 'vendido_a', label: 'Vendido a' },
{ value: formData.operation_type === 1 ? 'exportado_a' : 'importador', label: formData.operation_type === 1 ? 'Exportado a' : 'Importador' }
]);
const shippedToHeaderOptions = $derived(
formData.operation_type === 1
? [
{ value: 'enviado_por', label: 'Enviado Por' },
{ value: 'destinatario', label: 'Destinatario' },
{ value: 'vendido_por', label: 'Vendido Por' },
{ value: 'consignado_a', label: 'Consignado a' },
{ value: 'vendido_a', label: 'Vendido a' },
{ value: 'exportado_a', label: 'Exportado a' },
{ value: 'enviado_a', label: 'Enviado a' },
{ value: 'transferido_a', label: 'Transferido a' },
{ value: 'donado_a', label: 'Donado a' },
{ value: 'notificar_a', label: 'Notificar a' }
]
: [
{ value: 'enviado_a', label: 'Enviado a' },
{ value: 'transferido_a', label: 'Transferido a' }
]
);
// Combinar clientes y proveedores para shipped_to
const allClientsProviders = [...clients, ...providers];
</script>
<!-- Layout de 2 columnas compacto -->
<div class="grid grid-cols-2 gap-4">
<!-- Columna Izquierda: Clientes - Proveedores - Agente Aduanal -->
<div class="border rounded-md p-3 space-y-3">
<div class="space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Datos del pedimento</h4>
<div class="grid grid-cols-4 gap-3 text-xs">
<div>
<span class="text-muted-foreground">Fecha del:</span>
<p class="font-medium">{formData.fecha_pedimento_del || '-'}</p>
</div>
<div>
<span class="text-muted-foreground">Fecha al:</span>
<p class="font-medium">{formData.fecha_pedimento_al || '-'}</p>
</div>
<div>
<span class="text-muted-foreground">Clave:</span>
<p class="font-medium">{formData.clave_pedimento || '-'}</p>
</div>
<div>
<span class="text-muted-foreground">Régimen:</span>
<p class="font-medium">{formData.regimen_pedimento || '-'}</p>
</div>
</div>
</div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Clientes - Proveedores - Agente Aduanal</h4>
<div class="space-y-1.5">
<Label for="provider_id" class="text-xs">Proveedor:</Label>
<Select.Root
type="single"
value={formData.provider_header || ''}
onValueChange={(v) => {
formData.provider_header = v ?? '';
}}
>
<Select.Trigger id="provider_header" class="h-7 text-xs">
<span class="truncate">
{formData.provider_header
? providerHeaderOptions.find(o => o.value === formData.provider_header)?.label || formData.provider_header
: 'Selecciona encabezado...'}
</span>
</Select.Trigger>
<Select.Content>
{#each providerHeaderOptions as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={formData.provider_id ? String(formData.provider_id) : ''}
onValueChange={(v) => {
formData.provider_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="provider_id" class="h-7 text-xs">
<span class="truncate">
{formData.provider_id
? providers.find(p => p.id === formData.provider_id)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each providers as provider}
<Select.Item value={String(provider.id)}>
{provider.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="sold_to_id" class="text-xs">Consignado a:</Label>
<Select.Root
type="single"
value={formData.sold_to_header || ''}
onValueChange={(v) => {
formData.sold_to_header = v ?? '';
}}
>
<Select.Trigger id="sold_to_header" class="h-7 text-xs">
<span class="truncate">
{formData.sold_to_header
? soldToHeaderOptions.find(o => o.value === formData.sold_to_header)?.label || formData.sold_to_header
: 'Selecciona encabezado...'}
</span>
</Select.Trigger>
<Select.Content>
{#each soldToHeaderOptions as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={formData.sold_to_id ? String(formData.sold_to_id) : ''}
onValueChange={(v) => {
formData.sold_to_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="sold_to_id" class="h-7 text-xs">
<span class="truncate">
{formData.sold_to_id
? clients.find(c => c.id === formData.sold_to_id)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each clients as client}
<Select.Item value={String(client.id)}>
{client.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="shipped_to_id" class="text-xs">Enviado a:</Label>
<Select.Root
type="single"
value={formData.shipped_to_header || ''}
onValueChange={(v) => {
formData.shipped_to_header = v ?? '';
}}
>
<Select.Trigger id="shipped_to_header" class="h-7 text-xs">
<span class="truncate">
{formData.shipped_to_header
? shippedToHeaderOptions.find(o => o.value === formData.shipped_to_header)?.label || formData.shipped_to_header
: 'Selecciona encabezado...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each shippedToHeaderOptions as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={formData.shipped_to_id ? String(formData.shipped_to_id) : ''}
onValueChange={(v) => {
formData.shipped_to_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="shipped_to_id" class="h-7 text-xs">
<span class="truncate">
{formData.shipped_to_id
? allClientsProviders.find(cp => cp.id === formData.shipped_to_id)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each allClientsProviders as cp}
<Select.Item value={String(cp.id)}>
{cp.name} ({cp.type === 'client' ? 'C' : 'P'})
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1.5">
<Label for="customs_broker_id" class="text-xs">Agente Aduanal Mex:</Label>
<Select.Root
type="single"
value={formData.customs_broker_id || ''}
onValueChange={(v) => {
formData.customs_broker_id = v || null;
}}
>
<Select.Trigger id="customs_broker_id" class="h-7 text-xs">
<span class="truncate">
{formData.customs_broker_id
? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_id)?.name || '...'
: '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsBrokers as broker}
<Select.Item value={broker.broker_key}>
{broker.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="customs_broker_us_id" class="text-xs">Agente Aduanal Ame:</Label>
<Select.Root
type="single"
value={formData.customs_broker_us_id || ''}
onValueChange={(v) => {
formData.customs_broker_us_id = v || null;
}}
>
<Select.Trigger id="customs_broker_us_id" class="h-7 text-xs">
<span class="truncate">
{formData.customs_broker_us_id
? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_us_id)?.name || '...'
: '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsBrokers as broker}
<Select.Item value={broker.broker_key}>
{broker.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
</div>
<!-- Columna Derecha: Tipo de Moneda y Transportista -->
<div class="space-y-3">
<!-- Tipo de Moneda - Pesos Netos y Brutos -->
<div class="border rounded-md p-3 space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Tipo de Moneda - Pesos Netos y Brutos</h4>
<!-- Radio buttons para tipo de moneda -->
<div class="space-y-1.5">
<RadioGroup.Root bind:value={formData.currency_mode} class="flex gap-4">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="extranjera" id="currency-extranjera" class="h-4 w-4" />
<Label for="currency-extranjera" class="text-xs font-normal cursor-pointer">Extranjera (Dlls)</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="nacional" id="currency-nacional" class="h-4 w-4" />
<Label for="currency-nacional" class="text-xs font-normal cursor-pointer">Nacional (Pesos)</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="captura" id="currency-captura" class="h-4 w-4" />
<Label for="currency-captura" class="text-xs font-normal cursor-pointer">De Captura</Label>
</div>
</RadioGroup.Root>
</div>
<div class="grid grid-cols-3 gap-2">
<div class="space-y-1.5">
<Label for="currency_type" class="text-xs">Moneda:</Label>
<Select.Root
type="single"
value={formData.currency_type || ''}
onValueChange={(v) => {
formData.currency_type = v ?? '';
}}
>
<Select.Trigger id="currency_type" class="h-7 text-xs">
<span class="truncate">
{formData.currency_type || '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each currencyTypes as currencyType}
<Select.Item value={currencyType.code}>
{currencyType.code}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="weight_type" class="text-xs">Tipo Peso:</Label>
<Select.Root
type="single"
value={formData.weight_type || ''}
onValueChange={(v) => {
formData.weight_type = v ?? '';
}}
>
<Select.Trigger id="weight_type" class="h-7 text-xs">
<span class="truncate">
{formData.weight_type || '...'}
</span>
</Select.Trigger>
<Select.Content>
{#each weightTypeOptions as weightType}
<Select.Item value={weightType.value}>
{weightType.value}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="iva_factor" class="text-xs">IVA:</Label>
<Input id="iva_factor" type="number" step="0.0001" bind:value={formData.iva_factor} placeholder="0.16" class="h-7 text-xs" />
</div>
</div>
<div class="space-y-1.5">
<Label for="invoice_type" class="text-xs">Tipo de Cambio:</Label>
<Select.Root
type="single"
value={formData.invoice_type || ''}
onValueChange={(v) => {
formData.invoice_type = v ?? '';
}}
>
<Select.Trigger id="invoice_type" class="h-7 text-xs">
<span class="truncate">
{formData.invoice_type
? `${formData.invoice_type}`
: '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each invoiceTypes as type}
<Select.Item value={type.key}>
{type.key} - {type.description}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<!-- Transportista -->
<div class="border rounded-md p-3 space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Transportista</h4>
<div class="grid grid-cols-3 gap-2">
<div class="space-y-1.5">
<Label for="carrier_id" class="text-xs">Clave:</Label>
<Input id="carrier_id" type="number" bind:value={formData.carrier_id} class="h-7 text-xs" />
</div>
<div class="col-span-2 space-y-1.5">
<Label for="transport_type" class="text-xs">Clave Transporte:</Label>
<Select.Root
type="single"
value={formData.transport_type || ''}
onValueChange={(v) => {
formData.transport_type = v ?? '';
}}
>
<Select.Trigger id="transport_type" class="h-7 text-xs">
<span class="truncate">
{formData.transport_type || '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each transportTypes as transportType}
<Select.Item value={transportType.transport_code}>
{transportType.transport_code}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="space-y-1.5">
<Label for="driver_name" class="text-xs">Conductor:</Label>
<Input id="driver_name" bind:value={formData.driver_name} class="h-7 text-xs" />
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1.5">
<Label for="transport_type" class="text-xs">Tipo Transporte:</Label>
<Input id="transport_id" bind:value={formData.transport_id} class="h-7 text-xs" />
</div>
<div class="space-y-1.5">
<Label for="transport_num" class="text-xs">Placas:</Label>
<Input id="transport_num" bind:value={formData.transport_num} class="h-7 text-xs" />
</div>
</div>
<div class="space-y-1.5">
<Label for="aduana" class="text-xs">Aduana y Sección de Despacho:</Label>
<Input id="aduana" bind:value={formData.aduana} class="h-7 text-xs" />
</div>
<div class="space-y-1.5">
<Label for="clave_regimen_aduanero" class="text-xs">Clave de Régimen Aduanero:</Label>
<Input id="clave_regimen_aduanero" bind:value={formData.clave_regimen_aduanero} class="h-7 text-xs" />
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,176 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import { Switch } from '$lib/components/ui/switch';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
let {
invoice,
formData = $bindable(),
invoiceTypes = [],
pedimentos = [],
defaultOperationType = undefined,
defaultInvoiceType = undefined,
}: {
invoice: Invoice | null;
formData?: any;
invoiceTypes?: InvoiceType[];
pedimentos?: Pedimento[];
defaultOperationType?: number | null;
defaultInvoiceType?: string | null;
} = $props();
function handlePedimentoChange(pedimentoId: string) {
if (!pedimentoId) return;
const selectedPedimento = pedimentos.find(p => p.id === parseInt(pedimentoId));
if (!selectedPedimento) return;
// Actualizar los campos del pedimento en formData
formData.fecha_pedimento_del = selectedPedimento.pedimento_dates?.start_date || '';
formData.fecha_pedimento_al = selectedPedimento.pedimento_dates?.end_date || '';
formData.clave_pedimento = selectedPedimento.pedimento_code || '';
formData.regimen_pedimento = selectedPedimento.regime || '';
// Construir el número de pedimento completo
const pedimentoNumber = `${selectedPedimento.year || ''}-${selectedPedimento.customs_office || ''}-${selectedPedimento.license || ''}-${selectedPedimento.pedimento_number || ''}`.replace(/^-+|-+$/g, '');
formData.pedimento = pedimentoNumber;
}
if (!formData) {
let operationType: number | null = null;
if (invoice?.operation_type) {
operationType = invoice.operation_type === 'exp' ? 1 : 2;
} else if (defaultOperationType !== undefined) {
operationType = defaultOperationType ?? null;
}
formData = {
is_pedimento_pending: false,
pedimento_id: null,
pedimento: invoice?.compliance_mx?.pedimento || '',
remesa: invoice?.compliance_mx?.remesa || '',
invoice_number: invoice?.invoice_number || '',
invoice_date: invoice?.invoice_date || '',
emission_date: '',
operation_type: operationType,
invoice_type: invoice?.invoice_type || (defaultInvoiceType ?? ''),
// Campos del pedimento (se llenarán al seleccionar un pedimento)
fecha_pedimento_del: '',
fecha_pedimento_al: '',
clave_pedimento: '',
regimen_pedimento: '',
};
}
</script>
<!-- Datos Principales en una fila compacta (reusable across tabs) -->
<div class="grid grid-cols-12 gap-3 items-end pb-3">
<div class="col-span-1 space-y-1">
<Label for="operation_type" class="text-xs">Tipo de Operación *</Label>
<Select.Root
type="single"
value={formData.operation_type !== null ? String(formData.operation_type) : ''}
onValueChange={(v) => {
formData.operation_type = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="operation_type" class="h-8 text-sm">
<span class="truncate">
{formData.operation_type !== null
? (formData.operation_type === 1 ? 'Exp' : 'Imp')
: '...'}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="1">Exportación</Select.Item>
<Select.Item value="2">Importación</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="col-span-1 space-y-1">
<Label for="operation_type" class="text-xs">Tipo de Operación *</Label>
<Select.Root
type="single"
value={formData.invoice_type || ''}
onValueChange={(v) => {
formData.invoice_type = v ?? '';
}}
>
<Select.Trigger id="invoice_type" class="h-7 text-xs">
<span class="truncate">
{formData.invoice_type
? `${formData.invoice_type}`
: '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each invoiceTypes as type}
<Select.Item value={type.key}>
{type.key} - {type.description}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="col-span-1 space-y-1 pb-1 items-center flex flex-col">
<Label for="is_pedimento_pending" class="text-xs">Pedimento Pendiente?</Label>
<Switch
id="is_pedimento_pending"
checked={formData.is_pedimento_pending}
onCheckedChange={(checked) => {
formData.is_pedimento_pending = checked;
}}
/>
</div>
<div class="col-span-2 space-y-1">
<Label for="pedimento" class="text-xs">Pedimento</Label>
<Select.Root
type="single"
value={formData.pedimento_id ? String(formData.pedimento_id) : ''}
onValueChange={(v) => {
formData.pedimento_id = v ? parseInt(v) : null;
if (v) {
handlePedimentoChange(v);
}
}}
>
<Select.Trigger id="pedimento" class="h-8 text-sm">
<span class="truncate">
{formData.pedimento || 'Selecciona pedimento...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each pedimentos as pedimento}
<Select.Item value={String(pedimento.id)}>
{pedimento.year}-{pedimento.customs_office}-{pedimento.license}-{pedimento.pedimento_number}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="col-span-1 space-y-1">
<Label for="remesa" class="text-xs">Remesa</Label>
<Input id="remesa" bind:value={formData.remesa} class="h-8 text-sm" />
</div>
<div class="col-span-2 space-y-1">
<Label for="invoice_number" class="text-xs">Núm. Factura *</Label>
<Input id="invoice_number" bind:value={formData.invoice_number} class="h-8 text-sm font-medium" required />
</div>
<div class="col-span-2 space-y-1">
<Label for="invoice_date" class="text-xs">Fecha Factura</Label>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} class="h-8 text-sm" />
</div>
<div class="col-span-2 space-y-1">
<Label for="emission_date" class="text-xs">Fecha Emisión</Label>
<Input id="emission_date" type="date" bind:value={formData.emission_date} class="h-8 text-sm" />
</div>
</div>

View File

@@ -0,0 +1,404 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Textarea } from "$lib/components/ui/textarea/index.js";
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
let {
invoice,
formData = $bindable(),
exists = $bindable(),
seals = [],
incoterms = [],
enclosure = [],
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
seals?: any[];
incoterms?: any[];
enclosure?: any[];
} = $props();
if (!formData && invoice) {
formData = {
// Invoice header fields
observation_es: invoice.observation_es || '',
observation_en: invoice.observation_en || '',
alternate_invoice: invoice.alternate_invoice || '',
// Compliance MX fields
pedimento: invoice.compliance_mx?.pedimento || '',
pedimento_code: invoice.compliance_mx?.pedimento_code || '',
pedimento_k1: invoice.compliance_mx?.pedimento_k1 || '',
remesa: invoice.compliance_mx?.remesa || null,
aduana: invoice.compliance_mx?.aduana || '',
port_of_entry: invoice.compliance_mx?.port_of_entry || '',
destination: invoice.compliance_mx?.destination || '',
manifest_number: invoice.compliance_mx?.manifest_number || '',
provider_header: invoice.compliance_mx?.provider_header || '',
provider_id: invoice.compliance_mx?.provider_id || null,
sold_to_header: invoice.compliance_mx?.sold_to_header || '',
sold_to_id: invoice.compliance_mx?.sold_to_id || null,
shipped_to_header: invoice.compliance_mx?.shipped_to_header || '',
shipped_to_id: invoice.compliance_mx?.shipped_to_id || null,
shipped_by_header: invoice.compliance_mx?.shipped_by_header || '',
shipped_by_id: invoice.compliance_mx?.shipped_by_id || null,
customs_broker_id: invoice.compliance_mx?.customs_broker_id || null,
broker_invoice_num: invoice.compliance_mx?.broker_invoice_num || '',
broker_invoice_date: invoice.compliance_mx?.broker_invoice_date || '',
is_mixed: invoice.compliance_mx?.is_mixed || null,
waste_type: invoice.compliance_mx?.waste_type || '',
scrap_type: invoice.compliance_mx?.scrap_type || '',
appendix_17: invoice.compliance_mx?.appendix_17 || null,
is_regime_change: invoice.compliance_mx?.is_regime_change || '',
which_exchange_rate: invoice.compliance_mx?.which_exchange_rate || '',
value_method: invoice.compliance_mx?.value_method || '',
act_value: invoice.compliance_mx?.act_value || '',
is_pedimento_pending: invoice.compliance_mx?.is_pedimento_pending || false,
is_owner_of_goods: invoice.compliance_mx?.is_owner_of_goods || '',
generate_balances: invoice.compliance_mx?.generate_balances || '',
was_reviewed_by_company: invoice.compliance_mx?.was_reviewed_by_company || false,
edocument: invoice.compliance_mx?.edocument || '',
electronic_signature: invoice.compliance_mx?.electronic_signature || '',
certificate_number: invoice.compliance_mx?.certificate_number || '',
niu_number: invoice.compliance_mx?.niu_number || '',
bill_of_lading_count: invoice.compliance_mx?.bill_of_lading_count || '',
addendum_vu: invoice.compliance_mx?.addendum_vu || '',
origin_destination_cove: invoice.compliance_mx?.origin_destination_cove || '',
vucem_operation_num: invoice.compliance_mx?.vucem_operation_num || '',
customs_person_line: invoice.compliance_mx?.customs_person_line || null,
contingency_mode: invoice.compliance_mx?.contingency_mode || false,
enclosure: invoice.compliance_mx?.enclosure || '',
guide_type_to_identify: invoice.compliance_mx?.guide_type_to_identify || '',
location: invoice.compliance_mx?.location || '',
dot_code: invoice.compliance_mx?.dot_code || '',
subdivision: invoice.compliance_mx?.subdivision || '',
acts_as: invoice.compliance_mx?.acts_as || '',
movement_type: invoice.compliance_mx?.movement_type || '',
office_document: invoice.compliance_mx?.office_document || '',
reason_export: invoice.compliance_mx?.reason_export || '',
signature_key: invoice.compliance_mx?.signature_key || '',
sem_id: invoice.compliance_mx?.sem_id || null,
// Financials fields (incrementables)
freight: invoice.financials?.freight || null,
insurance_value: invoice.financials?.insurance_value || null,
insurance: invoice.financials?.insurance || null,
packaging: invoice.financials?.packaging || null,
other_increments: invoice.financials?.other_increments || null,
total_increments_mn: invoice.financials?.total_increments_mn || null,
total_increments_me: invoice.financials?.total_increments_me || null,
// Logistics fields
incoterm: invoice.logistics?.[0]?.incoterm || ''
};
exists = true;
} else if (!formData) {
formData = {
// Invoice header fields
observation_es: '',
observation_en: '',
alternate_invoice: '',
// Compliance MX fields
pedimento: '',
pedimento_code: '',
pedimento_k1: '',
remesa: null,
aduana: '',
port_of_entry: '',
destination: '',
manifest_number: '',
provider_header: '',
provider_id: null,
sold_to_header: '',
sold_to_id: null,
shipped_to_header: '',
shipped_to_id: null,
shipped_by_header: '',
shipped_by_id: null,
customs_broker_id: null,
broker_invoice_num: '',
broker_invoice_date: '',
is_mixed: null,
waste_type: '',
scrap_type: '',
appendix_17: null,
is_regime_change: '',
which_exchange_rate: '',
value_method: '',
act_value: '',
is_pedimento_pending: false,
is_owner_of_goods: '',
generate_balances: '',
was_reviewed_by_company: false,
edocument: '',
electronic_signature: '',
certificate_number: '',
niu_number: '',
bill_of_lading_count: '',
addendum_vu: '',
origin_destination_cove: '',
vucem_operation_num: '',
customs_person_line: null,
contingency_mode: false,
enclosure: '',
guide_type_to_identify: '',
location: '',
dot_code: '',
subdivision: '',
acts_as: '',
movement_type: '',
office_document: '',
reason_export: '',
signature_key: '',
sem_id: null,
// Financials fields
freight: null,
insurance_value: null,
insurance: null,
packaging: null,
other_increments: null,
total_increments_mn: null,
total_increments_me: null,
// Logistics fields
incoterm: ''
};
exists = false;
}
</script>
<div class="grid grid-cols-2 grid-rows-5 gap-4">
<div class="border rounded-md p-3 space-y-3 row-span-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Observacion de la factura mexicana y bilingue:</h4>
<div class="space-y-1">
<Textarea
id="observation_es"
bind:value={formData.observation_es}
class="min-h-[150px] max-h-[150px] text-sm font-medium"
placeholder="Escribe tus observaciones aqui."
/>
</div>
</div>
<div class="border rounded-md p-3 space-y-3 row-span-2 col-start-1 row-start-3">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Observacion de la factura americana:</h4>
<Textarea
id="observation_en"
bind:value={formData.observation_en}
class="min-h-[150px] max-h-[150px] text-sm font-medium"
placeholder="Escribe tus observaciones aqui."
/>
</div>
<div class="border rounded-md p-3 space-y-3 col-span-2 col-start-1 row-start-5">
<div class="grid grid-cols-4 gap-3">
<div class="space-y-1">
<Label for="num_seals" class="text-xs">Num Precintos:</Label>
<Select.Root
type="single"
value={formData.num_seals ? String(formData.num_seals) : ''}
onValueChange={(v) => {
formData.num_seals = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="num_seals" class="h-7 text-xs">
<span class="truncate">
{formData.num_seals
? seals.find(p => p.id === formData.num_seals)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each seals as seal}
<Select.Item value={String(seal.id)}>
{seal.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="movement_type" class="text-xs">Tipo Movimiento:</Label>
<Input
id="movement_type"
bind:value={formData.movement_type}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="alternate_invoice" class="text-xs">Factura Alterna:</Label>
<Input
id="alternate_invoice"
bind:value={formData.alternate_invoice}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="valuation_method" class="text-xs">Met. Valoracion:</Label>
<Select.Root
type="single"
value={formData.valuation_method ? String(formData.valuation_method) : ''}
onValueChange={(v) => {
formData.valuation_method = v;
}}
>
<Select.Trigger id="valuation_method" class="h-7 text-xs">
<span class="truncate">
{formData.valuation_method || 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value="general">General</Select.Item>
<Select.Item value="devalued">Devaluado</Select.Item>
<Select.Item value="special">Especial</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
<div class="border rounded-md p-3 space-y-3 row-span-3 col-start-2 row-start-1">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Incrementables:</h4>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label for="freight" class="text-xs">Flete:</Label>
</div>
<div class="space-y-1">
<Input
id="freight"
type="number"
bind:value={formData.freight}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="insurance_value" class="text-xs">Val. Seguros:</Label>
</div>
<div class="space-y-1">
<Input
id="insurance_value"
type="number"
bind:value={formData.insurance_value}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="insurance" class="text-xs">Seguros:</Label>
</div>
<div class="space-y-1">
<Input
id="insurance"
type="number"
bind:value={formData.insurance}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="packaging" class="text-xs">Embalajes:</Label>
</div>
<div class="space-y-1">
<Input
id="packaging"
type="number"
bind:value={formData.packaging}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="other_increments" class="text-xs">Otros incrementables:</Label>
</div>
<div class="space-y-1">
<Input
id="other_increments"
type="number"
bind:value={formData.other_increments}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="total_increments_mn" class="text-xs">Total Incrementables:</Label>
</div>
<div class="space-y-1">
<Input
id="total_increments_mn"
type="number"
bind:value={formData.total_increments_mn}
class="h-8 text-sm font-medium"
disabled
/>
</div>
<div class="space-y-1">
</div>
<div class="space-y-1">
<Input
id="total_increments_me"
type="number"
bind:value={formData.total_increments_me}
class="h-8 text-sm font-medium"
disabled
/>
</div>
</div>
</div>
<div class="border rounded-md p-3 space-y-3 col-start-2 row-start-4">
<div class="grid grid-cols-2 gap-3">
<div class="space-y-1">
<Label for="incoterm" class="text-xs">Incoterm:</Label>
<Select.Root
type="single"
value={formData.incoterm ? String(formData.incoterm) : ''}
onValueChange={(v) => {
formData.incoterm = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="incoterm" class="h-7 text-xs">
<span class="truncate">
{formData.incoterm
? incoterms.find(p => p.id === formData.incoterm)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each incoterms as inco}
<Select.Item value={String(inco.id)}>
{inco.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="enclosure" class="text-xs">Recinto:</Label>
<Select.Root
type="single"
value={formData.enclosure ? String(formData.enclosure) : ''}
onValueChange={(v) => {
formData.enclosure = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="enclosure" class="h-7 text-xs">
<span class="truncate">
{formData.enclosure
? enclosure.find(p => p.id === formData.enclosure)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each enclosure as rec}
<Select.Item value={String(rec.id)}>
{rec.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,285 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group';
import * as Select from '$lib/components/ui/select';
import { Textarea } from '$lib/components/ui/textarea';
import { Plus, Upload } from 'lucide-svelte';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
let {
invoice,
formData = $bindable(),
exists = $bindable()
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
} = $props();
if (!formData && invoice?.logistics && invoice.logistics.length > 0) {
formData = invoice.logistics.map(l => ({
// Carrier info
carrier_id: l.carrier_id || '',
transport_id: l.transport_id || '',
transport_us_id: l.transport_us_id || '',
transport_type: l.transport_type || null,
transport_num: l.transport_num || '',
transport_mode: l.transport_mode || '',
driver_name: l.driver_name || '',
is_rail: l.is_rail || '',
rail_id: l.rail_id || '',
// Vehicle & tracking
vehicle_num: l.vehicle_num || '',
license_plate: l.license_plate || '',
license_plate_complete: l.license_plate_complete || '',
trailer_num: l.trailer_num || '',
seal_number: l.seal_number || '',
guide_number: l.guide_number || '',
bill_number: l.bill_number || '',
reference_number: l.reference_number || '',
shipment_number: l.shipment_number || '',
// Incoterms
incoterm: l.incoterm || '',
// Identifiers
identifier_1: l.identifier_1 || '',
complement_1: l.complement_1 || '',
identifier_2: l.identifier_2 || '',
complement_2: l.complement_2 || '',
// Weight & container
weight_type: l.weight_type || '',
container_types: l.container_types || '',
vehicle_data: l.vehicle_data || '',
// Locations
origin_location: l.origin_location || '',
destination_location: l.destination_location || '',
transport_itinerary: l.transport_itinerary || '',
destination_goods: l.destination_goods || '',
// Dates
entry_exit_date: l.entry_exit_date || '',
delivery_date: l.delivery_date || '',
// Delivery control
delivered_status: l.delivered_status || '',
received_by: l.received_by || '',
// Payment
payment_date: l.payment_date || '',
payment_receipt_num: l.payment_receipt_num || '',
// CTM
is_ctm_process: l.is_ctm_process || ''
}));
exists = true;
} else if (!formData) {
formData = [];
exists = false;
}
// Campos adicionales que van en otros recursos
let transportMode = $state('TRUCK');
// is_mixed va en compliance_mx
let isMixed = $state(invoice?.compliance_mx?.is_mixed ? 'yes' : 'no');
// related_doc_id va en invoice header
let relationDocsId = $state(invoice?.related_doc_id?.toString() || '0');
// electronic_signature va en compliance_mx
let code_signature = $state(invoice?.compliance_mx?.code_signature || '');
let electronicSignature = $state(invoice?.compliance_mx?.electronic_signature || '');
// Estos campos no existen en el schema del backend
let mandatoryPerson = $state('0');
let rfc = $state('');
let contingencyMode = $state(invoice?.compliance_mx?.contingency_mode || false);
let curp = $state('');
let rule3121PartiesII = $state<boolean>(false);
// origin_destination_cove va en compliance_mx
let cove = $state(invoice?.compliance_mx?.origin_destination_cove || '');
// vucem_operation_num va en compliance_mx
let operationNum = $state(invoice?.compliance_mx?.vucem_operation_num || '');
// addendum_vu va en compliance_mx
let adendas = $state(invoice?.compliance_mx?.addendum_vu || '');
// vu_observations va en invoice header
let observationsVU = $state(invoice?.vu_observations || '');
// certificate_number va en compliance_mx
let certifiedNumber = $state(invoice?.compliance_mx?.certificate_number || '');
// seal_value_2500 va en financials
let printStamp = $state(invoice?.financials?.seal_value_2500 || false);
// comments_status va en invoice header
let commentsStatus = $state(invoice?.comments_status || '');
const transportModes = [
{ value: 'TRUCK', label: 'Camión' },
{ value: 'TRAIN', label: 'Tren' },
{ value: 'SHIP', label: 'Marítimo' },
{ value: 'AIR', label: 'Aéreo' },
{ value: 'OTHER', label: 'Otro' }
];
function addLogistic() {
formData = [...formData, {
carrier_id: '',
transport_type: null,
driver_name: '',
vehicle_num: '',
license_plate: ''
}];
}
function loadInfo() {
// Función para cargar información
console.log('Cargar información');
}
</script>
<div class="grid grid-cols-3 grid-rows-1 gap-3">
<div class="border rounded-md p-3 space-y-3">
<!-- Modo de Transporte -->
<div class="space-y-2">
<Label for="transport-mode">Modo de Transporte:</Label>
<Select.Root type="single" value={transportMode} onValueChange={(value: string | undefined) => transportMode = value || 'TRUCK'}>
<Select.Trigger id="transport-mode">
{transportModes.find(m => m.value === transportMode)?.label || 'Seleccionar modo'}
</Select.Trigger>
<Select.Content>
{#each transportModes as mode}
<Select.Item value={mode.value}>
{mode.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<!-- Imprimir Sello -->
<div class="space-y-2">
<Label class="opacity-0">Spacer</Label>
<div class="flex items-center space-x-2">
<Checkbox id="print-stamp" bind:checked={printStamp} />
<Label for="print-stamp" class="font-normal">
Imprimir el Sello por Valor menor a 2500 dlls
</Label>
</div>
</div>
<!-- Es Mixto -->
<div class="space-y-2">
<Label class="opacity-0">Spacer</Label>
<Label>Es Mixto?</Label>
<RadioGroup bind:value={isMixed} class="flex gap-4">
<div class="flex items-center space-x-2">
<RadioGroupItem value="yes" id="mixed-yes" />
<Label for="mixed-yes" class="font-normal"></Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem value="no" id="mixed-no" />
<Label for="mixed-no" class="font-normal">No</Label>
</div>
</RadioGroup>
<Label class="opacity-0">Spacer</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox id="rule-3121" bind:checked={rule3121PartiesII} />
<Label for="rule-3121" class="font-normal">Regla 3.1.21 Partes II</Label>
</div>
<!-- Comentario estatus -->
<div class="space-y-2">
<Label class="opacity-0">Spacer</Label>
<Label>Comentario Estatus:</Label>
<Textarea
id="description_es"
bind:value={formData.description_es}
placeholder="Comentario estatus"
rows={3}
/>
</div>
</div>
<div class="border rounded-md p-3 space-y-3 col-span-2">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- ID Relación Docs -->
<div class="space-y-2">
<Label for="relation-docs-id">ID Relación Docs:</Label>
<Input id="relation-docs-id" bind:value={relationDocsId} />
</div>
<!-- Firma Electrónica -->
<div class="space-y-2 md:col-span-2">
<Label for="electronic-sig-1">Firma Electrónica:</Label>
<div class="flex gap-2">
<Input id="electronic-sig-1" bind:value={code_signature} class="flex-1" />
<Button variant="outline" size="icon">
<Upload class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Mandatario/Persona Autorizada -->
<div class="space-y-2">
<Label for="mandatory-person">Mandatario/Persona Autorizada:</Label>
<Input id="mandatory-person" bind:value={mandatoryPerson} />
</div>
<!-- RFC -->
<div class="space-y-2">
<Label id="rfc" for="rfc">RFC: {rfc}</Label>
</div>
<!-- CURP -->
<div class="space-y-2">
<Label id="curp" for="curp">CURP: {curp}</Label>
</div>
<!-- Modo Contingencia -->
<div class="space-y-2 col-span-4">
<div class="flex items-center space-x-2">
<Checkbox id="contingency-mode" bind:checked={contingencyMode} />
<Label for="contingency-mode" class="font-normal">Modo Contingencia</Label>
</div>
<!-- COVE -->
<div class="space-y-2">
<Label for="cove">COVE:</Label>
<Input id="cove" bind:value={cove} placeholder="COVE" />
</div>
<!-- Número de Operación -->
<div class="space-y-2">
<Label for="operation-num">Núm Operación:</Label>
<Input id="operation-num" bind:value={operationNum} />
</div>
<!-- Adenda(s) -->
<div class="space-y-2 md:col-span-2">
<Label for="adendas">Adenda(s):</Label>
<Input id="adendas" bind:value={adendas} />
</div>
<!-- Observaciones VU -->
<div class="space-y-2 md:col-span-2">
<Label for="observations-vu">Observaciones VU:</Label>
<div class="flex gap-2">
<Textarea id="observations-vu" bind:value={observationsVU} class="flex-1 min-h-[60px]" />
<Button variant="outline" onclick={loadInfo}>
Cargar Info.
</Button>
</div>
</div>
<!-- Número Certificado -->
<div class="space-y-2">
<Label for="certified-num">Número Certificado:</Label>
<Input id="certified-num" bind:value={certifiedNumber} />
</div>
<!-- Firma Electrónica 2 -->
<div class="space-y-2 md:col-span-2">
<Label for="electronic-sig-2">Firma Electrónica:</Label>
<Input id="electronic-sig-2" bind:value={electronicSignature} />
</div>
</div>
</div>
</div>
</div>

View File

@@ -323,23 +323,19 @@ export function getSidebarData(): SidebarData {
items: [
{
title: m["sidebar.import_invoices.temporary"](),
//url: "/dashboard/invoices?operation_type=imp&invoice_type=TEM"
url: "/dashboard/invoices/importacion/temporal",
url: "/dashboard/invoices?operation_type=imp&invoice_type=TEM"
},
{
title: m["sidebar.import_invoices.definitive"](),
//url: "/dashboard/invoices?operation_type=imp&invoice_type=DEF",
url: "/dashboard/invoices/importacion/definitiva",
url: "/dashboard/invoices?operation_type=imp&invoice_type=DEF",
},
{
title: m["sidebar.import_invoices.mexican_purchases"](),
//url: "/dashboard/invoices?operation_type=imp&invoice_type=MEX",
url: "/dashboard/invoices/importacion/compras_mexicanas",
url: "/dashboard/invoices?operation_type=imp&invoice_type=MEX",
},
{
title: m["sidebar.import_invoices.regime_change"](),
//url: "/dashboard/invoices?operation_type=imp&invoice_type=CR",
url: "/dashboard/invoices/importacion/cambio_regimen",
url: "/dashboard/invoices?operation_type=imp&invoice_type=CR",
}
],
},
@@ -350,13 +346,11 @@ export function getSidebarData(): SidebarData {
items: [
{
title: m["sidebar.export_invoices.exportation"](),
//url: "/dashboard/invoices?operation_type=exp",
url: "/dashboard/invoices/exportacion/exportacion",
url: "/dashboard/invoices?operation_type=exp",
},
{
title: m["sidebar.export_invoices.repair"](),
//url: "/dashboard/invoices?operation_type=exp&invoice_type=REPAR",
url: "/dashboard/invoices/exportacion/reparacion",
url: "/dashboard/invoices?operation_type=exp&invoice_type=REPAR",
},
],
},

View File

@@ -0,0 +1,10 @@
import Root from "./radio-group.svelte";
import Item from "./radio-group-item.svelte";
export {
Root,
Item,
//
Root as RadioGroup,
Item as RadioGroupItem,
};

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
</script>
<RadioGroupPrimitive.Item
bind:ref
data-slot="radio-group-item"
class={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<div data-slot="radio-group-indicator" class="relative flex items-center justify-center">
{#if checked}
<CircleIcon
class="fill-primary absolute start-1/2 top-1/2 size-2 -translate-x-1/2 -translate-y-1/2"
/>
{/if}
</div>
{/snippet}
</RadioGroupPrimitive.Item>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value = $bindable(""),
...restProps
}: RadioGroupPrimitive.RootProps = $props();
</script>
<RadioGroupPrimitive.Root
bind:ref
bind:value
data-slot="radio-group"
class={cn("grid gap-3", className)}
{...restProps}
/>

View File

@@ -1,4 +1,4 @@
import type { PageServerLoad } from '../../$types';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import {
getAuthTokens,
@@ -41,36 +41,45 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
companies: parentData.companies || []
};
}
// Obtener filtro de tipo de operación
const operationType = 'imp'
const invoiceType = 'DEF'
// Construir parámetros de consulta
// Los query parameters invoice_type y operation_type se pasan al endpoint
const params = new URLSearchParams({
company_id: companyId.toString(),
page: '1',
page_size: '50'
});
// Agregar filtro de tipo si existe y no es 'all'
if (operationType && operationType !== 'all') {
params.append('operation_type', operationType);
}
// Agregar filtro de invoice_type si existe
if (invoiceType) {
params.append('invoice_type', invoiceType);
}
// Agregar filtros opcionales desde query parameters
const invoiceType = url.searchParams.get('invoice_type');
const operationType = url.searchParams.get('operation_type');
const invoiceNumber = url.searchParams.get('invoice_number');
const projectNumber = url.searchParams.get('project_number');
const year = url.searchParams.get('year');
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/invoices?${params.toString()}`,
{},
cookies,
fetch,
'/login'
);
if (invoiceType) params.append('invoice_type', invoiceType);
if (operationType) params.append('operation_type', operationType);
if (invoiceNumber) params.append('invoice_number', invoiceNumber);
if (projectNumber) params.append('project_number', projectNumber);
if (year) params.append('year', year);
// Cargar facturas e invoice types en paralelo
const [response, invoiceTypesResponse] = await Promise.all([
authenticatedFetch(
`v1/a76/invoices?${params.toString()}`,
{},
cookies,
fetch,
'/login'
),
authenticatedFetch(
'v1/public/refrence_data/invoice-types?page=1&page_size=100',
{},
cookies,
fetch,
'/login'
)
]);
if (!response.ok) {
return {
@@ -81,12 +90,15 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
error: 'Error al cargar facturas',
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
// Preservar los filtros aplicados
filters: {
invoice_type: invoiceType,
}
};
}
const data = await response.json();
const invoiceTypesData = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
return {
items: data.items || [],
@@ -95,8 +107,15 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
page_size: data.page_size || 50,
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
invoiceTypes: invoiceTypesData.items || [],
// Preservar los filtros aplicados
filters: {
invoice_type: invoiceType,
operation_type: operationType,
invoice_number: invoiceNumber,
project_number: projectNumber,
year: year
}
};
} catch (error) {
console.error('Error loading invoices:', error);
@@ -106,9 +125,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
operationType: 'all',
invoiceType: null
companies: parentData.companies || []
};
}
};

View File

@@ -0,0 +1,496 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { invoicesApi, type Invoice, type OperationType } from '$lib/api/dashboard/a76/invoices';
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
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';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para filtros
// Nota: Los query parameters invoice_type y operation_type se pueden usar para filtrar
// Ejemplo: /dashboard/invoices?invoice_type=TEM&operation_type=imp
let filters = $state({
operation_type: (data.filters?.operation_type || '') as '' | OperationType,
invoice_type: data.filters?.invoice_type || '',
invoice_number: data.filters?.invoice_number || '',
project_number: data.filters?.project_number || '',
year: data.filters?.year || ''
});
// Efecto reactivo para actualizar filtros cuando cambian los query parameters en la URL
$effect(() => {
if (browser) {
const searchParams = $page.url.searchParams;
const urlOperationType = searchParams.get('operation_type');
const urlInvoiceType = searchParams.get('invoice_type');
const urlInvoiceNumber = searchParams.get('invoice_number');
const urlProjectNumber = searchParams.get('project_number');
const urlYear = searchParams.get('year');
// Actualizar filtros si hay cambios en la URL
filters.operation_type = (urlOperationType || '') as '' | OperationType;
filters.invoice_type = urlInvoiceType || '';
filters.invoice_number = urlInvoiceNumber || '';
filters.project_number = urlProjectNumber || '';
filters.year = urlYear || '';
}
});
// Efecto para limpiar invoice_type si no es válido para el operation_type seleccionado
$effect(() => {
if (filters.invoice_type && filters.operation_type) {
const selectedOption = allInvoiceTypeOptions().find(opt => opt.value === filters.invoice_type);
if (selectedOption && selectedOption.operation !== 'both' && selectedOption.operation !== filters.operation_type) {
// El tipo de factura seleccionado no es válido para esta operación
filters.invoice_type = '';
}
}
});
// Efecto para actualizar la URL cuando cambien los filtros
$effect(() => {
if (browser) {
const params = new URLSearchParams();
if (filters.operation_type) params.set('operation_type', filters.operation_type);
if (filters.invoice_type) params.set('invoice_type', filters.invoice_type);
if (filters.invoice_number) params.set('invoice_number', filters.invoice_number);
if (filters.project_number) params.set('project_number', filters.project_number);
if (filters.year) params.set('year', filters.year);
const queryString = params.toString();
const newUrl = queryString ? `?${queryString}` : window.location.pathname;
// Solo actualizar si la URL es diferente (evitar loops infinitos)
if (window.location.search !== (queryString ? `?${queryString}` : '')) {
window.history.replaceState({}, '', newUrl);
}
}
});
// Efecto para aplicar filtros automáticamente cuando cambian
let filterTimeout: ReturnType<typeof setTimeout> | null = null;
$effect(() => {
// Observar cambios en los filtros
const _ = {
operation_type: filters.operation_type,
invoice_type: filters.invoice_type,
invoice_number: filters.invoice_number,
project_number: filters.project_number,
year: filters.year
};
// Debounce para evitar múltiples llamadas rápidas
if (filterTimeout) clearTimeout(filterTimeout);
filterTimeout = setTimeout(() => {
applyFilters();
}, 300); // Esperar 300ms después del último cambio
});
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
// Recargar los datos sin recargar la página completa
reloadData();
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
// Cleanup
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
};
}
});
// Estado para infinite scroll
let allItems = $state<Invoice[]>(data.items || []);
let currentPage = $state(data.page);
let pageSize = $state(50);
let totalItems = $state(data.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
error = null;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada';
return;
}
const filterParams = {
operation_type: filters.operation_type || undefined,
invoice_type: filters.invoice_type || undefined,
invoice_number: filters.invoice_number || undefined,
project_number: filters.project_number || undefined,
year: filters.year || undefined
};
const response = await invoicesApi.list(companyId, currentPage + 1, pageSize, filterParams);
if (response.error) {
console.error('📊 [Invoices] Error en loadMore:', response.error, 'Status:', response.status);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Agregar los nuevos items al array existente
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Invoices] Error loading more:', e);
} finally {
loading = false;
}
}
async function applyFilters() {
// Reset y recargar con filtros
loading = true;
error = null;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada';
return;
}
// Construir query parameters para el endpoint
// Los filtros se mapean a los parámetros del API
const filterParams = {
operation_type: filters.operation_type || undefined,
invoice_type: filters.invoice_type || undefined,
invoice_number: filters.invoice_number || undefined,
project_number: filters.project_number || undefined,
year: filters.year || undefined
};
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
if (response.error) {
console.error('📊 [Invoices] Error aplicando filtros:', response.error);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error aplicando filtros';
console.error('📊 [Invoices] Error applying filters:', e);
} finally {
loading = false;
}
}
function clearFilters() {
filters = {
operation_type: '',
invoice_type: '',
invoice_number: '',
project_number: '',
year: ''
};
applyFilters();
}
async function reloadData() {
// Reset y recargar desde el principio usando la API
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const companyId = companyStore.activeCompany.id;
const filterParams = {
operation_type: filters.operation_type || undefined,
invoice_type: filters.invoice_type || undefined,
invoice_number: filters.invoice_number || undefined,
project_number: filters.project_number || undefined,
year: filters.year || undefined
};
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
if (response.error) {
console.error('📊 [Invoices] Error en reloadData:', response.error);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
// Reemplazar todos los items con los nuevos datos
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error recargando datos';
console.error('📊 [Invoices] Error reloading:', e);
} finally {
loading = false;
}
}
function handleCreateClick() {
// Leer los filtros actuales desde la URL (que ya se actualizó con el $effect)
const params = new URLSearchParams(window.location.search);
// Mapear operation_type de 'imp'/'exp' a números 1/2
const operationType = params.get('operation_type');
if (operationType) {
const operationTypeNumber = operationType === 'exp' ? 1 : 2;
params.set('operation_type', operationTypeNumber.toString());
}
const queryString = params.toString();
const url = queryString
? `/dashboard/invoices/edit/new?${queryString}`
: '/dashboard/invoices/edit/new';
window.location.href = url;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Opciones de tipo de operación para el filtro
const operationTypeOptions = [
{ value: "", label: "Todas" },
{ value: 'imp', label: 'Importación' },
{ value: 'exp', label: 'Exportación' }
];
// Todas las opciones de tipo de factura con su operación correspondiente
// Ahora se cargan desde el servidor en lugar de estar hardcodeadas
const allInvoiceTypeOptions = $derived(() => {
const options = [{ value: "", label: "Todas", operation: "both" }];
// Agregar los tipos de factura del servidor
if (data.invoiceTypes) {
data.invoiceTypes.forEach((type: any) => {
options.push({
value: type.key,
label: type.description,
operation: type.operation
});
});
}
return options;
});
// Opciones de tipo de factura filtradas según el tipo de operación seleccionado
const invoiceTypeOptions = $derived(() => {
const allOptions = allInvoiceTypeOptions();
if (!filters.operation_type) {
return allOptions;
}
return allOptions.filter(option =>
option.operation === 'both' ||
option.operation === filters.operation_type
);
});
// Crear columnas con el callback onSuccess
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
<p class="text-muted-foreground">
Gestiona las facturas del sistema
</p>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nueva Factura
</Button>
</div>
<!-- Filtros -->
<Card.Root>
<Card.Header>
<Card.Title>Filtros</Card.Title>
<Card.Description>Filtra las facturas por diferentes criterios (los filtros se aplican automáticamente)</Card.Description>
</Card.Header>
<Card.Content>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div class="space-y-2">
<Label for="filter-operation-type">Tipo de Operación</Label>
<select
id="filter-operation-type"
bind:value={filters.operation_type}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{#each operationTypeOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<div class="space-y-2">
<Label for="filter-invoice-type">Tipo de Factura</Label>
<select
id="filter-invoice-type"
bind:value={filters.invoice_type}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{#each invoiceTypeOptions() as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<div class="space-y-2">
<Label for="filter-invoice-number">Número de Factura</Label>
<Input
id="filter-invoice-number"
bind:value={filters.invoice_number}
placeholder="Ej: INV-2024-001"
/>
</div>
<div class="space-y-2">
<Label for="filter-project-number">Número de Proyecto</Label>
<Input
id="filter-project-number"
bind:value={filters.project_number}
placeholder="Ej: PROJ-001"
/>
</div>
<div class="space-y-2">
<Label for="filter-year">Año</Label>
<Input
id="filter-year"
bind:value={filters.year}
placeholder="Ej: 2024"
maxlength={4}
/>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Facturas</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
</div>

View File

@@ -0,0 +1,244 @@
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
// Obtener el company_id de la cookie
const companyId = await getActiveCompanyId(cookies, fetch);
if (!companyId) {
throw error(400, 'No se encontró una compañía seleccionada');
}
// Leer filtros de query parameters para preseleccionar en creación
const operationTypeParam = url.searchParams.get('operation_type');
const invoiceTypeParam = url.searchParams.get('invoice_type');
// Parsear operation_type de forma segura
let parsedOperationType: number | null = null;
if (operationTypeParam) {
const parsed = parseInt(operationTypeParam, 10);
if (!isNaN(parsed)) {
parsedOperationType = parsed;
}
}
// Cargar datos de referencia necesarios
const invoiceTypesPromise = authenticatedFetch(
'v1/public/refrence_data/invoice-types?page=1&page_size=100',
{},
cookies,
fetch
);
const customsBrokersPromise = authenticatedFetch(
`v1/a76/customs-brokers?company_id=${companyId}&page=1&page_size=1000`,
{},
cookies,
fetch
);
// Cargar clientes y proveedores
const clientsPromise = authenticatedFetch(
`v1/a76/clients-providers?company_id=${companyId}&type=client&page=1&page_size=1000`,
{},
cookies,
fetch
);
const providersPromise = authenticatedFetch(
`v1/a76/clients-providers?company_id=${companyId}&type=provider&page=1&page_size=1000`,
{},
cookies,
fetch
);
// Cargar tipos de moneda, transporte, etc.
const currencyTypesPromise = authenticatedFetch(
'v1/public/refrence_data/currency-types?page=1&page_size=100',
{},
cookies,
fetch
);
const transportTypesPromise = authenticatedFetch(
'v1/public/refrence_data/transport-types?page=1&page_size=100',
{},
cookies,
fetch
);
const sealsPromise = authenticatedFetch(
'v1/public/refrence_data/seals?page=1&page_size=100',
{},
cookies,
fetch
);
const incotermsPromise = authenticatedFetch(
'v1/public/refrence_data/incoterms?page=1&page_size=100',
{},
cookies,
fetch
);
const enclosurePromise = authenticatedFetch(
'v1/public/refrence_data/enclosure?page=1&page_size=100',
{},
cookies,
fetch
);
const pedimentosPromise = authenticatedFetch(
`v1/a76/pedimentos?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
// Si el ID es "new", es una creación
if (params.id === 'new') {
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
sealsResponse,
incotermsResponse,
enclosureResponse,
pedimentosResponse
] = await Promise.all([
invoiceTypesPromise,
customsBrokersPromise,
clientsPromise,
providersPromise,
currencyTypesPromise,
transportTypesPromise,
sealsPromise,
incotermsPromise,
enclosurePromise,
pedimentosPromise
]);
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const enclosure = enclosureResponse.ok ? await enclosureResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
return {
invoice: null,
invoiceId: null,
isCreate: true,
invoiceTypes: invoiceTypes.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
enclosure: enclosure.items || [],
pedimentos: pedimentos.items || [],
// Filtros desde query parameters para preselección
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
}
const invoiceId = parseInt(params.id);
if (isNaN(invoiceId)) {
throw error(400, 'ID de factura inválido');
}
try {
// Cargar la factura desde el backend
const response = await authenticatedFetch(
`v1/a76/invoices/${invoiceId}?company_id=${companyId}`,
{},
cookies,
fetch
);
if (!response.ok) {
throw error(response.status, 'Error al cargar la factura');
}
const invoice = await response.json();
// Cargar también los datos de referencia para edición
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
sealsResponse,
incotermsResponse,
enclosureResponse,
pedimentosResponse
] = await Promise.all([
invoiceTypesPromise,
customsBrokersPromise,
clientsPromise,
providersPromise,
currencyTypesPromise,
transportTypesPromise,
sealsPromise,
incotermsPromise,
enclosurePromise,
pedimentosPromise
]);
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const enclosure = enclosureResponse.ok ? await enclosureResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
return {
invoice,
invoiceId,
isCreate: false,
invoiceTypes: invoiceTypes.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
enclosure: enclosure.items || [],
pedimentos: pedimentos.items || [],
// Filtros desde query parameters para preselección
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
} catch (err) {
console.error('Error loading invoice:', err);
throw error(500, 'Error al cargar la factura');
}
};

View File

@@ -0,0 +1,686 @@
<script lang="ts">
import { goto } from '$app/navigation';
import * as Tabs from '$lib/components/ui/tabs';
import * as Alert from '$lib/components/ui/alert';
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import { Separator } from '$lib/components/ui/separator';
import {
ArrowLeft,
CircleAlert,
CircleCheck,
FileText,
DollarSign,
Truck,
Package,
Eye,
LoaderCircle,
Save
} from 'lucide-svelte';
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte';
import { companyStore } from '$lib/stores/company.svelte';
// Importar los componentes de cada pestaña
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
import ObservationsTabForm from '$lib/components/dashboard/invoices/edit/observations-tab-form.svelte';
import ItemsTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte';
import OthersTabForm from '$lib/components/dashboard/invoices/edit/others-tab-form.svelte';
import InvoiceTopFields from '$lib/components/dashboard/invoices/edit/invoice-top-fields.svelte';
import ContinuationTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte';
// Importar la API de facturas
import { invoicesApi, type CreateInvoiceData, type UpdateInvoiceData, type OperationType } from '$lib/api/dashboard/a76/invoices';
import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
// Get sidebar context
const sidebar = useSidebar();
interface ExtendedPageData {
invoiceId?: number | null;
invoice?: any;
isCreate?: boolean;
invoiceTypes?: InvoiceType[];
customsBrokers?: CustomsBroker[];
clients?: ClientProvider[];
providers?: ClientProvider[];
seals?: any[];
incoterms?: any[];
enclosure?: any[];
currencyTypes?: any[];
transportTypes?: any[];
user?: any;
companies?: any[];
authenticated?: boolean;
filters?: {
operation_type?: number | null;
invoice_type?: string | null;
};
pedimentos?: any[];
}
let { data }: { data: ExtendedPageData } = $props();
let activeTab = $state('general');
let saving = $state(false);
let error = $state<string | null>(null);
let success = $state(false);
// ID de la factura
let invoiceId = $state<number | null>(data.invoiceId ?? null);
// Referencias a los componentes de formulario para obtener sus datos
let generalFormData = $state<any>(null);
let observationFormData = $state<any>(null);
let itemsFormData = $state<any>(null);
let othersFormData = $state<any>(null);
// Estados para saber si existen datos previos
let observationExists = $state(false);
let itemsExists = $state(false);
let othersExists = $state(false);
function handleBack() {
goto('/dashboard/invoices');
}
function getOperationColor(type?: string | null): 'default' | 'secondary' {
if (!type) return 'secondary';
return type === 'imp' ? 'default' : 'secondary';
}
async function handleSaveAll() {
saving = true;
error = null;
success = false;
try {
// Validar campos requeridos para creación
if (data.isCreate && generalFormData) {
const requiredFields = {
operation_type: 'Tipo de Operación',
invoice_type: 'Tipo de Factura',
invoice_number: 'Número de Factura'
};
const missingFields: string[] = [];
for (const [field, label] of Object.entries(requiredFields)) {
const value = (generalFormData as any)[field];
if (value === null || value === undefined || value === '') {
missingFields.push(label);
}
}
if (missingFields.length > 0) {
throw new Error(`Los siguientes campos son obligatorios: ${missingFields.join(', ')}`);
}
}
// Construir el payload unificado
const payload: CreateInvoiceData | UpdateInvoiceData = {
// Datos generales desde el formulario general
operation_type: generalFormData?.operation_type !== null && generalFormData?.operation_type !== undefined
? (generalFormData.operation_type === 1 ? 'exp' : 'imp') as OperationType
: undefined,
invoice_type: generalFormData?.invoice_type || undefined,
invoice_number: generalFormData?.invoice_number || undefined,
invoice_date: generalFormData?.invoice_date || undefined,
emission_date: generalFormData?.emission_date || undefined,
// Observation fields from observationFormData
observation_es: observationFormData?.observation_es || undefined,
observation_en: observationFormData?.observation_en || undefined,
alternate_invoice: observationFormData?.alternate_invoice || undefined,
};
// Solo agregar sub-recursos si tienen valores reales
// Compliance MX - combinar datos del formulario general y observations
const hasComplianceValue = generalFormData?.pedimento || generalFormData?.remesa || generalFormData?.aduana ||
generalFormData?.provider_id || generalFormData?.sold_to_id ||
generalFormData?.shipped_to_id || generalFormData?.customs_broker_id ||
observationFormData?.pedimento || observationFormData?.pedimento_code ||
observationFormData?.remesa || observationFormData?.aduana ||
observationFormData?.provider_id || observationFormData?.sold_to_id ||
observationFormData?.shipped_to_id || observationFormData?.shipped_by_id ||
observationFormData?.customs_broker_id || observationFormData?.is_mixed ||
observationFormData?.waste_type || observationFormData?.appendix_17 ||
observationFormData?.edocument || observationFormData?.electronic_signature ||
observationFormData?.sem_id || observationFormData?.enclosure ||
observationFormData?.incoterm;
if (hasComplianceValue) {
payload.compliance_mx = {
// Pedimento fields
pedimento: generalFormData?.pedimento || observationFormData?.pedimento || null,
pedimento_code: observationFormData?.pedimento_code || null,
pedimento_k1: observationFormData?.pedimento_k1 || null,
remesa: generalFormData?.remesa || observationFormData?.remesa || null,
aduana: generalFormData?.aduana || observationFormData?.aduana || null,
port_of_entry: observationFormData?.port_of_entry || null,
destination: observationFormData?.destination || null,
manifest_number: observationFormData?.manifest_number || null,
// Client/Provider fields
provider_header: generalFormData?.provider_header || observationFormData?.provider_header || null,
provider_id: generalFormData?.provider_id || observationFormData?.provider_id || null,
sold_to_header: generalFormData?.sold_to_header || observationFormData?.sold_to_header || null,
sold_to_id: generalFormData?.sold_to_id || observationFormData?.sold_to_id || null,
shipped_to_header: generalFormData?.shipped_to_header || observationFormData?.shipped_to_header || null,
shipped_to_id: generalFormData?.shipped_to_id || observationFormData?.shipped_to_id || null,
shipped_by_header: observationFormData?.shipped_by_header || null,
shipped_by_id: observationFormData?.shipped_by_id || null,
// Customs broker fields
customs_broker_id: generalFormData?.customs_broker_id || observationFormData?.customs_broker_id || null,
customs_broker_us_id: observationFormData?.customs_broker_us_id || null,
broker_invoice_num: observationFormData?.broker_invoice_num || null,
broker_invoice_date: observationFormData?.broker_invoice_date || null,
// Flags & regimes
is_mixed: observationFormData?.is_mixed || null,
waste_type: observationFormData?.waste_type || null,
scrap_type: observationFormData?.scrap_type || null,
appendix_17: observationFormData?.appendix_17 || null,
is_regime_change: observationFormData?.is_regime_change || null,
which_exchange_rate: observationFormData?.which_exchange_rate || null,
value_method: observationFormData?.value_method || null,
act_value: observationFormData?.act_value || null,
is_pedimento_pending: observationFormData?.is_pedimento_pending || null,
// Ownership & balances
is_owner_of_goods: observationFormData?.is_owner_of_goods || null,
generate_balances: observationFormData?.generate_balances || null,
was_reviewed_by_company: observationFormData?.was_reviewed_by_company || null,
// VUCEM / Digital
edocument: observationFormData?.edocument || null,
electronic_signature: observationFormData?.electronic_signature || null,
certificate_number: observationFormData?.certificate_number || null,
niu_number: observationFormData?.niu_number || null,
bill_of_lading_count: observationFormData?.bill_of_lading_count || null,
addendum_vu: observationFormData?.addendum_vu || null,
origin_destination_cove: observationFormData?.origin_destination_cove || null,
vucem_operation_num: observationFormData?.vucem_operation_num || null,
customs_person_line: observationFormData?.customs_person_line || null,
// Additional control
contingency_mode: observationFormData?.contingency_mode || null,
enclosure: observationFormData?.enclosure || null,
guide_type_to_identify: observationFormData?.guide_type_to_identify || null,
location: observationFormData?.location || null,
// DOT & official
dot_code: observationFormData?.dot_code || null,
subdivision: observationFormData?.subdivision || null,
acts_as: observationFormData?.acts_as || null,
movement_type: observationFormData?.movement_type || null,
office_document: observationFormData?.office_document || null,
reason_export: observationFormData?.reason_export || null,
signature_key: observationFormData?.signature_key || null,
// SM specific
sem_id: observationFormData?.sem_id || null,
};
}
// Financials - combinar datos del formulario general y items
const hasFinancialsValue = generalFormData?.currency_type ||
generalFormData?.iva_factor ||
itemsFormData?.currency || itemsFormData?.exchange_rate ||
itemsFormData?.value_mn || itemsFormData?.value_me ||
itemsFormData?.customs_value_mn || itemsFormData?.freight ||
itemsFormData?.insurance;
if (hasFinancialsValue) {
payload.financials = {
// Currency
currency: itemsFormData?.currency || null,
currency_type: generalFormData?.currency_type || itemsFormData?.currency_type || null,
exchange_rate: itemsFormData?.exchange_rate || null,
exchange_rate_mm: itemsFormData?.exchange_rate_mm || null,
// Merchandise values
value_mn: itemsFormData?.value_mn || null,
value_me: itemsFormData?.value_me || null,
value_mc: itemsFormData?.value_mc || null,
// Customs value
customs_value_mn: itemsFormData?.customs_value_mn || null,
customs_value_me: itemsFormData?.customs_value_me || null,
// Raw materials
raw_material_value_mn: itemsFormData?.raw_material_value_mn || null,
raw_material_value_me: itemsFormData?.raw_material_value_me || null,
// Aggregate value
aggregate_value_mn: itemsFormData?.aggregate_value_mn || null,
aggregate_value_me: itemsFormData?.aggregate_value_me || null,
aggregate_value_mc: itemsFormData?.aggregate_value_mc || null,
// Mexican merchandise value
mexican_value_mn: itemsFormData?.mexican_value_mn || null,
mexican_value_me: itemsFormData?.mexican_value_me || null,
mexican_value_mc: itemsFormData?.mexican_value_mc || null,
// National packaging
national_packaging_mn: itemsFormData?.national_packaging_mn || null,
national_packaging_me: itemsFormData?.national_packaging_me || null,
national_packaging_mc: itemsFormData?.national_packaging_mc || null,
// Costs & increments
freight: itemsFormData?.freight || observationFormData?.freight || null,
insurance: itemsFormData?.insurance || observationFormData?.insurance || null,
insurance_value: itemsFormData?.insurance_value || observationFormData?.insurance_value || null,
packaging: itemsFormData?.packaging || observationFormData?.packaging || null,
other_increments: itemsFormData?.other_increments || observationFormData?.other_increments || null,
total_increments_mn: itemsFormData?.total_increments_mn || observationFormData?.total_increments_mn || null,
total_increments_me: itemsFormData?.total_increments_me || observationFormData?.total_increments_me || null,
// Taxes
iva_mn: itemsFormData?.iva_mn || null,
iva_me: itemsFormData?.iva_me || null,
iva_mc: itemsFormData?.iva_mc || null,
iva_factor: generalFormData?.iva_factor || itemsFormData?.iva_factor || null,
tax_value_me: itemsFormData?.tax_value_me || null,
seal_value_2500: itemsFormData?.seal_value_2500 || null,
// Weights & quantities
total_quantity: itemsFormData?.total_quantity || null,
gross_weight: itemsFormData?.gross_weight || null,
net_weight: itemsFormData?.net_weight || null,
bundle_count: itemsFormData?.bundle_count || null,
weight_factor: itemsFormData?.weight_factor || null,
};
}
// Logistics - combinar datos del formulario general con othersFormData
const hasLogisticsFromGeneral = generalFormData?.carrier_id ||
generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num;
if (hasLogisticsFromGeneral || (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0)) {
// Si hay datos en el formulario general, crear/actualizar el primer elemento
if (hasLogisticsFromGeneral) {
const logisticsEntry = {
carrier_id: generalFormData?.carrier_id || null,
transport_type: generalFormData?.transport_type || null,
transport_mode: null,
driver_name: generalFormData?.driver_name || null,
is_rail: null,
rail_id: null,
vehicle_num: generalFormData?.transport_num || null,
license_plate: null,
seal_number: null,
guide_number: null,
entry_exit_date: null,
};
// Si también hay datos del formulario de others, combinarlos
if (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0) {
// Actualizar el primer elemento con datos del general
payload.logistics = [
{
// Carrier info
carrier_id: generalFormData?.carrier_id || othersFormData[0].carrier_id || null,
transport_id: othersFormData[0].transport_id || null,
transport_us_id: othersFormData[0].transport_us_id || null,
transport_type: generalFormData?.transport_type || othersFormData[0].transport_type || null,
transport_num: othersFormData[0].transport_num || null,
transport_mode: othersFormData[0].transport_mode || null,
driver_name: generalFormData?.driver_name || othersFormData[0].driver_name || null,
is_rail: othersFormData[0].is_rail || null,
rail_id: othersFormData[0].rail_id || null,
// Vehicle & tracking
vehicle_num: generalFormData?.transport_num || othersFormData[0].vehicle_num || null,
license_plate: othersFormData[0].license_plate || null,
license_plate_complete: othersFormData[0].license_plate_complete || null,
trailer_num: othersFormData[0].trailer_num || null,
seal_number: othersFormData[0].seal_number || null,
guide_number: othersFormData[0].guide_number || null,
bill_number: othersFormData[0].bill_number || null,
reference_number: othersFormData[0].reference_number || null,
shipment_number: othersFormData[0].shipment_number || null,
// Incoterms
incoterm: othersFormData[0].incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: othersFormData[0].identifier_1 || null,
complement_1: othersFormData[0].complement_1 || null,
identifier_2: othersFormData[0].identifier_2 || null,
complement_2: othersFormData[0].complement_2 || null,
// Weight & container info
weight_type: othersFormData[0].weight_type || null,
container_types: othersFormData[0].container_types || null,
vehicle_data: othersFormData[0].vehicle_data || null,
// Locations & routes
origin_location: othersFormData[0].origin_location || null,
destination_location: othersFormData[0].destination_location || null,
transport_itinerary: othersFormData[0].transport_itinerary || null,
destination_goods: othersFormData[0].destination_goods || null,
// Logistics dates
entry_exit_date: othersFormData[0].entry_exit_date || null,
delivery_date: othersFormData[0].delivery_date || null,
// Delivery control
delivered_status: othersFormData[0].delivered_status || null,
received_by: othersFormData[0].received_by || null,
// Payment info
payment_date: othersFormData[0].payment_date || null,
payment_receipt_num: othersFormData[0].payment_receipt_num || null,
// CTM process
is_ctm_process: othersFormData[0].is_ctm_process || null,
},
// Agregar los demás elementos si existen
...othersFormData.slice(1).map((item: any) => ({
// Carrier info
carrier_id: item.carrier_id || null,
transport_id: item.transport_id || null,
transport_us_id: item.transport_us_id || null,
transport_type: item.transport_type || null,
transport_num: item.transport_num || null,
transport_mode: item.transport_mode || null,
driver_name: item.driver_name || null,
is_rail: item.is_rail || null,
rail_id: item.rail_id || null,
// Vehicle & tracking
vehicle_num: item.vehicle_num || null,
license_plate: item.license_plate || null,
license_plate_complete: item.license_plate_complete || null,
trailer_num: item.trailer_num || null,
seal_number: item.seal_number || null,
guide_number: item.guide_number || null,
bill_number: item.bill_number || null,
reference_number: item.reference_number || null,
shipment_number: item.shipment_number || null,
// Incoterms
incoterm: item.incoterm || null,
// Identifiers & complements
identifier_1: item.identifier_1 || null,
complement_1: item.complement_1 || null,
identifier_2: item.identifier_2 || null,
complement_2: item.complement_2 || null,
// Weight & container info
weight_type: item.weight_type || null,
container_types: item.container_types || null,
vehicle_data: item.vehicle_data || null,
// Locations & routes
origin_location: item.origin_location || null,
destination_location: item.destination_location || null,
transport_itinerary: item.transport_itinerary || null,
destination_goods: item.destination_goods || null,
// Logistics dates
entry_exit_date: item.entry_exit_date || null,
delivery_date: item.delivery_date || null,
// Delivery control
delivered_status: item.delivered_status || null,
received_by: item.received_by || null,
// Payment info
payment_date: item.payment_date || null,
payment_receipt_num: item.payment_receipt_num || null,
// CTM process
is_ctm_process: item.is_ctm_process || null,
}))
];
} else {
// Solo datos del general
payload.logistics = [logisticsEntry];
}
} else {
// Solo datos del formulario others
payload.logistics = othersFormData.map((item: any) => ({
// Carrier info
carrier_id: item.carrier_id || null,
transport_id: item.transport_id || null,
transport_us_id: item.transport_us_id || null,
transport_type: item.transport_type || null,
transport_num: item.transport_num || null,
transport_mode: item.transport_mode || null,
driver_name: item.driver_name || null,
is_rail: item.is_rail || null,
rail_id: item.rail_id || null,
// Vehicle & tracking
vehicle_num: item.vehicle_num || null,
license_plate: item.license_plate || null,
license_plate_complete: item.license_plate_complete || null,
trailer_num: item.trailer_num || null,
seal_number: item.seal_number || null,
guide_number: item.guide_number || null,
bill_number: item.bill_number || null,
reference_number: item.reference_number || null,
shipment_number: item.shipment_number || null,
// Incoterms
incoterm: item.incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: item.identifier_1 || null,
complement_1: item.complement_1 || null,
identifier_2: item.identifier_2 || null,
complement_2: item.complement_2 || null,
// Weight & container info
weight_type: item.weight_type || null,
container_types: item.container_types || null,
vehicle_data: item.vehicle_data || null,
// Locations & routes
origin_location: item.origin_location || null,
destination_location: item.destination_location || null,
transport_itinerary: item.transport_itinerary || null,
destination_goods: item.destination_goods || null,
// Logistics dates
entry_exit_date: item.entry_exit_date || null,
delivery_date: item.delivery_date || null,
// Delivery control
delivered_status: item.delivered_status || null,
received_by: item.received_by || null,
// Payment info
payment_date: item.payment_date || null,
payment_receipt_num: item.payment_receipt_num || null,
// CTM process
is_ctm_process: item.is_ctm_process || null,
}));
}
}
// Eliminar campos undefined para no enviarlos
Object.keys(payload).forEach(key => {
if (payload[key as keyof typeof payload] === undefined) {
delete payload[key as keyof typeof payload];
}
});
let newInvoiceId = invoiceId;
if (data.isCreate) {
// Crear nueva factura con todos sus sub-recursos
const response = await invoicesApi.create(companyStore.activeCompany?.id || 0, payload as CreateInvoiceData);
if (response.error) {
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
throw new Error(errorMsg);
}
if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada');
newInvoiceId = response.data.id;
// Redirigir a la página de edición
await goto(`/dashboard/invoices/edit/${newInvoiceId}`);
return;
} else {
// Actualizar factura existente con todos sus sub-recursos
const response = await invoicesApi.update(invoiceId!, companyStore.activeCompany?.id || 0, payload as UpdateInvoiceData);
if (response.error) throw new Error(response.error);
}
success = true;
setTimeout(() => {
success = false;
}, 3000);
} catch (e) {
if (e instanceof Error && e.message.includes('401')) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
error = e instanceof Error ? e.message : 'Error al guardar los cambios';
}
console.error('Error saving all:', e);
} finally {
saving = false;
}
}
</script>
<div class="space-y-3">
<!-- Header -->
<div class="flex items-center justify-between">
<div class="space-y-1">
<div class="flex items-center gap-3">
<Button variant="ghost" size="icon" onclick={handleBack}>
<ArrowLeft size={20} />
</Button>
<h1 class="text-3xl font-bold tracking-tight">
{#if data.isCreate}
{@const invoiceType = generalFormData?.invoice_type || data.filters?.invoice_type}
{@const invoiceTypeInfo = invoiceType ? data.invoiceTypes?.find(t => t.key === invoiceType) : null}
Nueva Factura
{#if invoiceTypeInfo}
<span class="text-muted-foreground font-normal text-2xl">
- {invoiceTypeInfo.description}
</span>
{/if}
{:else}
Factura #{data.invoice.id}
{/if}
</h1>
{#if data.isCreate}
<Badge variant="default">Nueva</Badge>
{:else if data.invoice.operation_type}
<Badge variant={getOperationColor(data.invoice.operation_type)}>
{data.invoice.operation_type === 'imp' ? 'Importación' : 'Exportación'}
</Badge>
{/if}
</div>
<p class="text-muted-foreground">
{#if !data.isCreate && data.invoice.invoice_number}
Número: {data.invoice.invoice_number}
{:else}
Edita los detalles de la factura
{/if}
</p>
</div>
</div>
<Separator />
<!-- Alertas globales -->
{#if error}
<Alert.Root variant="destructive">
<CircleAlert size={16} />
<Alert.Title>Error</Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Root>
{/if}
{#if success}
<Alert.Root>
<CircleCheck size={16} />
<Alert.Title>Éxito</Alert.Title>
<Alert.Description>Todos los cambios se guardaron correctamente</Alert.Description>
</Alert.Root>
{/if}
<!-- Contenido de las tabs con padding inferior para el footer flotante -->
<div class="pb-48">
<Tabs.Root bind:value={activeTab}>
<InvoiceTopFields
invoice={data.invoice}
bind:formData={generalFormData}
invoiceTypes={data.invoiceTypes || []}
pedimentos={data.pedimentos || []}
defaultOperationType={data.filters?.operation_type ?? undefined}
defaultInvoiceType={data.filters?.invoice_type ?? undefined}
/>
<Tabs.Content value="general">
<GeneralTabForm
invoice={data.invoice}
bind:formData={generalFormData}
invoiceTypes={data.invoiceTypes || []}
customsBrokers={data.customsBrokers || []}
clients={data.clients || []}
providers={data.providers || []}
currencyTypes={data.currencyTypes || []}
transportTypes={data.transportTypes || []}
defaultOperationType={data.filters?.operation_type ?? undefined}
defaultInvoiceType={data.filters?.invoice_type ?? undefined}
/>
</Tabs.Content>
<Tabs.Content value="observations">
<ObservationsTabForm
invoice={data.invoice}
bind:formData={observationFormData}
bind:exists={observationExists}
seals={data.seals || []}
incoterms={data.incoterms || []}
enclosure={data.enclosure || []}
/>
</Tabs.Content>
<Tabs.Content value="items">
<ItemsTabForm
invoice={data.invoice}
bind:formData={itemsFormData}
bind:exists={itemsExists}
/>
</Tabs.Content>
<Tabs.Content value="others">
<OthersTabForm
invoice={data.invoice}
bind:formData={othersFormData}
bind:exists={othersExists}
/>
</Tabs.Content>
<Tabs.Content value="continuation">
<ContinuationTabForm
invoice={data.invoice}
bind:formData={itemsFormData}
bind:exists={itemsExists}
/>
</Tabs.Content>
</Tabs.Root>
</div>
</div>
<!-- Footer fijo en la parte inferior -->
<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] transition-[left] duration-200 ease-linear"
style:left={sidebar.isMobile ? '0' : (sidebar.open ? 'var(--sidebar-width)' : '0')}
>
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
<!-- Tabs Navigation -->
<Tabs.Root bind:value={activeTab}>
<div class="w-full overflow-x-auto pb-2">
<Tabs.List class="inline-flex md:grid md:w-full md:grid-cols-5">
<Tabs.Trigger value="general" disabled={false} class="whitespace-nowrap">
<FileText size={16} class="mr-2" />
General
</Tabs.Trigger>
<Tabs.Trigger value="observations" disabled={false} class="whitespace-nowrap">
<Eye size={16} class="mr-2" />
Observaciones
</Tabs.Trigger>
<Tabs.Trigger value="items" disabled={false} class="whitespace-nowrap">
<DollarSign size={16} class="mr-2" />
Partidas
</Tabs.Trigger>
<Tabs.Trigger value="others" disabled={false} class="whitespace-nowrap">
<Truck size={16} class="mr-2" />
Otros
</Tabs.Trigger>
<Tabs.Trigger value="continuation" disabled={false} class="whitespace-nowrap">
<Package size={16} class="mr-2" />
Cont.
</Tabs.Trigger>
</Tabs.List>
</div>
</Tabs.Root>
<!-- Botones de acción -->
<div class="flex justify-end gap-3">
<Button type="button" variant="outline" onclick={handleBack} disabled={saving}>
Cancelar
</Button>
<Button onclick={handleSaveAll} disabled={saving}>
{#if saving}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando todos los cambios...
{:else}
<Save size={16} class="mr-2" />
Guardar Todos los Cambios
{/if}
</Button>
</div>
</div>
</div>

View File

@@ -1,113 +0,0 @@
import type { PageServerLoad } from '../$types';
import { redirect } from '@sveltejs/kit';
import {
getAuthTokens,
authenticatedFetch
} from '$lib/server/api';
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
// Verificar autenticación
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
try {
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)
// 2. Cookie active_company_id (setted por el team-switcher)
// 3. Primera compañía del usuario (fallback)
const companyIdParam = url.searchParams.get('company_id');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
? parseInt(companyIdParam)
: cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
// Si aún no hay companyId, mostrar error
if (!companyId) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'No se encontró una compañía seleccionada',
companies: parentData.companies || []
};
}
// Obtener filtros de la URL
const operationType = 'exp';
const invoiceType = url.searchParams.get('invoice_type') || '';
// Construir parámetros de consulta
const params = new URLSearchParams({
company_id: companyId.toString(),
page: '1',
page_size: '50'
});
// Agregar filtro de tipo de operación si existe y no es 'all'
if (operationType && operationType !== 'all') {
params.append('operation_type', operationType);
}
// Agregar filtro de invoice_type si existe y no está vacío
if (invoiceType && invoiceType !== '') {
params.append('invoice_type', invoiceType);
}
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/invoices?${params.toString()}`,
{},
cookies,
fetch,
'/login'
);
if (!response.ok) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'exp',
invoiceType: invoiceType || null
};
}
const data = await response.json();
return {
items: data.items || [],
total: data.total || 0,
page: data.page || 1,
page_size: data.page_size || 50,
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'exp',
invoiceType: invoiceType || null
};
} catch (error) {
console.error('Error loading invoices:', error);
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
operationType: 'exp',
invoiceType: null
};
}
};

View File

@@ -1,382 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoicesApi, type Invoice } from '$lib/api/dashboard/a76/invoices';
import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
import CreateEditDialog from '$lib/components/dashboard/invoices/create-edit-dialog.svelte';
import DetailsDialog from '$lib/components/dashboard/invoices/details-dialog.svelte';
import DeleteDialog from '$lib/components/dashboard/invoices/delete-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto, invalidate } from '$app/navigation';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
// Recibir data del servidor
interface PageData {
items: Invoice[];
total: number;
page: number;
page_size: number;
error?: string;
companies: any[];
currentCompanyId?: number;
operationType?: string;
invoiceType?: string | null;
}
let { data }: { data: PageData } = $props();
// Estado para los diálogos
let showCreateDialog = $state(false);
let showDetailsDialog = $state(false);
let showDeleteDialog = $state(false);
let selectedInvoice = $state<Invoice | null>(null);
// Estado para el filtro de tipo (inicializado desde data del servidor)
let selectedType = $state<string>(data.operationType || 'all');
let selectedInvoiceType = $state<string | null>(data.invoiceType || null);
let availableInvoiceTypes = $state<InvoiceType[]>([]);
let loadingInvoiceTypes = $state(false);
// Actualizar URL cuando cambia el filtro
function handleTypeChange(value: string) {
selectedType = value;
const url = new URL(window.location.href);
if (value === 'all') {
url.searchParams.delete('operation_type');
url.searchParams.delete('invoice_type');
selectedInvoiceType = null;
} else {
url.searchParams.set('operation_type', value);
// Mantener invoice_type si existe
if (selectedInvoiceType) {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Actualizar URL cuando cambia el filtro de invoice_type
function handleInvoiceTypeChange(value: string) {
selectedInvoiceType = value === 'all' ? null : value;
const url = new URL(window.location.href);
if (!selectedInvoiceType) {
url.searchParams.delete('invoice_type');
} else {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
if (selectedType !== 'all') {
url.searchParams.set('operation_type', selectedType);
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Cargar tipos de factura disponibles según operation_type
async function loadInvoiceTypes(operationType: string) {
if (operationType === 'all') {
availableInvoiceTypes = [];
return;
}
loadingInvoiceTypes = true;
try {
const response = await invoiceTypesApi.list(1, 100, operationType);
if (response.data) {
availableInvoiceTypes = response.data.items;
}
} catch (e) {
console.error('Error loading invoice types:', e);
availableInvoiceTypes = [];
} finally {
loadingInvoiceTypes = false;
}
}
// Efecto para cargar tipos de factura cuando cambia selectedType
$effect(() => {
loadInvoiceTypes(selectedType);
});
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
invalidate('app:invoices');
};
// Escuchar eventos de facturas
const handleInvoiceView = (event: CustomEvent<Invoice>) => {
handleView(event.detail);
};
const handleInvoiceEdit = (event: CustomEvent<Invoice>) => {
handleEdit(event.detail);
};
const handleInvoiceDelete = (event: CustomEvent<Invoice>) => {
handleDelete(event.detail);
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
window.addEventListener('invoiceView', handleInvoiceView as EventListener);
window.addEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.addEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
window.removeEventListener('invoiceView', handleInvoiceView as EventListener);
window.removeEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.removeEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
};
}
});
// Estado para infinite scroll - inicializar con data del servidor
let allItems = $state<Invoice[]>(data.items || []);
let currentPage = $state(data.page || 1);
let pageSize = $state(data.page_size || 50);
let totalItems = $state(data.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Actualizar datos cuando cambia data del servidor
$effect(() => {
allItems = data.items || [];
currentPage = data.page || 1;
totalItems = data.total || 0;
error = data.error || null;
selectedInvoiceType = data.invoiceType || null;
selectedType = data.operationType || 'all';
});
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: any = {};
if (selectedType !== 'all') {
filters.operation_type = selectedType;
}
if (selectedInvoiceType) {
filters.invoice_type = selectedInvoiceType;
}
const response = await invoicesApi.list(
companyStore.activeCompany.id,
currentPage + 1,
pageSize,
Object.keys(filters).length > 0 ? filters : undefined
);
if (response.error) {
console.error('Error en loadMore:', response.error, 'Status:', response.status);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('Error loading more:', e);
} finally {
loading = false;
}
}
async function reloadData() {
// Invalidar datos para que el servidor recargue
await invalidate('app:invoices');
}
function handleCreateClick() {
selectedInvoice = null;
showCreateDialog = true;
}
function handleView(invoice: Invoice) {
selectedInvoice = invoice;
showDetailsDialog = true;
}
function handleEdit(invoice: Invoice) {
selectedInvoice = invoice;
showCreateDialog = true;
}
function handleDelete(invoice: Invoice) {
selectedInvoice = invoice;
showDeleteDialog = true;
}
function handleSuccess() {
reloadData();
}
// Crear columnas
const columns = createColumns();
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
<p class="text-muted-foreground">
Gestiona las facturas de importación y exportación
</p>
</div>
<Button href="/dashboard/invoices/exportacion/exportacion/new">
<Plus class="mr-2" size={16} />
Nueva Factura
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Facturas</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
{#if companyStore.activeCompany}
- Compañía: {companyStore.activeCompany.name}
{/if}
</Card.Description>
</div>
<div class="flex items-center gap-2">
<Select.Root type="single" value={selectedType} onValueChange={handleTypeChange}>
<Select.Trigger class="w-[180px]">
{selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todas</Select.Item>
<Select.Item value="imp">Importación</Select.Item>
<Select.Item value="exp">Exportación</Select.Item>
</Select.Content>
</Select.Root>
{#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
<Select.Root
type="single"
value={selectedInvoiceType || 'all'}
onValueChange={handleInvoiceTypeChange}
disabled={loadingInvoiceTypes}
>
<Select.Trigger class="w-[250px]">
<span class="truncate">
{#if loadingInvoiceTypes}
Cargando...
{:else if selectedInvoiceType}
{(() => {
const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
})()}
{:else}
Todos los tipos
{/if}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todos los tipos</Select.Item>
{#each availableInvoiceTypes as invType}
<Select.Item value={invType.key}>
<span class="block truncate max-w-[300px]" title={`${invType.key} - ${invType.description}`}>
{invType.key} - {invType.description}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{/if}
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</div>
</Card.Header>
<Card.Content>
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
</div>
<!-- Diálogos -->
<CreateEditDialog
bind:open={showCreateDialog}
bind:item={selectedInvoice}
defaultOperationType={selectedType !== 'all' ? selectedType as 'imp' | 'exp' : undefined}
defaultInvoiceType={selectedInvoiceType || undefined}
onSuccess={handleSuccess}
/>
<DetailsDialog
bind:open={showDetailsDialog}
invoice={selectedInvoice}
/>
<DeleteDialog
bind:open={showDeleteDialog}
item={selectedInvoice}
onSuccess={handleSuccess}
/>

View File

@@ -1,401 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Select from "$lib/components/ui/select";
import * as Tabs from "$lib/components/ui/tabs";
import * as Card from "$lib/components/ui/card";
import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices";
import { companyStore } from "$lib/stores/company.svelte";
import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte';
// DATOS FIJOS PARA ESTA CARPETA (EXPORTACIÓN)
const FIXED_OP_TYPE = "exp";
// ESTADO DEL FORMULARIO
let loading = $state(false);
let error = $state<string | null>(null);
let formData = $state({
// Header fields
operation_type: FIXED_OP_TYPE as "imp" | "exp",
invoice_type: "", // Se llena con el Select
invoice_number: "",
project_number: "",
purchase_order: "",
related_doc_id: null as number | null,
invoice_date: new Date().toISOString().split('T')[0], // Fecha de hoy
traffic_light_status: "",
observation_es: "",
observation_en: "",
comments_status: "",
cfdi_uuid: "",
path_pdf: "",
path_xml: "",
// Compliance MX fields
pedimento: "",
pedimento_code: "",
remesa: null as number | null,
aduana: "",
customs_broker_id: "",
provider_id: "",
sold_to_id: "",
shipped_to_id: "",
shipped_by_id: "",
is_mixed: false,
waste_type: "",
appendix_17: null as number | null,
edocument: "",
// Financials fields
currency: "MXN",
exchange_rate: null as number | null,
value_mn: null as number | null,
value_me: null as number | null,
customs_value_mn: null as number | null,
freight: null as number | null,
insurance: null as number | null,
iva_mn: null as number | null,
iva_factor: null as number | null,
total_quantity: null as number | null,
gross_weight: null as number | null,
net_weight: null as number | null,
bundle_count: null as number | null
});
// 👇 TU LISTA DE TIPOS DE EXPORTACIÓN
const INVOICE_TYPES_OPTIONS = [
{ value: "DONAC", label: "DONAC - DONACION" },
{ value: "EXDEF", label: "EXDEF - EXPORTACION DEFINITIVA" },
{ value: "MATDE", label: "MATDE - MATERIA PRIMA O MATERIAL DEVUELTO" },
{ value: "NODES", label: "NODES - NO HACE DESCARGA" },
{ value: "PTERM", label: "PTERM - PRODUCTO TERMINADO Y VIRTUALES" },
{ value: "SCRAP", label: "SCRAP - SCRAP" },
{ value: "VEMEX", label: "VEMEX - VENTAS EN MEXICO" },
{ value: "VIRTU", label: "VIRTU - VIRTUALES" },
];
async function handleSubmit(e: Event) {
e.preventDefault();
if (!companyStore.activeCompany?.id) {
error = "Error: No se detecta la compañía activa.";
return;
}
loading = true;
error = null;
try {
// Construimos el Payload
const payload: CreateInvoiceData = {
// Header (Usamos el valor del Select para invoice_type)
operation_type: FIXED_OP_TYPE,
invoice_type: formData.invoice_type,
invoice_number: formData.invoice_number,
project_number: formData.project_number,
purchase_order: formData.purchase_order,
related_doc_id: formData.related_doc_id,
invoice_date: formData.invoice_date,
traffic_light_status: formData.traffic_light_status,
observation_es: formData.observation_es,
observation_en: formData.observation_en,
comments_status: formData.comments_status,
cfdi_uuid: formData.cfdi_uuid,
path_pdf: formData.path_pdf,
path_xml: formData.path_xml,
// Compliance (Agrupado)
compliance_mx: {
pedimento: formData.pedimento,
pedimento_code: formData.pedimento_code,
remesa: formData.remesa,
aduana: formData.aduana,
customs_broker_id: formData.customs_broker_id,
provider_id: formData.provider_id,
sold_to_id: formData.sold_to_id,
shipped_to_id: formData.shipped_to_id,
shipped_by_id: formData.shipped_by_id,
is_mixed: formData.is_mixed,
waste_type: formData.waste_type,
appendix_17: formData.appendix_17,
edocument: formData.edocument
},
// Financials (Agrupado)
financials: {
currency: formData.currency,
exchange_rate: formData.exchange_rate,
value_mn: formData.value_mn,
value_me: formData.value_me,
customs_value_mn: formData.customs_value_mn,
freight: formData.freight,
insurance: formData.insurance,
iva_mn: formData.iva_mn,
iva_factor: formData.iva_factor,
total_quantity: formData.total_quantity,
gross_weight: formData.gross_weight,
net_weight: formData.net_weight,
bundle_count: formData.bundle_count
}
};
const response = await invoicesApi.create(companyStore.activeCompany.id, payload);
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando...';
setTimeout(() => window.location.reload(), 1500);
} else {
error = response.error;
}
return;
}
// ÉXITO: Volvemos a la lista
goto('/dashboard/invoices/exportacion/exportacion');
} catch (e) {
error = e instanceof Error ? e.message : "Error al guardar";
console.error("Error saving:", e);
} finally {
loading = false;
}
}
</script>
<div class="w-full mx-auto py-6 px-4 space-y-6 pb-48">
<div class="flex items-center gap-4">
<Button variant="outline" size="icon" href="/dashboard/invoices/exportacion/exportacion">
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">Nueva Factura Exportación</h1>
<p class="text-muted-foreground">Ingresa los datos para registrar la operación.</p>
</div>
</div>
{#if error}
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
⚠️ {error}
</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-6">
<Card.Root>
<Card.Content class="p-6">
<Tabs.Root value="general" class="w-full">
<Tabs.Content value="general" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label>Tipo de Operación</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">Exportación</div>
</div>
<div class="space-y-2">
<Label>Tipo de Factura</Label>
<Select.Root
type="single"
value={formData.invoice_type}
onValueChange={(v) => formData.invoice_type = v}
>
<Select.Trigger>
{#if formData.invoice_type}
{INVOICE_TYPES_OPTIONS.find(t => t.value === formData.invoice_type)?.label}
{:else}
Seleccionar tipo
{/if}
</Select.Trigger>
<Select.Content class="max-h-[300px] overflow-y-auto">
{#each INVOICE_TYPES_OPTIONS as type}
<Select.Item value={type.value}>
{type.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-2">
<Label for="invoice_number">Número de Factura *</Label>
<Input id="invoice_number" bind:value={formData.invoice_number} required placeholder="Número de factura" />
</div>
<div class="space-y-2">
<Label for="project_number">Número de Proyecto</Label>
<Input id="project_number" bind:value={formData.project_number} placeholder="Número de proyecto" />
</div>
<div class="space-y-2">
<Label for="purchase_order">Orden de Compra</Label>
<Input id="purchase_order" bind:value={formData.purchase_order} placeholder="Orden de compra" />
</div>
<div class="space-y-2">
<Label for="invoice_date">Fecha de Factura *</Label>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} required />
</div>
<div class="space-y-2">
<Label for="traffic_light_status">Semáforo</Label>
<Input id="traffic_light_status" bind:value={formData.traffic_light_status} placeholder="Estado del semáforo" />
</div>
<div class="space-y-2">
<Label for="cfdi_uuid">CFDI UUID</Label>
<Input id="cfdi_uuid" bind:value={formData.cfdi_uuid} placeholder="UUID del CFDI" />
</div>
</div>
<div class="grid grid-cols-1 gap-4 pt-4">
<div class="space-y-2">
<Label for="observation_es">Observaciones (Español)</Label>
<Input id="observation_es" bind:value={formData.observation_es} placeholder="Observaciones en español" />
</div>
<div class="space-y-2">
<Label for="observation_en">Observaciones (Inglés)</Label>
<Input id="observation_en" bind:value={formData.observation_en} placeholder="Observaciones en inglés" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="compliance" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="pedimento">Pedimento</Label>
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Número de pedimento" />
</div>
<div class="space-y-2">
<Label for="pedimento_code">Código de Pedimento</Label>
<Input id="pedimento_code" bind:value={formData.pedimento_code} placeholder="R1, K1, etc." />
</div>
<div class="space-y-2">
<Label for="remesa">Remesa</Label>
<Input id="remesa" type="number" bind:value={formData.remesa} placeholder="Número de remesa" />
</div>
<div class="space-y-2">
<Label for="aduana">Aduana</Label>
<Input id="aduana" bind:value={formData.aduana} placeholder="Código de aduana" />
</div>
<div class="space-y-2">
<Label for="customs_broker_id">Agente Aduanal</Label>
<Input id="customs_broker_id" bind:value={formData.customs_broker_id} placeholder="ID del agente aduanal" />
</div>
<div class="space-y-2">
<Label for="provider_id">Proveedor</Label>
<Input id="provider_id" bind:value={formData.provider_id} placeholder="ID del proveedor" />
</div>
<div class="space-y-2">
<Label for="edocument">E-Document</Label>
<Input id="edocument" bind:value={formData.edocument} placeholder="Número de e-document" />
</div>
<div class="space-y-2 flex items-center gap-2 pt-8">
<input id="is_mixed" type="checkbox" bind:checked={formData.is_mixed} class="h-4 w-4" />
<Label for="is_mixed" class="!mt-0">Operación Mixta</Label>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="financials" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="currency">Moneda</Label>
<Input id="currency" bind:value={formData.currency} placeholder="MXN, USD, etc." />
</div>
<div class="space-y-2">
<Label for="exchange_rate">Tipo de Cambio</Label>
<Input id="exchange_rate" type="number" step="0.000001" bind:value={formData.exchange_rate} placeholder="Tipo de cambio" />
</div>
<div class="space-y-2">
<Label for="value_mn">Valor MN</Label>
<Input id="value_mn" type="number" step="0.01" bind:value={formData.value_mn} placeholder="Valor MN" />
</div>
<div class="space-y-2">
<Label for="value_me">Valor ME</Label>
<Input id="value_me" type="number" step="0.01" bind:value={formData.value_me} placeholder="Valor ME" />
</div>
<div class="space-y-2">
<Label for="customs_value_mn">Valor Aduana MN</Label>
<Input id="customs_value_mn" type="number" step="0.01" bind:value={formData.customs_value_mn} placeholder="Valor Aduana MN" />
</div>
<div class="space-y-2">
<Label for="freight">Flete</Label>
<Input id="freight" type="number" step="0.01" bind:value={formData.freight} placeholder="Costo Flete" />
</div>
<div class="space-y-2">
<Label for="insurance">Seguro</Label>
<Input id="insurance" type="number" step="0.01" bind:value={formData.insurance} placeholder="Costo Seguro" />
</div>
<div class="space-y-2">
<Label for="total_quantity">Cantidad Total</Label>
<Input id="total_quantity" type="number" step="0.01" bind:value={formData.total_quantity} placeholder="Cantidad Total" />
</div>
<div class="space-y-2">
<Label for="gross_weight">Peso Bruto</Label>
<Input id="gross_weight" type="number" step="0.01" bind:value={formData.gross_weight} placeholder="Peso Bruto" />
</div>
<div class="space-y-2">
<Label for="net_weight">Peso Neto</Label>
<Input id="net_weight" type="number" step="0.01" bind:value={formData.net_weight} placeholder="Peso Neto" />
</div>
<div class="space-y-2">
<Label for="bundle_count">Número de Bultos</Label>
<Input id="bundle_count" type="number" bind:value={formData.bundle_count} placeholder="Num. Bultos" />
</div>
</div>
</Tabs.Content>
<Tabs.List class="grid grid-cols-3 fixed bottom-24 left-1/2 -translate-x-1/2 w-[90%] max-w-lg z-40 shadow-xl bg-background border rounded-xl">
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="compliance">Cumplimiento</Tabs.Trigger>
<Tabs.Trigger value="financials">Financieros</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</Card.Content>
</Card.Root>
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<div class="w-full mx-auto flex justify-end gap-4 px-4">
<Button variant="outline" href="/dashboard/invoices/exportacion/exportacion">
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
<Save class="mr-2 h-4 w-4" />
Guardar Factura
{/if}
</Button>
</div>
</div>
</form>
</div>

View File

@@ -1,114 +0,0 @@
import type { PageServerLoad } from '../$types';
import { redirect } from '@sveltejs/kit';
import {
getAuthTokens,
authenticatedFetch
} from '$lib/server/api';
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
// Verificar autenticación
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
try {
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)
// 2. Cookie active_company_id (setted por el team-switcher)
// 3. Primera compañía del usuario (fallback)
const companyIdParam = url.searchParams.get('company_id');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
? parseInt(companyIdParam)
: cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
// Si aún no hay companyId, mostrar error
if (!companyId) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'No se encontró una compañía seleccionada',
companies: parentData.companies || []
};
}
// Obtener filtro de tipo de operación
const operationType = 'exp'
const invoiceType = 'REPAR'
// Construir parámetros de consulta
const params = new URLSearchParams({
company_id: companyId.toString(),
page: '1',
page_size: '50'
});
// Agregar filtro de tipo si existe y no es 'all'
if (operationType && operationType !== 'all') {
params.append('operation_type', operationType);
}
// Agregar filtro de invoice_type si existe
if (invoiceType) {
params.append('invoice_type', invoiceType);
}
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/invoices?${params.toString()}`,
{},
cookies,
fetch,
'/login'
);
if (!response.ok) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
};
}
const data = await response.json();
return {
items: data.items || [],
total: data.total || 0,
page: data.page || 1,
page_size: data.page_size || 50,
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
};
} catch (error) {
console.error('Error loading invoices:', error);
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
operationType: 'all',
invoiceType: null
};
}
};

View File

@@ -1,381 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoicesApi, type Invoice } from '$lib/api/dashboard/a76/invoices';
import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
import CreateEditDialog from '$lib/components/dashboard/invoices/create-edit-dialog.svelte';
import DetailsDialog from '$lib/components/dashboard/invoices/details-dialog.svelte';
import DeleteDialog from '$lib/components/dashboard/invoices/delete-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto, invalidate } from '$app/navigation';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
// Recibir data del servidor
interface PageData {
items: Invoice[];
total: number;
page: number;
page_size: number;
error?: string;
companies: any[];
currentCompanyId?: number;
operationType?: string;
invoiceType?: string | null;
}
let { data }: { data: PageData } = $props();
// Estado para los diálogos
let showCreateDialog = $state(false);
let showDetailsDialog = $state(false);
let showDeleteDialog = $state(false);
let selectedInvoice = $state<Invoice | null>(null);
// Estado para el filtro de tipo (inicializado desde data del servidor)
let selectedType = $state<string>(data.operationType || 'all');
let selectedInvoiceType = $state<string | null>(data.invoiceType || null);
let availableInvoiceTypes = $state<InvoiceType[]>([]);
let loadingInvoiceTypes = $state(false);
// Actualizar URL cuando cambia el filtro
function handleTypeChange(value: string) {
selectedType = value;
const url = new URL(window.location.href);
if (value === 'all') {
url.searchParams.delete('operation_type');
url.searchParams.delete('invoice_type');
selectedInvoiceType = null;
} else {
url.searchParams.set('operation_type', value);
// Mantener invoice_type si existe
if (selectedInvoiceType) {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Actualizar URL cuando cambia el filtro de invoice_type
function handleInvoiceTypeChange(value: string) {
selectedInvoiceType = value === 'all' ? null : value;
const url = new URL(window.location.href);
if (!selectedInvoiceType) {
url.searchParams.delete('invoice_type');
} else {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
if (selectedType !== 'all') {
url.searchParams.set('operation_type', selectedType);
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Cargar tipos de factura disponibles según operation_type
async function loadInvoiceTypes(operationType: string) {
if (operationType === 'all') {
availableInvoiceTypes = [];
return;
}
loadingInvoiceTypes = true;
try {
const response = await invoiceTypesApi.list(1, 100, operationType);
if (response.data) {
availableInvoiceTypes = response.data.items;
}
} catch (e) {
console.error('Error loading invoice types:', e);
availableInvoiceTypes = [];
} finally {
loadingInvoiceTypes = false;
}
}
// Efecto para cargar tipos de factura cuando cambia selectedType
$effect(() => {
loadInvoiceTypes(selectedType);
});
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
invalidate('app:invoices');
};
// Escuchar eventos de facturas
const handleInvoiceView = (event: CustomEvent<Invoice>) => {
handleView(event.detail);
};
const handleInvoiceEdit = (event: CustomEvent<Invoice>) => {
handleEdit(event.detail);
};
const handleInvoiceDelete = (event: CustomEvent<Invoice>) => {
handleDelete(event.detail);
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
window.addEventListener('invoiceView', handleInvoiceView as EventListener);
window.addEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.addEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
window.removeEventListener('invoiceView', handleInvoiceView as EventListener);
window.removeEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.removeEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
};
}
});
// Estado para infinite scroll - inicializar con data del servidor
let allItems = $state<Invoice[]>(data.items || []);
let currentPage = $state(data.page || 1);
let pageSize = $state(data.page_size || 50);
let totalItems = $state(data.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Actualizar datos cuando cambia data del servidor
$effect(() => {
allItems = data.items || [];
currentPage = data.page || 1;
totalItems = data.total || 0;
error = data.error || null;
selectedInvoiceType = data.invoiceType || null;
selectedType = data.operationType || 'all';
});
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: any = {};
if (selectedType !== 'all') {
filters.operation_type = selectedType;
}
if (selectedInvoiceType) {
filters.invoice_type = selectedInvoiceType;
}
const response = await invoicesApi.list(
companyStore.activeCompany.id,
currentPage + 1,
pageSize,
Object.keys(filters).length > 0 ? filters : undefined
);
if (response.error) {
console.error('Error en loadMore:', response.error, 'Status:', response.status);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('Error loading more:', e);
} finally {
loading = false;
}
}
async function reloadData() {
// Invalidar datos para que el servidor recargue
await invalidate('app:invoices');
}
function handleCreateClick() {
goto('/dashboard/invoices/importacion/reparacion/new');
}
function handleView(invoice: Invoice) {
selectedInvoice = invoice;
showDetailsDialog = true;
}
function handleEdit(invoice: Invoice) {
selectedInvoice = invoice;
showCreateDialog = true;
}
function handleDelete(invoice: Invoice) {
selectedInvoice = invoice;
showDeleteDialog = true;
}
function handleSuccess() {
reloadData();
}
// Crear columnas
const columns = createColumns();
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
<p class="text-muted-foreground">
Gestiona las facturas de importación y exportación
</p>
</div>
<Button href="/dashboard/invoices/exportacion/reparacion/new">
<Plus class="mr-2" size={16} />
Nueva Factura
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Facturas</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
{#if companyStore.activeCompany}
- Compañía: {companyStore.activeCompany.name}
{/if}
</Card.Description>
</div>
<div class="flex items-center gap-2">
<Select.Root type="single" value={selectedType} onValueChange={handleTypeChange}>
<Select.Trigger class="w-[180px]">
{selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todas</Select.Item>
<Select.Item value="imp">Importación</Select.Item>
<Select.Item value="exp">Exportación</Select.Item>
</Select.Content>
</Select.Root>
{#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
<Select.Root
type="single"
value={selectedInvoiceType || 'all'}
onValueChange={handleInvoiceTypeChange}
disabled={loadingInvoiceTypes}
>
<Select.Trigger class="w-[250px]">
<span class="truncate">
{#if loadingInvoiceTypes}
Cargando...
{:else if selectedInvoiceType}
{(() => {
const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
})()}
{:else}
Todos los tipos
{/if}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todos los tipos</Select.Item>
{#each availableInvoiceTypes as invType}
<Select.Item value={invType.key}>
<span class="block truncate max-w-[300px]" title={`${invType.key} - ${invType.description}`}>
{invType.key} - {invType.description}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{/if}
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</div>
</Card.Header>
<Card.Content>
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
</div>
<!-- Diálogos -->
<CreateEditDialog
bind:open={showCreateDialog}
bind:item={selectedInvoice}
defaultOperationType={selectedType !== 'all' ? selectedType as 'imp' | 'exp' : undefined}
defaultInvoiceType={selectedInvoiceType || undefined}
onSuccess={handleSuccess}
/>
<DetailsDialog
bind:open={showDetailsDialog}
invoice={selectedInvoice}
/>
<DeleteDialog
bind:open={showDeleteDialog}
item={selectedInvoice}
onSuccess={handleSuccess}
/>

View File

@@ -1,325 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Tabs from "$lib/components/ui/tabs";
import * as Card from "$lib/components/ui/card";
import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices";
import { companyStore } from "$lib/stores/company.svelte";
import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte';
// DATOS FIJOS PARA ESTA CARPETA (DEFINITIVA)
const FIXED_OP_TYPE = "exp";
const FIXED_INV_TYPE = "REPAR";
// ESTADO DEL FORMULARIO
let loading = $state(false);
let error = $state<string | null>(null);
let formData = $state({
// Header fields
operation_type: FIXED_OP_TYPE as "imp" | "exp",
invoice_type: FIXED_INV_TYPE,
invoice_number: "",
project_number: "",
purchase_order: "",
related_doc_id: null as number | null,
invoice_date: new Date().toISOString().split('T')[0], // Fecha de hoy por defecto
traffic_light_status: "",
observation_es: "",
observation_en: "",
comments_status: "",
cfdi_uuid: "",
path_pdf: "",
path_xml: "",
// Compliance MX fields
pedimento: "",
pedimento_code: "",
remesa: null as number | null,
aduana: "",
customs_broker_id: "",
provider_id: "",
sold_to_id: "",
shipped_to_id: "",
shipped_by_id: "",
is_mixed: false,
waste_type: "",
appendix_17: null as number | null,
edocument: "",
// Financials fields
currency: "MXN",
exchange_rate: null as number | null,
value_mn: null as number | null,
value_me: null as number | null,
customs_value_mn: null as number | null,
freight: null as number | null,
insurance: null as number | null,
iva_mn: null as number | null,
iva_factor: null as number | null,
total_quantity: null as number | null,
gross_weight: null as number | null,
net_weight: null as number | null,
bundle_count: null as number | null
});
async function handleSubmit(e: Event) {
e.preventDefault();
// VALIDACIÓN DE SEGURIDAD (Por si se refresca la página)
if (!companyStore.activeCompany?.id) {
error = "Error: No se detecta la compañía activa. Por favor, vuelve al dashboard y selecciona una compañía.";
return;
}
loading = true;
error = null;
try {
// Construimos el Payload (LÓGICA ORIGINAL QUE FUNCIONA)
const payload: CreateInvoiceData = {
...formData,
// Aseguramos que se envíen los fijos
operation_type: FIXED_OP_TYPE,
invoice_type: FIXED_INV_TYPE
};
const response = await invoicesApi.create(companyStore.activeCompany.id, payload);
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando...';
setTimeout(() => window.location.reload(), 1500);
} else {
error = response.error;
}
return;
}
// ÉXITO: Volvemos a la lista
goto('/dashboard/invoices/exportacion/reparacion');
} catch (e) {
error = e instanceof Error ? e.message : "Error al guardar";
console.error("Error saving:", e);
} finally {
loading = false;
}
}
</script>
<div class="w-full mx-auto py-6 px-4 space-y-6 pb-48">
<div class="flex items-center gap-4">
<Button variant="outline" size="icon" href="/dashboard/invoices/exportacion/reparacion">
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">Nueva Factura Reparacion</h1>
<p class="text-muted-foreground">Ingresa los datos para registrar la exportación.</p>
</div>
</div>
{#if error}
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
{error}
</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-6">
<Card.Root>
<Card.Content class="p-6">
<Tabs.Root value="general" class="w-full">
<Tabs.Content value="general" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label>Tipo de Operación</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">Importación</div>
</div>
<div class="space-y-2">
<Label>Tipo de Factura</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">REPARACION (REPAR)</div>
</div>
<div class="space-y-2">
<Label for="invoice_number">Número de Factura *</Label>
<Input id="invoice_number" bind:value={formData.invoice_number} required placeholder="Número de factura" />
</div>
<div class="space-y-2">
<Label for="project_number">Número de Proyecto</Label>
<Input id="project_number" bind:value={formData.project_number} placeholder="Número de proyecto" />
</div>
<div class="space-y-2">
<Label for="purchase_order">Orden de Compra</Label>
<Input id="purchase_order" bind:value={formData.purchase_order} placeholder="Orden de compra" />
</div>
<div class="space-y-2">
<Label for="invoice_date">Fecha de Factura *</Label>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} required />
</div>
<div class="space-y-2">
<Label for="traffic_light_status">Semáforo</Label>
<Input id="traffic_light_status" bind:value={formData.traffic_light_status} placeholder="Estado del semáforo" />
</div>
<div class="space-y-2">
<Label for="cfdi_uuid">CFDI UUID</Label>
<Input id="cfdi_uuid" bind:value={formData.cfdi_uuid} placeholder="UUID del CFDI" />
</div>
</div>
<div class="grid grid-cols-1 gap-4 pt-4">
<div class="space-y-2">
<Label for="observation_es">Observaciones (Español)</Label>
<Input id="observation_es" bind:value={formData.observation_es} placeholder="Observaciones en español" />
</div>
<div class="space-y-2">
<Label for="observation_en">Observaciones (Inglés)</Label>
<Input id="observation_en" bind:value={formData.observation_en} placeholder="Observaciones en inglés" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="compliance" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="pedimento">Pedimento</Label>
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Número de pedimento" />
</div>
<div class="space-y-2">
<Label for="pedimento_code">Código de Pedimento</Label>
<Input id="pedimento_code" bind:value={formData.pedimento_code} placeholder="R1, K1, etc." />
</div>
<div class="space-y-2">
<Label for="remesa">Remesa</Label>
<Input id="remesa" type="number" bind:value={formData.remesa} placeholder="Número de remesa" />
</div>
<div class="space-y-2">
<Label for="aduana">Aduana</Label>
<Input id="aduana" bind:value={formData.aduana} placeholder="Código de aduana" />
</div>
<div class="space-y-2">
<Label for="customs_broker_id">Agente Aduanal</Label>
<Input id="customs_broker_id" bind:value={formData.customs_broker_id} placeholder="ID del agente aduanal" />
</div>
<div class="space-y-2">
<Label for="provider_id">Proveedor</Label>
<Input id="provider_id" bind:value={formData.provider_id} placeholder="ID del proveedor" />
</div>
<div class="space-y-2">
<Label for="edocument">E-Document</Label>
<Input id="edocument" bind:value={formData.edocument} placeholder="Número de e-document" />
</div>
<div class="space-y-2 flex items-center gap-2 pt-8">
<input id="is_mixed" type="checkbox" bind:checked={formData.is_mixed} class="h-4 w-4" />
<Label for="is_mixed" class="!mt-0">Operación Mixta</Label>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="financials" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="currency">Moneda</Label>
<Input id="currency" bind:value={formData.currency} placeholder="MXN, USD, etc." />
</div>
<div class="space-y-2">
<Label for="exchange_rate">Tipo de Cambio</Label>
<Input id="exchange_rate" type="number" step="0.000001" bind:value={formData.exchange_rate} placeholder="Tipo de cambio" />
</div>
<div class="space-y-2">
<Label for="value_mn">Valor MN</Label>
<Input id="value_mn" type="number" step="0.01" bind:value={formData.value_mn} placeholder="Valor MN" />
</div>
<div class="space-y-2">
<Label for="value_me">Valor ME</Label>
<Input id="value_me" type="number" step="0.01" bind:value={formData.value_me} placeholder="Valor ME" />
</div>
<div class="space-y-2">
<Label for="customs_value_mn">Valor Aduana MN</Label>
<Input id="customs_value_mn" type="number" step="0.01" bind:value={formData.customs_value_mn} placeholder="Valor Aduana MN" />
</div>
<div class="space-y-2">
<Label for="freight">Flete</Label>
<Input id="freight" type="number" step="0.01" bind:value={formData.freight} placeholder="Costo Flete" />
</div>
<div class="space-y-2">
<Label for="insurance">Seguro</Label>
<Input id="insurance" type="number" step="0.01" bind:value={formData.insurance} placeholder="Costo Seguro" />
</div>
<div class="space-y-2">
<Label for="total_quantity">Cantidad Total</Label>
<Input id="total_quantity" type="number" step="0.01" bind:value={formData.total_quantity} placeholder="Cantidad Total" />
</div>
<div class="space-y-2">
<Label for="gross_weight">Peso Bruto</Label>
<Input id="gross_weight" type="number" step="0.01" bind:value={formData.gross_weight} placeholder="Peso Bruto" />
</div>
<div class="space-y-2">
<Label for="net_weight">Peso Neto</Label>
<Input id="net_weight" type="number" step="0.01" bind:value={formData.net_weight} placeholder="Peso Neto" />
</div>
<div class="space-y-2">
<Label for="bundle_count">Número de Bultos</Label>
<Input id="bundle_count" type="number" bind:value={formData.bundle_count} placeholder="Num. Bultos" />
</div>
</div>
</Tabs.Content>
<Tabs.List class="grid grid-cols-3 fixed bottom-24 left-1/2 -translate-x-1/2 w-[90%] max-w-lg z-40 shadow-xl bg-background border rounded-xl">
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="compliance">Cumplimiento</Tabs.Trigger>
<Tabs.Trigger value="financials">Financieros</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</Card.Content>
</Card.Root>
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<div class="w-full mx-auto flex justify-end gap-4 px-4">
<Button variant="outline" href="/dashboard/invoices/exportacion/reparacion">
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
<Save class="mr-2 h-4 w-4" />
Guardar Factura
{/if}
</Button>
</div>
</div>
</form>
</div>

View File

@@ -1,114 +0,0 @@
import type { PageServerLoad } from '../../$types';
import { redirect } from '@sveltejs/kit';
import {
getAuthTokens,
authenticatedFetch
} from '$lib/server/api';
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
// Verificar autenticación
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
try {
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)
// 2. Cookie active_company_id (setted por el team-switcher)
// 3. Primera compañía del usuario (fallback)
const companyIdParam = url.searchParams.get('company_id');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
? parseInt(companyIdParam)
: cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
// Si aún no hay companyId, mostrar error
if (!companyId) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'No se encontró una compañía seleccionada',
companies: parentData.companies || []
};
}
// Obtener filtro de tipo de operación
const operationType = 'imp'
const invoiceType = 'CR'
// Construir parámetros de consulta
const params = new URLSearchParams({
company_id: companyId.toString(),
page: '1',
page_size: '50'
});
// Agregar filtro de tipo si existe y no es 'all'
if (operationType && operationType !== 'all') {
params.append('operation_type', operationType);
}
// Agregar filtro de invoice_type si existe
if (invoiceType) {
params.append('invoice_type', invoiceType);
}
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/invoices?${params.toString()}`,
{},
cookies,
fetch,
'/login'
);
if (!response.ok) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
};
}
const data = await response.json();
return {
items: data.items || [],
total: data.total || 0,
page: data.page || 1,
page_size: data.page_size || 50,
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
};
} catch (error) {
console.error('Error loading invoices:', error);
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
operationType: 'all',
invoiceType: null
};
}
};

View File

@@ -1,382 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoicesApi, type Invoice } from '$lib/api/dashboard/a76/invoices';
import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
import CreateEditDialog from '$lib/components/dashboard/invoices/create-edit-dialog.svelte';
import DetailsDialog from '$lib/components/dashboard/invoices/details-dialog.svelte';
import DeleteDialog from '$lib/components/dashboard/invoices/delete-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto, invalidate } from '$app/navigation';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
// Recibir data del servidor
interface PageData {
items: Invoice[];
total: number;
page: number;
page_size: number;
error?: string;
companies: any[];
currentCompanyId?: number;
operationType?: string;
invoiceType?: string | null;
}
let { data }: { data: PageData } = $props();
// Estado para los diálogos
let showCreateDialog = $state(false);
let showDetailsDialog = $state(false);
let showDeleteDialog = $state(false);
let selectedInvoice = $state<Invoice | null>(null);
// Estado para el filtro de tipo (inicializado desde data del servidor)
let selectedType = $state<string>(data.operationType || 'all');
let selectedInvoiceType = $state<string | null>(data.invoiceType || null);
let availableInvoiceTypes = $state<InvoiceType[]>([]);
let loadingInvoiceTypes = $state(false);
// Actualizar URL cuando cambia el filtro
function handleTypeChange(value: string) {
selectedType = value;
const url = new URL(window.location.href);
if (value === 'all') {
url.searchParams.delete('operation_type');
url.searchParams.delete('invoice_type');
selectedInvoiceType = null;
} else {
url.searchParams.set('operation_type', value);
// Mantener invoice_type si existe
if (selectedInvoiceType) {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Actualizar URL cuando cambia el filtro de invoice_type
function handleInvoiceTypeChange(value: string) {
selectedInvoiceType = value === 'all' ? null : value;
const url = new URL(window.location.href);
if (!selectedInvoiceType) {
url.searchParams.delete('invoice_type');
} else {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
if (selectedType !== 'all') {
url.searchParams.set('operation_type', selectedType);
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Cargar tipos de factura disponibles según operation_type
async function loadInvoiceTypes(operationType: string) {
if (operationType === 'all') {
availableInvoiceTypes = [];
return;
}
loadingInvoiceTypes = true;
try {
const response = await invoiceTypesApi.list(1, 100, operationType);
if (response.data) {
availableInvoiceTypes = response.data.items;
}
} catch (e) {
console.error('Error loading invoice types:', e);
availableInvoiceTypes = [];
} finally {
loadingInvoiceTypes = false;
}
}
// Efecto para cargar tipos de factura cuando cambia selectedType
$effect(() => {
loadInvoiceTypes(selectedType);
});
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
invalidate('app:invoices');
};
// Escuchar eventos de facturas
const handleInvoiceView = (event: CustomEvent<Invoice>) => {
handleView(event.detail);
};
const handleInvoiceEdit = (event: CustomEvent<Invoice>) => {
handleEdit(event.detail);
};
const handleInvoiceDelete = (event: CustomEvent<Invoice>) => {
handleDelete(event.detail);
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
window.addEventListener('invoiceView', handleInvoiceView as EventListener);
window.addEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.addEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
window.removeEventListener('invoiceView', handleInvoiceView as EventListener);
window.removeEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.removeEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
};
}
});
// Estado para infinite scroll - inicializar con data del servidor
let allItems = $state<Invoice[]>(data.items || []);
let currentPage = $state(data.page || 1);
let pageSize = $state(data.page_size || 50);
let totalItems = $state(data.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Actualizar datos cuando cambia data del servidor
$effect(() => {
allItems = data.items || [];
currentPage = data.page || 1;
totalItems = data.total || 0;
error = data.error || null;
selectedInvoiceType = data.invoiceType || null;
selectedType = data.operationType || 'all';
});
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: any = {};
if (selectedType !== 'all') {
filters.operation_type = selectedType;
}
if (selectedInvoiceType) {
filters.invoice_type = selectedInvoiceType;
}
const response = await invoicesApi.list(
companyStore.activeCompany.id,
currentPage + 1,
pageSize,
Object.keys(filters).length > 0 ? filters : undefined
);
if (response.error) {
console.error('Error en loadMore:', response.error, 'Status:', response.status);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('Error loading more:', e);
} finally {
loading = false;
}
}
async function reloadData() {
// Invalidar datos para que el servidor recargue
await invalidate('app:invoices');
}
function handleCreateClick() {
selectedInvoice = null;
showCreateDialog = true;
}
function handleView(invoice: Invoice) {
selectedInvoice = invoice;
showDetailsDialog = true;
}
function handleEdit(invoice: Invoice) {
selectedInvoice = invoice;
showCreateDialog = true;
}
function handleDelete(invoice: Invoice) {
selectedInvoice = invoice;
showDeleteDialog = true;
}
function handleSuccess() {
reloadData();
}
// Crear columnas
const columns = createColumns();
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
<p class="text-muted-foreground">
Gestiona las facturas de importación y exportación
</p>
</div>
<Button href="/dashboard/invoices/importacion/cambio_regimen/new">
<Plus class="mr-2" size={16} />
Nueva Factura
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Facturas</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
{#if companyStore.activeCompany}
- Compañía: {companyStore.activeCompany.name}
{/if}
</Card.Description>
</div>
<div class="flex items-center gap-2">
<Select.Root type="single" value={selectedType} onValueChange={handleTypeChange}>
<Select.Trigger class="w-[180px]">
{selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todas</Select.Item>
<Select.Item value="imp">Importación</Select.Item>
<Select.Item value="exp">Exportación</Select.Item>
</Select.Content>
</Select.Root>
{#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
<Select.Root
type="single"
value={selectedInvoiceType || 'all'}
onValueChange={handleInvoiceTypeChange}
disabled={loadingInvoiceTypes}
>
<Select.Trigger class="w-[250px]">
<span class="truncate">
{#if loadingInvoiceTypes}
Cargando...
{:else if selectedInvoiceType}
{(() => {
const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
})()}
{:else}
Todos los tipos
{/if}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todos los tipos</Select.Item>
{#each availableInvoiceTypes as invType}
<Select.Item value={invType.key}>
<span class="block truncate max-w-[300px]" title={`${invType.key} - ${invType.description}`}>
{invType.key} - {invType.description}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{/if}
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</div>
</Card.Header>
<Card.Content>
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
</div>
<!-- Diálogos -->
<CreateEditDialog
bind:open={showCreateDialog}
bind:item={selectedInvoice}
defaultOperationType={selectedType !== 'all' ? selectedType as 'imp' | 'exp' : undefined}
defaultInvoiceType={selectedInvoiceType || undefined}
onSuccess={handleSuccess}
/>
<DetailsDialog
bind:open={showDetailsDialog}
invoice={selectedInvoice}
/>
<DeleteDialog
bind:open={showDeleteDialog}
item={selectedInvoice}
onSuccess={handleSuccess}
/>

View File

@@ -1,325 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Tabs from "$lib/components/ui/tabs";
import * as Card from "$lib/components/ui/card";
import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices";
import { companyStore } from "$lib/stores/company.svelte";
import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte';
// DATOS FIJOS PARA ESTA CARPETA (DEFINITIVA)
const FIXED_OP_TYPE = "imp";
const FIXED_INV_TYPE = "CR";
// ESTADO DEL FORMULARIO
let loading = $state(false);
let error = $state<string | null>(null);
let formData = $state({
// Header fields
operation_type: FIXED_OP_TYPE as "imp" | "exp",
invoice_type: FIXED_INV_TYPE,
invoice_number: "",
project_number: "",
purchase_order: "",
related_doc_id: null as number | null,
invoice_date: new Date().toISOString().split('T')[0], // Fecha de hoy por defecto
traffic_light_status: "",
observation_es: "",
observation_en: "",
comments_status: "",
cfdi_uuid: "",
path_pdf: "",
path_xml: "",
// Compliance MX fields
pedimento: "",
pedimento_code: "",
remesa: null as number | null,
aduana: "",
customs_broker_id: "",
provider_id: "",
sold_to_id: "",
shipped_to_id: "",
shipped_by_id: "",
is_mixed: false,
waste_type: "",
appendix_17: null as number | null,
edocument: "",
// Financials fields
currency: "MXN",
exchange_rate: null as number | null,
value_mn: null as number | null,
value_me: null as number | null,
customs_value_mn: null as number | null,
freight: null as number | null,
insurance: null as number | null,
iva_mn: null as number | null,
iva_factor: null as number | null,
total_quantity: null as number | null,
gross_weight: null as number | null,
net_weight: null as number | null,
bundle_count: null as number | null
});
async function handleSubmit(e: Event) {
e.preventDefault();
// VALIDACIÓN DE SEGURIDAD (Por si se refresca la página)
if (!companyStore.activeCompany?.id) {
error = "Error: No se detecta la compañía activa. Por favor, vuelve al dashboard y selecciona una compañía.";
return;
}
loading = true;
error = null;
try {
// Construimos el Payload (LÓGICA ORIGINAL QUE FUNCIONA)
const payload: CreateInvoiceData = {
...formData,
// Aseguramos que se envíen los fijos
operation_type: FIXED_OP_TYPE,
invoice_type: FIXED_INV_TYPE
};
const response = await invoicesApi.create(companyStore.activeCompany.id, payload);
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando...';
setTimeout(() => window.location.reload(), 1500);
} else {
error = response.error;
}
return;
}
// ÉXITO: Volvemos a la lista
goto('/dashboard/invoices/importacion/cambio_regimen');
} catch (e) {
error = e instanceof Error ? e.message : "Error al guardar";
console.error("Error saving:", e);
} finally {
loading = false;
}
}
</script>
<div class="w-full mx-auto py-6 px-4 space-y-6 pb-48">
<div class="flex items-center gap-4">
<Button variant="outline" size="icon" href="/dashboard/invoices/importacion/cambio_regimen">
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">Nueva Factura Cambio Regimen</h1>
<p class="text-muted-foreground">Ingresa los datos para registrar la importación.</p>
</div>
</div>
{#if error}
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
⚠️ {error}
</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-6">
<Card.Root>
<Card.Content class="p-6">
<Tabs.Root value="general" class="w-full">
<Tabs.Content value="general" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label>Tipo de Operación</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">Importación</div>
</div>
<div class="space-y-2">
<Label>Tipo de Factura</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">CAMBIO REGIMEN (CR)</div>
</div>
<div class="space-y-2">
<Label for="invoice_number">Número de Factura *</Label>
<Input id="invoice_number" bind:value={formData.invoice_number} required placeholder="Número de factura" />
</div>
<div class="space-y-2">
<Label for="project_number">Número de Proyecto</Label>
<Input id="project_number" bind:value={formData.project_number} placeholder="Número de proyecto" />
</div>
<div class="space-y-2">
<Label for="purchase_order">Orden de Compra</Label>
<Input id="purchase_order" bind:value={formData.purchase_order} placeholder="Orden de compra" />
</div>
<div class="space-y-2">
<Label for="invoice_date">Fecha de Factura *</Label>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} required />
</div>
<div class="space-y-2">
<Label for="traffic_light_status">Semáforo</Label>
<Input id="traffic_light_status" bind:value={formData.traffic_light_status} placeholder="Estado del semáforo" />
</div>
<div class="space-y-2">
<Label for="cfdi_uuid">CFDI UUID</Label>
<Input id="cfdi_uuid" bind:value={formData.cfdi_uuid} placeholder="UUID del CFDI" />
</div>
</div>
<div class="grid grid-cols-1 gap-4 pt-4">
<div class="space-y-2">
<Label for="observation_es">Observaciones (Español)</Label>
<Input id="observation_es" bind:value={formData.observation_es} placeholder="Observaciones en español" />
</div>
<div class="space-y-2">
<Label for="observation_en">Observaciones (Inglés)</Label>
<Input id="observation_en" bind:value={formData.observation_en} placeholder="Observaciones en inglés" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="compliance" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="pedimento">Pedimento</Label>
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Número de pedimento" />
</div>
<div class="space-y-2">
<Label for="pedimento_code">Código de Pedimento</Label>
<Input id="pedimento_code" bind:value={formData.pedimento_code} placeholder="R1, K1, etc." />
</div>
<div class="space-y-2">
<Label for="remesa">Remesa</Label>
<Input id="remesa" type="number" bind:value={formData.remesa} placeholder="Número de remesa" />
</div>
<div class="space-y-2">
<Label for="aduana">Aduana</Label>
<Input id="aduana" bind:value={formData.aduana} placeholder="Código de aduana" />
</div>
<div class="space-y-2">
<Label for="customs_broker_id">Agente Aduanal</Label>
<Input id="customs_broker_id" bind:value={formData.customs_broker_id} placeholder="ID del agente aduanal" />
</div>
<div class="space-y-2">
<Label for="provider_id">Proveedor</Label>
<Input id="provider_id" bind:value={formData.provider_id} placeholder="ID del proveedor" />
</div>
<div class="space-y-2">
<Label for="edocument">E-Document</Label>
<Input id="edocument" bind:value={formData.edocument} placeholder="Número de e-document" />
</div>
<div class="space-y-2 flex items-center gap-2 pt-8">
<input id="is_mixed" type="checkbox" bind:checked={formData.is_mixed} class="h-4 w-4" />
<Label for="is_mixed" class="!mt-0">Operación Mixta</Label>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="financials" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="currency">Moneda</Label>
<Input id="currency" bind:value={formData.currency} placeholder="MXN, USD, etc." />
</div>
<div class="space-y-2">
<Label for="exchange_rate">Tipo de Cambio</Label>
<Input id="exchange_rate" type="number" step="0.000001" bind:value={formData.exchange_rate} placeholder="Tipo de cambio" />
</div>
<div class="space-y-2">
<Label for="value_mn">Valor MN</Label>
<Input id="value_mn" type="number" step="0.01" bind:value={formData.value_mn} placeholder="Valor MN" />
</div>
<div class="space-y-2">
<Label for="value_me">Valor ME</Label>
<Input id="value_me" type="number" step="0.01" bind:value={formData.value_me} placeholder="Valor ME" />
</div>
<div class="space-y-2">
<Label for="customs_value_mn">Valor Aduana MN</Label>
<Input id="customs_value_mn" type="number" step="0.01" bind:value={formData.customs_value_mn} placeholder="Valor Aduana MN" />
</div>
<div class="space-y-2">
<Label for="freight">Flete</Label>
<Input id="freight" type="number" step="0.01" bind:value={formData.freight} placeholder="Costo Flete" />
</div>
<div class="space-y-2">
<Label for="insurance">Seguro</Label>
<Input id="insurance" type="number" step="0.01" bind:value={formData.insurance} placeholder="Costo Seguro" />
</div>
<div class="space-y-2">
<Label for="total_quantity">Cantidad Total</Label>
<Input id="total_quantity" type="number" step="0.01" bind:value={formData.total_quantity} placeholder="Cantidad Total" />
</div>
<div class="space-y-2">
<Label for="gross_weight">Peso Bruto</Label>
<Input id="gross_weight" type="number" step="0.01" bind:value={formData.gross_weight} placeholder="Peso Bruto" />
</div>
<div class="space-y-2">
<Label for="net_weight">Peso Neto</Label>
<Input id="net_weight" type="number" step="0.01" bind:value={formData.net_weight} placeholder="Peso Neto" />
</div>
<div class="space-y-2">
<Label for="bundle_count">Número de Bultos</Label>
<Input id="bundle_count" type="number" bind:value={formData.bundle_count} placeholder="Num. Bultos" />
</div>
</div>
</Tabs.Content>
<Tabs.List class="grid grid-cols-3 fixed bottom-24 left-1/2 -translate-x-1/2 w-[90%] max-w-lg z-40 shadow-xl bg-background border rounded-xl">
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="compliance">Cumplimiento</Tabs.Trigger>
<Tabs.Trigger value="financials">Financieros</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</Card.Content>
</Card.Root>
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<div class="w-full mx-auto flex justify-end gap-4 px-4">
<Button variant="outline" href="/dashboard/invoices/importacion/cambio_regimen">
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
<Save class="mr-2 h-4 w-4" />
Guardar Factura
{/if}
</Button>
</div>
</div>
</form>
</div>

View File

@@ -1,114 +0,0 @@
import type { PageServerLoad } from '../../$types';
import { redirect } from '@sveltejs/kit';
import {
getAuthTokens,
authenticatedFetch
} from '$lib/server/api';
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
// Verificar autenticación
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
try {
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)
// 2. Cookie active_company_id (setted por el team-switcher)
// 3. Primera compañía del usuario (fallback)
const companyIdParam = url.searchParams.get('company_id');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
? parseInt(companyIdParam)
: cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
// Si aún no hay companyId, mostrar error
if (!companyId) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'No se encontró una compañía seleccionada',
companies: parentData.companies || []
};
}
// Obtener filtro de tipo de operación
const operationType = 'imp'
const invoiceType = 'MEX'
// Construir parámetros de consulta
const params = new URLSearchParams({
company_id: companyId.toString(),
page: '1',
page_size: '50'
});
// Agregar filtro de tipo si existe y no es 'all'
if (operationType && operationType !== 'all') {
params.append('operation_type', operationType);
}
// Agregar filtro de invoice_type si existe
if (invoiceType) {
params.append('invoice_type', invoiceType);
}
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/invoices?${params.toString()}`,
{},
cookies,
fetch,
'/login'
);
if (!response.ok) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
};
}
const data = await response.json();
return {
items: data.items || [],
total: data.total || 0,
page: data.page || 1,
page_size: data.page_size || 50,
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
};
} catch (error) {
console.error('Error loading invoices:', error);
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
operationType: 'all',
invoiceType: null
};
}
};

View File

@@ -1,382 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoicesApi, type Invoice } from '$lib/api/dashboard/a76/invoices';
import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
import CreateEditDialog from '$lib/components/dashboard/invoices/create-edit-dialog.svelte';
import DetailsDialog from '$lib/components/dashboard/invoices/details-dialog.svelte';
import DeleteDialog from '$lib/components/dashboard/invoices/delete-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto, invalidate } from '$app/navigation';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
// Recibir data del servidor
interface PageData {
items: Invoice[];
total: number;
page: number;
page_size: number;
error?: string;
companies: any[];
currentCompanyId?: number;
operationType?: string;
invoiceType?: string | null;
}
let { data }: { data: PageData } = $props();
// Estado para los diálogos
let showCreateDialog = $state(false);
let showDetailsDialog = $state(false);
let showDeleteDialog = $state(false);
let selectedInvoice = $state<Invoice | null>(null);
// Estado para el filtro de tipo (inicializado desde data del servidor)
let selectedType = $state<string>(data.operationType || 'all');
let selectedInvoiceType = $state<string | null>(data.invoiceType || null);
let availableInvoiceTypes = $state<InvoiceType[]>([]);
let loadingInvoiceTypes = $state(false);
// Actualizar URL cuando cambia el filtro
function handleTypeChange(value: string) {
selectedType = value;
const url = new URL(window.location.href);
if (value === 'all') {
url.searchParams.delete('operation_type');
url.searchParams.delete('invoice_type');
selectedInvoiceType = null;
} else {
url.searchParams.set('operation_type', value);
// Mantener invoice_type si existe
if (selectedInvoiceType) {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Actualizar URL cuando cambia el filtro de invoice_type
function handleInvoiceTypeChange(value: string) {
selectedInvoiceType = value === 'all' ? null : value;
const url = new URL(window.location.href);
if (!selectedInvoiceType) {
url.searchParams.delete('invoice_type');
} else {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
if (selectedType !== 'all') {
url.searchParams.set('operation_type', selectedType);
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Cargar tipos de factura disponibles según operation_type
async function loadInvoiceTypes(operationType: string) {
if (operationType === 'all') {
availableInvoiceTypes = [];
return;
}
loadingInvoiceTypes = true;
try {
const response = await invoiceTypesApi.list(1, 100, operationType);
if (response.data) {
availableInvoiceTypes = response.data.items;
}
} catch (e) {
console.error('Error loading invoice types:', e);
availableInvoiceTypes = [];
} finally {
loadingInvoiceTypes = false;
}
}
// Efecto para cargar tipos de factura cuando cambia selectedType
$effect(() => {
loadInvoiceTypes(selectedType);
});
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
invalidate('app:invoices');
};
// Escuchar eventos de facturas
const handleInvoiceView = (event: CustomEvent<Invoice>) => {
handleView(event.detail);
};
const handleInvoiceEdit = (event: CustomEvent<Invoice>) => {
handleEdit(event.detail);
};
const handleInvoiceDelete = (event: CustomEvent<Invoice>) => {
handleDelete(event.detail);
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
window.addEventListener('invoiceView', handleInvoiceView as EventListener);
window.addEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.addEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
window.removeEventListener('invoiceView', handleInvoiceView as EventListener);
window.removeEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.removeEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
};
}
});
// Estado para infinite scroll - inicializar con data del servidor
let allItems = $state<Invoice[]>(data.items || []);
let currentPage = $state(data.page || 1);
let pageSize = $state(data.page_size || 50);
let totalItems = $state(data.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Actualizar datos cuando cambia data del servidor
$effect(() => {
allItems = data.items || [];
currentPage = data.page || 1;
totalItems = data.total || 0;
error = data.error || null;
selectedInvoiceType = data.invoiceType || null;
selectedType = data.operationType || 'all';
});
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: any = {};
if (selectedType !== 'all') {
filters.operation_type = selectedType;
}
if (selectedInvoiceType) {
filters.invoice_type = selectedInvoiceType;
}
const response = await invoicesApi.list(
companyStore.activeCompany.id,
currentPage + 1,
pageSize,
Object.keys(filters).length > 0 ? filters : undefined
);
if (response.error) {
console.error('Error en loadMore:', response.error, 'Status:', response.status);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('Error loading more:', e);
} finally {
loading = false;
}
}
async function reloadData() {
// Invalidar datos para que el servidor recargue
await invalidate('app:invoices');
}
function handleCreateClick() {
selectedInvoice = null;
showCreateDialog = true;
}
function handleView(invoice: Invoice) {
selectedInvoice = invoice;
showDetailsDialog = true;
}
function handleEdit(invoice: Invoice) {
selectedInvoice = invoice;
showCreateDialog = true;
}
function handleDelete(invoice: Invoice) {
selectedInvoice = invoice;
showDeleteDialog = true;
}
function handleSuccess() {
reloadData();
}
// Crear columnas
const columns = createColumns();
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
<p class="text-muted-foreground">
Gestiona las facturas de importación y exportación
</p>
</div>
<Button href="/dashboard/invoices/importacion/compras_mexicanas/new">
<Plus class="mr-2" size={16} />
Nueva Factura
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Facturas</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
{#if companyStore.activeCompany}
- Compañía: {companyStore.activeCompany.name}
{/if}
</Card.Description>
</div>
<div class="flex items-center gap-2">
<Select.Root type="single" value={selectedType} onValueChange={handleTypeChange}>
<Select.Trigger class="w-[180px]">
{selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todas</Select.Item>
<Select.Item value="imp">Importación</Select.Item>
<Select.Item value="exp">Exportación</Select.Item>
</Select.Content>
</Select.Root>
{#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
<Select.Root
type="single"
value={selectedInvoiceType || 'all'}
onValueChange={handleInvoiceTypeChange}
disabled={loadingInvoiceTypes}
>
<Select.Trigger class="w-[250px]">
<span class="truncate">
{#if loadingInvoiceTypes}
Cargando...
{:else if selectedInvoiceType}
{(() => {
const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
})()}
{:else}
Todos los tipos
{/if}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todos los tipos</Select.Item>
{#each availableInvoiceTypes as invType}
<Select.Item value={invType.key}>
<span class="block truncate max-w-[300px]" title={`${invType.key} - ${invType.description}`}>
{invType.key} - {invType.description}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{/if}
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</div>
</Card.Header>
<Card.Content>
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
</div>
<!-- Diálogos -->
<CreateEditDialog
bind:open={showCreateDialog}
bind:item={selectedInvoice}
defaultOperationType={selectedType !== 'all' ? selectedType as 'imp' | 'exp' : undefined}
defaultInvoiceType={selectedInvoiceType || undefined}
onSuccess={handleSuccess}
/>
<DetailsDialog
bind:open={showDetailsDialog}
invoice={selectedInvoice}
/>
<DeleteDialog
bind:open={showDeleteDialog}
item={selectedInvoice}
onSuccess={handleSuccess}
/>

View File

@@ -1,325 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Tabs from "$lib/components/ui/tabs";
import * as Card from "$lib/components/ui/card";
import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices";
import { companyStore } from "$lib/stores/company.svelte";
import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte';
// DATOS FIJOS PARA ESTA CARPETA (DEFINITIVA)
const FIXED_OP_TYPE = "imp";
const FIXED_INV_TYPE = "MEX";
// ESTADO DEL FORMULARIO
let loading = $state(false);
let error = $state<string | null>(null);
let formData = $state({
// Header fields
operation_type: FIXED_OP_TYPE as "imp" | "exp",
invoice_type: FIXED_INV_TYPE,
invoice_number: "",
project_number: "",
purchase_order: "",
related_doc_id: null as number | null,
invoice_date: new Date().toISOString().split('T')[0], // Fecha de hoy por defecto
traffic_light_status: "",
observation_es: "",
observation_en: "",
comments_status: "",
cfdi_uuid: "",
path_pdf: "",
path_xml: "",
// Compliance MX fields
pedimento: "",
pedimento_code: "",
remesa: null as number | null,
aduana: "",
customs_broker_id: "",
provider_id: "",
sold_to_id: "",
shipped_to_id: "",
shipped_by_id: "",
is_mixed: false,
waste_type: "",
appendix_17: null as number | null,
edocument: "",
// Financials fields
currency: "MXN",
exchange_rate: null as number | null,
value_mn: null as number | null,
value_me: null as number | null,
customs_value_mn: null as number | null,
freight: null as number | null,
insurance: null as number | null,
iva_mn: null as number | null,
iva_factor: null as number | null,
total_quantity: null as number | null,
gross_weight: null as number | null,
net_weight: null as number | null,
bundle_count: null as number | null
});
async function handleSubmit(e: Event) {
e.preventDefault();
// VALIDACIÓN DE SEGURIDAD (Por si se refresca la página)
if (!companyStore.activeCompany?.id) {
error = "Error: No se detecta la compañía activa. Por favor, vuelve al dashboard y selecciona una compañía.";
return;
}
loading = true;
error = null;
try {
// Construimos el Payload (LÓGICA ORIGINAL QUE FUNCIONA)
const payload: CreateInvoiceData = {
...formData,
// Aseguramos que se envíen los fijos
operation_type: FIXED_OP_TYPE,
invoice_type: FIXED_INV_TYPE
};
const response = await invoicesApi.create(companyStore.activeCompany.id, payload);
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando...';
setTimeout(() => window.location.reload(), 1500);
} else {
error = response.error;
}
return;
}
// ÉXITO: Volvemos a la lista
goto('/dashboard/invoices/importacion/compras_mexicanas');
} catch (e) {
error = e instanceof Error ? e.message : "Error al guardar";
console.error("Error saving:", e);
} finally {
loading = false;
}
}
</script>
<div class="w-full mx-auto py-6 px-4 space-y-6 pb-48">
<div class="flex items-center gap-4">
<Button variant="outline" size="icon" href="/dashboard/invoices/importacion/compras_mexicanas">
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">Nueva Factura Compras Mexicanas</h1>
<p class="text-muted-foreground">Ingresa los datos para registrar la importación.</p>
</div>
</div>
{#if error}
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
{error}
</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-6">
<Card.Root>
<Card.Content class="p-6">
<Tabs.Root value="general" class="w-full">
<Tabs.Content value="general" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label>Tipo de Operación</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">Importación</div>
</div>
<div class="space-y-2">
<Label>Tipo de Factura</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">COMPRAS MEXICANAS (MEX)</div>
</div>
<div class="space-y-2">
<Label for="invoice_number">Número de Factura *</Label>
<Input id="invoice_number" bind:value={formData.invoice_number} required placeholder="Número de factura" />
</div>
<div class="space-y-2">
<Label for="project_number">Número de Proyecto</Label>
<Input id="project_number" bind:value={formData.project_number} placeholder="Número de proyecto" />
</div>
<div class="space-y-2">
<Label for="purchase_order">Orden de Compra</Label>
<Input id="purchase_order" bind:value={formData.purchase_order} placeholder="Orden de compra" />
</div>
<div class="space-y-2">
<Label for="invoice_date">Fecha de Factura *</Label>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} required />
</div>
<div class="space-y-2">
<Label for="traffic_light_status">Semáforo</Label>
<Input id="traffic_light_status" bind:value={formData.traffic_light_status} placeholder="Estado del semáforo" />
</div>
<div class="space-y-2">
<Label for="cfdi_uuid">CFDI UUID</Label>
<Input id="cfdi_uuid" bind:value={formData.cfdi_uuid} placeholder="UUID del CFDI" />
</div>
</div>
<div class="grid grid-cols-1 gap-4 pt-4">
<div class="space-y-2">
<Label for="observation_es">Observaciones (Español)</Label>
<Input id="observation_es" bind:value={formData.observation_es} placeholder="Observaciones en español" />
</div>
<div class="space-y-2">
<Label for="observation_en">Observaciones (Inglés)</Label>
<Input id="observation_en" bind:value={formData.observation_en} placeholder="Observaciones en inglés" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="compliance" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="pedimento">Pedimento</Label>
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Número de pedimento" />
</div>
<div class="space-y-2">
<Label for="pedimento_code">Código de Pedimento</Label>
<Input id="pedimento_code" bind:value={formData.pedimento_code} placeholder="R1, K1, etc." />
</div>
<div class="space-y-2">
<Label for="remesa">Remesa</Label>
<Input id="remesa" type="number" bind:value={formData.remesa} placeholder="Número de remesa" />
</div>
<div class="space-y-2">
<Label for="aduana">Aduana</Label>
<Input id="aduana" bind:value={formData.aduana} placeholder="Código de aduana" />
</div>
<div class="space-y-2">
<Label for="customs_broker_id">Agente Aduanal</Label>
<Input id="customs_broker_id" bind:value={formData.customs_broker_id} placeholder="ID del agente aduanal" />
</div>
<div class="space-y-2">
<Label for="provider_id">Proveedor</Label>
<Input id="provider_id" bind:value={formData.provider_id} placeholder="ID del proveedor" />
</div>
<div class="space-y-2">
<Label for="edocument">E-Document</Label>
<Input id="edocument" bind:value={formData.edocument} placeholder="Número de e-document" />
</div>
<div class="space-y-2 flex items-center gap-2 pt-8">
<input id="is_mixed" type="checkbox" bind:checked={formData.is_mixed} class="h-4 w-4" />
<Label for="is_mixed" class="!mt-0">Operación Mixta</Label>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="financials" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="currency">Moneda</Label>
<Input id="currency" bind:value={formData.currency} placeholder="MXN, USD, etc." />
</div>
<div class="space-y-2">
<Label for="exchange_rate">Tipo de Cambio</Label>
<Input id="exchange_rate" type="number" step="0.000001" bind:value={formData.exchange_rate} placeholder="Tipo de cambio" />
</div>
<div class="space-y-2">
<Label for="value_mn">Valor MN</Label>
<Input id="value_mn" type="number" step="0.01" bind:value={formData.value_mn} placeholder="Valor MN" />
</div>
<div class="space-y-2">
<Label for="value_me">Valor ME</Label>
<Input id="value_me" type="number" step="0.01" bind:value={formData.value_me} placeholder="Valor ME" />
</div>
<div class="space-y-2">
<Label for="customs_value_mn">Valor Aduana MN</Label>
<Input id="customs_value_mn" type="number" step="0.01" bind:value={formData.customs_value_mn} placeholder="Valor Aduana MN" />
</div>
<div class="space-y-2">
<Label for="freight">Flete</Label>
<Input id="freight" type="number" step="0.01" bind:value={formData.freight} placeholder="Costo Flete" />
</div>
<div class="space-y-2">
<Label for="insurance">Seguro</Label>
<Input id="insurance" type="number" step="0.01" bind:value={formData.insurance} placeholder="Costo Seguro" />
</div>
<div class="space-y-2">
<Label for="total_quantity">Cantidad Total</Label>
<Input id="total_quantity" type="number" step="0.01" bind:value={formData.total_quantity} placeholder="Cantidad Total" />
</div>
<div class="space-y-2">
<Label for="gross_weight">Peso Bruto</Label>
<Input id="gross_weight" type="number" step="0.01" bind:value={formData.gross_weight} placeholder="Peso Bruto" />
</div>
<div class="space-y-2">
<Label for="net_weight">Peso Neto</Label>
<Input id="net_weight" type="number" step="0.01" bind:value={formData.net_weight} placeholder="Peso Neto" />
</div>
<div class="space-y-2">
<Label for="bundle_count">Número de Bultos</Label>
<Input id="bundle_count" type="number" bind:value={formData.bundle_count} placeholder="Num. Bultos" />
</div>
</div>
</Tabs.Content>
<Tabs.List class="grid grid-cols-3 fixed bottom-24 left-1/2 -translate-x-1/2 w-[90%] max-w-lg z-40 shadow-xl bg-background border rounded-xl">
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="compliance">Cumplimiento</Tabs.Trigger>
<Tabs.Trigger value="financials">Financieros</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</Card.Content>
</Card.Root>
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<div class="w-full mx-auto flex justify-end gap-4 px-4">
<Button variant="outline" href="/dashboard/invoices/importacion/compras_mexicanas">
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
<Save class="mr-2 h-4 w-4" />
Guardar Factura
{/if}
</Button>
</div>
</div>
</form>
</div>

View File

@@ -1,390 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoicesApi, type Invoice } from '$lib/api/dashboard/a76/invoices';
import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
import CreateEditDialog from '$lib/components/dashboard/invoices/create-edit-dialog.svelte';
import DetailsDialog from '$lib/components/dashboard/invoices/details-dialog.svelte';
import DeleteDialog from '$lib/components/dashboard/invoices/delete-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto, invalidate } from '$app/navigation';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
// Recibir data del servidor
interface PageData {
items: Invoice[];
total: number;
page: number;
page_size: number;
error?: string;
companies: any[];
currentCompanyId?: number;
operationType?: string;
invoiceType?: string | null;
}
let { data }: { data: PageData } = $props();
// Estado para los diálogos
let showCreateDialog = $state(false);
let showDetailsDialog = $state(false);
let showDeleteDialog = $state(false);
let selectedInvoice = $state<Invoice | null>(null);
// Estado para el filtro de tipo (inicializado desde data del servidor)
let selectedType = $state<string>(data.operationType || 'all');
let selectedInvoiceType = $state<string | null>(data.invoiceType || null);
let availableInvoiceTypes = $state<InvoiceType[]>([]);
let loadingInvoiceTypes = $state(false);
// Actualizar URL cuando cambia el filtro
function handleTypeChange(value: string) {
selectedType = value;
const url = new URL(window.location.href);
if (value === 'all') {
url.searchParams.delete('operation_type');
url.searchParams.delete('invoice_type');
selectedInvoiceType = null;
} else {
url.searchParams.set('operation_type', value);
// Mantener invoice_type si existe
if (selectedInvoiceType) {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Actualizar URL cuando cambia el filtro de invoice_type
function handleInvoiceTypeChange(value: string) {
selectedInvoiceType = value === 'all' ? null : value;
const url = new URL(window.location.href);
if (!selectedInvoiceType) {
url.searchParams.delete('invoice_type');
} else {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
if (selectedType !== 'all') {
url.searchParams.set('operation_type', selectedType);
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Cargar tipos de factura disponibles según operation_type
async function loadInvoiceTypes(operationType: string) {
if (operationType === 'all') {
availableInvoiceTypes = [];
return;
}
loadingInvoiceTypes = true;
try {
const response = await invoiceTypesApi.list(1, 100, operationType);
if (response.data) {
availableInvoiceTypes = response.data.items;
}
} catch (e) {
console.error('Error loading invoice types:', e);
availableInvoiceTypes = [];
} finally {
loadingInvoiceTypes = false;
}
}
// Efecto para cargar tipos de factura cuando cambia selectedType
$effect(() => {
loadInvoiceTypes(selectedType);
});
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
invalidate('app:invoices');
};
// Escuchar eventos de facturas
const handleInvoiceView = (event: CustomEvent<Invoice>) => {
handleView(event.detail);
};
const handleInvoiceEdit = (event: CustomEvent<Invoice>) => {
// Esta es la función que realmente abre la ventana emergente
selectedInvoice = event.detail; // Carga los datos
showCreateDialog = true; // Abre el modal
};
window.addEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
return () => {
window.removeEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
};
const handleInvoiceDelete = (event: CustomEvent<Invoice>) => {
handleDelete(event.detail);
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
window.addEventListener('invoiceView', handleInvoiceView as EventListener);
window.addEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.addEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
window.removeEventListener('invoiceView', handleInvoiceView as EventListener);
window.removeEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.removeEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
};
}
});
// Estado para infinite scroll - inicializar con data del servidor
let allItems = $state<Invoice[]>(data.items || []);
let currentPage = $state(data.page || 1);
let pageSize = $state(data.page_size || 50);
let totalItems = $state(data.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Actualizar datos cuando cambia data del servidor
$effect(() => {
allItems = data.items || [];
currentPage = data.page || 1;
totalItems = data.total || 0;
error = data.error || null;
selectedInvoiceType = data.invoiceType || null;
selectedType = data.operationType || 'all';
});
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: any = {};
if (selectedType !== 'all') {
filters.operation_type = selectedType;
}
if (selectedInvoiceType) {
filters.invoice_type = selectedInvoiceType;
}
const response = await invoicesApi.list(
companyStore.activeCompany.id,
currentPage + 1,
pageSize,
Object.keys(filters).length > 0 ? filters : undefined
);
if (response.error) {
console.error('Error en loadMore:', response.error, 'Status:', response.status);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('Error loading more:', e);
} finally {
loading = false;
}
}
async function reloadData() {
// Invalidar datos para que el servidor recargue
await invalidate('app:invoices');
}
function handleCreateClick() {
selectedInvoice = null;
showCreateDialog = true;
}
function handleView(invoice: Invoice) {
selectedInvoice = invoice;
showDetailsDialog = true;
}
function handleEdit(invoice: Invoice) {
selectedInvoice = invoice;
showCreateDialog = true;
}
function handleDelete(invoice: Invoice) {
selectedInvoice = invoice;
showDeleteDialog = true;
}
function handleSuccess() {
reloadData();
}
// Crear columnas
const columns = createColumns();
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
<p class="text-muted-foreground">
Gestiona las facturas de importación y exportación
</p>
</div>
<Button href="/dashboard/invoices/importacion/definitiva/new">
<Plus class="mr-2" size={16} />
Nueva Factura
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Facturas</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
{#if companyStore.activeCompany}
- Compañía: {companyStore.activeCompany.name}
{/if}
</Card.Description>
</div>
<div class="flex items-center gap-2">
<Select.Root type="single" value={selectedType} onValueChange={handleTypeChange}>
<Select.Trigger class="w-[180px]">
{selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todas</Select.Item>
<Select.Item value="imp">Importación</Select.Item>
<Select.Item value="exp">Exportación</Select.Item>
</Select.Content>
</Select.Root>
{#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
<Select.Root
type="single"
value={selectedInvoiceType || 'all'}
onValueChange={handleInvoiceTypeChange}
disabled={loadingInvoiceTypes}
>
<Select.Trigger class="w-[250px]">
<span class="truncate">
{#if loadingInvoiceTypes}
Cargando...
{:else if selectedInvoiceType}
{(() => {
const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
})()}
{:else}
Todos los tipos
{/if}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todos los tipos</Select.Item>
{#each availableInvoiceTypes as invType}
<Select.Item value={invType.key}>
<span class="block truncate max-w-[300px]" title={`${invType.key} - ${invType.description}`}>
{invType.key} - {invType.description}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{/if}
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</div>
</Card.Header>
<Card.Content>
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
</div>
<!-- Diálogos -->
<CreateEditDialog
bind:open={showCreateDialog}
bind:item={selectedInvoice}
defaultOperationType={selectedType !== 'all' ? selectedType as 'imp' | 'exp' : undefined}
defaultInvoiceType={selectedInvoiceType || undefined}
onSuccess={handleSuccess}
/>
<DetailsDialog
bind:open={showDetailsDialog}
invoice={selectedInvoice}
/>
<DeleteDialog
bind:open={showDeleteDialog}
item={selectedInvoice}
onSuccess={handleSuccess}
/>

View File

@@ -1,325 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Tabs from "$lib/components/ui/tabs";
import * as Card from "$lib/components/ui/card";
import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices";
import { companyStore } from "$lib/stores/company.svelte";
import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte';
// DATOS FIJOS PARA ESTA CARPETA (DEFINITIVA)
const FIXED_OP_TYPE = "imp";
const FIXED_INV_TYPE = "DEF";
// ESTADO DEL FORMULARIO
let loading = $state(false);
let error = $state<string | null>(null);
let formData = $state({
// Header fields
operation_type: FIXED_OP_TYPE as "imp" | "exp",
invoice_type: FIXED_INV_TYPE,
invoice_number: "",
project_number: "",
purchase_order: "",
related_doc_id: null as number | null,
invoice_date: new Date().toISOString().split('T')[0], // Fecha de hoy por defecto
traffic_light_status: "",
observation_es: "",
observation_en: "",
comments_status: "",
cfdi_uuid: "",
path_pdf: "",
path_xml: "",
// Compliance MX fields
pedimento: "",
pedimento_code: "",
remesa: null as number | null,
aduana: "",
customs_broker_id: "",
provider_id: "",
sold_to_id: "",
shipped_to_id: "",
shipped_by_id: "",
is_mixed: false,
waste_type: "",
appendix_17: null as number | null,
edocument: "",
// Financials fields
currency: "MXN",
exchange_rate: null as number | null,
value_mn: null as number | null,
value_me: null as number | null,
customs_value_mn: null as number | null,
freight: null as number | null,
insurance: null as number | null,
iva_mn: null as number | null,
iva_factor: null as number | null,
total_quantity: null as number | null,
gross_weight: null as number | null,
net_weight: null as number | null,
bundle_count: null as number | null
});
async function handleSubmit(e: Event) {
e.preventDefault();
// VALIDACIÓN DE SEGURIDAD (Por si se refresca la página)
if (!companyStore.activeCompany?.id) {
error = "Error: No se detecta la compañía activa. Por favor, vuelve al dashboard y selecciona una compañía.";
return;
}
loading = true;
error = null;
try {
// Construimos el Payload (LÓGICA ORIGINAL QUE FUNCIONA)
const payload: CreateInvoiceData = {
...formData,
// Aseguramos que se envíen los fijos
operation_type: FIXED_OP_TYPE,
invoice_type: FIXED_INV_TYPE
};
const response = await invoicesApi.create(companyStore.activeCompany.id, payload);
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando...';
setTimeout(() => window.location.reload(), 1500);
} else {
error = response.error;
}
return;
}
// ÉXITO: Volvemos a la lista
goto('/dashboard/invoices/importacion/definitiva');
} catch (e) {
error = e instanceof Error ? e.message : "Error al guardar";
console.error("Error saving:", e);
} finally {
loading = false;
}
}
</script>
<div class="w-full mx-auto py-6 px-4 space-y-6 pb-48">
<div class="flex items-center gap-4">
<Button variant="outline" size="icon" href="/dashboard/invoices/importacion/definitiva">
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">Nueva Factura Definitiva</h1>
<p class="text-muted-foreground">Ingresa los datos para registrar la importación.</p>
</div>
</div>
{#if error}
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
⚠️ {error}
</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-6">
<Card.Root>
<Card.Content class="p-6">
<Tabs.Root value="general" class="w-full">
<Tabs.Content value="general" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label>Tipo de Operación</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">Importación</div>
</div>
<div class="space-y-2">
<Label>Tipo de Factura</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">DEFINITIVA (DEF)</div>
</div>
<div class="space-y-2">
<Label for="invoice_number">Número de Factura *</Label>
<Input id="invoice_number" bind:value={formData.invoice_number} required placeholder="Número de factura" />
</div>
<div class="space-y-2">
<Label for="project_number">Número de Proyecto</Label>
<Input id="project_number" bind:value={formData.project_number} placeholder="Número de proyecto" />
</div>
<div class="space-y-2">
<Label for="purchase_order">Orden de Compra</Label>
<Input id="purchase_order" bind:value={formData.purchase_order} placeholder="Orden de compra" />
</div>
<div class="space-y-2">
<Label for="invoice_date">Fecha de Factura *</Label>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} required />
</div>
<div class="space-y-2">
<Label for="traffic_light_status">Semáforo</Label>
<Input id="traffic_light_status" bind:value={formData.traffic_light_status} placeholder="Estado del semáforo" />
</div>
<div class="space-y-2">
<Label for="cfdi_uuid">CFDI UUID</Label>
<Input id="cfdi_uuid" bind:value={formData.cfdi_uuid} placeholder="UUID del CFDI" />
</div>
</div>
<div class="grid grid-cols-1 gap-4 pt-4">
<div class="space-y-2">
<Label for="observation_es">Observaciones (Español)</Label>
<Input id="observation_es" bind:value={formData.observation_es} placeholder="Observaciones en español" />
</div>
<div class="space-y-2">
<Label for="observation_en">Observaciones (Inglés)</Label>
<Input id="observation_en" bind:value={formData.observation_en} placeholder="Observaciones en inglés" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="compliance" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="pedimento">Pedimento</Label>
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Número de pedimento" />
</div>
<div class="space-y-2">
<Label for="pedimento_code">Código de Pedimento</Label>
<Input id="pedimento_code" bind:value={formData.pedimento_code} placeholder="R1, K1, etc." />
</div>
<div class="space-y-2">
<Label for="remesa">Remesa</Label>
<Input id="remesa" type="number" bind:value={formData.remesa} placeholder="Número de remesa" />
</div>
<div class="space-y-2">
<Label for="aduana">Aduana</Label>
<Input id="aduana" bind:value={formData.aduana} placeholder="Código de aduana" />
</div>
<div class="space-y-2">
<Label for="customs_broker_id">Agente Aduanal</Label>
<Input id="customs_broker_id" bind:value={formData.customs_broker_id} placeholder="ID del agente aduanal" />
</div>
<div class="space-y-2">
<Label for="provider_id">Proveedor</Label>
<Input id="provider_id" bind:value={formData.provider_id} placeholder="ID del proveedor" />
</div>
<div class="space-y-2">
<Label for="edocument">E-Document</Label>
<Input id="edocument" bind:value={formData.edocument} placeholder="Número de e-document" />
</div>
<div class="space-y-2 flex items-center gap-2 pt-8">
<input id="is_mixed" type="checkbox" bind:checked={formData.is_mixed} class="h-4 w-4" />
<Label for="is_mixed" class="!mt-0">Operación Mixta</Label>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="financials" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="currency">Moneda</Label>
<Input id="currency" bind:value={formData.currency} placeholder="MXN, USD, etc." />
</div>
<div class="space-y-2">
<Label for="exchange_rate">Tipo de Cambio</Label>
<Input id="exchange_rate" type="number" step="0.000001" bind:value={formData.exchange_rate} placeholder="Tipo de cambio" />
</div>
<div class="space-y-2">
<Label for="value_mn">Valor MN</Label>
<Input id="value_mn" type="number" step="0.01" bind:value={formData.value_mn} placeholder="Valor MN" />
</div>
<div class="space-y-2">
<Label for="value_me">Valor ME</Label>
<Input id="value_me" type="number" step="0.01" bind:value={formData.value_me} placeholder="Valor ME" />
</div>
<div class="space-y-2">
<Label for="customs_value_mn">Valor Aduana MN</Label>
<Input id="customs_value_mn" type="number" step="0.01" bind:value={formData.customs_value_mn} placeholder="Valor Aduana MN" />
</div>
<div class="space-y-2">
<Label for="freight">Flete</Label>
<Input id="freight" type="number" step="0.01" bind:value={formData.freight} placeholder="Costo Flete" />
</div>
<div class="space-y-2">
<Label for="insurance">Seguro</Label>
<Input id="insurance" type="number" step="0.01" bind:value={formData.insurance} placeholder="Costo Seguro" />
</div>
<div class="space-y-2">
<Label for="total_quantity">Cantidad Total</Label>
<Input id="total_quantity" type="number" step="0.01" bind:value={formData.total_quantity} placeholder="Cantidad Total" />
</div>
<div class="space-y-2">
<Label for="gross_weight">Peso Bruto</Label>
<Input id="gross_weight" type="number" step="0.01" bind:value={formData.gross_weight} placeholder="Peso Bruto" />
</div>
<div class="space-y-2">
<Label for="net_weight">Peso Neto</Label>
<Input id="net_weight" type="number" step="0.01" bind:value={formData.net_weight} placeholder="Peso Neto" />
</div>
<div class="space-y-2">
<Label for="bundle_count">Número de Bultos</Label>
<Input id="bundle_count" type="number" bind:value={formData.bundle_count} placeholder="Num. Bultos" />
</div>
</div>
</Tabs.Content>
<Tabs.List class="grid grid-cols-3 fixed bottom-24 left-1/2 -translate-x-1/2 w-[90%] max-w-lg z-40 shadow-xl bg-background border rounded-xl">
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="compliance">Cumplimiento</Tabs.Trigger>
<Tabs.Trigger value="financials">Financieros</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</Card.Content>
</Card.Root>
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<div class="w-full mx-auto flex justify-end gap-4 px-4">
<Button variant="outline" href="/dashboard/invoices/importacion/definitiva">
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
<Save class="mr-2 h-4 w-4" />
Guardar Factura
{/if}
</Button>
</div>
</div>
</form>
</div>

View File

@@ -1,114 +0,0 @@
import type { PageServerLoad } from '../$types';
import { redirect } from '@sveltejs/kit';
import {
getAuthTokens,
authenticatedFetch
} from '$lib/server/api';
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
// Verificar autenticación
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
try {
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)
// 2. Cookie active_company_id (setted por el team-switcher)
// 3. Primera compañía del usuario (fallback)
const companyIdParam = url.searchParams.get('company_id');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
? parseInt(companyIdParam)
: cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
// Si aún no hay companyId, mostrar error
if (!companyId) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'No se encontró una compañía seleccionada',
companies: parentData.companies || []
};
}
// Obtener filtro de tipo de operación
const operationType = 'imp'
const invoiceType = 'TEM'
// Construir parámetros de consulta
const params = new URLSearchParams({
company_id: companyId.toString(),
page: '1',
page_size: '50'
});
// Agregar filtro de tipo si existe y no es 'all'
if (operationType && operationType !== 'all') {
params.append('operation_type', operationType);
}
// Agregar filtro de invoice_type si existe
if (invoiceType) {
params.append('invoice_type', invoiceType);
}
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/invoices?${params.toString()}`,
{},
cookies,
fetch,
'/login'
);
if (!response.ok) {
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
};
}
const data = await response.json();
return {
items: data.items || [],
total: data.total || 0,
page: data.page || 1,
page_size: data.page_size || 50,
companies: parentData.companies || [],
currentCompanyId: companyId,
operationType: operationType || 'all',
invoiceType: invoiceType || null
};
} catch (error) {
console.error('Error loading invoices:', error);
return {
items: [],
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
operationType: 'all',
invoiceType: null
};
}
};

View File

@@ -1,382 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoicesApi, type Invoice } from '$lib/api/dashboard/a76/invoices';
import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
import CreateEditDialog from '$lib/components/dashboard/invoices/create-edit-dialog.svelte';
import DetailsDialog from '$lib/components/dashboard/invoices/details-dialog.svelte';
import DeleteDialog from '$lib/components/dashboard/invoices/delete-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select';
import { Plus, RefreshCw } from 'lucide-svelte';
import { goto, invalidate } from '$app/navigation';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
// Recibir data del servidor
interface PageData {
items: Invoice[];
total: number;
page: number;
page_size: number;
error?: string;
companies: any[];
currentCompanyId?: number;
operationType?: string;
invoiceType?: string | null;
}
let { data }: { data: PageData } = $props();
// Estado para los diálogos
let showCreateDialog = $state(false);
let showDetailsDialog = $state(false);
let showDeleteDialog = $state(false);
let selectedInvoice = $state<Invoice | null>(null);
// Estado para el filtro de tipo (inicializado desde data del servidor)
let selectedType = $state<string>(data.operationType || 'all');
let selectedInvoiceType = $state<string | null>(data.invoiceType || null);
let availableInvoiceTypes = $state<InvoiceType[]>([]);
let loadingInvoiceTypes = $state(false);
// Actualizar URL cuando cambia el filtro
function handleTypeChange(value: string) {
selectedType = value;
const url = new URL(window.location.href);
if (value === 'all') {
url.searchParams.delete('operation_type');
url.searchParams.delete('invoice_type');
selectedInvoiceType = null;
} else {
url.searchParams.set('operation_type', value);
// Mantener invoice_type si existe
if (selectedInvoiceType) {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Actualizar URL cuando cambia el filtro de invoice_type
function handleInvoiceTypeChange(value: string) {
selectedInvoiceType = value === 'all' ? null : value;
const url = new URL(window.location.href);
if (!selectedInvoiceType) {
url.searchParams.delete('invoice_type');
} else {
url.searchParams.set('invoice_type', selectedInvoiceType);
}
if (selectedType !== 'all') {
url.searchParams.set('operation_type', selectedType);
}
goto(url.toString(), { keepFocus: true, noScroll: true });
}
// Cargar tipos de factura disponibles según operation_type
async function loadInvoiceTypes(operationType: string) {
if (operationType === 'all') {
availableInvoiceTypes = [];
return;
}
loadingInvoiceTypes = true;
try {
const response = await invoiceTypesApi.list(1, 100, operationType);
if (response.data) {
availableInvoiceTypes = response.data.items;
}
} catch (e) {
console.error('Error loading invoice types:', e);
availableInvoiceTypes = [];
} finally {
loadingInvoiceTypes = false;
}
}
// Efecto para cargar tipos de factura cuando cambia selectedType
$effect(() => {
loadInvoiceTypes(selectedType);
});
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
invalidate('app:invoices');
};
// Escuchar eventos de facturas
const handleInvoiceView = (event: CustomEvent<Invoice>) => {
handleView(event.detail);
};
const handleInvoiceEdit = (event: CustomEvent<Invoice>) => {
handleEdit(event.detail);
};
const handleInvoiceDelete = (event: CustomEvent<Invoice>) => {
handleDelete(event.detail);
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
window.addEventListener('invoiceView', handleInvoiceView as EventListener);
window.addEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.addEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
window.removeEventListener('invoiceView', handleInvoiceView as EventListener);
window.removeEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
window.removeEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
};
}
});
// Estado para infinite scroll - inicializar con data del servidor
let allItems = $state<Invoice[]>(data.items || []);
let currentPage = $state(data.page || 1);
let pageSize = $state(data.page_size || 50);
let totalItems = $state(data.total || 0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Actualizar datos cuando cambia data del servidor
$effect(() => {
allItems = data.items || [];
currentPage = data.page || 1;
totalItems = data.total || 0;
error = data.error || null;
selectedInvoiceType = data.invoiceType || null;
selectedType = data.operationType || 'all';
});
async function loadMore() {
if (loading || !hasMore || !companyStore.activeCompany) return;
loading = true;
error = null;
try {
const filters: any = {};
if (selectedType !== 'all') {
filters.operation_type = selectedType;
}
if (selectedInvoiceType) {
filters.invoice_type = selectedInvoiceType;
}
const response = await invoicesApi.list(
companyStore.activeCompany.id,
currentPage + 1,
pageSize,
Object.keys(filters).length > 0 ? filters : undefined
);
if (response.error) {
console.error('Error en loadMore:', response.error, 'Status:', response.status);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('Error loading more:', e);
} finally {
loading = false;
}
}
async function reloadData() {
// Invalidar datos para que el servidor recargue
await invalidate('app:invoices');
}
function handleCreateClick() {
selectedInvoice = null;
showCreateDialog = true;
}
function handleView(invoice: Invoice) {
selectedInvoice = invoice;
showDetailsDialog = true;
}
function handleEdit(invoice: Invoice) {
selectedInvoice = invoice;
showCreateDialog = true;
}
function handleDelete(invoice: Invoice) {
selectedInvoice = invoice;
showDeleteDialog = true;
}
function handleSuccess() {
reloadData();
}
// Crear columnas
const columns = createColumns();
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
<p class="text-muted-foreground">
Gestiona las facturas de importación y exportación
</p>
</div>
<Button href="/dashboard/invoices/importacion/temporal/new">
<Plus class="mr-2" size={16} />
Nueva Factura
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Facturas</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
{#if companyStore.activeCompany}
- Compañía: {companyStore.activeCompany.name}
{/if}
</Card.Description>
</div>
<div class="flex items-center gap-2">
<Select.Root type="single" value={selectedType} onValueChange={handleTypeChange}>
<Select.Trigger class="w-[180px]">
{selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todas</Select.Item>
<Select.Item value="imp">Importación</Select.Item>
<Select.Item value="exp">Exportación</Select.Item>
</Select.Content>
</Select.Root>
{#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
<Select.Root
type="single"
value={selectedInvoiceType || 'all'}
onValueChange={handleInvoiceTypeChange}
disabled={loadingInvoiceTypes}
>
<Select.Trigger class="w-[250px]">
<span class="truncate">
{#if loadingInvoiceTypes}
Cargando...
{:else if selectedInvoiceType}
{(() => {
const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
})()}
{:else}
Todos los tipos
{/if}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="all">Todos los tipos</Select.Item>
{#each availableInvoiceTypes as invType}
<Select.Item value={invType.key}>
<span class="block truncate max-w-[300px]" title={`${invType.key} - ${invType.description}`}>
{invType.key} - {invType.description}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{/if}
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</div>
</Card.Header>
<Card.Content>
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
</div>
<!-- Diálogos -->
<CreateEditDialog
bind:open={showCreateDialog}
bind:item={selectedInvoice}
defaultOperationType={selectedType !== 'all' ? selectedType as 'imp' | 'exp' : undefined}
defaultInvoiceType={selectedInvoiceType || undefined}
onSuccess={handleSuccess}
/>
<DetailsDialog
bind:open={showDetailsDialog}
invoice={selectedInvoice}
/>
<DeleteDialog
bind:open={showDeleteDialog}
item={selectedInvoice}
onSuccess={handleSuccess}
/>

View File

@@ -1,325 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Tabs from "$lib/components/ui/tabs";
import * as Card from "$lib/components/ui/card";
import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices";
import { companyStore } from "$lib/stores/company.svelte";
import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte';
// DATOS FIJOS PARA ESTA CARPETA (TEMINITIVA)
const FIXED_OP_TYPE = "imp";
const FIXED_INV_TYPE = "TEM";
// ESTADO DEL FORMULARIO
let loading = $state(false);
let error = $state<string | null>(null);
let formData = $state({
// Header fields
operation_type: FIXED_OP_TYPE as "imp" | "exp",
invoice_type: FIXED_INV_TYPE,
invoice_number: "",
project_number: "",
purchase_order: "",
related_doc_id: null as number | null,
invoice_date: new Date().toISOString().split('T')[0], // Fecha de hoy por defecto
traffic_light_status: "",
observation_es: "",
observation_en: "",
comments_status: "",
cfdi_uuid: "",
path_pdf: "",
path_xml: "",
// Compliance MX fields
pedimento: "",
pedimento_code: "",
remesa: null as number | null,
aduana: "",
customs_broker_id: "",
provider_id: "",
sold_to_id: "",
shipped_to_id: "",
shipped_by_id: "",
is_mixed: false,
waste_type: "",
appendix_17: null as number | null,
edocument: "",
// Financials fields
currency: "MXN",
exchange_rate: null as number | null,
value_mn: null as number | null,
value_me: null as number | null,
customs_value_mn: null as number | null,
freight: null as number | null,
insurance: null as number | null,
iva_mn: null as number | null,
iva_factor: null as number | null,
total_quantity: null as number | null,
gross_weight: null as number | null,
net_weight: null as number | null,
bundle_count: null as number | null
});
async function handleSubmit(e: Event) {
e.preventDefault();
// VALIDACIÓN DE SEGURIDAD (Por si se refresca la página)
if (!companyStore.activeCompany?.id) {
error = "Error: No se detecta la compañía activa. Por favor, vuelve al dashboard y selecciona una compañía.";
return;
}
loading = true;
error = null;
try {
// Construimos el Payload (LÓGICA ORIGINAL QUE FUNCIONA)
const payload: CreateInvoiceData = {
...formData,
// Aseguramos que se envíen los fijos
operation_type: FIXED_OP_TYPE,
invoice_type: FIXED_INV_TYPE
};
const response = await invoicesApi.create(companyStore.activeCompany.id, payload);
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando...';
setTimeout(() => window.location.reload(), 1500);
} else {
error = response.error;
}
return;
}
// ÉXITO: Volvemos a la lista
goto('/dashboard/invoices/importacion/temporal');
} catch (e) {
error = e instanceof Error ? e.message : "Error al guardar";
console.error("Error saving:", e);
} finally {
loading = false;
}
}
</script>
<div class="w-full mx-auto py-6 px-4 space-y-6 pb-48">
<div class="flex items-center gap-4">
<Button variant="outline" size="icon" href="/dashboard/invoices/importacion/temporal">
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">Nueva Factura Temporal</h1>
<p class="text-muted-foreground">Ingresa los datos para registrar la importación.</p>
</div>
</div>
{#if error}
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
⚠️ {error}
</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-6">
<Card.Root>
<Card.Content class="p-6">
<Tabs.Root value="general" class="w-full">
<Tabs.Content value="general" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label>Tipo de Operación</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">Importación</div>
</div>
<div class="space-y-2">
<Label>Tipo de Factura</Label>
<div class="px-3 py-2 bg-muted rounded-md text-sm font-medium">TEMPORAL (TEM)</div>
</div>
<div class="space-y-2">
<Label for="invoice_number">Número de Factura *</Label>
<Input id="invoice_number" bind:value={formData.invoice_number} required placeholder="Número de factura" />
</div>
<div class="space-y-2">
<Label for="project_number">Número de Proyecto</Label>
<Input id="project_number" bind:value={formData.project_number} placeholder="Número de proyecto" />
</div>
<div class="space-y-2">
<Label for="purchase_order">Orden de Compra</Label>
<Input id="purchase_order" bind:value={formData.purchase_order} placeholder="Orden de compra" />
</div>
<div class="space-y-2">
<Label for="invoice_date">Fecha de Factura *</Label>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} required />
</div>
<div class="space-y-2">
<Label for="traffic_light_status">Semáforo</Label>
<Input id="traffic_light_status" bind:value={formData.traffic_light_status} placeholder="Estado del semáforo" />
</div>
<div class="space-y-2">
<Label for="cfdi_uuid">CFDI UUID</Label>
<Input id="cfdi_uuid" bind:value={formData.cfdi_uuid} placeholder="UUID del CFDI" />
</div>
</div>
<div class="grid grid-cols-1 gap-4 pt-4">
<div class="space-y-2">
<Label for="observation_es">Observaciones (Español)</Label>
<Input id="observation_es" bind:value={formData.observation_es} placeholder="Observaciones en español" />
</div>
<div class="space-y-2">
<Label for="observation_en">Observaciones (Inglés)</Label>
<Input id="observation_en" bind:value={formData.observation_en} placeholder="Observaciones en inglés" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="compliance" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="pedimento">Pedimento</Label>
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Número de pedimento" />
</div>
<div class="space-y-2">
<Label for="pedimento_code">Código de Pedimento</Label>
<Input id="pedimento_code" bind:value={formData.pedimento_code} placeholder="R1, K1, etc." />
</div>
<div class="space-y-2">
<Label for="remesa">Remesa</Label>
<Input id="remesa" type="number" bind:value={formData.remesa} placeholder="Número de remesa" />
</div>
<div class="space-y-2">
<Label for="aduana">Aduana</Label>
<Input id="aduana" bind:value={formData.aduana} placeholder="Código de aduana" />
</div>
<div class="space-y-2">
<Label for="customs_broker_id">Agente Aduanal</Label>
<Input id="customs_broker_id" bind:value={formData.customs_broker_id} placeholder="ID del agente aduanal" />
</div>
<div class="space-y-2">
<Label for="provider_id">Proveedor</Label>
<Input id="provider_id" bind:value={formData.provider_id} placeholder="ID del proveedor" />
</div>
<div class="space-y-2">
<Label for="edocument">E-Document</Label>
<Input id="edocument" bind:value={formData.edocument} placeholder="Número de e-document" />
</div>
<div class="space-y-2 flex items-center gap-2 pt-8">
<input id="is_mixed" type="checkbox" bind:checked={formData.is_mixed} class="h-4 w-4" />
<Label for="is_mixed" class="!mt-0">Operación Mixta</Label>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="financials" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="currency">Moneda</Label>
<Input id="currency" bind:value={formData.currency} placeholder="MXN, USD, etc." />
</div>
<div class="space-y-2">
<Label for="exchange_rate">Tipo de Cambio</Label>
<Input id="exchange_rate" type="number" step="0.000001" bind:value={formData.exchange_rate} placeholder="Tipo de cambio" />
</div>
<div class="space-y-2">
<Label for="value_mn">Valor MN</Label>
<Input id="value_mn" type="number" step="0.01" bind:value={formData.value_mn} placeholder="Valor MN" />
</div>
<div class="space-y-2">
<Label for="value_me">Valor ME</Label>
<Input id="value_me" type="number" step="0.01" bind:value={formData.value_me} placeholder="Valor ME" />
</div>
<div class="space-y-2">
<Label for="customs_value_mn">Valor Aduana MN</Label>
<Input id="customs_value_mn" type="number" step="0.01" bind:value={formData.customs_value_mn} placeholder="Valor Aduana MN" />
</div>
<div class="space-y-2">
<Label for="freight">Flete</Label>
<Input id="freight" type="number" step="0.01" bind:value={formData.freight} placeholder="Costo Flete" />
</div>
<div class="space-y-2">
<Label for="insurance">Seguro</Label>
<Input id="insurance" type="number" step="0.01" bind:value={formData.insurance} placeholder="Costo Seguro" />
</div>
<div class="space-y-2">
<Label for="total_quantity">Cantidad Total</Label>
<Input id="total_quantity" type="number" step="0.01" bind:value={formData.total_quantity} placeholder="Cantidad Total" />
</div>
<div class="space-y-2">
<Label for="gross_weight">Peso Bruto</Label>
<Input id="gross_weight" type="number" step="0.01" bind:value={formData.gross_weight} placeholder="Peso Bruto" />
</div>
<div class="space-y-2">
<Label for="net_weight">Peso Neto</Label>
<Input id="net_weight" type="number" step="0.01" bind:value={formData.net_weight} placeholder="Peso Neto" />
</div>
<div class="space-y-2">
<Label for="bundle_count">Número de Bultos</Label>
<Input id="bundle_count" type="number" bind:value={formData.bundle_count} placeholder="Num. Bultos" />
</div>
</div>
</Tabs.Content>
<Tabs.List class="grid grid-cols-3 fixed bottom-24 left-1/2 -translate-x-1/2 w-[90%] max-w-lg z-40 shadow-xl bg-background border rounded-xl">
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="compliance">Cumplimiento</Tabs.Trigger>
<Tabs.Trigger value="financials">Financieros</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</Card.Content>
</Card.Root>
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<div class="w-full mx-auto flex justify-end gap-4 px-4">
<Button variant="outline" href="/dashboard/invoices/importacion/temporal">
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
<Save class="mr-2 h-4 w-4" />
Guardar Factura
{/if}
</Button>
</div>
</div>
</form>
</div>