feat: add logistics tab form for invoice editing
feat: implement server-side loading for invoices page with authentication and filters feat: create invoices page with filtering, infinite scroll, and data table feat: add server-side loading for invoice edit page with data fetching feat: implement invoice edit page with tabbed interface and form handling
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -59,3 +59,4 @@ node_modules/
|
||||
|
||||
# Docker
|
||||
*.dockerignore
|
||||
postgres-data/
|
||||
1190
estructura.txt
1190
estructura.txt
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<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 type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
|
||||
let {
|
||||
invoice,
|
||||
formData = $bindable(),
|
||||
exists = $bindable(),
|
||||
customsBrokers = [],
|
||||
clients = [],
|
||||
providers = []
|
||||
}: {
|
||||
invoice: Invoice | null;
|
||||
formData?: any;
|
||||
exists?: boolean;
|
||||
customsBrokers?: any[];
|
||||
clients?: any[];
|
||||
providers?: any[];
|
||||
} = $props();
|
||||
|
||||
if (!formData && invoice?.compliance_mx) {
|
||||
formData = { ...invoice.compliance_mx };
|
||||
exists = true;
|
||||
} else if (!formData) {
|
||||
formData = {
|
||||
pedimento: '',
|
||||
pedimento_code: '',
|
||||
remesa: null,
|
||||
aduana: '',
|
||||
provider_id: '',
|
||||
sold_to_id: '',
|
||||
shipped_to_id: '',
|
||||
shipped_by_id: '',
|
||||
customs_broker_id: ''
|
||||
};
|
||||
exists = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Cumplimiento Aduanal</Card.Title>
|
||||
<Card.Description>Información de cumplimiento y aduanas</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 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="aduana">Aduana</Label>
|
||||
<Input id="aduana" bind:value={formData.aduana} placeholder="Código de aduana" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">Más campos por implementar...</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -0,0 +1,53 @@
|
||||
<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 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 = { ...invoice.financials };
|
||||
exists = true;
|
||||
} else if (!formData) {
|
||||
formData = {
|
||||
currency: '',
|
||||
exchange_rate: null,
|
||||
value_mn: null,
|
||||
value_me: null,
|
||||
freight: null,
|
||||
insurance: null,
|
||||
gross_weight: null,
|
||||
net_weight: null
|
||||
};
|
||||
exists = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Información Financiera</Card.Title>
|
||||
<Card.Description>Valores, monedas y datos financieros</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<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="0.00" />
|
||||
</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="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">Más campos por implementar...</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -0,0 +1,224 @@
|
||||
<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 * as Select from '$lib/components/ui/select';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
|
||||
|
||||
let {
|
||||
invoice,
|
||||
formData = $bindable(),
|
||||
invoiceTypes = [],
|
||||
defaultOperationType = undefined,
|
||||
defaultInvoiceType = undefined
|
||||
}: {
|
||||
invoice: Invoice | null;
|
||||
formData?: any;
|
||||
invoiceTypes?: InvoiceType[];
|
||||
defaultOperationType?: number | null;
|
||||
defaultInvoiceType?: string | null;
|
||||
} = $props();
|
||||
|
||||
if (!formData) {
|
||||
if (invoice) {
|
||||
// Editando una factura existente
|
||||
formData = {
|
||||
operation_type: invoice.operation_type || null,
|
||||
invoice_type: invoice.invoice_type || '',
|
||||
invoice_number: invoice.invoice_number || '',
|
||||
invoice_date: invoice.invoice_date || '',
|
||||
project_number: invoice.project_number || '',
|
||||
traffic_light_status: invoice.traffic_light_status || 'green',
|
||||
observation_es: invoice.observation_es || ''
|
||||
};
|
||||
} else {
|
||||
// Creando una nueva factura - usar valores por defecto de los filtros si están disponibles
|
||||
formData = {
|
||||
operation_type: defaultOperationType ?? null,
|
||||
invoice_type: defaultInvoiceType ?? '',
|
||||
invoice_number: '',
|
||||
invoice_date: '',
|
||||
project_number: '',
|
||||
traffic_light_status: 'green',
|
||||
observation_es: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Todas las opciones de tipo de factura disponibles con su campo de operación
|
||||
const allInvoiceTypeOptions = invoiceTypes.map(t => ({
|
||||
key: t.key,
|
||||
description: t.description,
|
||||
operation: t.operation // 'imp', 'exp', 'both'
|
||||
}));
|
||||
|
||||
// Filtrar tipos de factura basados en operation_type
|
||||
const filteredInvoiceTypes = $derived(() => {
|
||||
if (formData.operation_type === null) {
|
||||
// Mostrar todos
|
||||
return allInvoiceTypeOptions;
|
||||
}
|
||||
|
||||
// 1 = Exportación, 2 = Importación
|
||||
const targetOp = formData.operation_type === 1 ? 'exp' : 'imp';
|
||||
|
||||
return allInvoiceTypeOptions.filter(t =>
|
||||
t.operation === 'both' || t.operation === targetOp
|
||||
);
|
||||
});
|
||||
|
||||
// Limpiar invoice_type si ya no es válido para operation_type
|
||||
$effect(() => {
|
||||
if (!formData.invoice_type) return;
|
||||
|
||||
const isValid = filteredInvoiceTypes().some(t => t.key === formData.invoice_type);
|
||||
if (!isValid) {
|
||||
formData.invoice_type = '';
|
||||
}
|
||||
});
|
||||
|
||||
const operationOptions = [
|
||||
{ value: 1, label: 'Exportación' },
|
||||
{ value: 2, label: 'Importación' },
|
||||
];
|
||||
|
||||
const trafficLightOptions = [
|
||||
{ value: 'green', label: 'Verde' },
|
||||
{ value: 'yellow', label: 'Amarillo' },
|
||||
{ value: 'red', label: 'Rojo' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Información General</Card.Title>
|
||||
<Card.Description>
|
||||
Edita los datos principales de la factura
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="space-y-6">
|
||||
<!-- Fila 1: Tipo de Operación, Tipo de Factura -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="operation_type">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">
|
||||
<span class="truncate">
|
||||
{formData.operation_type !== null
|
||||
? operationOptions.find(o => o.value === formData.operation_type)?.label
|
||||
: 'Selecciona tipo...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each operationOptions as option}
|
||||
<Select.Item value={String(option.value)}>{option.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="invoice_type">Tipo de Factura *</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.invoice_type || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.invoice_type = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="invoice_type">
|
||||
<span class="truncate">
|
||||
{formData.invoice_type
|
||||
? `${formData.invoice_type} - ${filteredInvoiceTypes().find(t => t.key === formData.invoice_type)?.description || ''}`
|
||||
: 'Selecciona tipo...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each filteredInvoiceTypes() as type}
|
||||
<Select.Item value={type.key}>
|
||||
{type.key} - {type.description}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fila 2: Número de Factura, Fecha de Factura -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="invoice_number">Número de Factura *</Label>
|
||||
<Input
|
||||
id="invoice_number"
|
||||
bind:value={formData.invoice_number}
|
||||
placeholder="Ej: INV-2024-001"
|
||||
required
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fila 3: Número de Proyecto, Semáforo -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<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="traffic_light_status">Semáforo</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.traffic_light_status || 'green'}
|
||||
onValueChange={(v) => {
|
||||
formData.traffic_light_status = v ?? 'green';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="traffic_light_status">
|
||||
<span class="truncate">
|
||||
{trafficLightOptions.find(t => t.value === formData.traffic_light_status)?.label || 'Verde'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each trafficLightOptions as option}
|
||||
<Select.Item value={option.value}>{option.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Observaciones -->
|
||||
<div class="space-y-2">
|
||||
<Label for="observation_es">Observaciones (Español)</Label>
|
||||
<Textarea
|
||||
id="observation_es"
|
||||
bind:value={formData.observation_es}
|
||||
placeholder="Notas u observaciones adicionales..."
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -0,0 +1,73 @@
|
||||
<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 { Plus } 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 => ({ ...l }));
|
||||
exists = true;
|
||||
} else if (!formData) {
|
||||
formData = [];
|
||||
exists = false;
|
||||
}
|
||||
|
||||
function addLogistic() {
|
||||
formData = [...formData, {
|
||||
carrier_id: '',
|
||||
transport_type: null,
|
||||
driver_name: '',
|
||||
vehicle_num: '',
|
||||
license_plate: ''
|
||||
}];
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<Card.Title>Logística y Transporte</Card.Title>
|
||||
<Card.Description>Información de transportistas y vehículos</Card.Description>
|
||||
</div>
|
||||
<Button onclick={addLogistic} size="sm">
|
||||
<Plus size={16} class="mr-2" />
|
||||
Agregar
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-4">
|
||||
{#if formData.length === 0}
|
||||
<p class="text-sm text-muted-foreground text-center py-8">No hay registros de logística. Haz clic en "Agregar" para crear uno.</p>
|
||||
{:else}
|
||||
{#each formData as logistic, index}
|
||||
<div class="border rounded-lg p-4 space-y-4">
|
||||
<h4 class="font-semibold">Logística #{index + 1}</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Transportista</Label>
|
||||
<Input bind:value={logistic.carrier_id} placeholder="ID del transportista" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Conductor</Label>
|
||||
<Input bind:value={logistic.driver_name} placeholder="Nombre del conductor" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">Más campos por implementar...</p>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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 = 'exp'
|
||||
const invoiceType = 'REPAR'
|
||||
|
||||
// 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 || []
|
||||
};
|
||||
}
|
||||
};
|
||||
469
frontend/src/routes/dashboard/invoices/+page.svelte
Normal file
469
frontend/src/routes/dashboard/invoices/+page.svelte
Normal file
@@ -0,0 +1,469 @@
|
||||
<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 = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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() {
|
||||
// Construir URL con los filtros actuales como query parameters
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Mapear operation_type de 'imp'/'exp' a números 1/2
|
||||
if (filters.operation_type) {
|
||||
const operationTypeNumber = filters.operation_type === 'exp' ? 1 : 2;
|
||||
params.set('operation_type', operationTypeNumber.toString());
|
||||
}
|
||||
|
||||
if (filters.invoice_type) {
|
||||
params.set('invoice_type', filters.invoice_type);
|
||||
}
|
||||
|
||||
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</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form onsubmit={(e) => { e.preventDefault(); applyFilters(); }} 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 class="flex items-end gap-2 md:col-span-5">
|
||||
<Button type="submit" disabled={loading} class="flex-1">
|
||||
<Filter class="mr-2" size={16} />
|
||||
Filtrar
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onclick={clearFilters} disabled={loading}>
|
||||
<Trash2 size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
143
frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts
Normal file
143
frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
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
|
||||
);
|
||||
|
||||
// Si el ID es "new", es una creación
|
||||
if (params.id === 'new') {
|
||||
const [invoiceTypesResponse, customsBrokersResponse, clientsResponse, providersResponse] = await Promise.all([
|
||||
invoiceTypesPromise,
|
||||
customsBrokersPromise,
|
||||
clientsPromise,
|
||||
providersPromise
|
||||
]);
|
||||
|
||||
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: [] };
|
||||
|
||||
return {
|
||||
invoice: null,
|
||||
invoiceId: null,
|
||||
isCreate: true,
|
||||
invoiceTypes: invoiceTypes.items || [],
|
||||
customsBrokers: customsBrokers.items || [],
|
||||
clients: clients.items || [],
|
||||
providers: providers.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] = await Promise.all([
|
||||
invoiceTypesPromise,
|
||||
customsBrokersPromise,
|
||||
clientsPromise,
|
||||
providersPromise
|
||||
]);
|
||||
|
||||
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: [] };
|
||||
|
||||
return {
|
||||
invoice,
|
||||
invoiceId,
|
||||
isCreate: false,
|
||||
invoiceTypes: invoiceTypes.items || [],
|
||||
customsBrokers: customsBrokers.items || [],
|
||||
clients: clients.items || [],
|
||||
providers: providers.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');
|
||||
}
|
||||
};
|
||||
399
frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte
Normal file
399
frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte
Normal file
@@ -0,0 +1,399 @@
|
||||
<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,
|
||||
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 ComplianceTabForm from '$lib/components/dashboard/invoices/edit/compliance-tab-form.svelte';
|
||||
import FinancialsTabForm from '$lib/components/dashboard/invoices/edit/financials-tab-form.svelte';
|
||||
import LogisticsTabForm from '$lib/components/dashboard/invoices/edit/logistics-tab-form.svelte';
|
||||
|
||||
// Importar la API de facturas
|
||||
import { invoicesApi, type CreateInvoiceData, type UpdateInvoiceData } 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[];
|
||||
user?: any;
|
||||
companies?: any[];
|
||||
authenticated?: boolean;
|
||||
filters?: {
|
||||
operation_type?: number | null;
|
||||
invoice_type?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
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 complianceFormData = $state<any>(null);
|
||||
let financialsFormData = $state<any>(null);
|
||||
let logisticsFormData = $state<any>(null);
|
||||
|
||||
// Estados para saber si existen datos previos
|
||||
let complianceExists = $state(false);
|
||||
let financialsExists = $state(false);
|
||||
let logisticsExists = $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
|
||||
operation_type: generalFormData?.operation_type || undefined,
|
||||
invoice_type: generalFormData?.invoice_type || undefined,
|
||||
invoice_number: generalFormData?.invoice_number || undefined,
|
||||
project_number: generalFormData?.project_number || undefined,
|
||||
purchase_order: generalFormData?.purchase_order || undefined,
|
||||
related_doc_id: generalFormData?.related_doc_id || undefined,
|
||||
invoice_date: generalFormData?.invoice_date || undefined,
|
||||
traffic_light_status: generalFormData?.traffic_light_status || undefined,
|
||||
process_log: generalFormData?.process_log || undefined,
|
||||
observation_es: generalFormData?.observation_es || undefined,
|
||||
observation_en: generalFormData?.observation_en || undefined,
|
||||
comments_status: generalFormData?.comments_status || undefined,
|
||||
cfdi_uuid: generalFormData?.cfdi_uuid || undefined,
|
||||
path_pdf: generalFormData?.path_pdf || undefined,
|
||||
path_xml: generalFormData?.path_xml || undefined
|
||||
};
|
||||
|
||||
// Solo agregar sub-recursos si tienen valores reales
|
||||
|
||||
// Compliance MX - solo enviar si hay al menos un campo con valor
|
||||
if (complianceFormData) {
|
||||
const hasComplianceValue = complianceFormData.pedimento || complianceFormData.pedimento_code ||
|
||||
complianceFormData.remesa || complianceFormData.aduana ||
|
||||
complianceFormData.provider_id || complianceFormData.sold_to_id ||
|
||||
complianceFormData.shipped_to_id || complianceFormData.shipped_by_id ||
|
||||
complianceFormData.customs_broker_id;
|
||||
|
||||
if (hasComplianceValue) {
|
||||
payload.compliance_mx = {
|
||||
pedimento: complianceFormData.pedimento || null,
|
||||
pedimento_code: complianceFormData.pedimento_code || null,
|
||||
remesa: complianceFormData.remesa || null,
|
||||
aduana: complianceFormData.aduana || null,
|
||||
provider_header: complianceFormData.provider_header || null,
|
||||
provider_id: complianceFormData.provider_id || null,
|
||||
sold_to_header: complianceFormData.sold_to_header || null,
|
||||
sold_to_id: complianceFormData.sold_to_id || null,
|
||||
shipped_to_header: complianceFormData.shipped_to_header || null,
|
||||
shipped_to_id: complianceFormData.shipped_to_id || null,
|
||||
shipped_by_header: complianceFormData.shipped_by_header || null,
|
||||
shipped_by_id: complianceFormData.shipped_by_id || null,
|
||||
customs_broker_id: complianceFormData.customs_broker_id || null,
|
||||
is_mixed: complianceFormData.is_mixed || null,
|
||||
waste_type: complianceFormData.waste_type || null,
|
||||
appendix_17: complianceFormData.appendix_17 || null,
|
||||
edocument: complianceFormData.edocument || null,
|
||||
electronic_signature: complianceFormData.electronic_signature || null,
|
||||
sem_id: complianceFormData.sem_id || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Financials - solo enviar si hay al menos un campo con valor
|
||||
if (financialsFormData) {
|
||||
const hasFinancialValue = financialsFormData.currency || financialsFormData.exchange_rate ||
|
||||
financialsFormData.value_mn || financialsFormData.value_me ||
|
||||
financialsFormData.customs_value_mn || financialsFormData.freight ||
|
||||
financialsFormData.insurance;
|
||||
|
||||
if (hasFinancialValue) {
|
||||
payload.financials = {
|
||||
currency: financialsFormData.currency || null,
|
||||
currency_type: financialsFormData.currency_type || null,
|
||||
exchange_rate: financialsFormData.exchange_rate || null,
|
||||
value_mn: financialsFormData.value_mn || null,
|
||||
value_me: financialsFormData.value_me || null,
|
||||
customs_value_mn: financialsFormData.customs_value_mn || null,
|
||||
freight: financialsFormData.freight || null,
|
||||
insurance: financialsFormData.insurance || null,
|
||||
iva_mn: financialsFormData.iva_mn || null,
|
||||
iva_factor: financialsFormData.iva_factor || null,
|
||||
total_quantity: financialsFormData.total_quantity || null,
|
||||
gross_weight: financialsFormData.gross_weight || null,
|
||||
net_weight: financialsFormData.net_weight || null,
|
||||
bundle_count: financialsFormData.bundle_count || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Logistics - array, se envía si hay elementos
|
||||
if (logisticsFormData && Array.isArray(logisticsFormData) && logisticsFormData.length > 0) {
|
||||
payload.logistics = logisticsFormData.map((item: any) => ({
|
||||
carrier_id: item.carrier_id || null,
|
||||
transport_type: item.transport_type || 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_num: item.vehicle_num || null,
|
||||
license_plate: item.license_plate || null,
|
||||
seal_number: item.seal_number || null,
|
||||
guide_number: item.guide_number || null,
|
||||
entry_exit_date: item.entry_exit_date || 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-6">
|
||||
<!-- 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}
|
||||
Nueva Factura
|
||||
{: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} class="space-y-4">
|
||||
<Tabs.Content value="general">
|
||||
<GeneralTabForm
|
||||
invoice={data.invoice}
|
||||
bind:formData={generalFormData}
|
||||
invoiceTypes={data.invoiceTypes || []}
|
||||
defaultOperationType={data.filters?.operation_type ?? undefined}
|
||||
defaultInvoiceType={data.filters?.invoice_type ?? undefined}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="compliance">
|
||||
<ComplianceTabForm
|
||||
invoice={data.invoice}
|
||||
bind:formData={complianceFormData}
|
||||
bind:exists={complianceExists}
|
||||
customsBrokers={data.customsBrokers || []}
|
||||
clients={data.clients || []}
|
||||
providers={data.providers || []}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="financials">
|
||||
<FinancialsTabForm
|
||||
invoice={data.invoice}
|
||||
bind:formData={financialsFormData}
|
||||
bind:exists={financialsExists}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="logistics">
|
||||
<LogisticsTabForm
|
||||
invoice={data.invoice}
|
||||
bind:formData={logisticsFormData}
|
||||
bind:exists={logisticsExists}
|
||||
/>
|
||||
</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-4">
|
||||
<Tabs.Trigger value="general" disabled={false} class="whitespace-nowrap">
|
||||
<FileText size={16} class="mr-2" />
|
||||
General
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="compliance" disabled={false} class="whitespace-nowrap">
|
||||
<Package size={16} class="mr-2" />
|
||||
Cumplimiento
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="financials" disabled={false} class="whitespace-nowrap">
|
||||
<DollarSign size={16} class="mr-2" />
|
||||
Financieros
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="logistics" disabled={false} class="whitespace-nowrap">
|
||||
<Truck size={16} class="mr-2" />
|
||||
Logística
|
||||
</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>
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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 = 'DEF'
|
||||
|
||||
// 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
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user