Merge pull request 'feature/items' (#56) from feature/items into development
Reviewed-on: ADUANASOFT/anexo76#56
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Search, Loader2 } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: {
|
||||
open?: boolean;
|
||||
onSelect?: (classItem: any) => void;
|
||||
} = $props();
|
||||
|
||||
let searchQuery = $state('');
|
||||
let isSearching = $state(false);
|
||||
let classes = $state<any[]>([]);
|
||||
let displayedClasses = $state<any[]>([]);
|
||||
let currentPage = $state(1);
|
||||
let itemsPerPage = 10;
|
||||
|
||||
const filteredClasses = $derived(
|
||||
searchQuery
|
||||
? classes.filter(c =>
|
||||
c.class_code?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
c.description_es?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: classes
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
searchClasses();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
currentPage = 1;
|
||||
loadMoreClasses();
|
||||
});
|
||||
|
||||
async function searchClasses() {
|
||||
const activeCompanyId = companyStore?.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
toast.error('No hay compañía activa');
|
||||
return;
|
||||
}
|
||||
|
||||
isSearching = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api-sveltekit/classes?company_id=${activeCompanyId}&limit=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al buscar clases');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
classes = data.data?.items || [];
|
||||
loadMoreClasses();
|
||||
} catch (error) {
|
||||
console.error('Error searching classes:', error);
|
||||
toast.error('Error al buscar clases');
|
||||
classes = [];
|
||||
} finally {
|
||||
isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadMoreClasses() {
|
||||
const start = 0;
|
||||
const end = currentPage * itemsPerPage;
|
||||
displayedClasses = filteredClasses.slice(start, end);
|
||||
}
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
const target = e.target as HTMLDivElement;
|
||||
const threshold = 100;
|
||||
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
||||
|
||||
if (scrolledToBottom && displayedClasses.length < filteredClasses.length) {
|
||||
currentPage++;
|
||||
loadMoreClasses();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(classItem: any) {
|
||||
if (onSelect) {
|
||||
onSelect(classItem);
|
||||
}
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-4xl max-h-[80vh] flex flex-col">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Clase</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Busca y selecciona una clase para la partida
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="flex gap-2 mb-4">
|
||||
<div class="relative flex-1">
|
||||
<Search class="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar por código o descripción..."
|
||||
bind:value={searchQuery}
|
||||
class="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto border rounded-md" onscroll={handleScroll}>
|
||||
{#if isSearching}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<Loader2 class="w-6 h-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-[120px]">Código</Table.Head>
|
||||
<Table.Head>Descripción</Table.Head>
|
||||
<Table.Head class="w-[100px]">U.M.</Table.Head>
|
||||
<Table.Head class="w-[100px]"></Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if displayedClasses.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={4} class="text-center py-8 text-muted-foreground">
|
||||
No se encontraron clases
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each displayedClasses as classItem}
|
||||
<Table.Row class="cursor-pointer hover:bg-muted/50" onclick={() => handleSelect(classItem)}>
|
||||
<Table.Cell class="font-medium">{classItem.class_code}</Table.Cell>
|
||||
<Table.Cell>{classItem.description_es || classItem.description_en || '-'}</Table.Cell>
|
||||
<Table.Cell>{classItem.unit_of_measure || '-'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button size="sm" variant="ghost" onclick={() => handleSelect(classItem)}>
|
||||
Seleccionar
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,178 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (country: any) => void;
|
||||
} = $props();
|
||||
|
||||
let countries: any[] = $state([]);
|
||||
let filteredCountries: any[] = $state([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
async function loadCountries() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await fetch('/api-sveltekit/countries', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Countries data received:', data);
|
||||
console.log('First country sample:', data[0]);
|
||||
if (Array.isArray(data)) {
|
||||
countries = data;
|
||||
} else if (data.items && Array.isArray(data.items)) {
|
||||
countries = data.items;
|
||||
} else {
|
||||
console.error('Unexpected data format:', data);
|
||||
countries = [];
|
||||
}
|
||||
filteredCountries = countries;
|
||||
console.log('Total countries loaded:', countries.length);
|
||||
} else {
|
||||
error = `Error: ${response.status} - ${response.statusText}`;
|
||||
console.error('Error response:', await response.text());
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error loading countries';
|
||||
console.error('Error loading countries:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function filterCountries() {
|
||||
if (!searchTerm.trim()) {
|
||||
filteredCountries = countries;
|
||||
} else {
|
||||
const term = searchTerm.toLowerCase();
|
||||
filteredCountries = countries.filter(
|
||||
(country) =>
|
||||
country.m3_key?.toLowerCase().includes(term) ||
|
||||
country.mex_key?.toLowerCase().includes(term) ||
|
||||
country.description_es?.toLowerCase().includes(term) ||
|
||||
country.description_en?.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(country: any) {
|
||||
onSelect(country);
|
||||
open = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
loadCountries();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
filterCountries();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="!max-w-[70vw] w-[70vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">CATALOGO DE PAISES</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto px-6 py-4">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader2 class="w-8 h-8 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex items-center justify-center py-20 text-red-600">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="border rounded-md overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Clave M3</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Clave Mexicana</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Descripción Español</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Clave Americana</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold">Descripción Inglés</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredCountries as country, i}
|
||||
<tr
|
||||
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
onclick={() => handleSelect(country)}
|
||||
>
|
||||
<td class="px-3 py-2 border-r">{country.m3_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.mex_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.description_es || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{country.ame_key || ''}</td>
|
||||
<td class="px-3 py-2">{country.description_en || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredCountries.length === 0}
|
||||
<tr>
|
||||
<td colspan="5" class="px-3 py-8 text-center text-zinc-500">
|
||||
No se encontraron resultados
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<<
|
||||
</button>
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<
|
||||
</button>
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
>
|
||||
</button>
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
>>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-3 border-t bg-zinc-50 dark:bg-zinc-900 flex items-center justify-end gap-2">
|
||||
<Button variant="outline" size="sm">Editar</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -35,7 +35,7 @@
|
||||
<!-- 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>
|
||||
<legend class="text-xs font-semibold px-2 bg-zinc-200 dark:bg-zinc-700">Is</legend>
|
||||
<RadioGroup.Root
|
||||
value={isSubPartidaValue}
|
||||
onValueChange={setIsSubPartida}
|
||||
@@ -52,7 +52,7 @@
|
||||
</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">Contains Sub-Items</legend>
|
||||
<legend class="text-xs font-semibold px-2 bg-zinc-200 dark:bg-zinc-700">Contains Sub-Items</legend>
|
||||
<RadioGroup.Root
|
||||
value={containsSubPartidasValue}
|
||||
onValueChange={setContinueSubPartidas}
|
||||
@@ -72,10 +72,11 @@
|
||||
<!-- 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>
|
||||
<Label for="num_parte" class="text-xs">Part Number ID (opcional):</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="num_parte" bind:value={lineItem.part_number_id} class="h-7 text-xs" />
|
||||
<Input id="num_parte" type="number" bind:value={lineItem.part_number_id} class="h-7 text-xs" placeholder="Dejar vacío si no aplica" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">ID de número de parte existente en catálogo</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
|
||||
@@ -1,155 +1,183 @@
|
||||
<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';
|
||||
import * as Sheet from '$lib/components/ui/sheet';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { Loader2, Package, Save, X, FileText } from 'lucide-svelte';
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import type { Item } from '$lib/api/dashboard/a76/items';
|
||||
|
||||
// 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 {
|
||||
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');
|
||||
// Acceso directo a la primera línea para evitar repeticiones en el HTML
|
||||
let line = $derived(editingItem.lines?.[0]);
|
||||
</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: currentline
|
||||
</Sheet.Description>
|
||||
</Sheet.Header>
|
||||
<Sheet.Content side="right" class="w-full sm:max-w-[95vw] lg:max-w-[85vw] xl:max-w-[75vw] p-0 flex flex-col h-full bg-slate-50 dark:bg-black">
|
||||
|
||||
<header class="bg-white dark:bg-zinc-950 border-b dark:border-zinc-800 px-3 py-1.5 shadow-sm shrink-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-zinc-900 p-1 rounded">
|
||||
<Package class="w-3.5 h-3.5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<Sheet.Title class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 leading-tight">
|
||||
{isEditMode ? 'Editar Partida' : 'Nueva Partida - Activo Fijo'}
|
||||
</Sheet.Title>
|
||||
<p class="text-[10px] text-muted-foreground flex items-center gap-1.5">
|
||||
Factura: <span class="font-medium text-zinc-700 dark:text-zinc-300">{invoice?.invoice_number || 'N/A'}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onclick={() => open = false} class="h-7 w-7 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<X class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</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">
|
||||
{#if editingItem.lines && editingItem.lines.length > 0}
|
||||
<MainData
|
||||
bind:lineItem={editingItem.lines[0]}
|
||||
bind:quantities={editingItem.lines[0].quantity!}
|
||||
bind:financials={editingItem.lines[0].financial!}
|
||||
bind:customs={editingItem.lines[0].customs!}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-2 py-1.5">
|
||||
<div class="space-y-2">
|
||||
|
||||
{#if line}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-2">
|
||||
<div class="lg:col-span-8">
|
||||
<div class="bg-white dark:bg-zinc-900 rounded border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
||||
<div class="bg-zinc-50 dark:bg-zinc-800/50 px-2 py-1 border-b border-zinc-200 dark:border-zinc-800">
|
||||
<h3 class="text-[10px] font-semibold text-zinc-500 uppercase tracking-wide">Datos Principales</h3>
|
||||
</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!}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column (5 columns) -->
|
||||
{#if editingItem.lines && editingItem.lines.length > 0}
|
||||
<ItemConfiguration
|
||||
bind:lineItem={editingItem.lines[0]}
|
||||
bind:descriptions={editingItem.lines[0].description!}
|
||||
/>
|
||||
<div class="lg:col-span-4">
|
||||
<div class="bg-white dark:bg-zinc-900 rounded border border-zinc-200 dark:border-zinc-800 h-full overflow-hidden">
|
||||
<div class="bg-zinc-50 dark:bg-zinc-800/50 px-2 py-1 border-b border-zinc-200 dark:border-zinc-800">
|
||||
<h3 class="text-[10px] font-semibold text-zinc-500 uppercase tracking-wide">Configuración</h3>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<ItemConfiguration
|
||||
bind:lineItem={editingItem.lines![0]}
|
||||
bind:descriptions={editingItem.lines![0].description!}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs.Root value="generales" class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-5 bg-zinc-100 dark:bg-zinc-800/50 rounded p-0.5 gap-0.5">
|
||||
<Tabs.Trigger value="generales" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
|
||||
General
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="continuacion" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
|
||||
Continuación
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="series" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
|
||||
Series
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="etiquetado" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
|
||||
Etiquetado
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="identificadores" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
|
||||
IDs
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<div class="mt-1.5">
|
||||
<Tabs.Content value="generales" class="m-0">
|
||||
<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!}
|
||||
/>
|
||||
<SummarySection
|
||||
bind:financials={editingItem.lines![0].financial!}
|
||||
bind:quantities={editingItem.lines![0].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!}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="series" class="m-0 focus-visible:outline-none">
|
||||
<TabSeries bind:descriptions={editingItem.lines![0].description!} />
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="etiquetado" class="m-0 focus-visible:outline-none">
|
||||
<TabLabeling bind:descriptions={editingItem.lines![0].description!} />
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="identificadores" class="m-0 focus-visible:outline-none">
|
||||
<TabIdentifiers bind:lineItem={editingItem.lines![0]} />
|
||||
</Tabs.Content>
|
||||
</div>
|
||||
</Tabs.Root>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||
<Loader2 class="w-8 h-8 animate-spin mb-4 text-zinc-900 dark:text-zinc-100" />
|
||||
<p class="text-sm">Cargando datos de la partida...</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
{#if editingItem.lines && editingItem.lines.length > 0}
|
||||
<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!}
|
||||
/>
|
||||
<SummarySection
|
||||
bind:financials={editingItem.lines[0].financial!}
|
||||
bind:quantities={editingItem.lines[0].quantity!}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Continuation -->
|
||||
<Tabs.Content value="continuacion" class="space-y-3 mt-0">
|
||||
{#if editingItem.lines && editingItem.lines.length > 0}
|
||||
<TabContinuation bind:lineItem={editingItem.lines[0]} />
|
||||
<footer class="bg-white dark:bg-zinc-950 border-t border-zinc-200 dark:border-zinc-800 px-3 py-1.5 shadow-sm shrink-0">
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<Button variant="outline" size="sm" onclick={() => open = false} disabled={isSaving} class="h-7 text-xs px-2">
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button size="sm" onclick={onSave} disabled={isSaving} class="h-7 text-xs px-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white">
|
||||
{#if isSaving}
|
||||
<Loader2 class="w-3 h-3 mr-1 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
<Save class="w-3 h-3 mr-1" />
|
||||
{isEditMode ? 'Actualizar' : 'Crear'}
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
</Button>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Tab: Series -->
|
||||
<Tabs.Content value="series" class="space-y-3 mt-0">
|
||||
{#if editingItem.lines && editingItem.lines.length > 0}
|
||||
<TabSeries bind:descriptions={editingItem.lines[0].description!} />
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Labeling -->
|
||||
<Tabs.Content value="etiquetado" class="space-y-3 mt-0">
|
||||
{#if editingItem.lines && editingItem.lines.length > 0}
|
||||
<TabLabeling bind:descriptions={editingItem.lines[0].description!} />
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Identifiers -->
|
||||
<Tabs.Content value="identificadores" class="space-y-3 mt-0">
|
||||
{#if editingItem.lines && editingItem.lines.length > 0}
|
||||
<TabIdentifiers bind:lineItem={editingItem.lines[0]} />
|
||||
{/if}
|
||||
</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.Content>
|
||||
</Sheet.Root>
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Folder } from 'lucide-svelte';
|
||||
import UnitOfMeasureDialog from './unit-of-measure-dialog.svelte';
|
||||
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';
|
||||
|
||||
let {
|
||||
@@ -14,73 +20,150 @@
|
||||
financials: LineFinancials;
|
||||
customs: LineCustoms;
|
||||
} = $props();
|
||||
|
||||
let showClassDialog = $state(false);
|
||||
let showUnitDialog = $state(false);
|
||||
let showCountryDialog = $state(false);
|
||||
let showFractionDialog = $state(false);
|
||||
|
||||
// Initialize from existing data
|
||||
$effect(() => {
|
||||
if (lineItem.class_code) {
|
||||
// Do nothing, it's already set
|
||||
}
|
||||
});
|
||||
|
||||
function handleClassSelect(classItem: any) {
|
||||
lineItem.class_id = classItem.id;
|
||||
// Store the code in the lineItem for display
|
||||
(lineItem as any).class_code = classItem.class_code;
|
||||
}
|
||||
|
||||
function handleUnitSelect(unit: any) {
|
||||
lineItem.unit_of_measure = unit.id;
|
||||
}
|
||||
|
||||
function handleCountrySelect(country: any) {
|
||||
customs.origin_country = country.mex_key || country.m3_key;
|
||||
}
|
||||
|
||||
function handleFractionSelect(fraction: any) {
|
||||
customs.fraction = fraction.fraction;
|
||||
}
|
||||
</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" bind:value={lineItem.class_id} class="h-8 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ClassDialog bind:open={showClassDialog} onSelect={handleClassSelect} />
|
||||
<UnitOfMeasureDialog bind:open={showUnitDialog} onSelect={handleUnitSelect} />
|
||||
<CountryDialog bind:open={showCountryDialog} onSelect={handleCountrySelect} />
|
||||
<TariffFractionDialog bind:open={showFractionDialog} onSelect={handleFractionSelect} />
|
||||
|
||||
<!-- Quantity -->
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<div class="col-span-4 space-y-1">
|
||||
<fieldset class="border rounded-md p-3">
|
||||
<legend class="text-xs font-semibold px-2 bg-zinc-200 dark:bg-zinc-700">Main Data</legend>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Class - Full Width -->
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label for="clase" class="text-xs font-medium">* Clase (ID):</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input
|
||||
id="clase"
|
||||
type="number"
|
||||
bind:value={lineItem.class_id}
|
||||
class="h-8 text-xs flex-1"
|
||||
placeholder="ID de clase (1-6)"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
onclick={() => (showClassDialog = true)}
|
||||
>
|
||||
<Folder class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if (lineItem as any).class_code}
|
||||
<p class="text-xs text-muted-foreground">Código: {(lineItem as any).class_code}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Quantity and U.M. on the same row -->
|
||||
<div class="space-y-1">
|
||||
<Label for="cantidad" class="text-xs font-medium">* Quantity:</Label>
|
||||
<Input id="cantidad" type="number" step="0.00000001" min="0" bind:value={quantities.quantity} class="h-8 text-xs text-right" />
|
||||
</div>
|
||||
<div class="col-span-4 space-y-1">
|
||||
|
||||
<div class="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">
|
||||
<Input id="um" bind:value={lineItem.unit_of_measure} class="h-8 text-xs" placeholder="U.M." />
|
||||
</div>
|
||||
<Input id="um" type="number" bind:value={lineItem.unit_of_measure} class="h-8 text-xs flex-1" placeholder="ID de U.M." />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
onclick={() => (showUnitDialog = true)}
|
||||
>
|
||||
<Folder class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unit Cost and Fraction -->
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<div class="col-span-4 space-y-1">
|
||||
<!-- Unit Cost and Fraction -->
|
||||
<div class="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" step="0.00000001" min="0" bind:value={financials.unit_cost_usd} class="h-8 text-xs text-right flex-1" />
|
||||
<span class="text-xs text-blue-600 font-semibold">USD</span>
|
||||
<span class="text-xs text-zinc-900 dark:text-zinc-100 font-semibold">USD</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-5 space-y-1">
|
||||
|
||||
<div class="space-y-1">
|
||||
<Label for="fraccion" class="text-xs font-medium">Fraction:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="fraccion" bind:value={customs.fraction} class="h-8 text-xs text-center" />
|
||||
<Input id="fraccion" bind:value={customs.fraction} class="h-8 text-xs text-center flex-1" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
onclick={() => (showFractionDialog = true)}
|
||||
>
|
||||
<Folder class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Origin Country and Tariff Type -->
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<div class="col-span-4 space-y-1">
|
||||
<!-- Origin Country and Tariff Type -->
|
||||
<div class="space-y-1">
|
||||
<Label for="pais_origen" class="text-xs font-medium">* Origin Country:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="pais_origen" bind:value={customs.origin_country} class="h-8 text-xs" />
|
||||
<Input id="pais_origen" bind:value={customs.origin_country} class="h-8 text-xs flex-1" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
onclick={() => (showCountryDialog = true)}
|
||||
>
|
||||
<Folder class="w-4 h-4" />
|
||||
</Button>
|
||||
</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" bind:value={customs.fraction_type} class="flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background">
|
||||
<option value="GENERAL">GENERAL</option>
|
||||
<option value="PREFERENCIAL">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">
|
||||
<Input id="advalorem" bind:value={customs.advalorem} class="h-8 text-xs text-right" placeholder="0.00" />
|
||||
|
||||
<div class="flex items-end gap-6">
|
||||
<div class="space-y-1">
|
||||
<Label for="tipo_tarifa" class="text-xs font-medium">* Tariff Type:</Label>
|
||||
<select id="tipo_tarifa" bind:value={customs.fraction_type} class="flex h-8 w-auto min-w-[140px] rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background">
|
||||
<option value="GENERAL">GENERAL</option>
|
||||
<option value="PROSEC">PROSEC</option>
|
||||
<option value="ALADI">ALADI</option>
|
||||
<option value="TLCS">TLCS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Advalorem -->
|
||||
<div class="flex items-center gap-2 h-8 pb-0.5">
|
||||
<span class="text-xs font-medium text-muted-foreground">Advalorem:</span>
|
||||
<span class="text-xs text-zinc-900 dark:text-zinc-100 font-semibold">
|
||||
{customs.advalorem || '0'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</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>
|
||||
<span class="text-xs text-gray-900 dark:text-gray-100 font-semibold">KILOS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,40 +10,36 @@
|
||||
|
||||
<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">{quantities.quantity_temp_export?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Replacement or Change: <span class="text-blue-600">0.00000000</span></div>
|
||||
<div>Definitive: <span class="text-blue-600">{quantities.quantity_returned?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Returned Values: <span class="text-blue-600">{financials.value_returned_usd?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div class="col-span-2">Returned Values: <span class="text-blue-600">{financials.value_returned_mxn?.toFixed(8) || '0.00000000'}</span></div>
|
||||
</div>
|
||||
<div>Temporary: <span class="text-gray-900 dark:text-gray-100">{quantities.quantity_temp_export?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Replacement or Change: <span class="text-gray-900 dark:text-gray-100">0.00000000</span></div>
|
||||
<div>Definitive: <span class="text-gray-900 dark:text-gray-100">{quantities.quantity_returned?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Returned Values: <span class="text-gray-900 dark:text-gray-100">{financials.value_returned_usd?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div class="col-span-2">Returned Values: <span class="text-gray-900 dark:text-gray-100">{financials.value_returned_mxn?.toFixed(8) || '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-gray-900 dark:text-gray-100">{quantities.net_weight?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">0.00000000</span></div>
|
||||
<div>Whole: <span class="text-gray-900 dark:text-gray-100">{quantities.gross_weight?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">0.00000000</span></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<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">{quantities.net_weight?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div><span class="text-blue-600">0.00000000</span></div>
|
||||
<div>Whole: <span class="text-blue-600">{quantities.gross_weight?.toFixed(8) || '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">{financials.unit_cost_usd?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div><span class="text-blue-600">{financials.unit_cost_mxn?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Value: <span class="text-blue-600">{financials.value_usd?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div><span class="text-blue-600">{financials.value_mxn?.toFixed(8) || '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">{financials.unit_cost_capture?.toFixed(8) || '0.00000000'}</span> <span class="text-blue-600">USD</span></div>
|
||||
<div class="text-xs">Capture Value: <span class="text-blue-600">{financials.value_usd?.toFixed(8) || '0.00000000'}</span> <span class="text-blue-600">USD</span></div>
|
||||
<div class="text-xs">Customs Value: <span class="text-blue-600">{financials.customs_value_usd?.toFixed(8) || '0.00000000'}</span> <span class="text-blue-600">USD</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-gray-900 dark:text-gray-100">{financials.unit_cost_usd?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">{financials.unit_cost_mxn?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Value: <span class="text-gray-900 dark:text-gray-100">{financials.value_usd?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">{financials.value_mxn?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div class="text-xs">Capture Cost: <span class="text-gray-900 dark:text-gray-100">{financials.unit_cost_capture?.toFixed(8) || '0.00000000'}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
|
||||
<div class="text-xs">Capture Value: <span class="text-gray-900 dark:text-gray-100">{financials.value_usd?.toFixed(8) || '0.00000000'}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
|
||||
<div class="text-xs">Customs Value: <span class="text-gray-900 dark:text-gray-100">{financials.customs_value_usd?.toFixed(8) || '0.00000000'}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import type { LineItem } from '$lib/api/dashboard/a76/items';
|
||||
import type { LineItem, LineDescriptions } from '$lib/api/dashboard/a76/items';
|
||||
|
||||
let {
|
||||
lineItem = $bindable()
|
||||
lineItem = $bindable(),
|
||||
descriptions = $bindable()
|
||||
}: {
|
||||
lineItem: LineItem;
|
||||
descriptions: LineDescriptions;
|
||||
} = $props();
|
||||
|
||||
let taxPaidValue = $derived(lineItem.tax_payment ? 'si' : 'no');
|
||||
@@ -22,32 +24,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-2">
|
||||
<!-- 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>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<fieldset class="border rounded p-1.5 space-y-1">
|
||||
<legend class="text-xs font-semibold px-1.5 bg-gray-200 dark:bg-gray-700">TAX PAID</legend>
|
||||
<RadioGroup.Root
|
||||
value={taxPaidValue}
|
||||
onValueChange={setTaxPaid}
|
||||
class="flex gap-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
class="flex gap-2">
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<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">
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<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">
|
||||
<div class="space-y-0.5">
|
||||
<div class="space-y-0.5">
|
||||
<Label for="forma_pago" class="text-xs">Payment Method:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="forma_pago" bind:value={lineItem.payment_method} class="h-7 text-xs" />
|
||||
<Input id="forma_pago" bind:value={lineItem.payment_method} class="h-6 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<Label for="credito_iva" class="text-xs">VAT AND EXCISE TAX CREDITS.</Label>
|
||||
@@ -56,101 +58,102 @@
|
||||
|
||||
<!-- IGI Amount -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="space-y-1">
|
||||
<div class="space-y-0.5">
|
||||
<Label for="monto_igi" class="text-xs">IGI Amount: {lineItem.igi_amount || 0} <span class="text-xs">DOLLARS</span></Label>
|
||||
<Input id="monto_igi" type="number" bind:value={lineItem.igi_amount} class="h-8 text-xs" />
|
||||
<Input id="monto_igi" type="number" bind:value={lineItem.igi_amount} class="h-6 text-xs" />
|
||||
</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>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<fieldset class="border rounded p-1.5 space-y-1">
|
||||
<legend class="text-xs font-semibold px-1.5 bg-gray-200 dark:bg-gray-700">Has Certificate of Origin?</legend>
|
||||
<RadioGroup.Root
|
||||
value={hasCertificateValue}
|
||||
onValueChange={setHasCertificate}
|
||||
class="flex gap-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
class="flex gap-2">
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<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">
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<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">
|
||||
<div class="space-y-0.5 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" />
|
||||
<Input id="num_cert_origen" bind:value={lineItem.certificate_number} class="h-6 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">
|
||||
<div class="space-y-1">
|
||||
<div class="space-y-0.5">
|
||||
<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" />
|
||||
<Input id="localizacion_maquinaria" bind:value={descriptions.machinery_location} class="h-6 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" />
|
||||
<div class="flex items-center space-x-1.5 ">
|
||||
<Checkbox id="equipo_militar" bind:checked={lineItem.is_military_mcia} />
|
||||
<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">
|
||||
<div class="space-y-0.5">
|
||||
<Label for="lote" class="text-xs">Lot:</Label>
|
||||
<Input id="lote" class="h-7 text-xs" />
|
||||
<Input id="lote" bind:value={descriptions.lot} class="h-6 text-xs" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="space-y-0.5">
|
||||
<Label for="num_entrada" class="text-xs">Entry No.:</Label>
|
||||
<Input id="num_entrada" class="h-7 text-xs" />
|
||||
<Input id="num_entrada" bind:value={descriptions.entry_number} class="h-6 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-2">
|
||||
<!-- Permit and Eighth Rule Fraction -->
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-1">
|
||||
<div class="space-y-1">
|
||||
<div class="space-y-0.5">
|
||||
<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" />
|
||||
<Input id="permiso_regla_octava" bind:value={lineItem.octave_permit} class="h-6 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2 items-end">
|
||||
<div class="space-y-1 col-span-3">
|
||||
<div class="space-y-0.5 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" />
|
||||
<Input id="fraccion_regla_octava" bind:value={descriptions.eighth_rule_fraction} class="h-6 text-xs" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="space-y-0.5">
|
||||
<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" />
|
||||
<Input id="linea_regla" type="number" min="0" bind:value={descriptions.eighth_rule_line} class="h-6 text-xs text-right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consider in a31 -->
|
||||
<div class="flex items-center space-x-2 ">
|
||||
<Checkbox id="a31" />
|
||||
<div class="flex items-center space-x-1.5 ">
|
||||
<Checkbox id="a31" bind:checked={descriptions.consider_a31} />
|
||||
<Label for="a31" class="text-xs font-normal">Consider in A31</Label>
|
||||
</div>
|
||||
|
||||
<!-- Extra Description -->
|
||||
<div class="space-y-1">
|
||||
<div class="space-y-0.5">
|
||||
<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"
|
||||
bind:value={descriptions.description_spanish}
|
||||
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>
|
||||
|
||||
@@ -9,34 +9,18 @@
|
||||
<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" bind:value={lineItem.identifier} 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 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-2">
|
||||
<Label for="notas_identificadores" class="text-xs">Notes:</Label>
|
||||
<Label for="notas_identificadores" class="text-xs">Additional Identifiers / 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..."
|
||||
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>
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
<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" />
|
||||
<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" class="h-8 text-sm" />
|
||||
<Input id="tipo_etiqueta" bind:value={descriptions.entry_number} class="h-8 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (fraction: any) => void;
|
||||
} = $props();
|
||||
|
||||
let fractions: any[] = $state([]);
|
||||
let loading = $state(false);
|
||||
let loadingMore = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
// Pagination state
|
||||
let currentPage = $state(1);
|
||||
let totalPages = $state(1);
|
||||
let hasMore = $state(true);
|
||||
const pageSize = 100;
|
||||
|
||||
// Scroll container reference
|
||||
let scrollContainer: HTMLDivElement | null = $state(null);
|
||||
|
||||
async function loadFractions(page: number = 1, append: boolean = false) {
|
||||
if (page === 1) {
|
||||
loading = true;
|
||||
} else {
|
||||
loadingMore = true;
|
||||
}
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...(searchTerm.trim() && { search: searchTerm.trim() })
|
||||
});
|
||||
|
||||
const response = await fetch(`/api-sveltekit/tariff-fractions?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Tariff fractions data received:', data);
|
||||
|
||||
if (data.items && Array.isArray(data.items)) {
|
||||
if (append) {
|
||||
fractions = [...fractions, ...data.items];
|
||||
} else {
|
||||
fractions = data.items;
|
||||
}
|
||||
currentPage = data.page;
|
||||
totalPages = data.pages;
|
||||
hasMore = currentPage < totalPages;
|
||||
console.log(`Loaded page ${currentPage}/${totalPages}, total items: ${fractions.length}`);
|
||||
} else {
|
||||
console.error('Unexpected data format:', data);
|
||||
if (!append) fractions = [];
|
||||
}
|
||||
} else {
|
||||
error = `Error: ${response.status} - ${response.statusText}`;
|
||||
console.error('Error response:', await response.text());
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error loading tariff fractions';
|
||||
console.error('Error loading tariff fractions:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
loadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
if (!scrollContainer || loading || loadingMore || !hasMore) return;
|
||||
|
||||
const target = e.target as HTMLDivElement;
|
||||
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
||||
|
||||
// Load more when within 200px of bottom
|
||||
if (scrollBottom < 200) {
|
||||
loadFractions(currentPage + 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(fraction: any) {
|
||||
onSelect(fraction);
|
||||
open = false;
|
||||
}
|
||||
|
||||
// Effect to load initial data when dialog opens
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
fractions = [];
|
||||
currentPage = 1;
|
||||
hasMore = true;
|
||||
loadFractions(1, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Effect to reload when search changes
|
||||
let searchDebounce: ReturnType<typeof setTimeout>;
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
clearTimeout(searchDebounce);
|
||||
searchDebounce = setTimeout(() => {
|
||||
fractions = [];
|
||||
currentPage = 1;
|
||||
hasMore = true;
|
||||
loadFractions(1, false);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="!max-w-[80vw] w-[80vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">FRACCIONES ARANCELARIAS</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscar por código, fracción, descripción, NICO o UMT..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-500 mt-2">
|
||||
Mostrando {fractions.length} de {currentPage * pageSize} resultados
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
bind:this={scrollContainer}
|
||||
onscroll={handleScroll}
|
||||
class="flex-1 overflow-auto px-6 py-4"
|
||||
>
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader2 class="w-8 h-8 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex items-center justify-center py-20 text-red-600">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="border rounded-md overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white sticky top-0">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Código</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Fracción</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Descripción</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>NICO</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold">UMT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each fractions as fraction, i}
|
||||
<tr
|
||||
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
onclick={() => handleSelect(fraction)}
|
||||
>
|
||||
<td class="px-3 py-2 border-r">{fraction.code || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{fraction.fraction || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{fraction.description || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{fraction.nico || ''}</td>
|
||||
<td class="px-3 py-2 text-center">{fraction.umt || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if fractions.length === 0 && !loading}
|
||||
<tr>
|
||||
<td colspan="5" class="px-3 py-8 text-center text-zinc-500">
|
||||
No se encontraron resultados
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#if loadingMore}
|
||||
<div class="flex items-center justify-center py-4">
|
||||
<Loader2 class="w-6 h-6 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
<span class="ml-2 text-sm text-zinc-600 dark:text-zinc-400">Cargando más...</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !hasMore && fractions.length > 0}
|
||||
<div class="text-center py-4 text-sm text-zinc-500">
|
||||
Todos los resultados cargados
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-3 border-t bg-zinc-50 dark:bg-zinc-900 flex items-center justify-end gap-2">
|
||||
<Button variant="outline" size="sm">Editar</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (unit: any) => void;
|
||||
} = $props();
|
||||
|
||||
let units: any[] = $state([]);
|
||||
let filteredUnits: any[] = $state([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
async function loadUnits() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await fetch('/api-sveltekit/units-of-measure', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Units data received:', data);
|
||||
// El backend puede devolver { items: [...] } o directamente un array
|
||||
if (Array.isArray(data)) {
|
||||
units = data;
|
||||
} else if (data.items && Array.isArray(data.items)) {
|
||||
units = data.items;
|
||||
} else if (data.data && Array.isArray(data.data)) {
|
||||
units = data.data;
|
||||
} else {
|
||||
console.error('Unexpected data format:', data);
|
||||
units = [];
|
||||
}
|
||||
filteredUnits = units;
|
||||
} else {
|
||||
error = `Error: ${response.status} - ${response.statusText}`;
|
||||
console.error('Error response:', await response.text());
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error loading units of measure';
|
||||
console.error('Error loading units of measure:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function filterUnits() {
|
||||
if (!searchTerm.trim()) {
|
||||
filteredUnits = units;
|
||||
} else {
|
||||
const term = searchTerm.toLowerCase();
|
||||
filteredUnits = units.filter(
|
||||
(unit) =>
|
||||
unit.code?.toLowerCase().includes(term) ||
|
||||
unit.description?.toLowerCase().includes(term) ||
|
||||
unit.description_en?.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(unit: any) {
|
||||
onSelect(unit);
|
||||
open = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
loadUnits();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
filterUnits();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="!max-w-[70vw] w-[70vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">CATALOGOS DE UNIDADES DE MEDIDA</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto px-6 py-4">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader2 class="w-8 h-8 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex items-center justify-center py-20 text-red-600">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="border rounded-md overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>U.M.</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Descripción Español</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Abrév. Inglés</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Clave Aduana</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Clave Americana</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold">Clave O.M.A.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredUnits as unit, i}
|
||||
<tr
|
||||
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
onclick={() => handleSelect(unit)}
|
||||
>
|
||||
<td class="px-3 py-2 border-r">{unit.code || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{unit.description || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{unit.description_en || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{unit.customs_code || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{unit.american_code || ''}</td>
|
||||
<td class="px-3 py-2 text-center">{unit.oma_code || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredUnits.length === 0}
|
||||
<tr>
|
||||
<td colspan="6" class="px-3 py-8 text-center text-zinc-500">
|
||||
No se encontraron resultados
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<<
|
||||
</button>
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<
|
||||
</button>
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
>
|
||||
</button>
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
>>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-3 border-t bg-zinc-50 dark:bg-zinc-900 flex items-center justify-end gap-2">
|
||||
<Button variant="outline" size="sm">Editar</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -83,6 +83,8 @@
|
||||
if (response.data) {
|
||||
items = response.data.items || [];
|
||||
currentPage = 1;
|
||||
// Wait for derived state to update before loading items
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
loadMoreItems();
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -150,6 +152,8 @@
|
||||
tax_payment: false,
|
||||
payment_method: undefined,
|
||||
igi_amount: undefined,
|
||||
is_military_mcia: false,
|
||||
wildcard_field: undefined,
|
||||
// Nested relations
|
||||
financial: {
|
||||
unit_cost_usd: undefined,
|
||||
@@ -191,6 +195,10 @@
|
||||
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,
|
||||
@@ -263,18 +271,61 @@
|
||||
showDeleteDialog = true;
|
||||
}
|
||||
|
||||
// Helper function to check if an object has any meaningful values
|
||||
function hasValues(obj: any): boolean {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
return Object.values(obj).some(val =>
|
||||
val !== undefined && val !== null && val !== '' &&
|
||||
!(typeof val === 'object' && !hasValues(val))
|
||||
);
|
||||
}
|
||||
|
||||
// Clean nested data before sending to API
|
||||
function cleanLineData(line: any) {
|
||||
const cleaned: any = { ...line };
|
||||
|
||||
// Helper function to convert to number or undefined
|
||||
const toNumberOrUndefined = (value: any): number | undefined => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
const numValue = Number(value);
|
||||
return (!isNaN(numValue) && isFinite(numValue)) ? numValue : undefined;
|
||||
};
|
||||
|
||||
// Convert integer fields
|
||||
cleaned.part_number_id = toNumberOrUndefined(cleaned.part_number_id);
|
||||
cleaned.component_part_number_id = toNumberOrUndefined(cleaned.component_part_number_id);
|
||||
cleaned.class_id = toNumberOrUndefined(cleaned.class_id);
|
||||
cleaned.unit_of_measure = toNumberOrUndefined(cleaned.unit_of_measure);
|
||||
cleaned.alternate_unit = toNumberOrUndefined(cleaned.alternate_unit);
|
||||
|
||||
// Remove empty nested objects
|
||||
if (!hasValues(cleaned.financial)) delete cleaned.financial;
|
||||
if (!hasValues(cleaned.quantity)) delete cleaned.quantity;
|
||||
if (!hasValues(cleaned.customs)) delete cleaned.customs;
|
||||
if (!hasValues(cleaned.description)) delete cleaned.description;
|
||||
if (!hasValues(cleaned.reference)) delete cleaned.reference;
|
||||
if (!hasValues(cleaned.fa_data)) delete cleaned.fa_data;
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
async function saveNewItem() {
|
||||
if (!invoice?.id || !activeCompanyId) return;
|
||||
|
||||
isSaving = true;
|
||||
try {
|
||||
// Clean lines data before sending
|
||||
const cleanedLines = (editingItem.lines || []).map(cleanLineData);
|
||||
|
||||
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: editingItem.lines || []
|
||||
lines: cleanedLines
|
||||
});
|
||||
|
||||
// Recargar items
|
||||
@@ -300,12 +351,15 @@
|
||||
|
||||
isSaving = true;
|
||||
try {
|
||||
// Clean lines data before sending
|
||||
const cleanedLines = (editingItem.lines || []).map(cleanLineData);
|
||||
|
||||
await itemsApi.update(selectedItem.id, activeCompanyId, {
|
||||
reference_number: editingItem.reference_number,
|
||||
order: editingItem.order,
|
||||
warehouse: editingItem.warehouse,
|
||||
location: editingItem.location,
|
||||
lines: editingItem.lines || []
|
||||
lines: cleanedLines
|
||||
});
|
||||
|
||||
// Recargar items
|
||||
|
||||
@@ -157,7 +157,7 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
|
||||
}
|
||||
|
||||
// Logistics
|
||||
payload.logistics = buildLogisticsData(generalFormData, observationFormData);
|
||||
payload.logistics = buildLogisticsData(generalFormData, observationFormData, continuationFormData);
|
||||
|
||||
// Eliminar campos undefined para no enviarlos
|
||||
Object.keys(payload).forEach(key => {
|
||||
@@ -219,13 +219,30 @@ function buildFinancialsData(generalFormData: any, observationFormData: any, oth
|
||||
};
|
||||
}
|
||||
|
||||
function buildLogisticsData(generalFormData: any, observationFormData: any) {
|
||||
// Retornar logistics como objeto único
|
||||
function buildLogisticsData(generalFormData: any, observationFormData: any, continuationFormData: any) {
|
||||
// Retornar logistics como objeto único con datos de continuación
|
||||
return {
|
||||
carrier_id: generalFormData?.carrier_id || null,
|
||||
transport_type: generalFormData?.transport_type || 'none',
|
||||
driver_name: generalFormData?.driver_name || null,
|
||||
vehicle_num: generalFormData?.transport_num || null,
|
||||
vehicle_num: generalFormData?.transport_num || continuationFormData?.numero_tipo_transporte || null,
|
||||
incoterm: observationFormData?.incoterm || null,
|
||||
// Campos de continuación mapeados a logistics
|
||||
transport_num: continuationFormData?.numero_tipo_transporte || null,
|
||||
is_rail: continuationFormData?.es_ferrocarril === 'si' ? true : false,
|
||||
bill_number: continuationFormData?.numero_bl || null,
|
||||
guide_number: continuationFormData?.cantidad_guias_embarque ? String(continuationFormData.cantidad_guias_embarque) : null,
|
||||
destination_location: continuationFormData?.destino_origen || null,
|
||||
origin_location: continuationFormData?.puerto_entrada || null,
|
||||
// Checkboxes de continuación
|
||||
equipment_reviewed: continuationFormData?.fue_revisado_equipo || false,
|
||||
is_subdivision: continuationFormData?.sub_division || false,
|
||||
acts_as_cd: continuationFormData?.funge_como_cd || false,
|
||||
pedimento_arrived: continuationFormData?.llego_pedimento || false,
|
||||
// Semáforos de continuación
|
||||
green_light_mx: continuationFormData?.semaforo_verde_aduana_mexicana || false,
|
||||
green_light_us: continuationFormData?.semaforo_verde_aduana_americana || false,
|
||||
red_light_mx: continuationFormData?.semaforo_rojo_aduana_mexicana || false,
|
||||
red_light_us: continuationFormData?.semaforo_rojo_aduana_americana || false,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user