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:
AlexeerCT
2025-12-19 10:52:09 -06:00
parent 08fb5d25bf
commit e937f48de4
33 changed files with 1764 additions and 6247 deletions

View File

@@ -1,89 +1,250 @@
/**
* Definición de columnas para la tabla de facturas
*/
import type { ColumnDef } from "@tanstack/table-core";
import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js";
import { createRawSnippet } from "svelte";
import DataTableActions from "./data-table-actions.svelte";
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import DataTableActions from './data-table-actions.svelte';
export function createColumns() {
/**
* Formatea un número como moneda MXN
*/
function formatCurrencyMXN(value?: number | null): string {
if (value === null || value === undefined) return '-';
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value);
}
/**
* Formatea un número como moneda USD
*/
function formatCurrencyUSD(value?: number | null): string {
if (value === null || value === undefined) return '-';
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value);
}
/**
* Formatea una fecha
*/
function formatDate(date?: string | null): string {
if (!date) return '-';
return new Date(date).toLocaleDateString('es-MX', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
/**
* Obtiene el color del badge según el tipo de operación
*/
function getOperationTypeColor(type?: string | null): string {
if (!type) return 'bg-gray-100 text-gray-800';
return type === 'imp' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800';
}
/**
* Obtiene el color del badge según el semáforo fiscal
*/
function getTrafficLightColor(status?: string | null): string {
if (!status) return 'bg-gray-100 text-gray-800';
const statusLower = status.toLowerCase();
if (statusLower.includes('verde') || statusLower === 'green') return 'bg-green-100 text-green-800';
if (statusLower.includes('amarillo') || statusLower === 'yellow') return 'bg-yellow-100 text-yellow-800';
if (statusLower.includes('rojo') || statusLower === 'red') return 'bg-red-100 text-red-800';
return 'bg-gray-100 text-gray-800';
}
export function createColumns(onSuccess?: () => void): ColumnDef<Invoice>[] {
return [
{
accessorKey: 'id',
header: 'ID',
cell: (info: any) => info.getValue(),
enableSorting: true
},
{
accessorKey: 'operation_type',
header: 'Tipo',
cell: (info: any) => {
const type = info.getValue();
return type === 'imp' ? 'Importación' : type === 'exp' ? 'Exportación' : '-';
accessorKey: "id",
header: "ID",
cell: ({ row }) => {
const idSnippet = createRawSnippet<[{ id: number }]>((getId) => {
const { id } = getId();
return {
render: () =>
`<div class="font-medium">#${id}</div>`
};
});
return renderSnippet(idSnippet, { id: row.original.id });
}
},
{
accessorKey: 'invoice_number',
header: 'Número de Factura',
cell: (info: any) => info.getValue() || '-'
},
{
accessorKey: 'invoice_type',
header: 'Tipo Factura',
cell: (info: any) => info.getValue() || '-'
},
{
accessorKey: 'project_number',
header: 'Proyecto',
cell: (info: any) => info.getValue() || '-'
},
{
accessorKey: 'compliance_mx.pedimento',
header: 'Pedimento',
cell: (info: any) => {
const row = info.row.original;
return row.compliance_mx?.pedimento || '-';
accessorKey: "operation_type",
header: "Operación",
cell: ({ row }) => {
const type = row.original.operation_type;
const colorClass = getOperationTypeColor(type);
const label = type === 'imp' ? 'IMP' : type === 'exp' ? 'EXP' : 'N/A';
const typeSnippet = createRawSnippet<[{ label: string; colorClass: string }]>((getType) => {
const { label, colorClass } = getType();
return {
render: () =>
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
${label}
</span>`
};
});
return renderSnippet(typeSnippet, { label, colorClass });
}
},
{
accessorKey: 'invoice_date',
header: 'Fecha Factura',
cell: (info: any) => {
const date = info.getValue();
if (!date) return '-';
return new Date(date).toLocaleDateString('es-MX');
accessorKey: "invoice_number",
header: "Número de Factura",
cell: ({ row }) => {
const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getNumber) => {
const { number } = getNumber();
return {
render: () =>
`<code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">${number || 'N/A'}</code>`
};
});
return renderSnippet(numberSnippet, { number: row.original.invoice_number });
}
},
{
accessorKey: 'financials.value_mn',
header: 'Valor MN',
cell: (info: any) => {
const row = info.row.original;
const value = row.financials?.value_mn;
if (value === null || value === undefined) return '-';
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN'
}).format(value);
accessorKey: "invoice_type",
header: "Tipo",
cell: ({ row }) => {
const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
const { type } = getType();
return {
render: () =>
`<div class="capitalize">${type || '-'}</div>`
};
});
return renderSnippet(typeSnippet, { type: row.original.invoice_type });
}
},
{
accessorKey: 'traffic_light_status',
header: 'Semáforo',
cell: (info: any) => info.getValue() || '-'
},
{
accessorKey: 'capture_date',
header: 'Fecha Captura',
cell: (info: any) => {
const date = info.getValue();
if (!date) return '-';
return new Date(date).toLocaleDateString('es-MX');
accessorKey: "project_number",
header: "Proyecto",
cell: ({ row }) => {
const projectSnippet = createRawSnippet<[{ project?: string | null }]>((getProject) => {
const { project } = getProject();
return {
render: () =>
`<div>${project || '-'}</div>`
};
});
return renderSnippet(projectSnippet, { project: row.original.project_number });
}
},
{
id: 'actions',
header: 'Acciones',
cell: (info: any) => DataTableActions,
enableSorting: false
accessorKey: "compliance_mx.pedimento",
header: "Pedimento",
cell: ({ row }) => {
const pedimento = row.original.compliance_mx?.pedimento;
const pedimentoSnippet = createRawSnippet<[{ pedimento?: string | null }]>((getPedimento) => {
const { pedimento } = getPedimento();
return {
render: () =>
`<div class="text-sm">${pedimento || '-'}</div>`
};
});
return renderSnippet(pedimentoSnippet, { pedimento });
}
},
{
accessorKey: "financials.value_mn",
header: () => {
const headerSnippet = createRawSnippet(() => {
return {
render: () => `<div class="text-right">Valor MN</div>`
};
});
return renderSnippet(headerSnippet, {});
},
cell: ({ row }) => {
const valueMN = row.original.financials?.value_mn;
const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => {
const { value } = getValue();
return {
render: () =>
`<div class="text-right font-medium">${value}</div>`
};
});
return renderSnippet(valueSnippet, { value: formatCurrencyMXN(valueMN) });
}
},
{
accessorKey: "financials.value_me",
header: () => {
const headerSnippet = createRawSnippet(() => {
return {
render: () => `<div class="text-right">Valor ME</div>`
};
});
return renderSnippet(headerSnippet, {});
},
cell: ({ row }) => {
const valueME = row.original.financials?.value_me;
const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => {
const { value } = getValue();
return {
render: () =>
`<div class="text-right font-medium">${value}</div>`
};
});
return renderSnippet(valueSnippet, { value: formatCurrencyUSD(valueME) });
}
},
{
accessorKey: "traffic_light_status",
header: "Semáforo",
cell: ({ row }) => {
const status = row.original.traffic_light_status;
const colorClass = getTrafficLightColor(status);
const statusSnippet = createRawSnippet<[{ status?: string | null; colorClass: string }]>((getStatus) => {
const { status, colorClass } = getStatus();
return {
render: () =>
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
${status || '-'}
</span>`
};
});
return renderSnippet(statusSnippet, { status, colorClass });
}
},
{
accessorKey: "invoice_date",
header: "Fecha Factura",
cell: ({ row }) => {
const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => {
const { date } = getDate();
return {
render: () =>
`<div class="text-sm text-muted-foreground">${date}</div>`
};
});
return renderSnippet(dateSnippet, { date: formatDate(row.original.invoice_date) });
}
},
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { invoice: row.original, onSuccess });
}
}
];
}
// Mantener compatibilidad hacia atrás
export const columns = createColumns();

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

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