Merge pull request 'feature/items' (#37) from feature/items into development

Reviewed-on: ADUANASOFT/anexo76#37
This commit is contained in:
2025-12-31 15:06:24 +00:00
53 changed files with 4121 additions and 1030 deletions

View File

@@ -232,10 +232,10 @@ export interface InvoiceListResponse {
}
export interface CreateInvoiceData {
system?: string | null;
operation_type?: OperationType | null;
invoice_type?: string | null;
invoice_number?: string | null;
system: string;
operation_type: OperationType;
invoice_type: string;
invoice_number: string;
project_number?: string | null;
purchase_order?: string | null;
related_doc_id?: number | null;

View File

@@ -0,0 +1,121 @@
/**
* API Client para Items
* Gestiona las operaciones CRUD para items de facturas
*/
import { api } from '$lib/api';
// --- Interfaces ---
export interface Item {
id?: number;
invoice_id: number;
reference_number?: string;
order?: string;
guide_number?: string;
depreciation_date?: number;
rectification?: number;
warehouse?: string;
location?: string;
created_at?: string;
updated_at?: string;
}
export interface ItemListResponse {
items: Item[];
total: number;
skip: number;
limit: number;
}
export interface CreateItemData {
invoice_id: number;
reference_number?: string;
order?: string;
guide_number?: string;
depreciation_date?: number;
rectification?: number;
warehouse?: string;
location?: string;
}
export interface UpdateItemData {
reference_number?: string;
order?: string;
guide_number?: string;
depreciation_date?: number;
rectification?: boolean;
warehouse?: string;
location?: string;
}
/**
* API para Items
*/
export const itemsApi = {
/**
* Lista todos los items con paginación
*/
list: (companyId: number, skip = 0, limit = 100, invoiceId?: number) => {
const params = new URLSearchParams({
company_id: companyId.toString(),
skip: skip.toString(),
limit: limit.toString()
});
if (invoiceId) {
params.append('invoice_id', invoiceId.toString());
}
return api.get<ItemListResponse>(`/v1/a76/items/?${params.toString()}`);
},
/**
* Lista items por invoice ID
*/
listByInvoice: (invoiceId: number, companyId: number) => {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<ItemListResponse>(`/v1/a76/items/invoice/${invoiceId}/items?${params.toString()}`);
},
/**
* Obtiene un item por ID
*/
get: (itemId: number, companyId: number) => {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<Item>(`/v1/a76/items/${itemId}?${params.toString()}`);
},
/**
* Crea un nuevo item
*/
create: (companyId: number, data: CreateItemData) => {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.post<Item>(`/v1/a76/items/?${params.toString()}`, data);
},
/**
* Actualiza un item existente
*/
update: (itemId: number, companyId: number, data: UpdateItemData) => {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.put<Item>(`/v1/a76/items/${itemId}?${params.toString()}`, data);
},
/**
* Elimina un item
*/
delete: (itemId: number, companyId: number) => {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.delete(`/v1/a76/items/${itemId}?${params.toString()}`);
}
};

View File

@@ -17,6 +17,12 @@
providers = [],
currencyTypes = [],
transportTypes = [],
transporters = [],
vehicles = [],
drivers = [],
trailers = [],
customsSections = [],
codePedimentoRegimens = [],
defaultOperationType = undefined,
defaultInvoiceType = undefined
}: {
@@ -28,9 +34,15 @@
providers?: any[];
currencyTypes?: any[];
transportTypes?: any[];
transporters?: any[];
vehicles?: any[];
drivers?: any[];
trailers?: any[];
customsSections?: any[];
codePedimentoRegimens?: any[];
defaultOperationType?: number | null;
defaultInvoiceType?: string | null;
} = $props();
} = $props();
if (!formData) {
if (invoice) {
@@ -174,6 +186,13 @@
// Combinar clientes y proveedores para shipped_to
const allClientsProviders = [...clients, ...providers];
// Filtrar regímenes por tipo de operación (1='1' exp, 2='2' imp)
const filteredRegimens = $derived(
codePedimentoRegimens.filter(r =>
r.type_code === String(formData.operation_type)
)
);
</script>
<!-- Layout de 2 columnas compacto -->
@@ -203,20 +222,17 @@
</div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Clientes - Proveedores - Agente Aduanal</h4>
<div class="space-y-1.5">
<Label for="provider_id" class="text-xs">Proveedor:</Label>
<div class="grid grid-cols-4 gap-3 space-y-1.5">
<Select.Root
type="single"
value={formData.provider_header || ''}
value={formData.provider_header || providerHeaderOptions[0]?.value || ''}
onValueChange={(v) => {
formData.provider_header = v ?? '';
}}
>
<Select.Trigger id="provider_header" class="h-7 text-xs">
<Select.Trigger id="provider_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
<span class="truncate">
{formData.provider_header
? providerHeaderOptions.find(o => o.value === formData.provider_header)?.label || formData.provider_header
: 'Selecciona encabezado...'}
{providerHeaderOptions.find(o => o.value === (formData.provider_header || providerHeaderOptions[0]?.value))?.label || 'Selecciona encabezado...'}
</span>
</Select.Trigger>
<Select.Content>
@@ -234,7 +250,7 @@
formData.provider_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="provider_id" class="h-7 text-xs">
<Select.Trigger id="provider_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{formData.provider_id
? providers.find(p => p.id === formData.provider_id)?.name || 'Selecciona...'
@@ -248,23 +264,20 @@
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="sold_to_id" class="text-xs">Consignado a:</Label>
<div class="grid grid-cols-4 gap-3 space-y-1.5">
<Select.Root
type="single"
value={formData.sold_to_header || ''}
value={formData.sold_to_header || soldToHeaderOptions[0]?.value || ''}
onValueChange={(v) => {
formData.sold_to_header = v ?? '';
}}
>
<Select.Trigger id="sold_to_header" class="h-7 text-xs">
<Select.Trigger id="sold_to_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
<span class="truncate">
{formData.sold_to_header
? soldToHeaderOptions.find(o => o.value === formData.sold_to_header)?.label || formData.sold_to_header
: 'Selecciona encabezado...'}
{soldToHeaderOptions.find(o => o.value === (formData.sold_to_header || soldToHeaderOptions[0]?.value))?.label || 'Selecciona encabezado...'}
</span>
</Select.Trigger>
<Select.Content>
@@ -282,7 +295,7 @@
formData.sold_to_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="sold_to_id" class="h-7 text-xs">
<Select.Trigger id="sold_to_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{formData.sold_to_id
? clients.find(c => c.id === formData.sold_to_id)?.name || 'Selecciona...'
@@ -299,20 +312,17 @@
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="shipped_to_id" class="text-xs">Enviado a:</Label>
<div class="grid grid-cols-4 gap-3 space-y-1.5">
<Select.Root
type="single"
value={formData.shipped_to_header || ''}
value={formData.shipped_to_header || shippedToHeaderOptions[0]?.value || ''}
onValueChange={(v) => {
formData.shipped_to_header = v ?? '';
}}
>
<Select.Trigger id="shipped_to_header" class="h-7 text-xs">
<Select.Trigger id="shipped_to_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
<span class="truncate">
{formData.shipped_to_header
? shippedToHeaderOptions.find(o => o.value === formData.shipped_to_header)?.label || formData.shipped_to_header
: 'Selecciona encabezado...'}
{shippedToHeaderOptions.find(o => o.value === (formData.shipped_to_header || shippedToHeaderOptions[0]?.value))?.label || 'Selecciona encabezado...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
@@ -330,7 +340,7 @@
formData.shipped_to_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="shipped_to_id" class="h-7 text-xs">
<Select.Trigger id="shipped_to_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{formData.shipped_to_id
? allClientsProviders.find(cp => cp.id === formData.shipped_to_id)?.name || 'Selecciona...'
@@ -347,66 +357,66 @@
</Select.Root>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1.5">
<Label for="customs_broker_id" class="text-xs">Agente Aduanal Mex:</Label>
<Select.Root
type="single"
value={formData.customs_broker_id || ''}
onValueChange={(v) => {
formData.customs_broker_id = v || null;
}}
>
<Select.Trigger id="customs_broker_id" class="h-7 text-xs">
<span class="truncate">
{formData.customs_broker_id
? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_id)?.name || '...'
: '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsBrokers as broker}
<Select.Item value={broker.broker_key}>
{broker.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="customs_broker_id" class="text-xs">Agente Aduanal Mex:</Label>
<Select.Root
type="single"
value={formData.customs_broker_id || customsBrokers[0]?.broker_key || ''}
onValueChange={(v) => {
formData.customs_broker_id = v || null;
}}
>
<Select.Trigger id="customs_broker_id" class="h-7 text-xs min-w-[150px] max-w-[300px]">
<span class="truncate">
{customsBrokers.find(cb => cb.broker_key === (formData.customs_broker_id || customsBrokers[0]?.broker_key))?.name || '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsBrokers as broker}
<Select.Item value={broker.broker_key}>
{broker.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="customs_broker_us_id" class="text-xs">Agente Aduanal Ame:</Label>
<Select.Root
type="single"
value={formData.customs_broker_us_id || ''}
onValueChange={(v) => {
formData.customs_broker_us_id = v || null;
}}
>
<Select.Trigger id="customs_broker_us_id" class="h-7 text-xs">
<span class="truncate">
{formData.customs_broker_us_id
? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_us_id)?.name || '...'
: '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsBrokers as broker}
<Select.Item value={broker.broker_key}>
{broker.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="space-y-1.5">
<Label for="customs_broker_us_id" class="text-xs">Agente Aduanal Ame:</Label>
<Select.Root
type="single"
value={formData.customs_broker_us_id || ''}
onValueChange={(v) => {
formData.customs_broker_us_id = v || null;
}}
>
<Select.Trigger id="customs_broker_us_id" class=" min-w-[150px] h-7 text-xs">
<span class="truncate">
{formData.customs_broker_us_id
? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_us_id)?.name || '...'
: '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsBrokers as broker}
<Select.Item value={broker.broker_key}>
{broker.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<!-- Columna Derecha: Tipo de Moneda y Transportista -->
<div class="space-y-3">
<!-- Tipo de Moneda - Pesos Netos y Brutos -->
<div class="border rounded-md p-3 space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Tipo de Moneda - Pesos Netos y Brutos</h4>
<div class="flex justify-between">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Tipo de Moneda - Pesos Netos y Brutos</h4>
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Tipo de cambio: </h4>
</div>
<!-- Radio buttons para tipo de moneda -->
<div class="space-y-1.5">
@@ -425,8 +435,7 @@
</div>
</RadioGroup.Root>
</div>
<div class="grid grid-cols-3 gap-2">
{#if formData.currency_mode === 'captura'}
<div class="space-y-1.5">
<Label for="currency_type" class="text-xs">Moneda:</Label>
<Select.Root
@@ -436,12 +445,12 @@
formData.currency_type = v ?? '';
}}
>
<Select.Trigger id="currency_type" class="h-7 text-xs">
<Select.Trigger id="currency_type" class="h-7 text-xs min-w-[80px]">
<span class="truncate">
{formData.currency_type || '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Content class="min-w-[80px] max-h-[300px]">
{#each currencyTypes as currencyType}
<Select.Item value={currencyType.code}>
{currencyType.code}
@@ -450,7 +459,8 @@
</Select.Content>
</Select.Root>
</div>
{/if}
<div class="grid grid-cols-3 gap-2">
<div class="space-y-1.5">
<Label for="weight_type" class="text-xs">Tipo Peso:</Label>
<Select.Root
@@ -460,15 +470,15 @@
formData.weight_type = v ?? '';
}}
>
<Select.Trigger id="weight_type" class="h-7 text-xs">
<Select.Trigger id="weight_type" class="min-w-[150px] h-7 text-xs">
<span class="truncate">
{formData.weight_type || '...'}
</span>
</Select.Trigger>
<Select.Content>
{#each weightTypeOptions as weightType}
<Select.Item value={weightType.value}>
{weightType.value}
<Select.Item value={weightType.label}>
{weightType.label}
</Select.Item>
{/each}
</Select.Content>
@@ -479,33 +489,7 @@
<Label for="iva_factor" class="text-xs">IVA:</Label>
<Input id="iva_factor" type="number" step="0.0001" bind:value={formData.iva_factor} placeholder="0.16" class="h-7 text-xs" />
</div>
</div>
<div class="space-y-1.5">
<Label for="invoice_type" class="text-xs">Tipo de Cambio:</Label>
<Select.Root
type="single"
value={formData.invoice_type || ''}
onValueChange={(v) => {
formData.invoice_type = v ?? '';
}}
>
<Select.Trigger id="invoice_type" class="h-7 text-xs">
<span class="truncate">
{formData.invoice_type
? `${formData.invoice_type}`
: '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each invoiceTypes as type}
<Select.Item value={type.key}>
{type.key} - {type.description}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
</div>
<!-- Transportista -->
@@ -514,8 +498,33 @@
<div class="grid grid-cols-3 gap-2">
<div class="space-y-1.5">
<Label for="carrier_id" class="text-xs">Clave:</Label>
<Input id="carrier_id" type="number" bind:value={formData.carrier_id} class="h-7 text-xs" />
<Label for="carrier_id" class="text-xs">Transportista:</Label>
<Select.Root
type="single"
value={formData.carrier_id ? String(formData.carrier_id) : ''}
onValueChange={(v) => {
formData.carrier_id = v || null;
}}
>
<Select.Trigger id="carrier_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{#if formData.carrier_id}
{transporters.find(t => String(t.transporter_key) === String(formData.carrier_id))?.name || formData.carrier_id}
{:else if transporters.length > 0}
Selecciona transportista...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each transporters as transporter}
<Select.Item value={String(transporter.transporter_key)}>
{transporter.transporter_key}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="col-span-2 space-y-1.5">
@@ -527,15 +536,113 @@
formData.transport_type = v ?? '';
}}
>
<Select.Trigger id="transport_type" class="h-7 text-xs">
<Select.Trigger id="transport_type" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{formData.transport_type || '...'}
{#if formData.transport_type}
{vehicles.find(v => v.vehicle_key === formData.transport_type)?.vehicle_key || formData.transport_type}
{:else if vehicles.length > 0}
Selecciona vehículo...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each transportTypes as transportType}
<Select.Item value={transportType.transport_code}>
{transportType.transport_code}
{#each vehicles as vehicle}
<Select.Item value={vehicle.vehicle_key}>
{vehicle.vehicle_key} {vehicle.plate_number ? `- ${vehicle.plate_number}` : ''}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="space-y-1.5">
<Label for="driver_name" class="text-xs">Conductor:</Label>
<Select.Root
type="single"
value={formData.driver_name || ''}
onValueChange={(v) => {
formData.driver_name = v ?? '';
}}
>
<Select.Trigger id="driver_name" class="h-7 text-xs w-full">
<span class="truncate">
{#if formData.driver_name}
{formData.driver_name}
{:else if drivers.length > 0}
Selecciona conductor...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each drivers as driver}
<Select.Item value={driver.driver_name}>
{driver.driver_name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="grid grid-cols-4 gap-2">
<div class="space-y-1.5">
<Label for="transport_type" class="text-xs">Tipo Transporte:</Label>
<Select.Root
type="single"
value={formData.transport_id || 'Ninguno'}
onValueChange={(v) => {
formData.transport_id = v ?? 'Ninguno';
}}
>
<Select.Trigger id="transport_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{formData.transport_id || 'Ninguno'}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="Ninguno">Ninguno</Select.Item>
<Select.Item value="Transporte">Transporte</Select.Item>
<Select.Item value="Caja">Caja</Select.Item>
<Select.Item value="Placas">Placas</Select.Item>
<Select.Item value="Camión">Camión</Select.Item>
<Select.Item value="Buque">Buque</Select.Item>
<Select.Item value="Ferrobarcaza">Ferrobarcaza</Select.Item>
<Select.Item value="Contenedor">Contenedor</Select.Item>
<Select.Item value="Avion">Avion</Select.Item>
<Select.Item value="Gondola">Gondola</Select.Item>
<Select.Item value="Plataforma">Plataforma</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5 col-span-3">
<Label for="transport_num" class="text-xs">Placas:</Label>
<Select.Root
type="single"
value={formData.transport_num || ''}
onValueChange={(v) => {
formData.transport_num = v ?? '';
}}
>
<Select.Trigger id="transport_num" class="h-7 text-xs w-full">
<span class="truncate">
{#if formData.transport_num}
{trailers.find(t => t.trailer_number === formData.transport_num)?.plate_number || formData.transport_num}
{:else if trailers.length > 0}
Selecciona remolque...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each trailers as trailer}
<Select.Item value={trailer.trailer_number}>
{trailer.plate_number || trailer.trailer_number}
</Select.Item>
{/each}
</Select.Content>
@@ -543,31 +650,66 @@
</div>
</div>
<div class="space-y-1.5">
<Label for="driver_name" class="text-xs">Conductor:</Label>
<Input id="driver_name" bind:value={formData.driver_name} class="h-7 text-xs" />
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1.5">
<Label for="transport_type" class="text-xs">Tipo Transporte:</Label>
<Input id="transport_id" bind:value={formData.transport_id} class="h-7 text-xs" />
</div>
<div class="space-y-1.5">
<Label for="transport_num" class="text-xs">Placas:</Label>
<Input id="transport_num" bind:value={formData.transport_num} class="h-7 text-xs" />
</div>
</div>
<div class="space-y-1.5">
<Label for="aduana" class="text-xs">Aduana y Sección de Despacho:</Label>
<Input id="aduana" bind:value={formData.aduana} class="h-7 text-xs" />
<Select.Root
type="single"
value={formData.aduana || ''}
onValueChange={(v) => {
formData.aduana = v ?? '';
}}
>
<Select.Trigger id="aduana" class="h-7 text-xs w-full">
<span class="truncate">
{#if formData.aduana}
{customsSections.find(cs => cs.customs_code === formData.aduana)?.section_name || formData.aduana}
{:else if customsSections.length > 0}
Selecciona aduana...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsSections as section}
<Select.Item value={section.customs_code}>
{section.customs_code} - {section.section_name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="clave_regimen_aduanero" class="text-xs">Clave de Régimen Aduanero:</Label>
<Input id="clave_regimen_aduanero" bind:value={formData.clave_regimen_aduanero} class="h-7 text-xs" />
<Select.Root
type="single"
value={formData.clave_regimen_aduanero || ''}
onValueChange={(v) => {
formData.clave_regimen_aduanero = v ?? '';
}}
>
<Select.Trigger id="clave_regimen_aduanero" class="h-7 text-xs w-full">
<span class="truncate">
{#if formData.clave_regimen_aduanero}
{formData.clave_regimen_aduanero}
{:else if filteredRegimens.length > 0}
Selecciona régimen...
{:else if formData.operation_type}
Sin regímenes para tipo {formData.operation_type}
{:else}
Selecciona tipo de operación primero
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each filteredRegimens as regimen}
<Select.Item value={regimen.regimen_code}>
{regimen.regimen_code}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
</div>

View File

@@ -0,0 +1,72 @@
<script lang="ts">
import * as RadioGroup from '$lib/components/ui/radio-group';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
let {
isSubPartida = $bindable(),
continueSubPartidas = $bindable()
}: {
isSubPartida: string;
continueSubPartidas: string;
} = $props();
</script>
<div class="lg:col-span-5 space-y-3">
<!-- Is Item/Subitem and Continue Sub-Items -->
<div class="grid grid-cols-2 gap-2">
<fieldset class="border rounded-md p-2 space-y-2">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Is</legend>
<RadioGroup.Root bind:value={isSubPartida} class="flex gap-3">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="partida" id="partida" />
<Label for="partida" class="text-xs font-normal">Item</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="subpartida" id="subpartida" />
<Label for="subpartida" class="text-xs font-normal">Subitem</Label>
</div>
</RadioGroup.Root>
</fieldset>
<fieldset class="border rounded-md p-2 space-y-2">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Continue Sub-Items</legend>
<RadioGroup.Root bind:value={continueSubPartidas} class="flex gap-3">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="si" id="continue_si" />
<Label for="continue_si" class="text-xs font-normal">Yes</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="no" id="continue_no" />
<Label for="continue_no" class="text-xs font-normal">No</Label>
</div>
</RadioGroup.Root>
</fieldset>
</div>
<!-- Descriptions -->
<fieldset class="border rounded-md p-2 space-y-2">
<div class="space-y-1">
<Label for="num_parte" class="text-xs">Part Number:</Label>
<div class="flex gap-1">
<Input id="num_parte" class="h-7 text-xs" />
</div>
</div>
<div class="space-y-1">
<Label for="desc_espanol" class="text-xs">Description in Spanish:</Label>
<textarea
id="desc_espanol"
class="flex min-h-[60px] w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></textarea>
</div>
<div class="space-y-1">
<Label for="desc_ingles" class="text-xs">Description in English:</Label>
<textarea
id="desc_ingles"
class="flex min-h-[60px] w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></textarea>
</div>
</fieldset>
</div>

View File

@@ -0,0 +1,124 @@
<script lang="ts">
import * as Sheet from '$lib/components/ui/sheet';
import * as Tabs from '$lib/components/ui/tabs';
import { Button } from '$lib/components/ui/button';
import { Loader2 } from 'lucide-svelte';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import type { Item } from '$lib/api/dashboard/a76/items';
// Import child components
import MainData from './main-data.svelte';
import ItemConfiguration from './item-configuration.svelte';
import PackagesSection from './packages-section.svelte';
import SummarySection from './summary-section.svelte';
import TabContinuation from './tab-continuation.svelte';
import TabSeries from './tab-series.svelte';
import TabLabeling from './tab-labeling.svelte';
import TabIdentifiers from './tab-identifiers.svelte';
let {
open = $bindable(),
isEditMode = false,
editingItem = $bindable(),
invoice,
onSave,
isSaving = false
}: {
open: boolean;
isEditMode?: boolean;
editingItem: Partial<Item>;
invoice: Invoice | null;
onSave: () => void;
isSaving?: boolean;
} = $props();
let isSubPartida = $state('partida');
let continueSubPartidas = $state('no');
</script>
<style>
:global([data-tabs-trigger]) {
cursor: pointer;
}
</style>
<Sheet.Root bind:open={open}>
<Sheet.Content side="right" class="w-full sm:max-w-[95vw] lg:max-w-[80vw] xl:max-w-[70vw] overflow-y-auto overflow-x-hidden">
<Sheet.Header class="text-white -mx-4 -mt-3 px-6">
<Sheet.Title class="text-base font-semibold text-white">
Temporary Import Item
</Sheet.Title>
<Sheet.Description class="text-xs text-purple-100">
Order Number: {invoice?.invoice_number || 'N/A'} | Line: 1
</Sheet.Description>
</Sheet.Header>
<div class="w-full max-w-full overflow-x-hidden px-1">
<!-- Always visible section: Main data and right column -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4 mb-6">
<!-- Left column (7 columns) -->
<div class="lg:col-span-7 space-y-3">
<MainData />
</div>
<!-- Right column (5 columns) -->
<ItemConfiguration bind:isSubPartida={isSubPartida} bind:continueSubPartidas={continueSubPartidas} />
</div>
<!-- Tabs with additional content -->
<Tabs.Root value="generales" class="mt-6">
<Tabs.List class="grid w-full grid-cols-5 mb-4 h-9 gap-1">
<Tabs.Trigger value="generales" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">1) General</Tabs.Trigger>
<Tabs.Trigger value="continuacion" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">2) Continuation</Tabs.Trigger>
<Tabs.Trigger value="series" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">3) Series</Tabs.Trigger>
<Tabs.Trigger value="etiquetado" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">4) Labeling</Tabs.Trigger>
<Tabs.Trigger value="identificadores" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">5) Identifiers</Tabs.Trigger>
</Tabs.List>
<!-- Tab: General -->
<Tabs.Content value="generales" class="space-y-3 mt-0">
<div class="grid grid-cols-2 gap-3">
<PackagesSection />
<SummarySection />
</div>
</Tabs.Content>
<!-- Tab: Continuation -->
<Tabs.Content value="continuacion" class="space-y-3 mt-0">
<TabContinuation />
</Tabs.Content>
<!-- Tab: Series -->
<Tabs.Content value="series" class="space-y-3 mt-0">
<TabSeries />
</Tabs.Content>
<!-- Tab: Labeling -->
<Tabs.Content value="etiquetado" class="space-y-3 mt-0">
<TabLabeling />
</Tabs.Content>
<!-- Tab: Identifiers -->
<Tabs.Content value="identificadores" class="space-y-3 mt-0">
<TabIdentifiers />
</Tabs.Content>
</Tabs.Root>
</div>
<Sheet.Footer>
<div class="grid grid-cols-2 gap-2">
<Button variant="outline" onclick={() => open = false} disabled={isSaving}>
Cancel
</Button>
<Button onclick={onSave} disabled={isSaving}>
{#if isSaving}
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
Saving...
{:else}
{isEditMode ? 'Save Changes' : 'Add Item'}
{/if}
</Button>
</div>
</Sheet.Footer>
</Sheet.Content>
</Sheet.Root>

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
</script>
<fieldset class="border rounded-md p-3 space-y-3">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Main Data</legend>
<!-- Class -->
<div class="grid grid-cols-12 gap-2">
<div class="col-span-4 space-y-1">
<Label for="clase" class="text-xs font-medium">* Class:</Label>
<div class="flex gap-1">
<Input id="clase" class="h-8 text-xs" />
</div>
</div>
</div>
<!-- Quantity -->
<div class="grid grid-cols-12 gap-2">
<div class="col-span-4 space-y-1">
<Label for="cantidad" class="text-xs font-medium">* Quantity:</Label>
<Input id="cantidad" type="number" min="0" value="0.00000000" class="h-8 text-xs text-right" />
</div>
<div class="col-span-4 space-y-1">
<Label class="text-xs font-medium">U.M.:</Label>
<div class="flex gap-1">
<div class="h-8 flex items-center flex-1">
<span class="text-xs text-muted-foreground">-</span>
</div>
</div>
</div>
</div>
<!-- Unit Cost and Fraction -->
<div class="grid grid-cols-12 gap-2">
<div class="col-span-4 space-y-1">
<Label for="costo_unitario" class="text-xs font-medium">* Unit Cost:</Label>
<div class="flex items-center gap-2">
<Input id="costo_unitario" type="number" min="0" value="0.00000000" class="h-8 text-xs text-right flex-1" />
<span class="text-xs text-blue-600 font-semibold">USD</span>
</div>
</div>
<div class="col-span-5 space-y-1">
<Label for="fraccion" class="text-xs font-medium">Fraction:</Label>
<div class="flex gap-1">
<Input id="fraccion" value="0000 00 00" class="h-8 text-xs text-center" />
</div>
</div>
</div>
<!-- Origin Country and Tariff Type -->
<div class="grid grid-cols-12 gap-2">
<div class="col-span-4 space-y-1">
<Label for="pais_origen" class="text-xs font-medium">* Origin Country:</Label>
<div class="flex gap-1">
<Input id="pais_origen" class="h-8 text-xs" />
</div>
</div>
<div class="col-span-4 space-y-1">
<Label for="tipo_tarifa" class="text-xs font-medium">* Tariff Type:</Label>
<select id="tipo_tarifa" class="flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background">
<option>GENERAL</option>
<option>PREFERENCIAL</option>
</select>
</div>
<div class="col-span-4 space-y-1">
<Label class="text-xs font-medium">Advalorem:</Label>
<div class="h-8 flex items-center">
<span class="text-xs text-muted-foreground">0.00</span>
</div>
</div>
</div>
</fieldset>

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
</script>
<fieldset class="border rounded-md p-2 space-y-2">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">PACKAGES</legend>
<div class="grid grid-cols-12 gap-2 items-end">
<div class="col-span-2 space-y-1">
<Label for="cantidad_bultos" class="text-xs">Quantity:</Label>
<Input id="cantidad_bultos" type="number" min="0" value="0" class="h-7 text-xs text-right" />
</div>
<div class="col-span-3 space-y-1">
<Label for="clave_bultos" class="text-xs">Package Code:</Label>
<Input id="clave_bultos" class="h-7 text-xs" />
</div>
<div class="col-span-1 flex items-end">
</div>
</div>
<div class="grid grid-cols-12 gap-2 items-end">
<div class="col-span-2 space-y-1">
<Label for="peso_bultos" class="text-xs">Weight: 0</Label>
</div>
<div class="col-span-4 space-y-1">
<Label for="descripcion_bultos" class="text-xs">Description:</Label>
</div>
</div>
<!-- WEIGHTS subsection -->
<div class="border-t pt-2">
<div class="text-xs font-semibold mb-2">WEIGHTS</div>
<div class="grid grid-cols-6 gap-2 items-end">
<div class="col-span-2 space-y-1">
<Label for="peso_neto" class="text-xs">Net:</Label>
<Input id="peso_neto" type="number" min="0" value="0.00000000" class="h-7 text-xs text-right" />
</div>
<div class="col-span-2 space-y-1">
<Label for="peso_bruto" class="text-xs">Gross:</Label>
<Input id="peso_bruto" type="number" min="0" value="0.00000000" class="h-7 text-xs text-right" />
</div>
<div class="col-span-2 space-y-1">
<Label class="text-xs invisible">Space</Label>
<span class="text-xs text-red-600 font-semibold">KILOS</span>
</div>
</div>
</div>
<div class="grid grid-cols-12 gap-2 items-end">
<div class="col-span-2 space-y-1">
<Label for="no_permiso" class="text-xs">Permit No.:</Label>
<Input id="no_permiso" class="h-7 text-xs" />
</div>
<div class="col-span-3 space-y-1">
<Label for="pag_region" class="text-xs">Page/Region:</Label>
<Input id="pag_region" class="h-7 text-xs" />
</div>
</div>
<div class="grid grid-cols-12 gap-2 items-end">
<div class="col-span-4 space-y-1">
<Label for="fraccion_americana" class="text-xs">American Fraction:</Label>
<Input id="fraccion_americana" class="h-7 text-xs" />
</div>
<div class="col-span-3 space-y-1">
<Label class="text-xs invisible">Space</Label>
<span class="text-xs">Advalorem: 0.00</span>
</div>
</div>
<div class="grid grid-cols-9 gap-2 items-end">
<div class="col-span-3 space-y-1">
<Label for="marca" class="text-xs">Brand:</Label>
<Input id="marca" class="h-7 text-xs" />
</div>
<div class="col-span-3 space-y-1">
<Label for="modelo" class="text-xs">Model:</Label>
<Input id="modelo" class="h-7 text-xs" />
</div>
<div class="col-span-3 space-y-1">
<Label for="orden_compra" class="text-xs">Purchase Order:</Label>
<Input id="orden_compra" class="h-7 text-xs" />
</div>
</div>
</fieldset>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
</script>
<div>
<fieldset class="border rounded-md p-2 space-y-1 bg-red-50 dark:bg-red-950/20">
<legend class="text-xs font-semibold px-2 bg-red-700 text-white">GENERAL DATA</legend>
<div class="text-xs font-semibold">RETURN QUANTITY SUB-ITEMS</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div>Temporary: <span class="text-blue-600">0.00000000</span></div>
<div>Replacement or Change: <span class="text-blue-600">0.00000000</span></div>
<div>Definitive: <span class="text-blue-600">0.00000000</span></div>
<div>Returned Values: <span class="text-blue-600">0.00000000</span></div>
<div class="col-span-2">Returned Values: <span class="text-blue-600">0.00000000</span></div>
</div>
<div class="grid grid-cols-2 gap-2 text-xs pt-2 border-t">
<div class="font-semibold">WEIGHTS (KILOS)</div>
<div class="font-semibold">WEIGHTS (Pounds)</div>
<div>Net: <span class="text-blue-600">0.00000000</span></div>
<div><span class="text-blue-600">0.00000000</span></div>
<div>Whole: <span class="text-blue-600">0.00000000</span></div>
<div><span class="text-blue-600">0.00000000</span></div>
</div>
</fieldset>
<!-- COSTS AND VALUES -->
<fieldset class="border rounded-md p-2 space-y-1 bg-amber-50 dark:bg-amber-950/20">
<legend class="text-xs font-semibold px-2 bg-amber-700 text-white">COSTS AND VALUES</legend>
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="font-semibold">(Dollars)</div>
<div class="font-semibold">(Pesos)</div>
<div>Cost: <span class="text-blue-600">0.00000000</span></div>
<div><span class="text-blue-600">0.00000000</span></div>
<div>Value: <span class="text-blue-600">0.00000000</span></div>
<div><span class="text-blue-600">0.00000000</span></div>
</div>
<div class="space-y-1 pt-2 border-t">
<div class="text-xs">Capture Cost: <span class="text-blue-600">0.00000000</span> <span class="text-blue-600">USD</span></div>
<div class="text-xs">Capture Value: <span class="text-blue-600">0.00000000</span> <span class="text-blue-600">USD</span></div>
<div class="text-xs">Customs Value: <span class="text-blue-600">0.00000000</span> <span class="text-blue-600">USD</span></div>
</div>
</fieldset>
</div>

View File

@@ -0,0 +1,133 @@
<script lang="ts">
import * as RadioGroup from '$lib/components/ui/radio-group';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
</script>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Left Column -->
<div class="space-y-3">
<!-- TAX PAID -->
<div class="grid grid-cols-4 gap-3">
<fieldset class="border rounded-md p-2 space-y-2">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">TAX PAID</legend>
<RadioGroup.Root value="no" class="flex gap-3">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="si" id="pago_impuesto_si" />
<Label for="pago_impuesto_si" class="text-xs font-normal">Yes</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="no" id="pago_impuesto_no" />
<Label for="pago_impuesto_no" class="text-xs font-normal">No</Label>
</div>
</RadioGroup.Root>
</fieldset>
<div class="space-y-1">
<div class="space-y-1">
<Label for="forma_pago" class="text-xs">Payment Method:</Label>
<div class="flex gap-1">
<Input id="forma_pago" value="21" class="h-7 text-xs" />
</div>
</div>
<Label for="credito_iva" class="text-xs">VAT AND EXCISE TAX CREDITS.</Label>
</div>
</div>
<!-- IGI Amount -->
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label for="monto_igi" class="text-xs">IGI Amount: 0 <span class="text-xs">DOLLARS</span></Label>
</div>
</div>
<!-- Certificate of Origin -->
<div class="grid grid-cols-4 gap-3">
<fieldset class="border rounded-md p-2 space-y-2">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Has Certificate of Origin?</legend>
<RadioGroup.Root value="no" class="flex gap-3">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="si" id="cert_origen_si" />
<Label for="cert_origen_si" class="text-xs font-normal">Yes</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="no" id="cert_origen_no" />
<Label for="cert_origen_no" class="text-xs font-normal">No</Label>
</div>
</RadioGroup.Root>
</fieldset>
<div class="space-y-1 col-span-3">
<Label for="num_cert_origen" class="text-xs">Certificate of Origin No.:</Label>
<Input id="num_cert_origen" class="h-7 text-xs" />
<Label for="num_cert_origen" class="text-xs">End Date:</Label>
</div>
</div>
<!-- Location -->
<div class="space-y-2">
<div class="space-y-1">
<Label for="localizacion_maquinaria" class="text-xs">Machinery and equipment location:</Label>
<div class="flex gap-1">
<Input id="localizacion_maquinaria" class="h-7 text-xs" />
</div>
<Label for="localizacion_maquinaria" class="text-xs">Location variable</Label>
</div>
</div>
<!-- Military Equipment -->
<div class="flex items-center space-x-2 ">
<Checkbox id="equipo_militar" />
<Label for="equipo_militar" class="text-xs font-normal">Enable if Item Contains Military Equipment</Label>
</div>
<!-- Lot and Entry Number -->
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label for="lote" class="text-xs">Lot:</Label>
<Input id="lote" class="h-7 text-xs" />
</div>
<div class="space-y-1">
<Label for="num_entrada" class="text-xs">Entry No.:</Label>
<Input id="num_entrada" class="h-7 text-xs" />
</div>
</div>
</div>
<!-- Right Column -->
<div class="space-y-3">
<!-- Permit and Eighth Rule Fraction -->
<div class="space-y-2">
<div class="space-y-1">
<Label for="permiso_regla_octava" class="text-xs">Eighth Rule Permit:</Label>
<div class="flex gap-1">
<Input id="permiso_regla_octava" class="h-7 text-xs" />
</div>
</div>
<div class="grid grid-cols-4 gap-2 items-end">
<div class="space-y-1 col-span-3">
<Label for="fraccion_regla_octava" class="text-xs">Eighth Rule Fraction:</Label>
<Input id="fraccion_regla_octava" value="0000.00.00" class="h-7 text-xs" />
</div>
<div class="space-y-1">
<Label for="linea_regla" class="text-xs">Line:</Label>
<Input id="linea_regla" type="number" min="0" value="0" class="h-7 text-xs text-right" />
</div>
</div>
</div>
<!-- Consider in a31 -->
<div class="flex items-center space-x-2 ">
<Checkbox id="a31" />
<Label for="a31" class="text-xs font-normal">Consider in A31</Label>
</div>
<!-- Extra Description -->
<div class="space-y-1">
<Label for="desc_extra_espanol" class="text-xs">Extra Description in Spanish:</Label>
<textarea
id="desc_extra_espanol"
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></textarea>
</div>
</div>
</div>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
</script>
<fieldset class="border rounded-md p-3 space-y-3">
<legend class="text-xs font-semibold px-2 uppercase">Identifiers</legend>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="identificador1" class="text-xs">Identifier 1:</Label>
<Input id="identificador1" class="h-8 text-sm" />
</div>
<div class="space-y-2">
<Label for="identificador2" class="text-xs">Identifier 2:</Label>
<Input id="identificador2" class="h-8 text-sm" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="identificador3" class="text-xs">Identifier 3:</Label>
<Input id="identificador3" class="h-8 text-sm" />
</div>
<div class="space-y-2">
<Label for="identificador4" class="text-xs">Identifier 4:</Label>
<Input id="identificador4" class="h-8 text-sm" />
</div>
</div>
<div class="space-y-2">
<Label for="notas_identificadores" class="text-xs">Notes:</Label>
<textarea
id="notas_identificadores"
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="Notes about identifiers..."
></textarea>
</div>
</fieldset>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
</script>
<fieldset class="border rounded-md p-3 space-y-3">
<legend class="text-xs font-semibold px-2 uppercase">Labeling</legend>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="numero_etiqueta" class="text-xs">Label Number:</Label>
<Input id="numero_etiqueta" class="h-8 text-sm" />
</div>
<div class="space-y-2">
<Label for="tipo_etiqueta" class="text-xs">Label Type:</Label>
<Input id="tipo_etiqueta" class="h-8 text-sm" />
</div>
</div>
<div class="space-y-2">
<Label for="observaciones_etiqueta" class="text-xs">Observations:</Label>
<textarea
id="observaciones_etiqueta"
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="Labeling observations..."
></textarea>
</div>
</fieldset>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label';
</script>
<fieldset class="border rounded-md p-3 space-y-3">
<legend class="text-xs font-semibold px-2 uppercase">Serial Numbers</legend>
<div class="space-y-2">
<Label for="series" class="text-xs">Serial Numbers:</Label>
<textarea
id="series"
class="flex min-h-[100px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="Enter serial numbers, one per line..."
></textarea>
</div>
<div class="text-xs text-muted-foreground">
You can enter multiple serial numbers, one per line
</div>
</fieldset>

View File

@@ -0,0 +1,274 @@
<script lang="ts">
import * as Sheet from '$lib/components/ui/sheet';
import * as Tabs from '$lib/components/ui/tabs';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { Loader2 } from 'lucide-svelte';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import type { Item } from '$lib/api/dashboard/a76/items';
let {
open = $bindable(),
isEditMode = false,
editingItem = $bindable(),
invoice,
onSave,
isSaving = false
}: {
open: boolean;
isEditMode?: boolean;
editingItem: Partial<Item>;
invoice: Invoice | null;
onSave: () => void;
isSaving?: boolean;
} = $props();
</script>
<Sheet.Root bind:open={open}>
<Sheet.Content side="right" class="w-full sm:max-w-2xl overflow-y-auto">
<Sheet.Header>
<Sheet.Title>{isEditMode ? 'Editar Item' : 'Agregar Nuevo Item'} - SCAII (Inventario)</Sheet.Title>
<Sheet.Description>
{isEditMode ? 'Modifica los campos del inventario y guarda los cambios.' : 'Completa la información del nuevo item de inventario.'}
</Sheet.Description>
</Sheet.Header>
<Tabs.Root value="general" class="mt-6">
<Tabs.List class="grid w-full grid-cols-4">
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="clasificacion">Clasificación</Tabs.Trigger>
<Tabs.Trigger value="cantidades">Cantidades</Tabs.Trigger>
<Tabs.Trigger value="otros">Otros</Tabs.Trigger>
</Tabs.List>
<!-- Tab: General -->
<Tabs.Content value="general" class="space-y-4 mt-4">
<!-- Información de la Factura (Solo lectura) -->
<div class="rounded-lg border bg-muted/50 p-4 space-y-3">
<h4 class="text-sm font-medium">Información de la Factura (SCAII - Inventario)</h4>
{#if !invoice?.id}
<div class="text-sm text-amber-600 bg-amber-50 dark:bg-amber-950/20 p-3 rounded">
⚠️ Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la factura.
</div>
{:else}
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-muted-foreground">ID Factura:</span>
<span class="ml-2 font-medium">{invoice.id}</span>
</div>
<div>
<span class="text-muted-foreground">Tipo Operación:</span>
<span class="ml-2 font-medium uppercase">{invoice.operation_type || 'N/A'}</span>
</div>
<div class="col-span-2">
<span class="text-muted-foreground">Número de Factura:</span>
<span class="ml-2 font-medium">{invoice.invoice_number || 'Pendiente'}</span>
</div>
<div class="col-span-2">
<span class="text-muted-foreground">Sistema:</span>
<span class="ml-2 font-medium bg-blue-100 dark:bg-blue-900/30 px-2 py-1 rounded">SCAII (Inventory)</span>
</div>
</div>
{/if}
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="reference_number">Número de Referencia</Label>
<Input id="reference_number" bind:value={editingItem.reference_number} />
</div>
<div class="space-y-2">
<Label for="order">Orden de Compra/Venta</Label>
<Input
id="order"
bind:value={editingItem.order}
placeholder={invoice?.purchase_order || ''}
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="warehouse">Almacén</Label>
<Input id="warehouse" bind:value={editingItem.warehouse} />
</div>
<div class="space-y-2">
<Label for="location">Ubicación</Label>
<Input id="location" bind:value={editingItem.location} />
</div>
</div>
<!-- Campos específicos de SCAII -->
<div class="space-y-2">
<Label for="product_description">Descripción del Producto</Label>
<Input id="product_description" placeholder="Descripción detallada del producto" />
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="sku">SKU</Label>
<Input id="sku" placeholder="Código SKU del producto" />
</div>
<div class="space-y-2">
<Label for="batch">Lote</Label>
<Input id="batch" placeholder="Número de lote" />
</div>
</div>
</Tabs.Content>
<!-- Tab: Clasificación -->
<Tabs.Content value="clasificacion" class="space-y-4 mt-4">
<div class="space-y-4">
<div class="space-y-2">
<Label for="tariff_fraction">Fracción Arancelaria</Label>
<Input id="tariff_fraction" placeholder="8 dígitos" />
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="product_type">Tipo de Producto</Label>
<Input id="product_type" placeholder="Materia prima, producto terminado, etc." />
</div>
<div class="space-y-2">
<Label for="material_type">Tipo de Material</Label>
<Input id="material_type" placeholder="Metal, plástico, etc." />
</div>
</div>
<div class="space-y-2">
<Label for="product_code">Código de Producto</Label>
<Input id="product_code" placeholder="Código interno" />
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="country_origin">País de Origen</Label>
<Input id="country_origin" placeholder="Código del país" />
</div>
<div class="space-y-2">
<Label for="merchandise_category">Categoría de Mercancía</Label>
<Input id="merchandise_category" placeholder="Categoría" />
</div>
</div>
</div>
</Tabs.Content>
<!-- Tab: Cantidades -->
<Tabs.Content value="cantidades" class="space-y-4 mt-4">
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="quantity">Cantidad</Label>
<Input id="quantity" type="number" placeholder="0" />
</div>
<div class="space-y-2">
<Label for="unit">Unidad de Medida</Label>
<Input id="unit" placeholder="PZA, KG, M, etc." />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="net_weight">Peso Neto (KG)</Label>
<Input id="net_weight" type="number" step="0.01" placeholder="0.00" />
</div>
<div class="space-y-2">
<Label for="gross_weight">Peso Bruto (KG)</Label>
<Input id="gross_weight" type="number" step="0.01" placeholder="0.00" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="unit_value">Valor Unitario (USD)</Label>
<Input id="unit_value" type="number" step="0.01" placeholder="0.00" />
</div>
<div class="space-y-2">
<Label for="total_value">Valor Total (USD)</Label>
<Input id="total_value" type="number" step="0.01" placeholder="0.00" disabled />
</div>
</div>
<!-- Campos específicos de SCAII -->
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="packages">Número de Bultos</Label>
<Input id="packages" type="number" placeholder="0" />
</div>
<div class="space-y-2">
<Label for="package_type">Tipo de Empaque</Label>
<Input id="package_type" placeholder="Caja, pallet, etc." />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="imported_quantity">Cantidad Importada</Label>
<Input id="imported_quantity" type="number" placeholder="0" />
</div>
<div class="space-y-2">
<Label for="remaining_quantity">Cantidad Remanente</Label>
<Input id="remaining_quantity" type="number" placeholder="0" disabled />
</div>
</div>
</div>
</Tabs.Content>
<!-- Tab: Otros -->
<Tabs.Content value="otros" class="space-y-4 mt-4">
<div class="space-y-4">
<div class="space-y-2">
<Label for="brand">Marca</Label>
<Input id="brand" placeholder="Marca del producto" />
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="expiration_date">Fecha de Caducidad</Label>
<Input id="expiration_date" type="date" />
</div>
<div class="space-y-2">
<Label for="production_date">Fecha de Producción</Label>
<Input id="production_date" type="date" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="min_stock">Stock Mínimo</Label>
<Input id="min_stock" type="number" placeholder="0" />
</div>
<div class="space-y-2">
<Label for="max_stock">Stock Máximo</Label>
<Input id="max_stock" type="number" placeholder="0" />
</div>
</div>
<div class="space-y-2">
<Label for="observations">Observaciones</Label>
<textarea
id="observations"
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Notas adicionales sobre el inventario..."
></textarea>
</div>
</div>
</Tabs.Content>
</Tabs.Root>
<Sheet.Footer class="mt-6 gap-2">
<Button variant="outline" onclick={() => open = false} disabled={isSaving}>
Cancelar
</Button>
<Button onclick={onSave} disabled={isSaving}>
{#if isSaving}
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
Guardando...
{:else}
{isEditMode ? 'Guardar Cambios' : 'Agregar Item'}
{/if}
</Button>
</Sheet.Footer>
</Sheet.Content>
</Sheet.Root>

View File

@@ -0,0 +1,377 @@
<script lang="ts">
import * as Table from '$lib/components/ui/table';
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Plus, Pencil, Trash2, Loader2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import { itemsApi, type Item } from '$lib/api/dashboard/a76/items';
import { companyStore } from '$lib/stores/company.svelte';
import ItemSheetFa from './fa/item-sheet-fa.svelte';
import ItemSheetInv from './inv/item-sheet-inv.svelte';
let {
invoice,
formData = $bindable(),
exists = $bindable()
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
} = $props();
let imported = 0;
let net_weight = 0;
let gross_weight = 0;
let items = $state<Item[]>([]);
let displayedItems = $state<Item[]>([]);
let itemsPerPage = 20;
let currentPage = $state(1);
let tableContainer: HTMLDivElement | undefined = $state();
let isLoadingMore = $state(false);
let isLoadingItems = $state(false);
let isSaving = $state(false);
// Sheet states
let showItemSheet = $state(false);
let isEditMode = $state(false);
let showDeleteDialog = $state(false);
let selectedItem = $state<Item | null>(null);
let editingItem = $state<Partial<Item>>({
invoice_id: undefined,
reference_number: '',
order: '',
warehouse: '',
location: ''
});
// Determinar el tipo de sistema (SCAF o SCAII)
const invoiceSystem = $derived(invoice?.system || 'scaii'); // Por defecto SCAII si no se especifica
// Derived value para company ID
const activeCompanyId = $derived(companyStore.activeCompany?.id);
// Cargar items cuando la factura tenga ID
$effect(() => {
if (invoice?.id && activeCompanyId) {
loadItems();
}
});
async function loadItems() {
if (!invoice?.id || !activeCompanyId) return;
isLoadingItems = true;
try {
const response = await itemsApi.listByInvoice(invoice.id, activeCompanyId);
if (response.data) {
items = response.data.items || [];
currentPage = 1;
loadMoreItems();
}
} catch (error: any) {
console.error('Error loading items:', error);
const errorMessage = error?.response?.data?.detail || 'No se pudieron cargar los items de la factura.';
toast.error('Error al cargar items', {
description: errorMessage
});
} finally {
isLoadingItems = false;
}
}
function loadMoreItems() {
const start = 0;
const end = currentPage * itemsPerPage;
displayedItems = items.slice(start, end);
isLoadingMore = false;
}
function handleScroll(e: Event) {
const target = e.target as HTMLDivElement;
const threshold = 100;
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
if (scrolledToBottom && !isLoadingMore && displayedItems.length < items.length) {
isLoadingMore = true;
currentPage++;
loadMoreItems();
}
}
function handleAdd() {
// Validar que la factura esté guardada (tiene ID)
if (!invoice?.id) {
toast.warning('Factura no guardada', {
description: 'Debes guardar la factura primero antes de agregar partidas.',
duration: 5000,
});
return;
}
isEditMode = false;
showItemSheet = true;
// Auto-asignar valores desde la factura
editingItem = {
invoice_id: invoice.id,
reference_number: '',
order: invoice.purchase_order || '',
warehouse: '',
location: ''
};
}
function handleEdit(item: Item) {
isEditMode = true;
selectedItem = item;
editingItem = { ...item };
showItemSheet = true;
}
function handleDelete(item: Item) {
selectedItem = item;
showDeleteDialog = true;
}
async function saveNewItem() {
if (!invoice?.id || !activeCompanyId) return;
isSaving = true;
try {
const response = await itemsApi.create(activeCompanyId, {
invoice_id: invoice.id,
reference_number: editingItem.reference_number,
order: editingItem.order,
warehouse: editingItem.warehouse,
location: editingItem.location
});
// Recargar items
await loadItems();
showItemSheet = false;
toast.success('Item creado', {
description: 'El item se ha creado correctamente.'
});
} catch (error: any) {
console.error('Error creating item:', error);
const errorMessage = error?.response?.data?.detail || 'No se pudo crear el item. Intenta de nuevo.';
toast.error('Error al crear item', {
description: errorMessage
});
} finally {
isSaving = false;
}
}
async function saveEditedItem() {
if (!selectedItem?.id || !activeCompanyId) return;
isSaving = true;
try {
await itemsApi.update(selectedItem.id, activeCompanyId, {
reference_number: editingItem.reference_number,
order: editingItem.order,
warehouse: editingItem.warehouse,
location: editingItem.location
});
// Recargar items
await loadItems();
showItemSheet = false;
toast.success('Item actualizado', {
description: 'El item se ha actualizado correctamente.'
});
} catch (error: any) {
console.error('Error updating item:', error);
const errorMessage = error?.response?.data?.detail || 'No se pudo actualizar el item. Intenta de nuevo.';
toast.error('Error al actualizar item', {
description: errorMessage
});
} finally {
isSaving = false;
}
}
function saveItem() {
if (isEditMode) {
saveEditedItem();
} else {
saveNewItem();
}
}
async function confirmDelete() {
if (!selectedItem?.id || !activeCompanyId) return;
isSaving = true;
try {
await itemsApi.delete(selectedItem.id, activeCompanyId);
// Recargar items
await loadItems();
showDeleteDialog = false;
toast.success('Item eliminado', {
description: 'El item se ha eliminado correctamente.'
});
} catch (error: any) {
console.error('Error deleting item:', error);
const errorMessage = error?.response?.data?.detail || 'No se pudo eliminar el item. Intenta de nuevo.';
toast.error('Error al eliminar item', {
description: errorMessage
});
} finally {
isSaving = false;
}
}
</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">
<h3 class="text-sm font-semibold">Items de la Factura</h3>
<Button size="sm" onclick={handleAdd}>
<Plus class="w-4 h-4 mr-1" />
Agregar Item
</Button>
</div>
<div
bind:this={tableContainer}
onscroll={handleScroll}
class="max-h-[500px] overflow-auto border rounded-md"
>
<Table.Root>
<Table.Header class="sticky top-0 bg-background z-10">
<Table.Row>
<Table.Head>Referencia</Table.Head>
<Table.Head>Orden</Table.Head>
<Table.Head>Almacén</Table.Head>
<Table.Head>Ubicación</Table.Head>
<Table.Head class="text-right w-[120px]">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">
No hay items disponibles
</Table.Cell>
</Table.Row>
{:else}
{#each displayedItems as item (item.id)}
<Table.Row>
<Table.Cell>{item.reference_number || '-'}</Table.Cell>
<Table.Cell>{item.order || '-'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
<Table.Cell>{item.location || '-'}</Table.Cell>
<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" />
</Button>
<Button size="icon" variant="ghost" onclick={() => handleDelete(item)}>
<Trash2 class="w-4 h-4 text-destructive" />
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{#if isLoadingMore}
<Table.Row>
<Table.Cell colspan={7} class="text-center py-4">
<span class="text-sm text-muted-foreground">Cargando más items...</span>
</Table.Cell>
</Table.Row>
{/if}
{/if}
</Table.Body>
</Table.Root>
</div>
{#if items.length > 0}
<div class="text-xs text-muted-foreground text-right">
Mostrando {displayedItems.length} de {items.length} items
</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>
</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="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>
<!-- Item Sheet (Panel lateral para agregar/editar) -->
<!-- Renderizar el componente apropiado según el sistema -->
{#if invoiceSystem === 'fixed_asset'}
<ItemSheetFa
bind:open={showItemSheet}
{isEditMode}
bind:editingItem={editingItem}
{invoice}
onSave={saveItem}
{isSaving}
/>
{:else}
<ItemSheetInv
bind:open={showItemSheet}
{isEditMode}
bind:editingItem={editingItem}
{invoice}
onSave={saveItem}
{isSaving}
/>
{/if}
<!-- Delete Confirmation Dialog -->
<Dialog.Root bind:open={showDeleteDialog}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Confirmar Eliminación</Dialog.Title>
<Dialog.Description>
¿Está seguro que desea eliminar este item? Esta acción no se puede deshacer.
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<Button variant="outline" onclick={() => showDeleteDialog = false} disabled={isSaving}>
Cancelar
</Button>
<Button variant="destructive" onclick={confirmDelete} disabled={isSaving}>
{#if isSaving}
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
Eliminando...
{:else}
Eliminar
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,407 @@
import { goto } from '$app/navigation';
import { invoicesApi, type CreateInvoiceData, type UpdateInvoiceData, type OperationType } from '$lib/api/dashboard/a76/invoices';
interface FormDataSet {
generalFormData: any;
observationFormData: any;
itemsFormData: any;
othersFormData: any;
}
interface SaveInvoiceOptions {
invoiceId: number | null;
isCreate: boolean;
companyId: number;
formData: FormDataSet;
}
export async function saveInvoice(options: SaveInvoiceOptions): Promise<{ success: boolean; error?: string; newInvoiceId?: number }> {
const { invoiceId, isCreate, companyId, formData } = options;
const { generalFormData, observationFormData, itemsFormData, othersFormData } = formData;
try {
// Validar campos requeridos para creación
if (isCreate && generalFormData) {
const requiredFields = {
operation_type: 'Tipo de Operación',
invoice_type: 'Tipo de Factura',
invoice_number: 'Número de Factura'
};
const missingFields: string[] = [];
for (const [field, label] of Object.entries(requiredFields)) {
const value = generalFormData[field];
if (value === null || value === undefined || value === '') {
missingFields.push(label);
}
}
if (missingFields.length > 0) {
throw new Error(`Los siguientes campos son obligatorios: ${missingFields.join(', ')}`);
}
}
// Construir el payload unificado
const payload = buildInvoicePayload(formData);
let newInvoiceId = invoiceId;
if (isCreate) {
// Crear nueva factura con todos sus sub-recursos
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';
throw new Error(errorMsg);
}
if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada');
newInvoiceId = response.data.id;
// Redirigir a la página de edición
await goto(`/dashboard/invoices/edit/${newInvoiceId}`);
} else {
// Actualizar factura existente con todos sus sub-recursos
const response = await invoicesApi.update(invoiceId!, companyId, payload as UpdateInvoiceData);
if (response.error) throw new Error(response.error);
}
return { success: true, newInvoiceId: newInvoiceId ?? undefined };
} catch (e) {
const error = e instanceof Error ? e.message : 'Error al guardar los cambios';
return { success: false, error };
}
}
function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateInvoiceData {
const { generalFormData, observationFormData, itemsFormData, othersFormData } = formData;
const payload: CreateInvoiceData | UpdateInvoiceData = {
// Datos generales desde el formulario general
system: 'fixed_asset',
operation_type: generalFormData?.operation_type !== null && generalFormData?.operation_type !== undefined
? (generalFormData.operation_type === 1 ? 'exp' : 'imp') as OperationType
: undefined,
invoice_type: generalFormData?.invoice_type || undefined,
invoice_number: generalFormData?.invoice_number || undefined,
invoice_date: generalFormData?.invoice_date || undefined,
emission_date: generalFormData?.emission_date || undefined,
// Observation fields from observationFormData
observation_es: observationFormData?.observation_es || undefined,
observation_en: observationFormData?.observation_en || undefined,
alternate_invoice: observationFormData?.alternate_invoice || undefined,
};
// Solo agregar sub-recursos si tienen valores reales
// Compliance MX
const hasComplianceValue = generalFormData?.pedimento || generalFormData?.remesa || generalFormData?.aduana ||
generalFormData?.provider_id || generalFormData?.sold_to_id ||
generalFormData?.shipped_to_id || generalFormData?.customs_broker_id ||
observationFormData?.pedimento || observationFormData?.pedimento_code ||
observationFormData?.remesa || observationFormData?.aduana ||
observationFormData?.provider_id || observationFormData?.sold_to_id ||
observationFormData?.shipped_to_id || observationFormData?.shipped_by_id ||
observationFormData?.customs_broker_id || observationFormData?.is_mixed ||
observationFormData?.waste_type || observationFormData?.appendix_17 ||
observationFormData?.edocument || observationFormData?.electronic_signature ||
observationFormData?.sem_id || observationFormData?.enclosure ||
observationFormData?.incoterm;
if (hasComplianceValue) {
payload.compliance_mx = buildComplianceMxData(generalFormData, observationFormData);
}
// Financials
const hasFinancialsValue = generalFormData?.currency_type ||
generalFormData?.iva_factor ||
itemsFormData?.currency || itemsFormData?.exchange_rate ||
itemsFormData?.value_mn || itemsFormData?.value_me ||
itemsFormData?.customs_value_mn || itemsFormData?.freight ||
itemsFormData?.insurance;
if (hasFinancialsValue) {
payload.financials = buildFinancialsData(generalFormData, itemsFormData, observationFormData);
}
// Logistics
const hasLogisticsFromGeneral = generalFormData?.carrier_id ||
generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num;
if (hasLogisticsFromGeneral || (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0)) {
payload.logistics = buildLogisticsData(generalFormData, othersFormData, observationFormData);
}
// Eliminar campos undefined para no enviarlos
Object.keys(payload).forEach(key => {
if (payload[key as keyof typeof payload] === undefined) {
delete payload[key as keyof typeof payload];
}
});
return payload;
}
function buildComplianceMxData(generalFormData: any, observationFormData: any) {
return {
// Pedimento fields
pedimento: generalFormData?.pedimento || observationFormData?.pedimento || null,
pedimento_code: observationFormData?.pedimento_code || null,
pedimento_k1: observationFormData?.pedimento_k1 || null,
remesa: generalFormData?.remesa || observationFormData?.remesa || null,
aduana: generalFormData?.aduana || observationFormData?.aduana || null,
port_of_entry: observationFormData?.port_of_entry || null,
destination: observationFormData?.destination || null,
manifest_number: observationFormData?.manifest_number || null,
// Client/Provider fields
provider_header: generalFormData?.provider_header || observationFormData?.provider_header || null,
provider_id: generalFormData?.provider_id || observationFormData?.provider_id || null,
sold_to_header: generalFormData?.sold_to_header || observationFormData?.sold_to_header || null,
sold_to_id: generalFormData?.sold_to_id || observationFormData?.sold_to_id || null,
shipped_to_header: generalFormData?.shipped_to_header || observationFormData?.shipped_to_header || null,
shipped_to_id: generalFormData?.shipped_to_id || observationFormData?.shipped_to_id || null,
shipped_by_header: observationFormData?.shipped_by_header || null,
shipped_by_id: observationFormData?.shipped_by_id || null,
// Customs broker fields
customs_broker_id: generalFormData?.customs_broker_id || observationFormData?.customs_broker_id || null,
customs_broker_us_id: observationFormData?.customs_broker_us_id || null,
broker_invoice_num: observationFormData?.broker_invoice_num || null,
broker_invoice_date: observationFormData?.broker_invoice_date || null,
// Flags & regimes
is_mixed: observationFormData?.is_mixed || null,
waste_type: observationFormData?.waste_type || null,
scrap_type: observationFormData?.scrap_type || null,
appendix_17: observationFormData?.appendix_17 || null,
is_regime_change: observationFormData?.is_regime_change || null,
which_exchange_rate: observationFormData?.which_exchange_rate || null,
value_method: observationFormData?.value_method || null,
act_value: observationFormData?.act_value || null,
is_pedimento_pending: observationFormData?.is_pedimento_pending || null,
// Ownership & balances
is_owner_of_goods: observationFormData?.is_owner_of_goods || null,
generate_balances: observationFormData?.generate_balances || null,
was_reviewed_by_company: observationFormData?.was_reviewed_by_company || null,
// VUCEM / Digital
edocument: observationFormData?.edocument || null,
electronic_signature: observationFormData?.electronic_signature || null,
certificate_number: observationFormData?.certificate_number || null,
niu_number: observationFormData?.niu_number || null,
bill_of_lading_count: observationFormData?.bill_of_lading_count || null,
addendum_vu: observationFormData?.addendum_vu || null,
origin_destination_cove: observationFormData?.origin_destination_cove || null,
vucem_operation_num: observationFormData?.vucem_operation_num || null,
customs_person_line: observationFormData?.customs_person_line || null,
// Additional control
contingency_mode: observationFormData?.contingency_mode || null,
enclosure: observationFormData?.enclosure || null,
guide_type_to_identify: observationFormData?.guide_type_to_identify || null,
location: observationFormData?.location || null,
// DOT & official
dot_code: observationFormData?.dot_code || null,
subdivision: observationFormData?.subdivision || null,
acts_as: observationFormData?.acts_as || null,
movement_type: observationFormData?.movement_type || null,
office_document: observationFormData?.office_document || null,
reason_export: observationFormData?.reason_export || null,
signature_key: observationFormData?.signature_key || null,
// SM specific
sem_id: observationFormData?.sem_id || null,
};
}
function buildFinancialsData(generalFormData: any, itemsFormData: any, observationFormData: any) {
return {
// Currency
currency: itemsFormData?.currency || null,
currency_type: generalFormData?.currency_type || itemsFormData?.currency_type || null,
exchange_rate: itemsFormData?.exchange_rate || null,
exchange_rate_mm: itemsFormData?.exchange_rate_mm || null,
// Merchandise values
value_mn: itemsFormData?.value_mn || null,
value_me: itemsFormData?.value_me || null,
value_mc: itemsFormData?.value_mc || null,
// Customs value
customs_value_mn: itemsFormData?.customs_value_mn || null,
customs_value_me: itemsFormData?.customs_value_me || null,
// Raw materials
raw_material_value_mn: itemsFormData?.raw_material_value_mn || null,
raw_material_value_me: itemsFormData?.raw_material_value_me || null,
// Aggregate value
aggregate_value_mn: itemsFormData?.aggregate_value_mn || null,
aggregate_value_me: itemsFormData?.aggregate_value_me || null,
aggregate_value_mc: itemsFormData?.aggregate_value_mc || null,
// Mexican merchandise value
mexican_value_mn: itemsFormData?.mexican_value_mn || null,
mexican_value_me: itemsFormData?.mexican_value_me || null,
mexican_value_mc: itemsFormData?.mexican_value_mc || null,
// National packaging
national_packaging_mn: itemsFormData?.national_packaging_mn || null,
national_packaging_me: itemsFormData?.national_packaging_me || null,
national_packaging_mc: itemsFormData?.national_packaging_mc || null,
// Costs & increments
freight: itemsFormData?.freight || observationFormData?.freight || null,
insurance: itemsFormData?.insurance || observationFormData?.insurance || null,
insurance_value: itemsFormData?.insurance_value || observationFormData?.insurance_value || null,
packaging: itemsFormData?.packaging || observationFormData?.packaging || null,
other_increments: itemsFormData?.other_increments || observationFormData?.other_increments || null,
total_increments_mn: itemsFormData?.total_increments_mn || observationFormData?.total_increments_mn || null,
total_increments_me: itemsFormData?.total_increments_me || observationFormData?.total_increments_me || null,
// Taxes
iva_mn: itemsFormData?.iva_mn || null,
iva_me: itemsFormData?.iva_me || null,
iva_mc: itemsFormData?.iva_mc || null,
iva_factor: generalFormData?.iva_factor || itemsFormData?.iva_factor || null,
tax_value_me: itemsFormData?.tax_value_me || null,
seal_value_2500: itemsFormData?.seal_value_2500 || null,
// Weights & quantities
total_quantity: itemsFormData?.total_quantity || null,
gross_weight: itemsFormData?.gross_weight || null,
net_weight: itemsFormData?.net_weight || null,
bundle_count: itemsFormData?.bundle_count || null,
weight_factor: itemsFormData?.weight_factor || null,
};
}
function buildLogisticsData(generalFormData: any, othersFormData: any, observationFormData: any) {
const hasLogisticsFromGeneral = generalFormData?.carrier_id ||
generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num;
if (hasLogisticsFromGeneral) {
const logisticsEntry = buildLogisticsEntry(generalFormData, observationFormData);
// Si también hay datos del formulario de others, combinarlos
if (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0) {
// Actualizar el primer elemento con datos del general
return [
mergeLogisticsEntries(generalFormData, othersFormData[0], observationFormData),
// Agregar los demás elementos si existen
...othersFormData.slice(1).map((item: any) => buildLogisticsEntryFromOther(item, observationFormData))
];
} else {
// Solo datos del general
return [logisticsEntry];
}
} else {
// Solo datos del formulario others
return othersFormData.map((item: any) => buildLogisticsEntryFromOther(item, observationFormData));
}
}
function buildLogisticsEntry(generalFormData: any, observationFormData: any) {
return {
carrier_id: generalFormData?.carrier_id || null,
transport_type: generalFormData?.transport_type || null,
transport_mode: null,
driver_name: generalFormData?.driver_name || null,
is_rail: null,
rail_id: null,
vehicle_num: generalFormData?.transport_num || null,
license_plate: null,
seal_number: null,
guide_number: null,
entry_exit_date: null,
incoterm: observationFormData?.incoterm || null,
};
}
function mergeLogisticsEntries(generalFormData: any, otherData: any, observationFormData: any) {
return {
// Carrier info
carrier_id: generalFormData?.carrier_id || otherData.carrier_id || null,
transport_id: otherData.transport_id || null,
transport_us_id: otherData.transport_us_id || null,
transport_type: generalFormData?.transport_type || otherData.transport_type || null,
transport_num: otherData.transport_num || null,
transport_mode: otherData.transport_mode || null,
driver_name: generalFormData?.driver_name || otherData.driver_name || null,
is_rail: otherData.is_rail || null,
rail_id: otherData.rail_id || null,
// Vehicle & tracking
vehicle_num: generalFormData?.transport_num || otherData.vehicle_num || null,
license_plate: otherData.license_plate || null,
license_plate_complete: otherData.license_plate_complete || null,
trailer_num: otherData.trailer_num || null,
seal_number: otherData.seal_number || null,
guide_number: otherData.guide_number || null,
bill_number: otherData.bill_number || null,
reference_number: otherData.reference_number || null,
shipment_number: otherData.shipment_number || null,
// Incoterms
incoterm: otherData.incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: otherData.identifier_1 || null,
complement_1: otherData.complement_1 || null,
identifier_2: otherData.identifier_2 || null,
complement_2: otherData.complement_2 || null,
// Weight & container info
weight_type: otherData.weight_type || null,
container_types: otherData.container_types || null,
vehicle_data: otherData.vehicle_data || null,
// Locations & routes
origin_location: otherData.origin_location || null,
destination_location: otherData.destination_location || null,
transport_itinerary: otherData.transport_itinerary || null,
destination_goods: otherData.destination_goods || null,
// Logistics dates
entry_exit_date: otherData.entry_exit_date || null,
delivery_date: otherData.delivery_date || null,
// Delivery control
delivered_status: otherData.delivered_status || null,
received_by: otherData.received_by || null,
// Payment info
payment_date: otherData.payment_date || null,
payment_receipt_num: otherData.payment_receipt_num || null,
// CTM process
is_ctm_process: otherData.is_ctm_process || null,
};
}
function buildLogisticsEntryFromOther(item: any, observationFormData: any) {
return {
// Carrier info
carrier_id: item.carrier_id || null,
transport_id: item.transport_id || null,
transport_us_id: item.transport_us_id || null,
transport_type: item.transport_type || null,
transport_num: item.transport_num || null,
transport_mode: item.transport_mode || null,
driver_name: item.driver_name || null,
is_rail: item.is_rail || null,
rail_id: item.rail_id || null,
// Vehicle & tracking
vehicle_num: item.vehicle_num || null,
license_plate: item.license_plate || null,
license_plate_complete: item.license_plate_complete || null,
trailer_num: item.trailer_num || null,
seal_number: item.seal_number || null,
guide_number: item.guide_number || null,
bill_number: item.bill_number || null,
reference_number: item.reference_number || null,
shipment_number: item.shipment_number || null,
// Incoterms
incoterm: item.incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: item.identifier_1 || null,
complement_1: item.complement_1 || null,
identifier_2: item.identifier_2 || null,
complement_2: item.complement_2 || null,
// Weight & container info
weight_type: item.weight_type || null,
container_types: item.container_types || null,
vehicle_data: item.vehicle_data || null,
// Locations & routes
origin_location: item.origin_location || null,
destination_location: item.destination_location || null,
transport_itinerary: item.transport_itinerary || null,
destination_goods: item.destination_goods || null,
// Logistics dates
entry_exit_date: item.entry_exit_date || null,
delivery_date: item.delivery_date || null,
// Delivery control
delivered_status: item.delivered_status || null,
received_by: item.received_by || null,
// Payment info
payment_date: item.payment_date || null,
payment_receipt_num: item.payment_receipt_num || null,
// CTM process
is_ctm_process: item.is_ctm_process || null,
};
}

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import { Toaster } from 'svelte-sonner';
let { children } = $props();
</script>
@@ -9,4 +10,5 @@
<link rel="icon" href={favicon} />
</svelte:head>
<Toaster richColors position="top-right" />
{@render children?.()}

View File

@@ -30,6 +30,12 @@ export const GET: RequestHandler = async ({ params, cookies, fetch }) => {
providersResponse,
currencyTypesResponse,
transportTypesResponse,
transportersResponse,
vehiclesResponse,
driversResponse,
trailersResponse,
customsSectionsResponse,
codePedimentoRegimensResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse
@@ -41,6 +47,12 @@ export const GET: RequestHandler = async ({ params, cookies, fetch }) => {
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/transportation/transporters/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/transportation/vehicles/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/transportation/drivers/?company_id=${companyId}`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/transportation/trailers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/customs-sections/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/code-pedimento-regimens/?page=1&page_size=1000', {}, cookies, fetch),
authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch)
@@ -57,6 +69,12 @@ export const GET: RequestHandler = async ({ params, cookies, fetch }) => {
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] };
const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] };
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
@@ -69,6 +87,12 @@ export const GET: RequestHandler = async ({ params, cookies, fetch }) => {
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
transporters: transporters.items || [],
vehicles: vehicles.items || [],
drivers: drivers.items || [],
trailers: trailers.items || [],
customsSections: customsSections.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || []

View File

@@ -23,8 +23,12 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => {
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
sealsResponse,
transportTypesResponse, transportersResponse,
vehiclesResponse,
driversResponse,
trailersResponse,
customsSectionsResponse,
codePedimentoRegimensResponse, sealsResponse,
incotermsResponse,
pedimentosResponse
] = await Promise.all([
@@ -34,6 +38,12 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => {
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/transportation/transporters/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/transportation/vehicles/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/transportation/drivers/?company_id=${companyId}`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/transportation/trailers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/customs-sections/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/code-pedimento-regimens/?page=1&page_size=1000', {}, cookies, fetch),
authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch)
@@ -45,6 +55,12 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => {
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] };
const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] };
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
@@ -56,6 +72,12 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => {
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
transporters: transporters.items || [],
vehicles: vehicles.items || [],
drivers: drivers.items || [],
trailers: trailers.items || [],
customsSections: customsSections.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || []

View File

@@ -74,6 +74,48 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
fetch
);
const transportersPromise = authenticatedFetch(
`v1/a76/transportation/transporters/?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
const vehiclesPromise = authenticatedFetch(
`v1/a76/transportation/vehicles/?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
const driversPromise = authenticatedFetch(
`v1/a76/transportation/drivers/?company_id=${companyId}`,
{},
cookies,
fetch
);
const trailersPromise = authenticatedFetch(
`v1/a76/transportation/trailers/?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
const customsSectionsPromise = authenticatedFetch(
'v1/public/refrence_data/customs-sections/?page=1&page_size=100',
{},
cookies,
fetch
);
const codePedimentoRegimensPromise = authenticatedFetch(
'v1/public/refrence_data/code-pedimento-regimens/?page=1&page_size=1000',
{},
cookies,
fetch
);
const sealsPromise = authenticatedFetch(
`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`,
{},
@@ -105,6 +147,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
providersResponse,
currencyTypesResponse,
transportTypesResponse,
transportersResponse,
vehiclesResponse,
driversResponse,
trailersResponse,
customsSectionsResponse,
codePedimentoRegimensResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse
@@ -115,6 +163,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
providersPromise,
currencyTypesPromise,
transportTypesPromise,
transportersPromise,
vehiclesPromise,
driversPromise,
trailersPromise,
customsSectionsPromise,
codePedimentoRegimensPromise,
sealsPromise,
incotermsPromise,
pedimentosPromise
@@ -126,6 +180,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] };
const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] };
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
@@ -140,6 +200,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
transporters: transporters.items || [],
vehicles: vehicles.items || [],
drivers: drivers.items || [],
trailers: trailers.items || [],
customsSections: customsSections.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || [],
@@ -162,6 +228,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
providers: [],
currencyTypes: [],
transportTypes: [],
transporters: [],
vehicles: [],
drivers: [],
trailers: [],
customsSections: [],
codePedimentoRegimens: [],
seals: [],
incoterms: [],
pedimentos: [],
@@ -201,6 +273,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
providersResponse,
currencyTypesResponse,
transportTypesResponse,
transportersResponse,
vehiclesResponse,
driversResponse,
trailersResponse,
customsSectionsResponse,
codePedimentoRegimensResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse
@@ -211,6 +289,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
providersPromise,
currencyTypesPromise,
transportTypesPromise,
transportersPromise,
vehiclesPromise,
driversPromise,
trailersPromise,
customsSectionsPromise,
codePedimentoRegimensPromise,
sealsPromise,
incotermsPromise,
pedimentosPromise
@@ -222,6 +306,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] };
const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] };
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
@@ -236,6 +326,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
transporters: transporters.items || [],
vehicles: vehicles.items || [],
drivers: drivers.items || [],
trailers: trailers.items || [],
customsSections: customsSections.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || [],

View File

@@ -23,16 +23,16 @@
// Importar los componentes de cada pestaña
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
import ObservationsTabForm from '$lib/components/dashboard/invoices/edit/observations-tab-form.svelte';
import ItemsTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte';
import ItemsTabForm from '$lib/components/dashboard/invoices/edit/items/items-tab-form.svelte';
import OthersTabForm from '$lib/components/dashboard/invoices/edit/others-tab-form.svelte';
import InvoiceTopFields from '$lib/components/dashboard/invoices/edit/invoice-top-fields.svelte';
import ContinuationTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte';
// Importar la API de facturas
import { invoicesApi, type CreateInvoiceData, type UpdateInvoiceData, type OperationType } from '$lib/api/dashboard/a76/invoices';
import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
import { saveInvoice } from '$lib/components/dashboard/invoices/edit/save-invoice';
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
let companyStore: any = $state(undefined);
@@ -62,6 +62,12 @@
enclosure?: any[];
currencyTypes?: any[];
transportTypes?: any[];
transporters?: any[];
vehicles?: any[];
drivers?: any[];
trailers?: any[];
customsSections?: any[];
codePedimentoRegimens?: any[];
user?: any;
companies?: any[];
authenticated?: boolean;
@@ -108,398 +114,22 @@
success = false;
try {
// Validar campos requeridos para creación
if (data.isCreate && generalFormData) {
const requiredFields = {
operation_type: 'Tipo de Operación',
invoice_type: 'Tipo de Factura',
invoice_number: 'Número de Factura'
};
const missingFields: string[] = [];
for (const [field, label] of Object.entries(requiredFields)) {
const value = (generalFormData as any)[field];
if (value === null || value === undefined || value === '') {
missingFields.push(label);
}
}
if (missingFields.length > 0) {
throw new Error(`Los siguientes campos son obligatorios: ${missingFields.join(', ')}`);
}
}
// Construir el payload unificado
const payload: CreateInvoiceData | UpdateInvoiceData = {
// Datos generales desde el formulario general
operation_type: generalFormData?.operation_type !== null && generalFormData?.operation_type !== undefined
? (generalFormData.operation_type === 1 ? 'exp' : 'imp') as OperationType
: undefined,
invoice_type: generalFormData?.invoice_type || undefined,
invoice_number: generalFormData?.invoice_number || undefined,
invoice_date: generalFormData?.invoice_date || undefined,
emission_date: generalFormData?.emission_date || undefined,
// Observation fields from observationFormData
observation_es: observationFormData?.observation_es || undefined,
observation_en: observationFormData?.observation_en || undefined,
alternate_invoice: observationFormData?.alternate_invoice || undefined,
};
// Solo agregar sub-recursos si tienen valores reales
// Compliance MX - combinar datos del formulario general y observations
const hasComplianceValue = generalFormData?.pedimento || generalFormData?.remesa || generalFormData?.aduana ||
generalFormData?.provider_id || generalFormData?.sold_to_id ||
generalFormData?.shipped_to_id || generalFormData?.customs_broker_id ||
observationFormData?.pedimento || observationFormData?.pedimento_code ||
observationFormData?.remesa || observationFormData?.aduana ||
observationFormData?.provider_id || observationFormData?.sold_to_id ||
observationFormData?.shipped_to_id || observationFormData?.shipped_by_id ||
observationFormData?.customs_broker_id || observationFormData?.is_mixed ||
observationFormData?.waste_type || observationFormData?.appendix_17 ||
observationFormData?.edocument || observationFormData?.electronic_signature ||
observationFormData?.sem_id || observationFormData?.enclosure ||
observationFormData?.incoterm;
if (hasComplianceValue) {
payload.compliance_mx = {
// Pedimento fields
pedimento: generalFormData?.pedimento || observationFormData?.pedimento || null,
pedimento_code: observationFormData?.pedimento_code || null,
pedimento_k1: observationFormData?.pedimento_k1 || null,
remesa: generalFormData?.remesa || observationFormData?.remesa || null,
aduana: generalFormData?.aduana || observationFormData?.aduana || null,
port_of_entry: observationFormData?.port_of_entry || null,
destination: observationFormData?.destination || null,
manifest_number: observationFormData?.manifest_number || null,
// Client/Provider fields
provider_header: generalFormData?.provider_header || observationFormData?.provider_header || null,
provider_id: generalFormData?.provider_id || observationFormData?.provider_id || null,
sold_to_header: generalFormData?.sold_to_header || observationFormData?.sold_to_header || null,
sold_to_id: generalFormData?.sold_to_id || observationFormData?.sold_to_id || null,
shipped_to_header: generalFormData?.shipped_to_header || observationFormData?.shipped_to_header || null,
shipped_to_id: generalFormData?.shipped_to_id || observationFormData?.shipped_to_id || null,
shipped_by_header: observationFormData?.shipped_by_header || null,
shipped_by_id: observationFormData?.shipped_by_id || null,
// Customs broker fields
customs_broker_id: generalFormData?.customs_broker_id || observationFormData?.customs_broker_id || null,
customs_broker_us_id: observationFormData?.customs_broker_us_id || null,
broker_invoice_num: observationFormData?.broker_invoice_num || null,
broker_invoice_date: observationFormData?.broker_invoice_date || null,
// Flags & regimes
is_mixed: observationFormData?.is_mixed || null,
waste_type: observationFormData?.waste_type || null,
scrap_type: observationFormData?.scrap_type || null,
appendix_17: observationFormData?.appendix_17 || null,
is_regime_change: observationFormData?.is_regime_change || null,
which_exchange_rate: observationFormData?.which_exchange_rate || null,
value_method: observationFormData?.value_method || null,
act_value: observationFormData?.act_value || null,
is_pedimento_pending: observationFormData?.is_pedimento_pending || null,
// Ownership & balances
is_owner_of_goods: observationFormData?.is_owner_of_goods || null,
generate_balances: observationFormData?.generate_balances || null,
was_reviewed_by_company: observationFormData?.was_reviewed_by_company || null,
// VUCEM / Digital
edocument: observationFormData?.edocument || null,
electronic_signature: observationFormData?.electronic_signature || null,
certificate_number: observationFormData?.certificate_number || null,
niu_number: observationFormData?.niu_number || null,
bill_of_lading_count: observationFormData?.bill_of_lading_count || null,
addendum_vu: observationFormData?.addendum_vu || null,
origin_destination_cove: observationFormData?.origin_destination_cove || null,
vucem_operation_num: observationFormData?.vucem_operation_num || null,
customs_person_line: observationFormData?.customs_person_line || null,
// Additional control
contingency_mode: observationFormData?.contingency_mode || null,
enclosure: observationFormData?.enclosure || null,
guide_type_to_identify: observationFormData?.guide_type_to_identify || null,
location: observationFormData?.location || null,
// DOT & official
dot_code: observationFormData?.dot_code || null,
subdivision: observationFormData?.subdivision || null,
acts_as: observationFormData?.acts_as || null,
movement_type: observationFormData?.movement_type || null,
office_document: observationFormData?.office_document || null,
reason_export: observationFormData?.reason_export || null,
signature_key: observationFormData?.signature_key || null,
// SM specific
sem_id: observationFormData?.sem_id || null,
};
}
// Financials - combinar datos del formulario general y items
const hasFinancialsValue = generalFormData?.currency_type ||
generalFormData?.iva_factor ||
itemsFormData?.currency || itemsFormData?.exchange_rate ||
itemsFormData?.value_mn || itemsFormData?.value_me ||
itemsFormData?.customs_value_mn || itemsFormData?.freight ||
itemsFormData?.insurance;
if (hasFinancialsValue) {
payload.financials = {
// Currency
currency: itemsFormData?.currency || null,
currency_type: generalFormData?.currency_type || itemsFormData?.currency_type || null,
exchange_rate: itemsFormData?.exchange_rate || null,
exchange_rate_mm: itemsFormData?.exchange_rate_mm || null,
// Merchandise values
value_mn: itemsFormData?.value_mn || null,
value_me: itemsFormData?.value_me || null,
value_mc: itemsFormData?.value_mc || null,
// Customs value
customs_value_mn: itemsFormData?.customs_value_mn || null,
customs_value_me: itemsFormData?.customs_value_me || null,
// Raw materials
raw_material_value_mn: itemsFormData?.raw_material_value_mn || null,
raw_material_value_me: itemsFormData?.raw_material_value_me || null,
// Aggregate value
aggregate_value_mn: itemsFormData?.aggregate_value_mn || null,
aggregate_value_me: itemsFormData?.aggregate_value_me || null,
aggregate_value_mc: itemsFormData?.aggregate_value_mc || null,
// Mexican merchandise value
mexican_value_mn: itemsFormData?.mexican_value_mn || null,
mexican_value_me: itemsFormData?.mexican_value_me || null,
mexican_value_mc: itemsFormData?.mexican_value_mc || null,
// National packaging
national_packaging_mn: itemsFormData?.national_packaging_mn || null,
national_packaging_me: itemsFormData?.national_packaging_me || null,
national_packaging_mc: itemsFormData?.national_packaging_mc || null,
// Costs & increments
freight: itemsFormData?.freight || observationFormData?.freight || null,
insurance: itemsFormData?.insurance || observationFormData?.insurance || null,
insurance_value: itemsFormData?.insurance_value || observationFormData?.insurance_value || null,
packaging: itemsFormData?.packaging || observationFormData?.packaging || null,
other_increments: itemsFormData?.other_increments || observationFormData?.other_increments || null,
total_increments_mn: itemsFormData?.total_increments_mn || observationFormData?.total_increments_mn || null,
total_increments_me: itemsFormData?.total_increments_me || observationFormData?.total_increments_me || null,
// Taxes
iva_mn: itemsFormData?.iva_mn || null,
iva_me: itemsFormData?.iva_me || null,
iva_mc: itemsFormData?.iva_mc || null,
iva_factor: generalFormData?.iva_factor || itemsFormData?.iva_factor || null,
tax_value_me: itemsFormData?.tax_value_me || null,
seal_value_2500: itemsFormData?.seal_value_2500 || null,
// Weights & quantities
total_quantity: itemsFormData?.total_quantity || null,
gross_weight: itemsFormData?.gross_weight || null,
net_weight: itemsFormData?.net_weight || null,
bundle_count: itemsFormData?.bundle_count || null,
weight_factor: itemsFormData?.weight_factor || null,
};
}
// Logistics - combinar datos del formulario general con othersFormData
const hasLogisticsFromGeneral = generalFormData?.carrier_id ||
generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num;
if (hasLogisticsFromGeneral || (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0)) {
// Si hay datos en el formulario general, crear/actualizar el primer elemento
if (hasLogisticsFromGeneral) {
const logisticsEntry = {
carrier_id: generalFormData?.carrier_id || null,
transport_type: generalFormData?.transport_type || null,
transport_mode: null,
driver_name: generalFormData?.driver_name || null,
is_rail: null,
rail_id: null,
vehicle_num: generalFormData?.transport_num || null,
license_plate: null,
seal_number: null,
guide_number: null,
entry_exit_date: null,
};
// Si también hay datos del formulario de others, combinarlos
if (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0) {
// Actualizar el primer elemento con datos del general
payload.logistics = [
{
// Carrier info
carrier_id: generalFormData?.carrier_id || othersFormData[0].carrier_id || null,
transport_id: othersFormData[0].transport_id || null,
transport_us_id: othersFormData[0].transport_us_id || null,
transport_type: generalFormData?.transport_type || othersFormData[0].transport_type || null,
transport_num: othersFormData[0].transport_num || null,
transport_mode: othersFormData[0].transport_mode || null,
driver_name: generalFormData?.driver_name || othersFormData[0].driver_name || null,
is_rail: othersFormData[0].is_rail || null,
rail_id: othersFormData[0].rail_id || null,
// Vehicle & tracking
vehicle_num: generalFormData?.transport_num || othersFormData[0].vehicle_num || null,
license_plate: othersFormData[0].license_plate || null,
license_plate_complete: othersFormData[0].license_plate_complete || null,
trailer_num: othersFormData[0].trailer_num || null,
seal_number: othersFormData[0].seal_number || null,
guide_number: othersFormData[0].guide_number || null,
bill_number: othersFormData[0].bill_number || null,
reference_number: othersFormData[0].reference_number || null,
shipment_number: othersFormData[0].shipment_number || null,
// Incoterms
incoterm: othersFormData[0].incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: othersFormData[0].identifier_1 || null,
complement_1: othersFormData[0].complement_1 || null,
identifier_2: othersFormData[0].identifier_2 || null,
complement_2: othersFormData[0].complement_2 || null,
// Weight & container info
weight_type: othersFormData[0].weight_type || null,
container_types: othersFormData[0].container_types || null,
vehicle_data: othersFormData[0].vehicle_data || null,
// Locations & routes
origin_location: othersFormData[0].origin_location || null,
destination_location: othersFormData[0].destination_location || null,
transport_itinerary: othersFormData[0].transport_itinerary || null,
destination_goods: othersFormData[0].destination_goods || null,
// Logistics dates
entry_exit_date: othersFormData[0].entry_exit_date || null,
delivery_date: othersFormData[0].delivery_date || null,
// Delivery control
delivered_status: othersFormData[0].delivered_status || null,
received_by: othersFormData[0].received_by || null,
// Payment info
payment_date: othersFormData[0].payment_date || null,
payment_receipt_num: othersFormData[0].payment_receipt_num || null,
// CTM process
is_ctm_process: othersFormData[0].is_ctm_process || null,
},
// Agregar los demás elementos si existen
...othersFormData.slice(1).map((item: any) => ({
// Carrier info
carrier_id: item.carrier_id || null,
transport_id: item.transport_id || null,
transport_us_id: item.transport_us_id || null,
transport_type: item.transport_type || null,
transport_num: item.transport_num || null,
transport_mode: item.transport_mode || null,
driver_name: item.driver_name || null,
is_rail: item.is_rail || null,
rail_id: item.rail_id || null,
// Vehicle & tracking
vehicle_num: item.vehicle_num || null,
license_plate: item.license_plate || null,
license_plate_complete: item.license_plate_complete || null,
trailer_num: item.trailer_num || null,
seal_number: item.seal_number || null,
guide_number: item.guide_number || null,
bill_number: item.bill_number || null,
reference_number: item.reference_number || null,
shipment_number: item.shipment_number || null,
// Incoterms
incoterm: item.incoterm || null,
// Identifiers & complements
identifier_1: item.identifier_1 || null,
complement_1: item.complement_1 || null,
identifier_2: item.identifier_2 || null,
complement_2: item.complement_2 || null,
// Weight & container info
weight_type: item.weight_type || null,
container_types: item.container_types || null,
vehicle_data: item.vehicle_data || null,
// Locations & routes
origin_location: item.origin_location || null,
destination_location: item.destination_location || null,
transport_itinerary: item.transport_itinerary || null,
destination_goods: item.destination_goods || null,
// Logistics dates
entry_exit_date: item.entry_exit_date || null,
delivery_date: item.delivery_date || null,
// Delivery control
delivered_status: item.delivered_status || null,
received_by: item.received_by || null,
// Payment info
payment_date: item.payment_date || null,
payment_receipt_num: item.payment_receipt_num || null,
// CTM process
is_ctm_process: item.is_ctm_process || null,
}))
];
} else {
// Solo datos del general
payload.logistics = [logisticsEntry];
}
} else {
// Solo datos del formulario others
payload.logistics = othersFormData.map((item: any) => ({
// Carrier info
carrier_id: item.carrier_id || null,
transport_id: item.transport_id || null,
transport_us_id: item.transport_us_id || null,
transport_type: item.transport_type || null,
transport_num: item.transport_num || null,
transport_mode: item.transport_mode || null,
driver_name: item.driver_name || null,
is_rail: item.is_rail || null,
rail_id: item.rail_id || null,
// Vehicle & tracking
vehicle_num: item.vehicle_num || null,
license_plate: item.license_plate || null,
license_plate_complete: item.license_plate_complete || null,
trailer_num: item.trailer_num || null,
seal_number: item.seal_number || null,
guide_number: item.guide_number || null,
bill_number: item.bill_number || null,
reference_number: item.reference_number || null,
shipment_number: item.shipment_number || null,
// Incoterms
incoterm: item.incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: item.identifier_1 || null,
complement_1: item.complement_1 || null,
identifier_2: item.identifier_2 || null,
complement_2: item.complement_2 || null,
// Weight & container info
weight_type: item.weight_type || null,
container_types: item.container_types || null,
vehicle_data: item.vehicle_data || null,
// Locations & routes
origin_location: item.origin_location || null,
destination_location: item.destination_location || null,
transport_itinerary: item.transport_itinerary || null,
destination_goods: item.destination_goods || null,
// Logistics dates
entry_exit_date: item.entry_exit_date || null,
delivery_date: item.delivery_date || null,
// Delivery control
delivered_status: item.delivered_status || null,
received_by: item.received_by || null,
// Payment info
payment_date: item.payment_date || null,
payment_receipt_num: item.payment_receipt_num || null,
// CTM process
is_ctm_process: item.is_ctm_process || null,
}));
}
}
// Eliminar campos undefined para no enviarlos
Object.keys(payload).forEach(key => {
if (payload[key as keyof typeof payload] === undefined) {
delete payload[key as keyof typeof payload];
const result = await saveInvoice({
invoiceId,
isCreate: data.isCreate || false,
companyId: companyStore?.activeCompany?.id || 0,
formData: {
generalFormData,
observationFormData,
itemsFormData,
othersFormData
}
});
let newInvoiceId = invoiceId;
if (data.isCreate) {
// Crear nueva factura con todos sus sub-recursos
const response = await invoicesApi.create(companyStore?.activeCompany?.id || 0, payload as CreateInvoiceData);
if (response.error) {
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
throw new Error(errorMsg);
}
if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada');
newInvoiceId = response.data.id;
// Redirigir a la página de edición
await goto(`/dashboard/invoices/edit/${newInvoiceId}`);
return;
} else {
// Actualizar factura existente con todos sus sub-recursos
const response = await invoicesApi.update(invoiceId!, companyStore?.activeCompany?.id || 0, payload as UpdateInvoiceData);
if (response.error) throw new Error(response.error);
if (!result.success) {
throw new Error(result.error || 'Error al guardar la factura');
}
success = true;
setTimeout(() => {
success = false;
@@ -601,6 +231,12 @@
providers={data.providers || []}
currencyTypes={data.currencyTypes || []}
transportTypes={data.transportTypes || []}
transporters={data.transporters || []}
vehicles={data.vehicles || []}
drivers={data.drivers || []}
trailers={data.trailers || []}
customsSections={data.customsSections || []}
codePedimentoRegimens={data.codePedimentoRegimens || []}
defaultOperationType={data.filters?.operation_type ?? undefined}
defaultInvoiceType={data.filters?.invoice_type ?? undefined}
/>
@@ -647,7 +283,7 @@
<!-- Footer fijo en la parte inferior -->
<div
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5]"
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))]"
>
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
<!-- Tabs Navigation -->

View File

@@ -60,6 +60,12 @@ export const load: PageLoad = async ({ params, url, parent }) => {
providers: data.providers || [],
currencyTypes: data.currencyTypes || [],
transportTypes: data.transportTypes || [],
transporters: data.transporters || [],
vehicles: data.vehicles || [],
drivers: data.drivers || [],
trailers: data.trailers || [],
customsSections: data.customsSections || [],
codePedimentoRegimens: data.codePedimentoRegimens || [],
seals: data.seals || [],
incoterms: data.incoterms || [],
pedimentos: data.pedimentos || [],
@@ -80,6 +86,12 @@ export const load: PageLoad = async ({ params, url, parent }) => {
providers: [],
currencyTypes: [],
transportTypes: [],
transporters: [],
vehicles: [],
drivers: [],
trailers: [],
customsSections: [],
codePedimentoRegimens: [],
seals: [],
incoterms: [],
pedimentos: [],
@@ -116,6 +128,12 @@ export const load: PageLoad = async ({ params, url, parent }) => {
providers: data.providers || [],
currencyTypes: data.currencyTypes || [],
transportTypes: data.transportTypes || [],
transporters: data.transporters || [],
vehicles: data.vehicles || [],
drivers: data.drivers || [],
trailers: data.trailers || [],
customsSections: data.customsSections || [],
codePedimentoRegimens: data.codePedimentoRegimens || [],
seals: data.seals || [],
incoterms: data.incoterms || [],
pedimentos: data.pedimentos || [],