Partidas por tipo de factura en importacion

This commit is contained in:
2026-03-13 17:38:30 -05:00
parent dfa44aaff7
commit dada17b54c
15 changed files with 1015 additions and 56 deletions

View File

@@ -0,0 +1,115 @@
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog";
import * as Table from "$lib/components/ui/table";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Search, Loader2 } from "lucide-svelte";
import { getIdentifiers, type Identifier } from "$lib/api/dashboard/a76/general_catalogs/identifiers";
import { onMount } from "svelte";
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (identifier: Identifier) => void;
} = $props();
let identifiers = $state<Identifier[]>([]);
let loading = $state(false);
let searchTerm = $state("");
async function loadIdentifiers() {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) return;
loading = true;
try {
// Using a large limit for now to avoid complex pagination in the selector
const res = await getIdentifiers(1, 1000, companyId);
if (res.data) {
identifiers = res.data.items || [];
}
} catch (error) {
console.error("Error loading identifiers:", error);
} finally {
loading = false;
}
}
onMount(() => {
loadIdentifiers();
});
const filteredIdentifiers = $derived(
identifiers.filter(i =>
i.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
(i.description?.toLowerCase().includes(searchTerm.toLowerCase()) ?? false)
)
);
function handleSelect(identifier: Identifier) {
onSelect(identifier);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[600px] h-[600px] flex flex-col p-0 text-xs">
<Dialog.Header class="px-6 pt-6 pb-4 shrink-0">
<Dialog.Title>Seleccionar Identificador</Dialog.Title>
<Dialog.Description>
Busca y selecciona un identificador del catálogo (Apéndice 8).
</Dialog.Description>
</Dialog.Header>
<div class="px-6 pb-4 shrink-0">
<div class="relative">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por clave o descripción..."
class="pl-9 h-9 text-xs"
bind:value={searchTerm}
/>
</div>
</div>
<div class="flex-1 overflow-y-auto px-6 pb-6">
{#if loading}
<div class="flex h-full items-center justify-center">
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" />
</div>
{:else}
<Table.Root>
<Table.Header class="sticky top-0 bg-background z-10">
<Table.Row>
<Table.Head class="w-16">Clave</Table.Head>
<Table.Head>Descripción</Table.Head>
<Table.Head class="w-16">Nivel</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each filteredIdentifiers as item}
<Table.Row
class="cursor-pointer hover:bg-muted"
onclick={() => handleSelect(item)}
>
<Table.Cell class="font-bold">{item.code}</Table.Cell>
<Table.Cell>{item.description || '-'}</Table.Cell>
<Table.Cell>{item.level || '-'}</Table.Cell>
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={3} class="h-24 text-center text-muted-foreground">
No se encontraron identificadores.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -216,7 +216,7 @@
{ value: 'generales', label: 'General', visible: true },
{ value: 'continuacion', label: 'Continuación', visible: true },
{ value: 'series', label: 'Series', visible: true },
{ value: 'etiquetado', label: 'Etiquetado', visible: true },
{ value: 'etiquetado', label: 'Etiquetado', visible: visibility.showLabelingTab },
{ value: 'identificadores', label: 'IDs', visible: visibility.showIdentifiersTab }
].filter((tab) => tab.visible));
const tabListStyle = $derived(`grid-template-columns: repeat(${visibleTabs.length || 1}, minmax(0, 1fr));`);
@@ -544,13 +544,20 @@
/>
</Tabs.Content>
<Tabs.Content value="etiquetado" class="m-0 focus-visible:outline-none">
<TabLabeling bind:descriptions={editingItem.description!} />
</Tabs.Content>
{#if visibility.showLabelingTab}
<Tabs.Content value="etiquetado" class="m-0 focus-visible:outline-none">
<TabLabeling bind:lineItem={editingItem} bind:descriptions={editingItem.description!} {visibility} />
</Tabs.Content>
{/if}
{#if visibility.showIdentifiersTab}
<Tabs.Content value="identificadores" class="m-0 focus-visible:outline-none">
<TabIdentifiers bind:lineItem={editingItem} />
<TabIdentifiers
bind:lineItem={editingItem}
invoiceConsecutive={invoice?.id}
invoiceNumber={invoice?.invoice_number ?? ''}
{visibility}
/>
</Tabs.Content>
{/if}
</div>

View File

@@ -1,26 +1,376 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import type { Item } from '$lib/api/dashboard/a76/items';
import { Button } from '$lib/components/ui/button';
import * as Table from '$lib/components/ui/table';
import * as Dialog from '$lib/components/ui/dialog';
import { Plus, Pencil, Trash2, Search, Image as ImageIcon, Upload } from 'lucide-svelte';
import type { Item, Serie } from '$lib/api/dashboard/a76/items';
import type { IdentifierDetail } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
import IdentifierCatalogSelector from './identifier-catalog-selector.svelte';
import { companyStore } from '$lib/stores/company.svelte';
let { lineItem = $bindable() }: { lineItem: Partial<Item> } = $props();
let {
lineItem = $bindable(),
invoiceConsecutive = undefined,
invoiceNumber = '',
visibility = { showMexicanIdEnhanced: false }
}: {
lineItem: Partial<Item>,
invoiceConsecutive?: number,
invoiceNumber?: string,
visibility?: any
} = $props();
// Initialize identifiers if not present
if (!lineItem.identifiers) {
lineItem.identifiers = [];
}
// Initialize series if not present
if (!lineItem.series) {
lineItem.series = [];
}
let showModal = $state(false);
let showMexModal = $state(false);
let showCatalogSelector = $state(false);
let isEditing = $state(false);
let editingIndex = $state(-1);
// State for standard Identifiers
let currentDetail = $state<Partial<IdentifierDetail>>({
invoice_consecutive: invoiceConsecutive || null,
part_line: lineItem.line_number || null,
module: 'SCAF-IT',
identifier_code: '',
complement1: '',
complement2: '',
complement3: ''
});
// State for MEX Assets
let currentMexAsset = $state<Partial<Serie>>({
number_id: '',
import_invoice: invoiceNumber || '',
import_line: lineItem.line_number ?? undefined,
image_path: ''
});
// Standard ID Functions
function openInsert() {
isEditing = false;
currentDetail = {
invoice_consecutive: invoiceConsecutive ?? null,
part_line: lineItem.line_number ?? null,
module: 'SCAF-IT',
identifier_code: '',
complement1: '',
complement2: '',
complement3: ''
};
showModal = true;
}
function openEdit(index: number) {
isEditing = true;
editingIndex = index;
currentDetail = { ...lineItem.identifiers![index] };
showModal = true;
}
function deleteDetail(index: number) {
lineItem.identifiers = lineItem.identifiers!.filter((_, i) => i !== index);
}
function saveDetail() {
if (!lineItem.identifiers) lineItem.identifiers = [];
if (isEditing) {
lineItem.identifiers[editingIndex] = currentDetail as IdentifierDetail;
} else {
lineItem.identifiers = [...lineItem.identifiers, currentDetail as IdentifierDetail];
}
showModal = false;
}
// MEX Asset Functions
function openMexInsert() {
isEditing = false;
currentMexAsset = {
number_id: '',
import_invoice: invoiceNumber || '',
import_line: lineItem.line_number ?? undefined,
image_path: ''
};
showMexModal = true;
}
function openMexEdit(index: number) {
isEditing = true;
editingIndex = index;
currentMexAsset = { ...lineItem.series![index] };
showMexModal = true;
}
function deleteMexAsset(index: number) {
lineItem.series = lineItem.series!.filter((_, i) => i !== index);
}
function saveMexAsset() {
if (!lineItem.series) lineItem.series = [];
if (isEditing) {
lineItem.series[editingIndex] = currentMexAsset as Serie;
} else {
lineItem.series = [...lineItem.series, currentMexAsset as Serie];
}
showMexModal = false;
}
function handleCatalogSelect(identifier: any) {
currentDetail.identifier_code = identifier.code;
showCatalogSelector = false;
}
// File selection simulation for image_path
function triggerFileSelect() {
// In a real scenario, this would trigger an input type="file"
// For now we'll just simulate setting a path/name
const simulatedPath = `assets/img_${Date.now()}.jpg`;
currentMexAsset.image_path = simulatedPath;
}
</script>
<fieldset class="border rounded-md p-3 space-y-3">
<legend class="text-xs font-semibold px-2 uppercase">Identifiers</legend>
<div class="space-y-2">
<Label for="identificador1" class="text-xs">Main Identifier:</Label>
<Input id="identificador1" bind:value={lineItem.identifier} class="h-8 text-sm" />
</div>
<div class="space-y-4">
{#if visibility.showMexicanIdEnhanced}
<!-- MEX Specific Layout -->
<fieldset class="border rounded-md p-3">
<legend class="text-xs font-semibold px-2 uppercase flex items-center gap-2">
Activos / Num. Etiquetado (MEX)
</legend>
<div class="flex justify-end mb-2">
<Button size="sm" variant="outline" class="h-8 text-xs gap-1" onclick={openMexInsert}>
<Plus class="h-3 w-3" />
Insertar Activo
</Button>
</div>
<div class="space-y-2">
<Label for="notas_identificadores" class="text-xs">Additional Identifiers / Notes:</Label>
<textarea
id="notas_identificadores"
bind:value={lineItem.wildcard_field}
class="flex min-h-[120px] w-full rounded-md border-2 border-input dark:border-zinc-600 bg-background dark:text-white 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 dark:focus-visible:border-zinc-400 dark:focus-visible:ring-zinc-400/50"
placeholder="Additional identifiers or notes..."
></textarea>
</div>
</fieldset>
<div class="border rounded-md overflow-hidden bg-white dark:bg-zinc-900">
<Table.Root>
<Table.Header class="bg-gray-50 dark:bg-zinc-800">
<Table.Row class="h-8">
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Asset Number</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Num. Factura</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Línea</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8 text-center">Imagen</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8 w-20 text-center">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if lineItem.series && lineItem.series.length > 0}
{#each lineItem.series as asset, index}
<Table.Row class="h-8 hover:bg-gray-50 dark:hover:bg-zinc-800/50 transition-colors">
<Table.Cell class="py-1 text-xs font-semibold">{asset.number_id}</Table.Cell>
<Table.Cell class="py-1 text-xs">{asset.import_invoice || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs">{asset.import_line || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs text-center">
{#if asset.image_path}
<div class="flex justify-center">
<ImageIcon class="h-4 w-4 text-blue-500" />
</div>
{:else}
<span class="text-gray-400">-</span>
{/if}
</Table.Cell>
<Table.Cell class="py-1 text-xs">
<div class="flex items-center justify-center gap-1">
<Button size="icon" variant="ghost" class="h-6 w-6 text-blue-500 hover:text-blue-600 hover:bg-blue-50" onclick={() => openMexEdit(index)}>
<Pencil class="h-3 w-3" />
</Button>
<Button size="icon" variant="ghost" class="h-6 w-6 text-red-500 hover:text-red-600 hover:bg-red-50" onclick={() => deleteMexAsset(index)}>
<Trash2 class="h-3 w-3" />
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={5} class="h-20 text-center text-gray-400 text-xs italic">
No hay activos registrados.
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</fieldset>
{:else}
<!-- Standard ID Layout -->
<fieldset class="border rounded-md p-3">
<legend class="text-xs font-semibold px-2 uppercase flex items-center gap-2">
Tabla de Identificadores
</legend>
<div class="flex justify-end mb-2">
<Button size="sm" variant="outline" class="h-8 text-xs gap-1" onclick={openInsert}>
<Plus class="h-3 w-3" />
Insertar
</Button>
</div>
<div class="border rounded-md overflow-hidden bg-white dark:bg-zinc-900">
<Table.Root>
<Table.Header class="bg-gray-100 dark:bg-zinc-800">
<Table.Row class="h-8">
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Clave</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Compl. 1</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Compl. 2</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Compl. 3</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8 w-20 text-center">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if lineItem.identifiers && lineItem.identifiers.length > 0}
{#each lineItem.identifiers as idDetail, index}
<Table.Row class="h-8 hover:bg-gray-50 dark:hover:bg-zinc-800/50 transition-colors">
<Table.Cell class="py-1 text-xs font-semibold">{idDetail.identifier_code}</Table.Cell>
<Table.Cell class="py-1 text-xs">{idDetail.complement1 || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs">{idDetail.complement2 || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs">{idDetail.complement3 || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs">
<div class="flex items-center justify-center gap-1">
<Button size="icon" variant="ghost" class="h-6 w-6 text-blue-500 hover:text-blue-600 hover:bg-blue-50" onclick={() => openEdit(index)}>
<Pencil class="h-3 w-3" />
</Button>
<Button size="icon" variant="ghost" class="h-6 w-6 text-red-500 hover:text-red-600 hover:bg-red-50" onclick={() => deleteDetail(index)}>
<Trash2 class="h-3 w-3" />
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={5} class="h-20 text-center text-gray-400 text-xs italic">
No hay identificadores registrados.
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</fieldset>
{/if}
</div>
<!-- Modal Standard ID -->
<Dialog.Root bind:open={showModal}>
<Dialog.Content class="sm:max-w-[500px]">
<Dialog.Header>
<Dialog.Title>{isEditing ? 'Editar' : 'Insertar'} Identificador</Dialog.Title>
<Dialog.Description>
Ingrese los detalles del identificador para esta partida.
</Dialog.Description>
</Dialog.Header>
<div class="grid grid-cols-2 gap-4 py-4">
<div class="space-y-2">
<Label class="text-xs">Consecutivo Factura</Label>
<Input bind:value={currentDetail.invoice_consecutive} readonly class="h-8 text-xs bg-gray-50" />
</div>
<div class="space-y-2">
<Label class="text-xs">Partida (Número)</Label>
<Input bind:value={currentDetail.part_line} readonly class="h-8 text-xs bg-gray-50" />
</div>
<div class="space-y-2">
<Label class="text-xs">Módulo</Label>
<Input bind:value={currentDetail.module} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label class="text-xs">Clave</Label>
<div class="flex gap-1">
<Input bind:value={currentDetail.identifier_code} class="h-8 text-xs font-bold" placeholder="Clave" />
<Button size="icon" variant="outline" class="h-8 w-8 shrink-0" onclick={() => showCatalogSelector = true}>
<Search class="h-3 w-3" />
</Button>
</div>
</div>
<div class="col-span-2 grid grid-cols-3 gap-2">
<div class="space-y-2">
<Label class="text-xs">Complemento 1</Label>
<Input bind:value={currentDetail.complement1} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label class="text-xs">Complemento 2</Label>
<Input bind:value={currentDetail.complement2} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label class="text-xs">Complemento 3</Label>
<Input bind:value={currentDetail.complement3} class="h-8 text-xs" />
</div>
</div>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => showModal = false} class="h-8 text-xs">Cancelar</Button>
<Button onclick={saveDetail} class="h-8 text-xs">Guardar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<!-- Modal MEX Assets -->
<Dialog.Root bind:open={showMexModal}>
<Dialog.Content class="sm:max-w-[450px]">
<Dialog.Header>
<Dialog.Title>{isEditing ? 'Editar' : 'Insertar'} Asset Tag (Etiquetado)</Dialog.Title>
<Dialog.Description>
Detalles del etiquetado de activos para Compras Mexicanas.
</Dialog.Description>
</Dialog.Header>
<div class="space-y-4 py-4">
<!-- Info Section -->
<div class="bg-gray-50 dark:bg-zinc-800 p-3 rounded-md grid grid-cols-2 gap-3 border">
<div class="space-y-1">
<Label class="text-[10px] uppercase text-gray-500">Número de Factura</Label>
<p class="text-xs font-bold">{currentMexAsset.import_invoice || '-'}</p>
</div>
<div class="space-y-1">
<Label class="text-[10px] uppercase text-gray-500">Línea de Partida</Label>
<p class="text-xs font-bold">{currentMexAsset.import_line || '-'}</p>
</div>
</div>
<!-- Input Section -->
<div class="space-y-3">
<div class="space-y-2">
<Label class="text-xs">Asset Number</Label>
<Input bind:value={currentMexAsset.number_id} placeholder="Ingrese número de activo..." class="h-9 text-xs" />
</div>
<div class="space-y-2">
<Label class="text-xs">Imagen del Archivo</Label>
<div class="flex items-center gap-2">
<div class="flex-1 h-9 px-3 py-2 border rounded-md bg-white dark:bg-zinc-900 text-xs truncate italic text-gray-500">
{currentMexAsset.image_path || 'Ningún archivo seleccionado'}
</div>
<Button size="sm" variant="secondary" class="h-9 gap-1 text-xs" onclick={triggerFileSelect}>
<Upload class="h-3 w-3" />
Subir
</Button>
</div>
</div>
</div>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => showMexModal = false} class="h-8 text-xs">Cancelar</Button>
<Button onclick={saveMexAsset} class="h-8 text-xs">Guardar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<IdentifierCatalogSelector
bind:open={showCatalogSelector}
onSelect={handleCatalogSelect}
/>

View File

@@ -1,32 +1,256 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import type { LineDescriptions } from '$lib/api/dashboard/a76/items';
import { Button } from '$lib/components/ui/button';
import * as Table from '$lib/components/ui/table';
import { Plus, Pencil, Trash2, Folder } from 'lucide-svelte';
import type { Item, LineDescriptions, Serie } from '$lib/api/dashboard/a76/items';
import ValuationMethodSelector from './valuation-method-selector.svelte';
let { descriptions = $bindable() }: { descriptions: LineDescriptions } = $props();
let {
lineItem = $bindable(),
descriptions = $bindable(),
visibility
}: {
lineItem: Partial<Item>,
descriptions: LineDescriptions,
visibility: any
} = $props();
// Ensure series is an array
if (!lineItem.series) {
lineItem.series = [];
}
let valuationSelectorOpen = $state(false);
// Asset management state
let selectedAssetIndex = $state<number | null>(null);
let editingAsset = $state<Serie>({
row: 0,
number_id: '',
import_invoice: '',
import_line: undefined
});
function handleValuationMethodSelect(method: { key: string }) {
lineItem.valuation_method = method.key;
}
function addAsset() {
const newAsset: Serie = {
row: (lineItem.series?.length || 0) + 1,
number_id: '',
import_invoice: '',
import_line: undefined
};
lineItem.series = [...(lineItem.series || []), newAsset];
editAsset((lineItem.series || []).length - 1);
}
function editAsset(index: number) {
selectedAssetIndex = index;
editingAsset = { ...(lineItem.series?.[index] as Serie) };
}
function saveAsset() {
if (selectedAssetIndex !== null && lineItem.series) {
const updatedSeries = [...lineItem.series];
updatedSeries[selectedAssetIndex] = { ...editingAsset };
lineItem.series = updatedSeries;
selectedAssetIndex = null;
}
}
function deleteAsset(index: number) {
if (lineItem.series) {
lineItem.series = lineItem.series.filter((_, i) => i !== index);
// Re-index rows
lineItem.series = lineItem.series.map((s, i) => ({ ...s, row: i + 1 }));
if (selectedAssetIndex === index) {
selectedAssetIndex = null;
} else if (selectedAssetIndex !== null && selectedAssetIndex > index) {
selectedAssetIndex--;
}
}
}
function cancelAssetEdit() {
selectedAssetIndex = null;
}
</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" bind:value={descriptions.lot} 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" bind:value={descriptions.entry_number} class="h-8 text-sm" />
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<!-- Left Side: Labeling & Valuation -->
<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 uppercase">Labeling & Valuation</legend>
{#if visibility.showLabelingStandard}
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label for="numero_etiqueta" class="text-xs">Label Number:</Label>
<Input id="numero_etiqueta" bind:value={descriptions.lot} class="h-7 text-xs" />
</div>
<div class="space-y-1">
<Label for="tipo_etiqueta" class="text-xs">Label Type:</Label>
<Input id="tipo_etiqueta" bind:value={descriptions.entry_number} class="h-7 text-xs" />
</div>
</div>
{/if}
<div class="space-y-2">
<Label for="observaciones_etiqueta" class="text-xs">Observations:</Label>
<textarea
id="observaciones_etiqueta"
bind:value={descriptions.additional_info_spanish}
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>
<div class="grid grid-cols-2 gap-2">
{#if visibility.showLabelingEnhanced}
{#if visibility.showLabelingQuantity}
<div class="space-y-1">
<Label for="cantidad_importar" class="text-xs">Cantidad a importar:</Label>
<Input
id="cantidad_importar"
type="number"
bind:value={lineItem.quantity!.quantity}
class="h-7 text-xs"
/>
</div>
{/if}
<div class="space-y-1">
<Label for="valor_det" class="text-xs">Valor Det:</Label>
<Input
id="valor_det"
type="number"
step="0.00000001"
bind:value={lineItem.valuation_determined_value}
class="h-7 text-xs"
/>
</div>
{/if}
</div>
{#if visibility.showLabelingEnhanced}
<div class="space-y-1">
<Label for="metodos_valoracion" class="text-xs">Métodos de valoración:</Label>
<div class="flex gap-1">
<Input
id="metodos_valoracion"
bind:value={lineItem.valuation_method}
class="h-7 text-xs flex-1"
placeholder="Seleccione..."
/>
<Button
size="icon"
variant="outline"
class="h-7 w-7"
onclick={() => valuationSelectorOpen = true}
>
<Folder class="h-3 w-3" />
</Button>
</div>
</div>
{#if visibility.showUsageReason}
<div class="space-y-1">
<Label for="motivo_uso" class="text-xs">Motivo de uso:</Label>
<Input
id="motivo_uso"
bind:value={lineItem.valuation_reason}
class="h-7 text-xs"
placeholder="Especifique motivo..."
/>
</div>
{/if}
{/if}
{#if visibility.showLabelingStandard}
<div class="space-y-1">
<Label for="observaciones_etiqueta" class="text-xs">Observations:</Label>
<textarea
id="observaciones_etiqueta"
bind:value={descriptions.additional_info_spanish}
class="flex min-h-[60px] w-full rounded-md border border-input bg-background px-3 py-1.5 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"
placeholder="Labeling observations..."
></textarea>
</div>
{/if}
</fieldset>
{#if visibility.showLabelingEnhanced}
<!-- Right Side: Assets Table -->
<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 uppercase">Assets / Series</legend>
<div class="flex justify-end">
<Button size="sm" variant="outline" class="h-7 text-xs px-2" onclick={addAsset}>
<Plus class="h-3 h-3 mr-1" /> Insertar Activo
</Button>
</div>
<div class="rounded-md border overflow-hidden max-h-[180px] overflow-y-auto">
<Table.Root>
<Table.Header>
<Table.Row class="bg-muted/50 h-7">
<Table.Head class="w-8 text-center text-[10px] p-0 px-1">#</Table.Head>
<Table.Head class="text-[10px] p-0 px-1">Asset Num</Table.Head>
<Table.Head class="text-[10px] p-0 px-1">Factura</Table.Head>
<Table.Head class="text-[10px] p-0 px-1">Línea</Table.Head>
<Table.Head class="w-14 text-right text-[10px] p-0 px-2">Acc</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each lineItem.series || [] as asset, i}
<Table.Row class="hover:bg-muted/50 h-7">
<Table.Cell class="text-center text-[10px] p-0 px-1 font-medium">{asset.row || i+1}</Table.Cell>
<Table.Cell class="text-[10px] p-0 px-1 truncate max-w-[60px]">{asset.number_id || '-'}</Table.Cell>
<Table.Cell class="text-[10px] p-0 px-1 truncate max-w-[60px]">{asset.import_invoice || '-'}</Table.Cell>
<Table.Cell class="text-[10px] p-0 px-1">{asset.import_line || '-'}</Table.Cell>
<Table.Cell class="text-right p-0 px-2">
<div class="flex justify-end gap-0.5">
<Button variant="ghost" size="icon" class="h-5 w-5" onclick={() => editAsset(i)}>
<Pencil class="h-2.5 w-2.5" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5 text-destructive" onclick={() => deleteAsset(i)}>
<Trash2 class="h-2.5 w-2.5" />
</Button>
</div>
</Table.Cell>
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={5} class="h-12 text-center text-muted-foreground text-[10px]">
No hay activos.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
{#if selectedAssetIndex !== null}
<div class="p-2 border rounded bg-muted/20 space-y-2">
<div class="text-[10px] font-semibold uppercase">Editar #{editingAsset.row}</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-0.5">
<Label class="text-[10px]">Asset Number</Label>
<Input bind:value={editingAsset.number_id} class="h-6 text-[10px]" />
</div>
<div class="space-y-0.5">
<Label class="text-[10px]">Factura Impo</Label>
<Input bind:value={editingAsset.import_invoice} class="h-6 text-[10px]" />
</div>
<div class="space-y-0.5">
<Label class="text-[10px]">Linea Impo</Label>
<Input type="number" bind:value={editingAsset.import_line} class="h-6 text-[10px]" />
</div>
</div>
<div class="flex justify-end gap-1.5 pt-1">
<Button size="sm" variant="ghost" class="h-6 text-[10px] px-2" onclick={cancelAssetEdit}>Can</Button>
<Button size="sm" class="h-6 text-[10px] px-2" onclick={saveAsset}>Guar</Button>
</div>
</div>
{/if}
</fieldset>
{/if}
</div>
<ValuationMethodSelector
bind:open={valuationSelectorOpen}
onSelect={handleValuationMethodSelect}
/>

View File

@@ -0,0 +1,108 @@
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog";
import * as Table from "$lib/components/ui/table";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Search, Loader2 } from "lucide-svelte";
import { valuationMethodsApi, type ValuationMethod } from "$lib/api/dashboard/reference_data/valuation_methods";
import { onMount } from "svelte";
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (method: ValuationMethod) => void;
} = $props();
let methods = $state<ValuationMethod[]>([]);
let loading = $state(false);
let searchTerm = $state("");
async function loadMethods() {
loading = true;
try {
const res = await valuationMethodsApi.list(1, 100);
if (res.data) {
methods = res.data.items || [];
}
} catch (error) {
console.error("Error loading valuation methods:", error);
} finally {
loading = false;
}
}
onMount(() => {
loadMethods();
});
const filteredMethods = $derived(
methods.filter(m =>
m.key.toLowerCase().includes(searchTerm.toLowerCase()) ||
m.description.toLowerCase().includes(searchTerm.toLowerCase())
)
);
function handleSelect(method: ValuationMethod) {
onSelect(method);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[500px] h-[600px] flex flex-col p-0">
<Dialog.Header class="px-6 pt-6 pb-4 shrink-0">
<Dialog.Title>Seleccionar Método de Valoración</Dialog.Title>
<Dialog.Description>
Busca y selecciona un método de valoración de la lista.
</Dialog.Description>
</Dialog.Header>
<div class="px-6 pb-4 shrink-0">
<div class="relative">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por clave o descripción..."
class="pl-9 h-9"
bind:value={searchTerm}
/>
</div>
</div>
<div class="flex-1 overflow-y-auto px-6 pb-6">
{#if loading}
<div class="flex h-full items-center justify-center">
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" />
</div>
{:else}
<Table.Root>
<Table.Header class="sticky top-0 bg-background z-10">
<Table.Row>
<Table.Head class="w-20">Clave</Table.Head>
<Table.Head>Descripción</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each filteredMethods as method}
<Table.Row
class="cursor-pointer hover:bg-muted"
onclick={() => handleSelect(method)}
>
<Table.Cell class="font-medium">{method.key}</Table.Cell>
<Table.Cell>{method.description}</Table.Cell>
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={2} class="h-24 text-center text-muted-foreground">
No se encontraron métodos de valoración.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>