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,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",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user