feat: Implement Fixed Asset (FA) line item management

- Added new schemas for FA line items in the backend, including creation, update, and response DTOs.
- Updated existing line item schemas to include FA data.
- Modified database models to reflect new table names for line quantities and references.
- Enhanced ItemService to handle FA data during item creation and updates.
- Introduced new routes and service layer for FA line items, including CRUD operations.
- Updated frontend components to support FA line item data, including new fields and UI adjustments.
- Implemented data flattening for improved item display in the dashboard.
This commit is contained in:
2026-01-12 12:23:40 -06:00
parent 6845dcd670
commit 8dab1d6e46
19 changed files with 852 additions and 173 deletions

View File

@@ -76,15 +76,52 @@ export interface LineReferences {
serie_id?: number;
}
export interface FaLineItem {
id?: number;
tenant_id?: number;
company_id?: number;
// Asset information (SCAF specific)
asset_number?: string;
asset_photo?: string;
equipment_message?: string;
invoice_type_asset?: string;
return_import_invoice?: string;
return_import_date?: number;
movement_type_import?: string;
// Cross-references for import repair
search_invoice?: string;
search_line?: number;
// Search type
search_type?: string;
// Subitems
is_subitem?: boolean;
contains_subitems?: boolean;
includes_subitems?: boolean;
subitem_number?: number;
// Special flags
download?: boolean;
own_equipment?: boolean;
omit_annex31?: boolean;
// Timestamps
created_at?: string;
updated_at?: string;
}
export interface LineItem {
id?: number;
item_id?: number;
line_number: number;
// Identification
part_number?: string;
component_part_number?: string;
class_code?: string;
part_number_id?: string;
component_part_number_id?: string;
class_id?: number;
identifier?: string;
// Unit of Measure
@@ -106,6 +143,12 @@ export interface LineItem {
payment_method?: string;
igi_amount?: number;
// Computed fields from class_info relation
class_code?: string;
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;
@@ -113,6 +156,7 @@ export interface LineItem {
quantity?: LineQuantities;
description?: LineDescriptions;
reference?: LineReferences;
fa_data?: FaLineItem; // Fixed Asset specific data
}
export interface Item {

View File

@@ -12,20 +12,27 @@
descriptions: LineDescriptions;
} = $props();
// Initialize fa_data for fixed asset system
if (!lineItem.fa_data) {
lineItem.fa_data = {};
}
// Helper to map boolean to string for RadioGroup
let isSubPartidaValue = $derived(lineItem.is_subitem ? 'subpartida' : 'partida');
let isSubPartidaValue = $derived(lineItem.fa_data?.is_subitem ? 'subpartida' : 'partida');
function setIsSubPartida(val: string) {
lineItem.is_subitem = val === 'subpartida';
if (!lineItem.fa_data) lineItem.fa_data = {};
lineItem.fa_data.is_subitem = val === 'subpartida';
}
let continueSubPartidasValue = $derived(lineItem.includes_subitems ? 'si' : 'no');
let containsSubPartidasValue = $derived(lineItem.fa_data?.contains_subitems ? 'si' : 'no');
function setContinueSubPartidas(val: string) {
lineItem.includes_subitems = val === 'si';
if (!lineItem.fa_data) lineItem.fa_data = {};
lineItem.fa_data.contains_subitems = val === 'si';
}
</script>
<div class="lg:col-span-5 space-y-3">
<!-- Is Item/Subitem and Continue Sub-Items -->
<!-- Is Item/Subitem and Contains 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>
@@ -45,9 +52,9 @@
</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>
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Contains Sub-Items</legend>
<RadioGroup.Root
value={continueSubPartidasValue}
value={containsSubPartidasValue}
onValueChange={setContinueSubPartidas}
class="flex gap-3">
<div class="flex items-center space-x-2">
@@ -67,7 +74,7 @@
<div class="space-y-1">
<Label for="num_parte" class="text-xs">Part Number:</Label>
<div class="flex gap-1">
<Input id="num_parte" bind:value={lineItem.part_number} class="h-7 text-xs" />
<Input id="num_parte" bind:value={lineItem.part_number_id} class="h-7 text-xs" />
</div>
</div>

View File

@@ -24,7 +24,7 @@
<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" bind:value={lineItem.class_code} class="h-8 text-xs" />
<Input id="clase" bind:value={lineItem.class_id} class="h-8 text-xs" />
</div>
</div>
</div>

View File

@@ -25,9 +25,24 @@
let gross_weight = 0;
let items = $state<Item[]>([]);
let displayedItems = $state<Item[]>([]);
let displayedItems = $state<any[]>([]);
let itemsPerPage = 20;
let currentPage = $state(1);
// Aplanar items en líneas para la tabla
const flattenedLines = $derived(
items.flatMap(item =>
(item.lines || []).map(line => ({
...line,
item_id: item.id,
reference_number: item.reference_number,
order: item.order,
warehouse: item.warehouse,
location: item.location,
full_item: item
}))
)
);
let tableContainer: HTMLDivElement | undefined = $state();
let isLoadingMore = $state(false);
let isLoadingItems = $state(false);
@@ -84,7 +99,7 @@
function loadMoreItems() {
const start = 0;
const end = currentPage * itemsPerPage;
displayedItems = items.slice(start, end);
displayedItems = flattenedLines.slice(start, end);
isLoadingMore = false;
}
@@ -93,7 +108,7 @@
const threshold = 100;
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
if (scrolledToBottom && !isLoadingMore && displayedItems.length < items.length) {
if (scrolledToBottom && !isLoadingMore && displayedItems.length < flattenedLines.length) {
isLoadingMore = true;
currentPage++;
loadMoreItems();
@@ -122,18 +137,16 @@
lines: [{
line_number: 1,
// LineItem fields
part_number: undefined,
component_part_number: undefined,
class_code: undefined,
part_number_id: undefined,
component_part_number_id: 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,
is_subitem: false,
includes_subitems: false,
certificate_number: undefined,
tax_payment: false,
payment_method: undefined,
igi_amount: undefined,
@@ -186,11 +199,11 @@
};
}
function handleEdit(item: Item) {
function handleEdit(lineData: any) {
isEditMode = true;
selectedItem = item;
selectedItem = lineData.full_item;
// Deep clone and normalize numeric values
editingItem = normalizeItemData({ ...item });
editingItem = normalizeItemData({ ...lineData.full_item });
showItemSheet = true;
}
@@ -245,8 +258,8 @@
return item;
}
function handleDelete(item: Item) {
selectedItem = item;
function handleDelete(lineData: any) {
selectedItem = lineData.full_item;
showDeleteDialog = true;
}
@@ -364,11 +377,16 @@
>
<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.Row>
<Table.Head>Línea</Table.Head>
<Table.Head>P/S</Table.Head>
<Table.Head>Clase</Table.Head>
<Table.Head>Descripcion Clase</Table.Head>
<Table.Head>Cant. Importada</Table.Head>
<Table.Head>U.M.</Table.Head>
<Table.Head>Preferencia</Table.Head>
<Table.Head>Contiene Subpartida</Table.Head>
<Table.Head>Partida Principal</Table.Head>
<Table.Head class="text-right w-[120px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
@@ -382,10 +400,15 @@
{:else}
{#each displayedItems as item (item.id)}
<Table.Row>
<Table.Cell>{item.line_number}</Table.Cell>
<Table.Cell>{item.is_subitem ? 'S' : 'P'}</Table.Cell>
<Table.Cell>{item.class_code || '-'}</Table.Cell>
<Table.Cell>{item.class_description || '-'}</Table.Cell>
<Table.Cell>{item.quantity?.quantity || '0'}</Table.Cell>
<Table.Cell>{item.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{item.reference_number || '-'}</Table.Cell>
<Table.Cell>{item.order || '-'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
<Table.Cell>{item.location || '-'}</Table.Cell>
<Table.Cell>{item.contains_subitems || '-'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
<Table.Cell class="text-right">
<div class="flex justify-end gap-2">
<Button size="icon" variant="ghost" onclick={() => handleEdit(item)}>
@@ -410,9 +433,9 @@
</Table.Root>
</div>
{#if items.length > 0}
{#if flattenedLines.length > 0}
<div class="text-xs text-muted-foreground text-right">
Mostrando {displayedItems.length} de {items.length} items
Mostrando {displayedItems.length} de {flattenedLines.length} líneas
</div>
{/if}
</div>