refactor: restructure item interfaces and update related components

- Renamed LineItem interface to Item and adjusted properties accordingly.
- Updated CreateItemData and UpdateItemData interfaces to reflect new structure.
- Modified components to use the new Item interface, removing nested lines.
- Adjusted data binding in item configuration, main data, and other related components.
- Simplified item creation and editing logic by removing unnecessary nesting.
- Ensured all references to line items are updated to reflect the new structure.
This commit is contained in:
2026-02-18 09:56:50 -06:00
parent fe90702563
commit b94d0ac7fb
40 changed files with 1112 additions and 1352 deletions

View File

@@ -122,9 +122,9 @@ export interface FaLineItem {
updated_at?: string;
}
export interface LineItem {
id?: number;
item_id?: number;
export interface Item {
id?: number;
invoice_id: number;
line_number: number;
// Identification
@@ -162,20 +162,7 @@ export interface LineItem {
class_description?: string;
// Computed field from unit_of_measure_info relation
unit_of_measure_code?: string;
// Nested relations (Singular names to match backend Pydantic models)
customs?: LineCustoms;
financial?: LineFinancials;
quantity?: LineQuantities;
description?: LineDescriptions;
reference?: LineReferences;
fa_data?: FaLineItem; // Fixed Asset specific data
}
export interface Item {
id?: number;
invoice_id: number;
unit_of_measure_code?: string;
reference_number?: string;
order?: string;
guide_number?: string;
@@ -185,8 +172,14 @@ export interface Item {
location?: string;
created_at?: string;
updated_at?: string;
lines?: LineItem[];
// Nested relations (Singular names to match backend Pydantic models)
customs?: LineCustoms;
financial?: LineFinancials;
quantity?: LineQuantities;
description?: LineDescriptions;
reference?: LineReferences;
fa_data?: FaLineItem; // Fixed Asset specific data
}
export interface ItemListResponse {
@@ -196,28 +189,11 @@ export interface ItemListResponse {
limit: number;
}
export interface CreateItemData {
export interface CreateItemData extends Omit<Item, 'id' | 'created_at' | 'updated_at'> {
invoice_id: number;
reference_number?: string;
order?: string;
guide_number?: string;
depreciation_date?: number;
rectification?: number;
warehouse?: string;
location?: string;
lines?: LineItem[];
}
export interface UpdateItemData {
reference_number?: string;
order?: string;
guide_number?: string;
depreciation_date?: number;
rectification?: boolean;
warehouse?: string;
location?: string;
lines?: LineItem[];
}
export interface UpdateItemData extends Partial<Omit<Item, 'id' | 'invoice_id' | 'created_at' | 'updated_at'>> {}
/**
* API para Items

View File

@@ -5,13 +5,13 @@
import { Button } from '$lib/components/ui/button';
import { Folder } from 'lucide-svelte';
import PartNumberDialog from './part-number-dialog.svelte';
import type { LineItem, LineDescriptions } from '$lib/api/dashboard/a76/items';
import type { Item, LineDescriptions } from '$lib/api/dashboard/a76/items';
let {
lineItem = $bindable(),
descriptions = $bindable()
}: {
lineItem: LineItem;
lineItem: Partial<Item>;
descriptions: LineDescriptions;
} = $props();

View File

@@ -38,7 +38,7 @@
} = $props();
// Acceso directo a la primera línea para evitar repeticiones en el HTML
let line = $derived(editingItem.lines?.[0]);
let line = $derived(editingItem);
</script>
<Sheet.Root bind:open={open}>
@@ -77,10 +77,10 @@
</div>
<div class="p-2">
<MainData
bind:lineItem={editingItem.lines![0]}
bind:quantities={editingItem.lines![0].quantity!}
bind:financials={editingItem.lines![0].financial!}
bind:customs={editingItem.lines![0].customs!}
bind:lineItem={editingItem}
bind:quantities={editingItem.quantity!}
bind:financials={editingItem.financial!}
bind:customs={editingItem.customs!}
{invoice}
/>
</div>
@@ -94,8 +94,8 @@
</div>
<div class="p-2">
<ItemConfiguration
bind:lineItem={editingItem.lines![0]}
bind:descriptions={editingItem.lines![0].description!}
bind:lineItem={editingItem}
bind:descriptions={editingItem.description!}
/>
</div>
</div>
@@ -126,36 +126,36 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<PackagesSection
bind:item={editingItem}
bind:lineItem={editingItem.lines![0]}
bind:descriptions={editingItem.lines![0].description!}
bind:customs={editingItem.lines![0].customs!}
bind:quantities={editingItem.lines![0].quantity!}
bind:lineItem={editingItem}
bind:descriptions={editingItem.description!}
bind:customs={editingItem.customs!}
bind:quantities={editingItem.quantity!}
invoice={invoice}
/>
<SummarySection
bind:financials={editingItem.lines![0].financial!}
bind:quantities={editingItem.lines![0].quantity!}
bind:financials={editingItem.financial!}
bind:quantities={editingItem.quantity!}
/>
</div>
</Tabs.Content>
<Tabs.Content value="continuacion" class="m-0 focus-visible:outline-none">
<TabContinuation
bind:lineItem={editingItem.lines![0]}
bind:descriptions={editingItem.lines![0].description!}
bind:lineItem={editingItem}
bind:descriptions={editingItem.description!}
/>
</Tabs.Content>
<Tabs.Content value="series" class="m-0 focus-visible:outline-none">
<TabSeries bind:descriptions={editingItem.lines![0].description!} />
<TabSeries bind:descriptions={editingItem.description!} />
</Tabs.Content>
<Tabs.Content value="etiquetado" class="m-0 focus-visible:outline-none">
<TabLabeling bind:descriptions={editingItem.lines![0].description!} />
<TabLabeling bind:descriptions={editingItem.description!} />
</Tabs.Content>
<Tabs.Content value="identificadores" class="m-0 focus-visible:outline-none">
<TabIdentifiers bind:lineItem={editingItem.lines![0]} />
<TabIdentifiers bind:lineItem={editingItem} />
</Tabs.Content>
</div>
</Tabs.Root>

View File

@@ -7,7 +7,7 @@
import CountryDialog from './country-dialog.svelte';
import TariffFractionDialog from './tariff-fraction-dialog.svelte';
import ClassDialog from './class-dialog.svelte';
import type { LineItem, LineQuantities, LineFinancials, LineCustoms } from '$lib/api/dashboard/a76/items';
import type { Item, LineQuantities, LineFinancials, LineCustoms } from '$lib/api/dashboard/a76/items';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import { companyStore } from '$lib/stores/company.svelte';
@@ -18,7 +18,7 @@
customs = $bindable(),
invoice
}: {
lineItem: LineItem;
lineItem: Partial<Item>;
quantities: LineQuantities;
financials: LineFinancials;
customs: LineCustoms;
@@ -50,13 +50,6 @@
// Track previous class_id to detect changes
let previousClassId = $state<number | undefined>(undefined);
// Initialize from existing data
$effect(() => {
if (lineItem.class_code) {
// Do nothing, it's already set
}
});
// Watch for class_id changes and update descriptions automatically
$effect(() => {
const currentClassId = lineItem.class_id;
@@ -247,7 +240,7 @@
<div class="space-y-1">
<Label for="costo_unitario" class="text-xs font-medium">Unit Cost: <span class="text-red-500">*</span></Label>
<div class="flex items-center gap-2">
<Input id="costo_unitario" type="number" step="0.00000001" min="0" bind:value={financials.unit_cost_usd} class="h-8 text-xs text-right flex-1" />
<Input id="costo_unitario" type="number" step="0.00000001" min="0" bind:value={financials.unit_cost_capture} class="h-8 text-xs text-right flex-1" />
<span class="text-xs text-zinc-900 dark:text-zinc-100 font-semibold">USD</span>
</div>
</div>

View File

@@ -5,14 +5,14 @@
import { Checkbox } from '$lib/components/ui/checkbox';
import { Button } from '$lib/components/ui/button';
import { Folder } from 'lucide-svelte';
import type { LineItem, LineDescriptions } from '$lib/api/dashboard/a76/items';
import type { Item, LineDescriptions } from '$lib/api/dashboard/a76/items';
import PaymentMethodDialog from './payment-method-dialog.svelte';
let {
lineItem = $bindable(),
descriptions = $bindable()
}: {
lineItem: LineItem;
lineItem: Partial<Item>;
descriptions: LineDescriptions;
} = $props();
@@ -26,6 +26,14 @@
lineItem.has_certificate = val === 'si';
}
// Initialize boolean fields to prevent bind:checked={undefined} error
if (lineItem.is_military_mcia === undefined) {
lineItem.is_military_mcia = false;
}
if (descriptions.consider_a31 === undefined) {
descriptions.consider_a31 = false;
}
let paymentMethodDialogOpen = $state(false);
let payment_method_description = $state('');

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import type { LineItem } from '$lib/api/dashboard/a76/items';
import type { Item } from '$lib/api/dashboard/a76/items';
let { lineItem = $bindable() }: { lineItem: LineItem } = $props();
let { lineItem = $bindable() }: { lineItem: Partial<Item> } = $props();
</script>
<fieldset class="border rounded-md p-3 space-y-3">

View File

@@ -55,20 +55,19 @@
);
// Derived state for easier binding and safety
let line = $derived(editingItem.lines?.[0]);
let line = $derived(editingItem);
// Initialize missing nested objects if they don't exist
$effect(() => {
if (open && editingItem) {
if (!editingItem.lines) editingItem.lines = [{ line_number: 1 } as any];
if (editingItem.lines[0] && !editingItem.lines[0].quantity)
editingItem.lines[0].quantity = {} as any;
if (editingItem.lines[0] && !editingItem.lines[0].financial)
editingItem.lines[0].financial = {} as any;
if (editingItem.lines[0] && !editingItem.lines[0].customs)
editingItem.lines[0].customs = {} as any;
if (editingItem.lines[0] && !editingItem.lines[0].description)
editingItem.lines[0].description = {} as any;
if (editingItem && !editingItem.quantity)
editingItem.quantity = {} as any;
if (editingItem && !editingItem.financial)
editingItem.financial = {} as any;
if (editingItem && !editingItem.customs)
editingItem.customs = {} as any;
if (editingItem && !editingItem.description)
editingItem.description = {} as any;
}
});
</script>
@@ -86,9 +85,9 @@
{isEditMode
? 'Modifica los campos del inventario y guarda los cambios.'
: 'Completa la información del nuevo item de inventario.'}
{#if editingItem.lines && editingItem.lines.length > 1}
{#if editingItem}
<Badge variant="secondary" class="ml-2">
{editingItem.lines.length} items en esta partida
{editingItem} items en esta partida
</Badge>
{/if}
</Sheet.Description>

View File

@@ -74,26 +74,23 @@
// 4. Derived Values (Ordered correctly to avoid TDZ)
const flattenedLines = $derived.by(() => {
const sourceItems = items?.length ? items : formData?.items || [];
return (sourceItems || []).flatMap((item: any, itemIndex: number) => {
const lines = item?.lines || [];
return lines.map((line: any, idx: number) => ({
...line,
id: line?.id || `${item?.id || itemIndex}-line-${line?.line_number ?? idx + 1}`,
line_number: line?.line_number ?? idx + 1,
reference_number: line?.reference_number ?? item?.reference_number,
is_subitem: line?.is_subitem ?? false,
class_code: line?.class_code ?? line?.class_id,
class_description:
line?.class_description ||
line?.description?.description_spanish ||
line?.description?.description_english ||
'',
unit_of_measure_code: line?.quantity?.unit_of_measure || line?.unit_of_measure,
fa_data: line?.fa_data || {},
warehouse: line?.warehouse || item?.warehouse,
full_item: item
}));
});
return (sourceItems || []).map((item: any, itemIndex: number) => ({
...item,
id: item?.id || `item-${itemIndex}`,
line_number: item?.line_number ?? itemIndex + 1,
reference_number: item?.reference_number,
is_subitem: item?.is_subitem ?? false,
class_code: item?.class_code ?? item?.class_id,
class_description:
item?.class_description ||
item?.description?.description_spanish ||
item?.description?.description_english ||
'',
unit_of_measure_code: item?.quantity?.unit_of_measure || item?.unit_of_measure,
fa_data: item?.fa_data || {},
warehouse: item?.warehouse,
full_item: item
}));
});
const isAllSelected = $derived(
@@ -248,80 +245,76 @@
// Auto-asignar valores desde la factura con estructura completa
editingItem = {
invoice_id: invoice?.id,
line_number: 1,
// LineItem fields
part_number: undefined,
component_part_number: undefined,
class_id: undefined,
identifier: undefined,
unit_of_measure: undefined,
alternate_unit: undefined,
permit_number: undefined,
page_line: undefined,
has_certificate: false,
certificate_number: undefined,
tax_payment: false,
payment_method: undefined,
igi_amount: undefined,
is_military_mcia: false,
wildcard_field: undefined,
reference_number: '',
order: invoice?.purchase_order || '',
warehouse: '',
location: '',
lines: [
{
line_number: 1,
// LineItem fields
part_number: undefined,
component_part_number: undefined,
class_id: undefined,
identifier: undefined,
unit_of_measure: undefined,
alternate_unit: undefined,
permit_number: undefined,
page_line: undefined,
has_certificate: false,
certificate_number: undefined,
tax_payment: false,
payment_method: undefined,
igi_amount: undefined,
is_military_mcia: false,
wildcard_field: undefined,
// Nested relations
financial: {
unit_cost_usd: undefined,
unit_cost_mxn: undefined,
unit_cost_capture: undefined,
unit_cost_commercial_usd: undefined,
value_usd: undefined,
value_mxn: undefined,
value_returned_usd: undefined,
value_returned_mxn: undefined,
customs_value_usd: undefined
},
quantity: {
quantity: undefined,
unit_of_measure: undefined,
quantity_temp_export: undefined,
quantity_returned: undefined,
net_weight: undefined,
gross_weight: undefined,
package_id: undefined,
package_quantity: undefined,
package_description: undefined
},
customs: {
fraction: undefined,
fraction_type: undefined,
american_fraction: undefined,
origin_country: undefined,
destination_country: undefined,
advalorem: undefined,
advalorem_american: undefined,
sector: undefined
},
description: {
description_spanish: undefined,
description_english: undefined,
extra_description: undefined,
additional_info_spanish: undefined,
brand: undefined,
model: undefined,
has_serial: false,
eighth_rule_fraction: undefined,
eighth_rule_line: undefined,
consider_a31: false,
machinery_location: undefined
},
reference: {
serie_id: undefined
}
}
]
location: '',
// Nested relations
financial: {
unit_cost_usd: undefined,
unit_cost_mxn: undefined,
unit_cost_capture: undefined,
unit_cost_commercial_usd: undefined,
value_usd: undefined,
value_mxn: undefined,
value_returned_usd: undefined,
value_returned_mxn: undefined,
customs_value_usd: undefined
},
quantity: {
quantity: undefined,
unit_of_measure: undefined,
quantity_temp_export: undefined,
quantity_returned: undefined,
net_weight: undefined,
gross_weight: undefined,
package_id: undefined,
package_quantity: undefined,
package_description: undefined
},
customs: {
fraction: undefined,
fraction_type: undefined,
american_fraction: undefined,
origin_country: undefined,
destination_country: undefined,
advalorem: undefined,
advalorem_american: undefined,
sector: undefined
},
description: {
description_spanish: undefined,
description_english: undefined,
extra_description: undefined,
additional_info_spanish: undefined,
brand: undefined,
model: undefined,
has_serial: false,
eighth_rule_fraction: undefined,
eighth_rule_line: undefined,
consider_a31: false,
machinery_location: undefined
},
reference: {
serie_id: undefined
}
};
}
@@ -376,14 +369,14 @@
function saveItemToPreset() {
// Sanitizar datos para la plantilla
const cleanedItem = JSON.parse(JSON.stringify(editingItem));
let cleanedItem = JSON.parse(JSON.stringify(editingItem));
// Limpiar líneas para asegurar que son compatibles
if (cleanedItem.lines) {
cleanedItem.lines = cleanedItem.lines.map((line: any) => ({
...cleanLineData(line),
// Limpiar item para asegurar que es compatible
if (cleanedItem) {
cleanedItem = {
...cleanLineData(cleanedItem),
id: undefined // Las plantillas no deben tener IDs reales
}));
};
}
if (editingBuilderIndex !== null) {
@@ -409,35 +402,30 @@
function cloneItemForPreset(item: Item) {
const { id, tenant_id, company_id, created_at, updated_at, temp_id, ...rest } = item as any;
return {
...rest,
...sanitizeLineForPreset(rest),
id: undefined,
invoice_id: undefined,
lines: (item.lines || []).map(sanitizeLineForPreset)
invoice_id: undefined
};
}
function buildManualItem(draft: any, index: number) {
return {
return cleanLineData({
id: undefined,
temp_id: undefined,
invoice_id: undefined,
reference_number: draft.reference_number || undefined,
lines: [
cleanLineData({
line_number: index + 1,
description: {
description_spanish: draft.description || 'Sin descripción'
},
quantity: {
quantity: Number(draft.quantity) || 0
},
financial: {
unit_cost_usd:
draft.unit_cost_usd !== null ? Number(draft.unit_cost_usd) || 0 : undefined
}
})
]
};
line_number: index + 1,
description: {
description_spanish: draft.description || 'Sin descripción'
},
quantity: {
quantity: Number(draft.quantity) || 0
},
financial: {
unit_cost_usd:
draft.unit_cost_usd !== null ? Number(draft.unit_cost_usd) || 0 : undefined
}
});
}
function handleAddManualItem() {
@@ -466,15 +454,11 @@
}
// Inject into active sheet if open
// No se pueden inyectar múltiples líneas en un item, ya que ahora un item ES una línea
if (showItemSheet) {
const presetLines = selectedPreset.items.flatMap((item: any) => item.lines || []);
const cleanedNewLines = presetLines.map((line: any) => ({
...sanitizeLineForPreset(line),
id: undefined // Force new IDs
}));
editingItem.lines = [...(editingItem.lines || []), ...cleanedNewLines];
toast.success('Líneas inyectadas en la partida actual');
toast.warning('No se puede inyectar plantilla en modo edición', {
description: 'Las plantillas solo se pueden aplicar directamente a la factura'
});
showUsePresetDialog = false;
return;
}
@@ -517,26 +501,19 @@
isSavingPreset = true;
try {
// We group everything as ONE Partida Template for injection
const lines = builderItems.flatMap((item: Item, idx: number) => {
return (item.lines || []).map((line: any) => ({
...cleanLineData(line),
line_number: line.line_number || idx + 1, // Ensure line_number is present
// Each item in builderItems is already a LineItem (no nested lines)
const cleanedItems = builderItems.map((item: Item, idx: number) => {
return {
...cleanLineData(item),
line_number: item.line_number || idx + 1,
id: undefined // Ensure no IDs are saved in the preset
}));
};
});
const payloadItems = [
{
reference_number: builderItems[0]?.reference_number || undefined,
lines: lines
}
] as any;
await itemPresetsApi.create(activeCompanyId, {
name: createPresetName.trim(),
description: createPresetDescription.trim() || undefined,
items: payloadItems
items: cleanedItems
});
createPresetName = '';
@@ -569,22 +546,20 @@
// Enrich item with descriptive data for display
async function enrichItemData(item: Partial<Item>) {
if (!item.lines || item.lines.length === 0 || !activeCompanyId) return;
const line = item.lines[0];
if (!item || !activeCompanyId) return;
// Load class data
if (line.class_id) {
if (item.class_id) {
try {
const response = await fetch(
`/api-sveltekit/classes/${line.class_id}?company_id=${activeCompanyId}`,
`/api-sveltekit/classes/${item.class_id}?company_id=${activeCompanyId}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
const classData = await response.json();
(line as any).class_code = classData.class_code;
(line as any).class_unit_of_measure = classData.unit_of_measure;
(line as any).class_description = classData.description_es || classData.description_en;
(item as any).class_code = classData.class_code;
(item as any).class_unit_of_measure = classData.unit_of_measure;
(item as any).class_description = classData.description_es || classData.description_en;
}
} catch (error) {
console.error('Error loading class data:', error);
@@ -592,17 +567,17 @@
}
// Load part number data
if (line.part_number) {
if (item.part_number) {
try {
const response = await fetch(
`/api-sveltekit/parts/${line.part_number}?company_id=${activeCompanyId}`,
`/api-sveltekit/parts/${item.part_number}?company_id=${activeCompanyId}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
const partData = await response.json();
(line as any).part_number = partData.part_number;
(line as any).part_description_es = partData.description_spanish;
(line as any).part_description_en = partData.description_english;
(item as any).part_number = partData.part_number;
(item as any).part_description_es = partData.description_spanish;
(item as any).part_description_en = partData.description_english;
}
} catch (error) {
console.error('Error loading part data:', error);
@@ -610,16 +585,16 @@
}
// Load unit of measure data
if (line.unit_of_measure) {
if (item.unit_of_measure) {
try {
const response = await fetch(`/api-sveltekit/units-of-measure/${line.unit_of_measure}`, {
const response = await fetch(`/api-sveltekit/units-of-measure/${item.unit_of_measure}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
const unitData = await response.json();
(line as any).unit_code = unitData.code;
(line as any).unit_description = unitData.description || unitData.description_en;
(item as any).unit_code = unitData.code;
(item as any).unit_description = unitData.description || unitData.description_en;
}
} catch (error) {
console.error('Error loading unit data:', error);
@@ -627,17 +602,17 @@
}
// Load country data (if needed)
if (line.customs?.origin_country) {
if (item.customs?.origin_country) {
try {
const response = await fetch(
`/api-sveltekit/countries?search=${line.customs.origin_country}`,
`/api-sveltekit/countries?search=${item.customs.origin_country}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
const data = await response.json();
if (data.items && data.items.length > 0) {
const country = data.items[0];
(line.customs as any).origin_country_name =
(item.customs as any).origin_country_name =
country.description || country.description_en;
}
}
@@ -647,17 +622,17 @@
}
// Load fraction data (if needed)
if (line.customs?.fraction) {
if (item.customs?.fraction) {
try {
const response = await fetch(
`/api-sveltekit/tariff-fractions?search=${line.customs.fraction}`,
`/api-sveltekit/tariff-fractions?search=${item.customs.fraction}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
const data = await response.json();
if (data.items && data.items.length > 0) {
const fraction = data.items[0];
(line.customs as any).fraction_description = fraction.description;
(item.customs as any).fraction_description = fraction.description;
}
}
} catch (error) {
@@ -666,8 +641,8 @@
}
// Load package data (if needed)
const packageId = line.quantity?.package_id;
if (packageId && line.quantity) {
const packageId = item.quantity?.package_id;
if (packageId && item.quantity) {
try {
const response = await fetch('/api-sveltekit/packages', {
method: 'GET',
@@ -679,9 +654,9 @@
if (Array.isArray(packages)) {
const pkg = packages.find((p: any) => p.id === packageId);
if (pkg) {
(line.quantity as any).package_description = pkg.description_es || pkg.description_en || pkg.key;
(line.quantity as any).package_key = pkg.key;
(line.quantity as any).package_weight_unit = pkg.weight_unit || 0;
(item.quantity as any).package_description = pkg.description_es || pkg.description_en || pkg.key;
(item.quantity as any).package_key = pkg.key;
(item.quantity as any).package_weight_unit = pkg.weight_unit || 0;
}
}
}
@@ -691,7 +666,7 @@
}
// Load payment method description (if needed)
if (line.payment_method) {
if (item.payment_method) {
try {
const response = await fetch('/api-sveltekit/payment-methods', {
method: 'GET',
@@ -701,9 +676,9 @@
const data = await response.json();
const methods = data.items || data.data || data;
if (Array.isArray(methods)) {
const method = methods.find((m: any) => m.key === line.payment_method);
const method = methods.find((m: any) => m.key === item.payment_method);
if (method) {
(line as any).payment_method_description = method.description;
(item as any).payment_method_description = method.description;
}
}
}
@@ -715,58 +690,56 @@
// Normalize numeric values from strings to numbers
function normalizeItemData(item: Partial<Item>): Partial<Item> {
if (item.lines && item.lines.length > 0) {
item.lines = item.lines.map((line) => {
const normalizedLine = { ...line };
if (item) {
const normalizedItem = { ...item };
// Normalize financials
if (normalizedLine.financial) {
normalizedLine.financial = {
...normalizedLine.financial,
unit_cost_usd:
normalizedLine.financial.unit_cost_usd != null
? Number(normalizedLine.financial.unit_cost_usd)
: undefined,
unit_cost_mxn:
normalizedLine.financial.unit_cost_mxn != null
? Number(normalizedLine.financial.unit_cost_mxn)
: undefined,
value_usd:
normalizedLine.financial.value_usd != null
? Number(normalizedLine.financial.value_usd)
: undefined,
value_mxn:
normalizedLine.financial.value_mxn != null
? Number(normalizedLine.financial.value_mxn)
: undefined
};
}
// Normalize financials
if (normalizedItem.financial) {
normalizedItem.financial = {
...normalizedItem.financial,
unit_cost_usd:
normalizedItem.financial.unit_cost_usd != null
? Number(normalizedItem.financial.unit_cost_usd)
: undefined,
unit_cost_mxn:
normalizedItem.financial.unit_cost_mxn != null
? Number(normalizedItem.financial.unit_cost_mxn)
: undefined,
value_usd:
normalizedItem.financial.value_usd != null
? Number(normalizedItem.financial.value_usd)
: undefined,
value_mxn:
normalizedItem.financial.value_mxn != null
? Number(normalizedItem.financial.value_mxn)
: undefined
};
}
// Normalize quantities
if (normalizedLine.quantity) {
normalizedLine.quantity = {
...normalizedLine.quantity,
quantity:
normalizedLine.quantity.quantity != null
? Number(normalizedLine.quantity.quantity)
: undefined,
net_weight:
normalizedLine.quantity.net_weight != null
? Number(normalizedLine.quantity.net_weight)
: undefined,
gross_weight:
normalizedLine.quantity.gross_weight != null
? Number(normalizedLine.quantity.gross_weight)
: undefined,
package_quantity:
normalizedLine.quantity.package_quantity != null
? Number(normalizedLine.quantity.package_quantity)
: undefined
};
}
// Normalize quantities
if (normalizedItem.quantity) {
normalizedItem.quantity = {
...normalizedItem.quantity,
quantity:
normalizedItem.quantity.quantity != null
? Number(normalizedItem.quantity.quantity)
: undefined,
net_weight:
normalizedItem.quantity.net_weight != null
? Number(normalizedItem.quantity.net_weight)
: undefined,
gross_weight:
normalizedItem.quantity.gross_weight != null
? Number(normalizedItem.quantity.gross_weight)
: undefined,
package_quantity:
normalizedItem.quantity.package_quantity != null
? Number(normalizedItem.quantity.package_quantity)
: undefined
};
}
return normalizedLine;
});
return normalizedItem;
}
return item;
@@ -782,16 +755,12 @@
isSaving = true;
try {
// Clean lines data before sending
const cleanedLines = (editingItem.lines || []).map(cleanLineData);
// Clean item data before sending
const cleanedItem = cleanLineData(editingItem);
const response = await itemsApi.create(activeCompanyId, {
invoice_id: invoice.id,
reference_number: editingItem.reference_number,
order: editingItem.order,
warehouse: editingItem.warehouse,
location: editingItem.location,
lines: cleanedLines
...cleanedItem,
invoice_id: invoice.id
});
// Verificar si hay errores de validación
@@ -851,16 +820,10 @@
isSaving = true;
try {
// Clean lines data before sending
const cleanedLines = (editingItem.lines || []).map(cleanLineData);
// Clean item data before sending
const cleanedItem = cleanLineData(editingItem);
const response = await itemsApi.update(selectedItem.id, activeCompanyId, {
reference_number: editingItem.reference_number,
order: editingItem.order,
warehouse: editingItem.warehouse,
location: editingItem.location,
lines: cleanedLines
});
const response = await itemsApi.update(selectedItem.id, activeCompanyId, cleanedItem);
// Verificar si hay errores de validación
if ('error' in response) {
@@ -916,52 +879,51 @@
function saveItem() {
// Validar campos obligatorios antes de guardar
const line = editingItem.lines?.[0];
const missingFields: string[] = [];
if (!line) {
if (!editingItem) {
toast.warning('Error de datos', {
description: 'No se encontró información de la línea del item'
description: 'No se encontró información del item'
});
return;
}
// 1. Clase
if (!line.class_id) {
if (!editingItem.class_id) {
missingFields.push('Clase');
}
// 2. Cantidad
if (!line.quantity?.quantity || line.quantity.quantity <= 0) {
if (!editingItem.quantity?.quantity || editingItem.quantity.quantity <= 0) {
missingFields.push('Cantidad');
}
// 3. Unidad de Medida
if (!line.unit_of_measure) {
if (!editingItem.unit_of_measure) {
missingFields.push('U.M. (Unidad de Medida)');
}
// 4. Costo Unitario (al menos uno debe estar presente)
const hasCost =
line.financial?.unit_cost_usd ||
line.financial?.unit_cost_mxn ||
line.financial?.unit_cost_capture;
editingItem.financial?.unit_cost_usd ||
editingItem.financial?.unit_cost_mxn ||
editingItem.financial?.unit_cost_capture;
if (!hasCost) {
missingFields.push('Costo Unitario (USD, MXN o Captura)');
}
// 5. País de Origen
if (!line.customs?.origin_country) {
if (!editingItem.customs?.origin_country) {
missingFields.push('País de Origen');
}
// 6. Tipo de Tarifa
if (!line.customs?.fraction_type) {
if (!editingItem.customs?.fraction_type) {
missingFields.push('Tipo de Tarifa');
}
// 7. Descripción en Español
if (!line.description?.description_spanish?.trim()) {
if (!editingItem.description?.description_spanish?.trim()) {
missingFields.push('Descripción en Español');
}
@@ -1453,7 +1415,7 @@
<Table.Cell>
<div class="flex flex-col py-0.5">
<span class="font-medium text-sm text-foreground/90">
{item.lines?.[0]?.description?.description_spanish ||
{item?.description?.description_spanish ||
'Sin descripción'}
</span>
{#if item.reference_number}
@@ -1464,10 +1426,10 @@
</div>
</Table.Cell>
<Table.Cell class="text-center font-medium tabular-nums">
{item.lines?.[0]?.quantity?.quantity || 0}
{item?.quantity?.quantity || 0}
</Table.Cell>
<Table.Cell class="text-right tabular-nums text-muted-foreground">
${(item.lines?.[0]?.financial?.unit_cost_usd || 0).toLocaleString(
${(item?.financial?.unit_cost_usd || 0).toLocaleString(
undefined,
{ minimumFractionDigits: 2 }
)}
@@ -1584,7 +1546,7 @@
<Table.Cell>
<div class="flex flex-col">
<span class="font-medium text-sm">
{item.lines?.[0]?.description?.description_spanish || 'Sin descripción'}
{item?.[0]?.description?.description_spanish || 'Sin descripción'}
</span>
<span class="text-[10px] text-muted-foreground"
>Ref: {item.reference_number || '-'}</span
@@ -1592,7 +1554,7 @@
</div>
</Table.Cell>
<Table.Cell class="text-center text-sm">
{item.lines?.[0]?.quantity?.quantity || 0}
{item?.[0]?.quantity?.quantity || 0}
</Table.Cell>
<Table.Cell class="text-right pr-2">
<div class="flex justify-end gap-1">

View File

@@ -1,7 +1,6 @@
export interface Item {
id?: number;
lines?: any[];
[key: string]: any;
}
@@ -71,36 +70,34 @@ export function cleanLineData(line: any) {
// Normalize numeric values from strings to numbers (for editing)
export function normalizeItemData(item: Partial<Item>): Partial<Item> {
if (item.lines && item.lines.length > 0) {
item.lines = item.lines.map((line) => {
const normalizedLine = { ...line };
if (item) {
const normalizedItem = { ...item };
// Normalize financials
if (normalizedLine.financial) {
const f = normalizedLine.financial;
normalizedLine.financial = {
...f,
unit_cost_usd: f.unit_cost_usd != null ? Number(f.unit_cost_usd) : undefined,
unit_cost_mxn: f.unit_cost_mxn != null ? Number(f.unit_cost_mxn) : undefined,
value_usd: f.value_usd != null ? Number(f.value_usd) : undefined,
value_mxn: f.value_mxn != null ? Number(f.value_mxn) : undefined
};
}
// Normalize financials
if (normalizedItem.financial) {
const f = normalizedItem.financial;
normalizedItem.financial = {
...f,
unit_cost_usd: f.unit_cost_usd != null ? Number(f.unit_cost_usd) : undefined,
unit_cost_mxn: f.unit_cost_mxn != null ? Number(f.unit_cost_mxn) : undefined,
value_usd: f.value_usd != null ? Number(f.value_usd) : undefined,
value_mxn: f.value_mxn != null ? Number(f.value_mxn) : undefined
};
}
// Normalize quantities
if (normalizedLine.quantity) {
const q = normalizedLine.quantity;
normalizedLine.quantity = {
...q,
quantity: q.quantity != null ? Number(q.quantity) : undefined,
net_weight: q.net_weight != null ? Number(q.net_weight) : undefined,
gross_weight: q.gross_weight != null ? Number(q.gross_weight) : undefined,
package_quantity: q.package_quantity != null ? Number(q.package_quantity) : undefined
};
}
// Normalize quantities
if (normalizedItem.quantity) {
const q = normalizedItem.quantity;
normalizedItem.quantity = {
...q,
quantity: q.quantity != null ? Number(q.quantity) : undefined,
net_weight: q.net_weight != null ? Number(q.net_weight) : undefined,
gross_weight: q.gross_weight != null ? Number(q.gross_weight) : undefined,
package_quantity: q.package_quantity != null ? Number(q.package_quantity) : undefined
};
}
return normalizedLine;
});
return normalizedItem;
}
return item;

View File

@@ -153,17 +153,13 @@
function handleAddItem() {
isItemEditMode = false;
editingItemIndex = null;
editingItem = {
lines: [
{
line_number: (selectedPreset.items?.length || 0) + 1,
quantity: { quantity: 1 },
financial: { unit_cost_usd: 0 },
customs: {},
description: {},
reference: {}
}
]
editingItem = {
line_number: (selectedPreset.items?.length || 0) + 1,
quantity: { quantity: 1 },
financial: { unit_cost_usd: 0 },
customs: {},
description: {},
reference: {}
};
showItemSheet = true;
}
@@ -463,7 +459,7 @@
<Table.Cell>
<div class="flex flex-col">
<span class="font-medium text-sm">
{item.lines?.[0]?.description?.description_spanish || 'Sin descripción'}
{item?.description?.description_spanish || 'Sin descripción'}
</span>
{#if item.reference_number}
<span class="text-[10px] text-muted-foreground flex items-center gap-1">
@@ -473,12 +469,12 @@
</div>
</Table.Cell>
<Table.Cell class="text-center tabular-nums font-medium"
>{item.lines?.[0]?.quantity?.quantity || 0}</Table.Cell
>{item?.quantity?.quantity || 0}</Table.Cell
>
<Table.Cell
class="text-center tabular-nums font-medium text-emerald-600 dark:text-emerald-400"
>
${(item.lines?.[0]?.financial?.unit_cost_usd || 0).toLocaleString()}
${(item?.financial?.unit_cost_usd || 0).toLocaleString()}
</Table.Cell>
<Table.Cell class="text-right pr-6">
<div class="flex justify-end gap-1">