Cambio parcial en el formulario de invoices

This commit is contained in:
2026-02-09 17:39:24 -06:00
parent 5faddf065c
commit 0f50ae3d61
6 changed files with 1411 additions and 878 deletions

View File

@@ -103,6 +103,7 @@ export interface InvoiceFinancials {
insurance_value?: number | null;
packaging?: number | null;
other_increments?: number | null;
other_deductibles?: number | null;
total_increments_mn?: number | null;
total_increments_me?: number | null;
iva_mn?: number | null;
@@ -158,6 +159,8 @@ export interface InvoiceLogistics {
payment_date?: string | null;
payment_receipt_num?: string | null;
is_ctm_process?: string | null;
is_subdivision?: boolean | null;
acts_as_cd?: boolean | null;
}
export interface InvoiceSalesDetails {

View File

@@ -15,11 +15,13 @@
let {
invoice,
formData = $bindable(),
exists = $bindable()
exists = $bindable(),
operationType = undefined
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
operationType?: number;
} = $props();
let imported = 0;
@@ -49,6 +51,7 @@
let isLoadingMore = $state(false);
let isLoadingItems = $state(false);
let isSaving = $state(false);
let focusedLine = $state<any>(null);
// Sheet states
let showItemSheet = $state(false);
@@ -109,6 +112,10 @@
isLoadingMore = false;
}
function handleRowClick(line: any) {
focusedLine = line;
}
function handleScroll(e: Event) {
const target = e.target as HTMLDivElement;
const threshold = 100;
@@ -698,11 +705,11 @@
</script>
<div class="grid grid-cols-4 grid-rows-1 gap-3">
<div class="border rounded-md p-3 space-y-3 col-span-3">
<div class="flex justify-between items-center mb-3">
<div class="col-span-3 space-y-3 rounded-md border p-3">
<div class="mb-3 flex items-center justify-between">
<h3 class="text-sm font-semibold">Items de la Factura</h3>
<Button size="sm" onclick={handleAdd}>
<Plus class="w-4 h-4 mr-1" />
<Plus class="mr-1 h-4 w-4" />
Agregar Item
</Button>
</div>
@@ -710,49 +717,89 @@
<div
bind:this={tableContainer}
onscroll={handleScroll}
class="max-h-[500px] overflow-auto border rounded-md"
class="max-h-[500px] overflow-auto rounded-md border"
>
<Table.Root>
<Table.Header class="bg-background">
<Table.Row>
<Table.Head>Línea</Table.Head>
<Table.Head>P/S</Table.Head>
<Table.Head>Clase</Table.Head>
<Table.Head>Descripcion Clase</Table.Head>
<Table.Head>Cant. Importada</Table.Head>
<Table.Head>U.M.</Table.Head>
<Table.Head>Preferencia</Table.Head>
<Table.Head>Contiene Subpartida</Table.Head>
<Table.Head>Partida Principal</Table.Head>
<Table.Head class="text-right w-[120px]">Acciones</Table.Head>
{#if operationType === 1}
<Table.Head>Línea</Table.Head>
<Table.Head>Factura Impo</Table.Head>
<Table.Head>Lin Impo</Table.Head>
<Table.Head>P/S</Table.Head>
<Table.Head>Cant. Exportada</Table.Head>
<Table.Head>Clase</Table.Head>
<Table.Head>Num Parte</Table.Head>
<Table.Head>Descripción Esp.</Table.Head>
<Table.Head>Subpartidas</Table.Head>
<Table.Head>Partida Princ.</Table.Head>
{:else}
<Table.Head>Línea</Table.Head>
<Table.Head>P/S</Table.Head>
<Table.Head>Clase</Table.Head>
<Table.Head>Descripcion Clase</Table.Head>
<Table.Head>Cant. Importada</Table.Head>
<Table.Head>U.M.</Table.Head>
<Table.Head>Preferencia</Table.Head>
<Table.Head>Contiene Subpartida</Table.Head>
<Table.Head>Partida Principal</Table.Head>
{/if}
<Table.Head class="w-[120px] text-right">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if displayedItems.length === 0}
<Table.Row>
<Table.Cell colspan={7} class="text-center text-muted-foreground py-8">
<Table.Cell
colspan={operationType === 1 ? 11 : 10}
class="py-8 text-center text-muted-foreground"
>
No hay items disponibles
</Table.Cell>
</Table.Row>
{:else}
{#each displayedItems as item (item.id)}
<Table.Row>
<Table.Cell>{item.line_number}</Table.Cell>
<Table.Cell>{item.is_subitem ? 'S' : 'P'}</Table.Cell>
<Table.Cell>{item.class_code || '-'}</Table.Cell>
<Table.Cell>{item.class_description || '-'}</Table.Cell>
<Table.Cell>{item.quantity?.quantity || '0'}</Table.Cell>
<Table.Cell>{item.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{item.reference_number || '-'}</Table.Cell>
<Table.Cell>{item.fa_data?.contains_subitems ? 'Sí' : 'No'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
<Table.Row
onclick={() => handleRowClick(item)}
class="cursor-pointer transition-colors hover:bg-muted/50 {focusedLine?.id ===
item.id
? 'bg-muted ring-1 ring-primary/20 ring-inset'
: ''}"
>
{#if operationType === 1}
<Table.Cell>{item.line_number}</Table.Cell>
<Table.Cell>{item.fa_data?.search_invoice || '-'}</Table.Cell>
<Table.Cell>{item.fa_data?.search_line || '-'}</Table.Cell>
<Table.Cell>{item.is_subitem ? 'S' : 'P'}</Table.Cell>
<Table.Cell>{item.quantity?.quantity || '0'}</Table.Cell>
<Table.Cell>{item.class_code || '-'}</Table.Cell>
<Table.Cell>{item.part_number || '-'}</Table.Cell>
<Table.Cell
class="max-w-[200px] truncate"
title={item.description?.description_spanish}
>
{item.description?.description_spanish || '-'}
</Table.Cell>
<Table.Cell>{item.fa_data?.contains_subitems ? 'Sí' : 'No'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
{:else}
<Table.Cell>{item.line_number}</Table.Cell>
<Table.Cell>{item.is_subitem ? 'S' : 'P'}</Table.Cell>
<Table.Cell>{item.class_code || '-'}</Table.Cell>
<Table.Cell>{item.class_description || '-'}</Table.Cell>
<Table.Cell>{item.quantity?.quantity || '0'}</Table.Cell>
<Table.Cell>{item.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{item.reference_number || '-'}</Table.Cell>
<Table.Cell>{item.fa_data?.contains_subitems ? 'Sí' : 'No'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
{/if}
<Table.Cell class="text-right">
<div class="flex justify-end gap-2">
<Button size="icon" variant="ghost" onclick={() => handleEdit(item)}>
<Pencil class="w-4 h-4" />
<Pencil class="h-4 w-4" />
</Button>
<Button size="icon" variant="ghost" onclick={() => handleDelete(item)}>
<Trash2 class="w-4 h-4 text-destructive" />
<Trash2 class="h-4 w-4 text-destructive" />
</Button>
</div>
</Table.Cell>
@@ -760,7 +807,7 @@
{/each}
{#if isLoadingMore}
<Table.Row>
<Table.Cell colspan={7} class="text-center py-4">
<Table.Cell colspan={operationType === 1 ? 11 : 10} class="py-4 text-center">
<span class="text-sm text-muted-foreground">Cargando más items...</span>
</Table.Cell>
</Table.Row>
@@ -771,43 +818,66 @@
</div>
{#if flattenedLines.length > 0}
<div class="text-xs text-muted-foreground text-right">
<div class="text-right text-xs text-muted-foreground">
Mostrando {displayedItems.length} de {flattenedLines.length} líneas
</div>
{/if}
</div>
<div class="border rounded-md p-3 space-y-3 col-start-4">
<div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Cantidades:</h4>
<div>
<div class="grid grid-cols-2 gap-3">
<div>
Partidas: <span class="text-blue-400">{items.length || 0}</span>
</div>
<div>
Bultos: <span class="text-blue-400">0</span>
</div>
<div class="col-start-4 space-y-4">
{#if operationType === 1}
<!-- Top Box: Descripción en español -->
<div class="flex h-[200px] flex-col rounded-md border p-3">
<h4 class="mb-2 text-xs font-semibold text-muted-foreground uppercase">
Descripción en español:
</h4>
<div class="flex-1 overflow-auto rounded border bg-muted/30 p-2">
{#if focusedLine}
<p class="text-xs leading-relaxed font-medium">
{focusedLine.description?.description_spanish || 'Sin descripción disponible.'}
</p>
{:else}
<p class="flex h-full items-center justify-center text-xs text-muted-foreground italic">
Selecciona una fila para ver la descripción.
</p>
{/if}
</div>
</div>
Importada:<span class="text-blue-400">{imported || 0}</span> <br />
Peso neto: <span class="text-blue-400">{net_weight || 0}</span><br />
Peso bruto: <span class="text-blue-400">{gross_weight || 0}</span> <br />
{/if}
<!-- Bottom Box (or only box in Import): Quantities and Values -->
<div class="space-y-3 rounded-md border p-3">
<div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Cantidades:</h4>
<div>
<div class="grid grid-cols-2 gap-3">
<div>
Partidas: <span class="text-blue-400">{items.length || 0}</span>
</div>
<div>
Bultos: <span class="text-blue-400">0</span>
</div>
</div>
</div>
Importada:<span class="text-blue-400">{imported || 0}</span> <br />
Peso neto: <span class="text-blue-400">{net_weight || 0}</span><br />
Peso bruto: <span class="text-blue-400">{gross_weight || 0}</span> <br />
</div>
<h4 class="col-span-2 text-xs font-semibold text-muted-foreground uppercase">
Valores de importacion:
</h4>
Dolares:<span class="text-blue-400">0</span> <span class="text-red-400">USD</span> <br />
Pesos: <span class="text-blue-400">0</span> <span class="text-red-400">MXN</span><br />
De Captura: <span class="text-blue-400">0</span> <span class="text-red-400">USD</span>
<h4 class="col-span-2 text-xs font-semibold text-muted-foreground uppercase opacity-0">
spacer
</h4>
Aduana:<span class="text-blue-400">0</span> <span class="text-red-400">USD</span><br />
Aduana: <span class="text-blue-400">0</span> <span class="text-red-400">MXN</span><br />
</div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase col-span-2">
Valores de importacion:
</h4>
Dolares:<span class="text-blue-400">0</span> <span class="text-red-400">USD</span> <br />
Pesos: <span class="text-blue-400">0</span> <span class="text-red-400">MXN</span><br />
De Captura: <span class="text-blue-400">0</span> <span class="text-red-400">USD</span>
<h4 class="text-xs font-semibold text-muted-foreground uppercase col-span-2 opacity-0">
spacer
</h4>
Aduana:<span class="text-blue-400">0</span> <span class="text-red-400">USD</span><br />
Aduana: <span class="text-blue-400">0</span> <span class="text-red-400">MXN</span><br />
</div>
</div>
@@ -850,7 +920,7 @@
</Button>
<Button variant="destructive" onclick={confirmDelete} disabled={isSaving}>
{#if isSaving}
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Eliminando...
{:else}
Eliminar

View File

@@ -1,31 +1,33 @@
<script lang="ts">
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Textarea } from "$lib/components/ui/textarea/index.js";
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import { Textarea } from '$lib/components/ui/textarea/index.js';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
let {
let {
invoice,
formData = $bindable(),
exists = $bindable(),
seals = [],
incoterms = [],
enclosure = [],
}: {
operationType = undefined
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
seals?: any[];
incoterms?: any[];
enclosure?: any[];
operationType?: number;
} = $props();
if (!formData && invoice) {
formData = {
// Campos de observaciones
observation_es: invoice.observation_es || '',
observation_en: invoice.observation_en || '',
observation_en: invoice.observation_en || '',
// Incrementables
freight: invoice.financials?.freight || null,
insurance_value: invoice.financials?.insurance_value || null,
@@ -35,20 +37,30 @@
total_increments_mn: invoice.financials?.total_increments_mn || null,
total_increments_me: invoice.financials?.total_increments_me || null,
// Incoterm y recinto
incoterm: invoice.logistics?.[0]?.incoterm || null,
incoterm: invoice.logistics?.incoterm || null,
enclosure: invoice.compliance_mx?.enclosure || null,
// Campos de esta pestaña
num_seals: null,
movement_type: invoice.compliance_mx?.movement_type || '',
alternate_invoice: invoice.alternate_invoice || '',
valuation_method: null
valuation_method: invoice.compliance_mx?.value_method || null,
// New fields for Export
other_deductibles: null, // Need to confirm where this maps
proforma_number: invoice.proforma_number || '',
sub_division: invoice.compliance_mx?.subdivision || '',
acts_as_cd: invoice.logistics?.acts_as_cd || false,
identifier_1: invoice.logistics?.identifier_1 || '',
complement_1: invoice.logistics?.complement_1 || '',
identifier_2: invoice.logistics?.identifier_2 || '',
complement_2: invoice.logistics?.complement_2 || '',
office_document: invoice.compliance_mx?.office_document || ''
};
exists = true;
} else if (!formData) {
formData = {
// Campos de observaciones
observation_es: '',
observation_en: '',
observation_en: '',
// Incrementables
freight: null,
insurance_value: null,
@@ -64,245 +76,462 @@
num_seals: null,
movement_type: '',
alternate_invoice: '',
valuation_method: null
valuation_method: null,
// New fields for Export
other_deductibles: null,
proforma_number: '',
sub_division: '',
acts_as_cd: false,
identifier_1: '',
complement_1: '',
identifier_2: '',
complement_2: '',
office_document: ''
};
exists = false;
}
</script>
<div class="grid grid-cols-2 grid-rows-5 gap-4">
<div class="border rounded-md p-3 space-y-3 row-span-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Observacion de la factura mexicana y bilingue:</h4>
<div class="space-y-1">
<Textarea
id="observation_es"
bind:value={formData.observation_es}
class="min-h-[150px] max-h-[150px] text-sm font-medium"
placeholder="Escribe tus observaciones aqui."
/>
</div>
</div>
<div class="border rounded-md p-3 space-y-3 row-span-2 col-start-1 row-start-3">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Observacion de la factura americana:</h4>
<Textarea
id="observation_en"
bind:value={formData.observation_en}
class="min-h-[150px] max-h-[150px] text-sm font-medium"
placeholder="Escribe tus observaciones aqui."
/>
</div>
<div class="border rounded-md p-3 space-y-3 col-span-2 col-start-1 row-start-5">
<div class="grid grid-cols-4 gap-3">
<div class="space-y-1">
<Label for="num_seals" class="text-xs">Num Precintos:</Label>
<Select.Root
type="single"
value={formData.num_seals ? String(formData.num_seals) : ''}
onValueChange={(v) => {
formData.num_seals = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="num_seals" class="h-7 text-xs">
<span class="truncate">
{formData.num_seals
? seals.find(p => p.id === formData.num_seals)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each seals as seal}
<Select.Item value={String(seal.id)}>
{seal.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="movement_type" class="text-xs">Tipo Movimiento:</Label>
<Input
id="movement_type"
bind:value={formData.movement_type}
class="h-8 text-sm font-medium"
<div class="grid grid-cols-2 gap-4">
<!-- Left Column: Observations and Catalog Info -->
<div class="space-y-4">
<!-- Observations Section -->
<div class="space-y-4 rounded-md border p-3">
<div class="space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
{operationType === 1
? 'Observaciones de la factura mexicana:'
: 'Observacion de la factura mexicana y bilingue:'}
</h4>
<Textarea
id="observation_es"
bind:value={formData.observation_es}
class="max-h-[150px] min-h-[150px] text-sm font-medium"
placeholder="Escribe tus observaciones aqui."
/>
</div>
<div class="space-y-1">
<Label for="alternate_invoice" class="text-xs">Factura Alterna:</Label>
<Input
id="alternate_invoice"
bind:value={formData.alternate_invoice}
class="h-8 text-sm font-medium"
<div class="space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
Observaciones de la factura americana:
</h4>
<Textarea
id="observation_en"
bind:value={formData.observation_en}
class="max-h-[150px] min-h-[150px] text-sm font-medium"
placeholder="Escribe tus observaciones aqui."
/>
</div>
<div class="space-y-1">
<Label for="valuation_method" class="text-xs">Met. Valoracion:</Label>
<Select.Root
type="single"
value={formData.valuation_method ? String(formData.valuation_method) : ''}
onValueChange={(v) => {
formData.valuation_method = v;
}}
>
<Select.Trigger id="valuation_method" class="h-7 text-xs">
<span class="truncate">
{formData.valuation_method || 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value="general">General</Select.Item>
<Select.Item value="devalued">Devaluado</Select.Item>
<Select.Item value="special">Especial</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
{#if operationType === 1}
<!-- Identificadores (Export Only) -->
<div class="space-y-4 rounded-md border bg-muted/20 p-4">
<h4 class="mb-1 border-b pb-2 text-xs font-bold text-muted-foreground uppercase">
Identificadores
</h4>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="identifier_1" class="text-xs font-semibold">Primero:</Label>
<Input
id="identifier_1"
bind:value={formData.identifier_1}
class="h-8 text-xs"
placeholder="Clave..."
/>
</div>
<div class="space-y-1.5">
<Label for="identifier_2" class="text-xs font-semibold">Segundo:</Label>
<Input
id="identifier_2"
bind:value={formData.identifier_2}
class="h-8 text-xs"
placeholder="Clave..."
/>
</div>
</div>
</div>
<!-- Complementos (Export Only) -->
<div class="space-y-4 rounded-md border bg-muted/20 p-4">
<h4 class="mb-1 border-b pb-2 text-xs font-bold text-muted-foreground uppercase">
Complementos
</h4>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="complement_1" class="text-xs font-semibold">1:</Label>
<Input id="complement_1" bind:value={formData.complement_1} class="h-8 text-xs" />
</div>
<div class="space-y-1.5">
<Label for="complement_2" class="text-xs font-semibold">2:</Label>
<Input id="complement_2" bind:value={formData.complement_2} class="h-8 text-xs" />
</div>
</div>
</div>
<!-- Oficio (Export Only) -->
<div class="rounded-md border bg-muted/20 p-3">
<div class="space-y-1.5">
<Label for="office_document" class="text-xs font-semibold">Oficio:</Label>
<Input id="office_document" bind:value={formData.office_document} class="h-8 text-xs" />
</div>
</div>
{/if}
</div>
<div class="border rounded-md p-3 space-y-3 row-span-3 col-start-2 row-start-1">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Incrementables:</h4>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label for="freight" class="text-xs">Flete:</Label>
</div>
<div class="space-y-1">
<Input
id="freight"
<!-- Right Column: Values, Flags and Logistics -->
<div class="space-y-4">
<!-- Incrementables Section -->
<div class="space-y-3 rounded-md border p-3">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Incrementables:</h4>
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<div class="flex items-center">
<Label for="freight" class="text-xs">Flete:</Label>
</div>
<Input
id="freight"
type="number"
bind:value={formData.freight}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="insurance_value" class="text-xs">Val. Seguros:</Label>
</div>
<div class="space-y-1">
<Input
id="insurance_value"
type="number"
bind:value={formData.insurance_value}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="insurance" class="text-xs">Seguros:</Label>
</div>
<div class="space-y-1">
<Input
id="insurance"
type="number"
bind:value={formData.insurance}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="packaging" class="text-xs">Embalajes:</Label>
</div>
<div class="space-y-1">
<Input
id="packaging"
type="number"
bind:value={formData.packaging}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="other_increments" class="text-xs">Otros incrementables:</Label>
</div>
<div class="space-y-1">
<Input
id="other_increments"
type="number"
bind:value={formData.other_increments}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="total_increments_mn" class="text-xs">Total Incrementables:</Label>
</div>
<div class="space-y-1">
<Input
id="total_increments_mn"
type="number"
bind:value={formData.total_increments_mn}
bind:value={formData.freight}
class="h-8 text-sm font-medium"
disabled
/>
</div>
<div class="space-y-1">
</div>
<div class="space-y-1">
<Input
id="total_increments_me"
<div class="flex items-center">
<Label for="insurance" class="text-xs">Seguros:</Label>
</div>
<Input
id="insurance"
type="number"
bind:value={formData.total_increments_me}
bind:value={formData.insurance}
class="h-8 text-sm font-medium"
disabled
/>
<div class="flex items-center">
<Label for="packaging" class="text-xs">Embalajes:</Label>
</div>
<Input
id="packaging"
type="number"
bind:value={formData.packaging}
class="h-8 text-sm font-medium"
/>
<div class="flex items-center">
<Label for="other_increments" class="text-xs leading-none">Otros increm.:</Label>
</div>
<Input
id="other_increments"
type="number"
bind:value={formData.other_increments}
class="h-8 text-sm font-medium"
/>
{#if operationType === 1}
<div class="flex items-center">
<Label for="other_deductibles" class="text-xs leading-none">Otros deduc.:</Label>
</div>
<Input
id="other_deductibles"
type="number"
bind:value={formData.other_deductibles}
class="h-8 border-orange-200 text-sm font-medium focus-visible:ring-orange-200"
/>
{/if}
</div>
</div>
</div>
<div class="border rounded-md p-3 space-y-3 col-start-2 row-start-4">
<div class="grid grid-cols-2 gap-3">
<div class="space-y-1">
<Label for="incoterm" class="text-xs">Incoterm:</Label>
<Select.Root
type="single"
value={formData.incoterm ? String(formData.incoterm) : ''}
onValueChange={(v) => {
formData.incoterm = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="incoterm" class="h-7 text-xs">
<span class="truncate">
{formData.incoterm
? incoterms.find(p => p.id === formData.incoterm)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each incoterms as inco}
<Select.Item value={String(inco.id)}>
{inco.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{#if operationType === 1}
<!-- Precintos & Tipo Mov Group (No Title) -->
<div class="grid grid-cols-2 gap-4 rounded-md border bg-muted/20 p-3">
<div class="space-y-1">
<Label for="num_seals_exp" class="text-xs font-semibold">Número de Precinto:</Label>
<Select.Root
type="single"
value={formData.num_seals ? String(formData.num_seals) : ''}
onValueChange={(v) => {
formData.num_seals = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="num_seals_exp" class="h-8 text-xs">
<span class="truncate">
{formData.num_seals
? seals.find((p) => p.id === formData.num_seals)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each seals as seal}
<Select.Item value={String(seal.id)}>
{seal.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="movement_type_exp" class="text-xs font-semibold">Tipo Movimiento:</Label>
<Input
id="movement_type_exp"
bind:value={formData.movement_type}
class="h-8 text-xs font-medium"
/>
</div>
</div>
<div class="space-y-1">
<Label for="enclosure" class="text-xs">Recinto:</Label>
<Select.Root
type="single"
value={formData.enclosure ? String(formData.enclosure) : ''}
onValueChange={(v) => {
formData.enclosure = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="enclosure" class="h-7 text-xs">
<span class="truncate">
{formData.enclosure
? enclosure.find(p => p.id === formData.enclosure)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each enclosure as rec}
<Select.Item value={String(rec.id)}>
{rec.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<!-- Factura Alterna & Flags Section -->
<div class="space-y-4 rounded-md border bg-muted/20 p-4">
<h4 class="mb-1 border-b pb-2 text-xs font-bold text-muted-foreground uppercase">
Factura Alterna & Flags
</h4>
<div class="grid grid-cols-2 gap-x-6 gap-y-4">
<div class="space-y-1.5">
<Label for="alternate_invoice_exp" class="text-xs font-semibold">Factura Alterna:</Label
>
<Input
id="alternate_invoice_exp"
bind:value={formData.alternate_invoice}
class="h-8 text-xs"
/>
</div>
<div class="space-y-1.5">
<Label for="proforma_number" class="text-xs font-semibold">Número de Proforma:</Label>
<Input id="proforma_number" bind:value={formData.proforma_number} class="h-8 text-xs" />
</div>
<div class="space-y-2 pt-1">
<Label class="mb-2 block text-xs font-semibold">Sub División:</Label>
<div class="flex items-center gap-4">
<label class="group flex cursor-pointer items-center gap-2">
<div
class="relative flex h-4 w-4 items-center justify-center rounded-full border-2 border-zinc-400 transition-all {formData.sub_division ===
'si'
? 'border-zinc-800 bg-zinc-800'
: ''}"
>
<input
type="radio"
value="si"
bind:group={formData.sub_division}
class="sr-only"
/>
{#if formData.sub_division === 'si'}<div
class="h-1.5 w-1.5 rounded-full bg-white"
></div>{/if}
</div>
<span class="text-xs font-medium transition-colors group-hover:text-zinc-800"
>Si</span
>
</label>
<label class="group flex cursor-pointer items-center gap-2">
<div
class="relative flex h-4 w-4 items-center justify-center rounded-full border-2 border-zinc-400 transition-all {formData.sub_division ===
'no'
? 'border-zinc-800 bg-zinc-800'
: ''}"
>
<input
type="radio"
value="no"
bind:group={formData.sub_division}
class="sr-only"
/>
{#if formData.sub_division === 'no'}<div
class="h-1.5 w-1.5 rounded-full bg-white"
></div>{/if}
</div>
<span class="text-xs font-medium transition-colors group-hover:text-zinc-800"
>No</span
>
</label>
</div>
</div>
<div class="space-y-2 pt-1">
<Label class="mb-2 block text-xs font-semibold">Funge como CD:</Label>
<div class="flex items-center gap-4">
<label class="group flex cursor-pointer items-center gap-2">
<div
class="relative flex h-4 w-4 items-center justify-center rounded-full border-2 border-zinc-400 transition-all {formData.acts_as_cd ===
true
? 'border-zinc-800 bg-zinc-800'
: ''}"
>
<input
type="radio"
value={true}
bind:group={formData.acts_as_cd}
class="sr-only"
/>
{#if formData.acts_as_cd === true}
<div class="h-1.5 w-1.5 rounded-full bg-white"></div>{/if}
</div>
<span class="text-xs font-medium transition-colors group-hover:text-zinc-800"
>Si</span
>
</label>
<label class="group flex cursor-pointer items-center gap-2">
<div
class="relative flex h-4 w-4 items-center justify-center rounded-full border-2 border-zinc-400 transition-all {formData.acts_as_cd ===
false
? 'border-zinc-800 bg-zinc-800'
: ''}"
>
<input
type="radio"
value={false}
bind:group={formData.acts_as_cd}
class="sr-only"
/>
{#if formData.acts_as_cd === false}
<div class="h-1.5 w-1.5 rounded-full bg-white"></div>{/if}
</div>
<span class="text-xs font-medium transition-colors group-hover:text-zinc-800"
>No</span
>
</label>
</div>
</div>
</div>
</div>
{/if}
<!-- Incoterm & Recinto (Show for all, layout adjusted) -->
<div class="space-y-3 rounded-md border p-3">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1">
<Label for="incoterm" class="text-xs">Incoterm:</Label>
<Select.Root
type="single"
value={formData.incoterm ? String(formData.incoterm) : ''}
onValueChange={(v) => {
formData.incoterm = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="incoterm" class="h-7 text-xs">
<span class="truncate"
>{formData.incoterm
? incoterms.find((p) => p.id === formData.incoterm)?.name || 'Selecciona...'
: 'Selecciona...'}</span
>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each incoterms as inco}
<Select.Item value={String(inco.id)}>{inco.name}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="enclosure" class="text-xs">Recinto:</Label>
<Select.Root
type="single"
value={formData.enclosure ? String(formData.enclosure) : ''}
onValueChange={(v) => {
formData.enclosure = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="enclosure" class="h-7 text-xs">
<span class="truncate"
>{formData.enclosure
? enclosure.find((p) => p.id === formData.enclosure)?.name || 'Selecciona...'
: 'Selecciona...'}</span
>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each enclosure as rec}
<Select.Item value={String(rec.id)}>{rec.name}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
{#if operationType === 1}
<div class="space-y-1 pt-1">
<Label for="valuation_method_exp" class="text-xs font-semibold"
>Método de Valoración:</Label
>
<Select.Root
type="single"
value={formData.valuation_method ? String(formData.valuation_method) : ''}
onValueChange={(v) => {
formData.valuation_method = v;
}}
>
<Select.Trigger id="valuation_method_exp" class="h-8 text-xs">
<span class="truncate">{formData.valuation_method || 'Selecciona...'}</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="general">General</Select.Item>
<Select.Item value="devalued">Devaluado</Select.Item>
<Select.Item value="special">Especial</Select.Item>
</Select.Content>
</Select.Root>
</div>
{/if}
</div>
</div>
</div>
<!-- Extra Bottom Row for Import Mode specific fields (Original Layout preserved for Import) -->
{#if operationType !== 1}
<div class="col-span-2 space-y-3 rounded-md border p-3">
<div class="grid grid-cols-4 gap-3">
<div class="space-y-1">
<Label for="num_seals" class="text-xs">Num Precintos:</Label>
<Select.Root
type="single"
value={formData.num_seals ? String(formData.num_seals) : ''}
onValueChange={(v) => {
formData.num_seals = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="num_seals" class="h-7 text-xs">
<span class="truncate"
>{formData.num_seals
? seals.find((p) => p.id === formData.num_seals)?.name || 'Selecciona...'
: 'Selecciona...'}</span
>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each seals as seal}<Select.Item value={String(seal.id)}>{seal.name}</Select.Item
>{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="movement_type" class="text-xs">Tipo Movimiento:</Label>
<Input
id="movement_type"
bind:value={formData.movement_type}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="alternate_invoice" class="text-xs">Factura Alterna:</Label>
<Input
id="alternate_invoice"
bind:value={formData.alternate_invoice}
class="h-8 text-sm font-medium"
/>
</div>
<div class="space-y-1">
<Label for="valuation_method" class="text-xs">Met. Valoracion:</Label>
<Select.Root
type="single"
value={formData.valuation_method ? String(formData.valuation_method) : ''}
onValueChange={(v) => {
formData.valuation_method = v;
}}
>
<Select.Trigger id="valuation_method" class="h-7 text-xs">
<span class="truncate">{formData.valuation_method || 'Selecciona...'}</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value="general">General</Select.Item>
<Select.Item value="devalued">Devaluado</Select.Item>
<Select.Item value="special">Especial</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
{/if}
</div>

View File

@@ -45,7 +45,7 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<SaveInvo
};
const missingFields: string[] = [];
// Validar campos de InvoiceTopFieldsFormData
if (!InvoiceTopFieldsFormData?.operation_type) {
missingFields.push(requiredFields.operation_type);
@@ -73,11 +73,11 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<SaveInvo
if (isCreate) {
// Crear nueva factura con todos sus sub-recursos
const response = await invoicesApi.create(companyId, payload as CreateInvoiceData);
const response = await invoicesApi.create(companyId, payload as CreateInvoiceData);
if (response.error) {
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
const error: any = new Error(errorMsg);
error.validationErrors = response.validationErrors;
error.validationErrors = response.validationErrors;
throw error;
}
if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada');
@@ -88,10 +88,10 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<SaveInvo
} else {
// Actualizar factura existente con todos sus sub-recursos
const updatePayload = { ...payload, id: invoiceId } as UpdateInvoiceData;
const response = await invoicesApi.update(invoiceId!, companyId, updatePayload);
const response = await invoicesApi.update(invoiceId!, companyId, updatePayload);
if (response.error) {
const error: any = new Error(response.error);
error.validationErrors = response.validationErrors;
error.validationErrors = response.validationErrors;
throw error;
}
}
@@ -116,6 +116,7 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
invoice_number: InvoiceTopFieldsFormData?.invoice_number || undefined,
invoice_date: InvoiceTopFieldsFormData?.invoice_date || undefined,
emission_date: InvoiceTopFieldsFormData?.emission_date || undefined,
proforma_number: observationFormData?.proforma_number || undefined,
// Observation fields from observationFormData
observation_es: observationFormData?.observation_es || undefined,
observation_en: observationFormData?.observation_en || undefined,
@@ -138,7 +139,8 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
othersFormData?.cove || othersFormData?.operation_num ||
othersFormData?.adendas || othersFormData?.certified_number ||
othersFormData?.code_signature || othersFormData?.electronic_signature ||
continuationFormData?.puerto_entrada;
continuationFormData?.puerto_entrada || generalFormData?.manifest_number ||
generalFormData?.shipped_by_id || generalFormData?.shipped_by_header;
if (hasComplianceValue) {
payload.compliance_mx = buildComplianceMxData(InvoiceTopFieldsFormData, generalFormData, othersFormData, observationFormData, continuationFormData);
@@ -177,6 +179,7 @@ function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: a
is_pedimento_pending: Boolean(InvoiceTopFieldsFormData?.is_pedimento_pending || false),
// Fields from generalFormData
aduana: generalFormData?.aduana || null,
manifest_number: generalFormData?.manifest_number || null,
port_of_entry: continuationFormData?.puerto_entrada || null,
provider_header: generalFormData?.provider_header || null,
provider_id: generalFormData?.provider_id || null,
@@ -184,6 +187,8 @@ function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: a
sold_to_id: generalFormData?.sold_to_id || null,
shipped_to_header: generalFormData?.shipped_to_header || null,
shipped_to_id: generalFormData?.shipped_to_id || null,
shipped_by_header: generalFormData?.shipped_by_header || null,
shipped_by_id: generalFormData?.shipped_by_id || null,
customs_broker_id: generalFormData?.customs_broker_id ? Number(generalFormData.customs_broker_id) : null,
customs_broker_us_id: generalFormData?.customs_broker_us_id ? Number(generalFormData.customs_broker_us_id) : null,
// Fields from observationFormData
@@ -198,6 +203,8 @@ function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: a
certificate_number: othersFormData?.certified_number || null,
code_signature: othersFormData?.code_signature || null,
electronic_signature: othersFormData?.electronic_signature || null,
subdivision: observationFormData?.sub_division || null,
office_document: observationFormData?.office_document || null,
};
}
@@ -206,13 +213,14 @@ function buildFinancialsData(generalFormData: any, observationFormData: any, oth
// Currency from generalFormData
currency_type: generalFormData?.currency_type || null,
currency: generalFormData?.currency || null,
iva_factor: generalFormData?.iva_factor || null,
iva_factor: generalFormData?.iva_factor !== null && generalFormData?.iva_factor !== undefined ? String(generalFormData.iva_factor) : null,
// Costs & increments from observationFormData
freight: observationFormData?.freight || null,
insurance: observationFormData?.insurance || null,
insurance_value: observationFormData?.insurance_value || null,
packaging: observationFormData?.packaging || null,
other_increments: observationFormData?.other_increments || null,
other_deductibles: observationFormData?.other_deductibles || null,
total_increments_mn: observationFormData?.total_increments_mn || null,
total_increments_me: observationFormData?.total_increments_me || null,
// Seal value from othersFormData
@@ -230,6 +238,13 @@ function buildLogisticsData(generalFormData: any, observationFormData: any, cont
incoterm: observationFormData?.incoterm || null,
// Campos de continuación mapeados a logistics
transport_num: continuationFormData?.numero_tipo_transporte || null,
// Identificadores y Complementos
identifier_1: observationFormData?.identifier_1 || null,
complement_1: observationFormData?.complement_1 || null,
identifier_2: observationFormData?.identifier_2 || null,
complement_2: observationFormData?.complement_2 || null,
acts_as_cd: observationFormData?.acts_as_cd || continuationFormData?.funge_como_cd || false,
is_subdivision: (observationFormData?.sub_division === 'si') || continuationFormData?.sub_division || false,
is_rail: continuationFormData?.es_ferrocarril === 'si' ? true : false,
bill_number: continuationFormData?.numero_bl || null,
guide_number: continuationFormData?.cantidad_guias_embarque ? String(continuationFormData.cantidad_guias_embarque) : null,
@@ -237,8 +252,6 @@ function buildLogisticsData(generalFormData: any, observationFormData: any, cont
origin_location: continuationFormData?.puerto_entrada || null,
// Checkboxes de continuación
equipment_reviewed: continuationFormData?.fue_revisado_equipo || false,
is_subdivision: continuationFormData?.sub_division || false,
acts_as_cd: continuationFormData?.funge_como_cd || false,
pedimento_arrived: continuationFormData?.llego_pedimento || false,
// Semáforos de continuación
green_light_mx: continuationFormData?.semaforo_verde_aduana_mexicana || false,

View File

@@ -112,6 +112,27 @@
let showExchangeRateDialog = $state(false);
let missingExchangeRateDate = $state('');
// Derivados reactivos para el tipo de operación
let operationTypeText = $derived.by(() => {
const type =
InvoiceTopFieldsFormData?.operation_type ||
data.invoice?.operation_type ||
data.filters?.operation_type;
if (type === 'exp') return 'EXPORTACIÓN';
if (type === 'imp') return 'IMPORTACIÓN';
return 'TIPO NO DEFINIDO';
});
let operationColorClass = $derived.by(() => {
const type =
InvoiceTopFieldsFormData?.operation_type ||
data.invoice?.operation_type ||
data.filters?.operation_type;
if (type === 'exp') return 'bg-blue-500/10 text-blue-600 border-blue-500/20';
if (type === 'imp') return 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20';
return 'bg-muted text-muted-foreground border-border';
});
async function checkExchangeRate(date: string): Promise<boolean> {
if (!date || !companyStore?.activeCompany?.id) return true;
@@ -293,7 +314,7 @@
: null}
Nueva Factura
{#if invoiceTypeInfo}
<span class="text-muted-foreground font-normal text-2xl">
<span class="text-2xl font-normal text-muted-foreground">
- {invoiceTypeInfo.description}
</span>
{/if}
@@ -301,12 +322,11 @@
Factura #{data.invoice.id}
{/if}
</h1>
<Badge variant="outline" class="px-3 py-1 text-sm font-bold {operationColorClass}">
{operationTypeText}
</Badge>
{#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>
<Badge variant="secondary">Borrador</Badge>
{/if}
</div>
<p class="text-muted-foreground">
@@ -324,6 +344,37 @@
<!-- Contenido de las tabs con padding inferior para el footer flotante -->
<div class="pb-48">
<Tabs.Root bind:value={activeTab}>
<div class="mb-4 rounded-lg border p-4 shadow-sm transition-colors {operationColorClass}">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-background/50">
{#if InvoiceTopFieldsFormData?.operation_type === 'exp'}
<Package size={20} />
{:else}
<Truck size={20} />
{/if}
</div>
<div>
<h3 class="text-sm font-semibold tracking-wider uppercase">
Operación de {operationTypeText}
</h3>
<p class="text-xs opacity-80">
{#if InvoiceTopFieldsFormData?.operation_type === 'exp'}
Factura para salida de mercancías del territorio nacional.
{:else if InvoiceTopFieldsFormData?.operation_type === 'imp'}
Factura para entrada de mercancías al territorio nacional.
{:else}
Por favor, seleccione el tipo de operación para continuar.
{/if}
</p>
</div>
</div>
<Badge variant="outline" class="bg-background/20 font-mono">
{InvoiceTopFieldsFormData?.operation_type?.toUpperCase() || '---'}
</Badge>
</div>
</div>
<InvoiceTopFields
invoice={data.invoice}
bind:formData={InvoiceTopFieldsFormData}
@@ -372,6 +423,15 @@
seals={data.seals || []}
incoterms={data.incoterms || []}
enclosure={data.enclosure || []}
operationType={InvoiceTopFieldsFormData?.operation_type === 'exp'
? 1
: InvoiceTopFieldsFormData?.operation_type === 'imp'
? 2
: data.invoice?.operation_type === 'exp'
? 1
: data.invoice?.operation_type === 'imp'
? 2
: undefined}
/>
</Tabs.Content>
@@ -380,6 +440,15 @@
invoice={data.invoice}
bind:formData={itemsFormData}
bind:exists={itemsExists}
operationType={InvoiceTopFieldsFormData?.operation_type === 'exp'
? 1
: InvoiceTopFieldsFormData?.operation_type === 'imp'
? 2
: data.invoice?.operation_type === 'exp'
? 1
: data.invoice?.operation_type === 'imp'
? 2
: undefined}
/>
</Tabs.Content>
@@ -420,9 +489,9 @@
}}
/>
<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] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
>
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
<div class="mx-auto max-w-[1400px] space-y-4 px-4 py-4">
<!-- Tabs Navigation -->
<Tabs.Root bind:value={activeTab}>
<div class="w-full overflow-x-auto pb-2">