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

Reviewed-on: ADUANASOFT/anexo76#56
This commit is contained in:
2026-01-20 18:53:49 +00:00
28 changed files with 1620 additions and 388 deletions

View File

@@ -68,6 +68,12 @@ export interface LineDescriptions {
brand?: string;
model?: string;
has_serial?: boolean;
lot?: string;
entry_number?: string;
eighth_rule_fraction?: string;
eighth_rule_line?: number;
consider_a31?: boolean;
machinery_location?: string;
}
export interface LineReferences {
@@ -125,23 +131,28 @@ export interface LineItem {
identifier?: string;
// Unit of Measure
unit_of_measure?: string;
alternate_unit?: string;
unit_of_measure?: number;
alternate_unit?: number;
// Permits
permit_number?: string;
page_line?: string;
has_certificate?: boolean;
certificate_number?: string;
octave_permit?: string;
// Flags
is_subitem?: boolean;
includes_subitems?: boolean;
tax_payment?: boolean;
is_military_mcia?: boolean;
// Payment
payment_method?: string;
igi_amount?: number;
// Additional notes
wildcard_field?: string;
// Computed fields from class_info relation
class_code?: string;

View File

@@ -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>

View File

@@ -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">
&lt;&lt;
</button>
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
&lt;
</button>
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
&gt;
</button>
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
&gt;&gt;
</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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">
&lt;&lt;
</button>
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
&lt;
</button>
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
&gt;
</button>
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
&gt;&gt;
</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>

View File

@@ -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

View File

@@ -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,
};
}

View File

@@ -0,0 +1,91 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
// Obtener company_id de la cookie
const companyId = cookies.get('active_company_id');
if (!companyId) {
return new Response(
JSON.stringify({ error: 'No company selected' }),
{
status: 400,
headers: {
'Content-Type': 'application/json'
}
}
);
}
// Configurar la URL de la API usando las variables de entorno
let apiUrl = process.env.INTERNAL_API_URL;
if (!apiUrl) {
apiUrl = process.env.VITE_API_URL;
// Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR)
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
}
// Normalizar la URL
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
// Get query parameters and add company_id
const searchParams = new URLSearchParams(url.search);
searchParams.set('company_id', companyId);
const queryString = searchParams.toString();
try {
const fetchUrl = `${baseUrl}v1/a76/classes?${queryString}`;
console.log('Fetching classes from:', fetchUrl);
const response = await fetch(
fetchUrl,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
}
);
if (!response.ok) {
const errorText = await response.text();
console.error('Error response from backend:', errorText);
return new Response(
JSON.stringify({
error: 'Failed to fetch classes',
details: errorText
}),
{
status: response.status,
headers: {
'Content-Type': 'application/json'
}
}
);
}
const data = await response.json();
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Error in classes API route:', error);
return new Response(
JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
};

View File

@@ -0,0 +1,68 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
// Configurar la URL de la API usando las variables de entorno
let apiUrl = process.env.INTERNAL_API_URL;
if (!apiUrl) {
apiUrl = process.env.VITE_API_URL;
// Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR)
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
}
// Normalizar la URL
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
// Get query parameters - tariff_fractions es catálogo global (no requiere company_id)
const searchParams = new URLSearchParams(url.search);
const queryString = searchParams.toString();
try {
const fetchUrl = `${baseUrl}v1/a76/tariff-fractions?${queryString}`;
console.log('Fetching tariff fractions from:', fetchUrl);
console.log('Token:', token ? 'Present' : 'Missing');
const response = await fetch(
fetchUrl,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
}
);
const data = await response.json();
console.log('Response status:', response.status);
console.log('Response data:', JSON.stringify(data).substring(0, 200));
if (!response.ok) {
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
'Content-Type': 'application/json'
}
});
}
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Error fetching tariff fractions:', error);
return new Response(
JSON.stringify({ error: 'Failed to fetch tariff fractions' }),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
};

View File

@@ -0,0 +1,84 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ cookies, url }) => {
const token = cookies.get('access_token');
// Obtener company_id de la cookie
const companyId = cookies.get('active_company_id');
if (!companyId) {
return new Response(
JSON.stringify({ error: 'No company selected' }),
{
status: 400,
headers: {
'Content-Type': 'application/json'
}
}
);
}
// Configurar la URL de la API usando las variables de entorno
let apiUrl = process.env.INTERNAL_API_URL;
if (!apiUrl) {
apiUrl = process.env.VITE_API_URL;
// Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR)
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
}
// Normalizar la URL
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
// Get query parameters and add company_id
const searchParams = new URLSearchParams(url.search);
searchParams.set('company_id', companyId);
const queryString = searchParams.toString();
try {
const fetchUrl = `${baseUrl}v1/a76/units-of-measure?${queryString}`;
console.log('Fetching units from:', fetchUrl);
console.log('Token:', token ? 'Present' : 'Missing');
const response = await fetch(
fetchUrl,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
}
);
const data = await response.json();
console.log('Response status:', response.status);
console.log('Response data:', JSON.stringify(data).substring(0, 200));
if (!response.ok) {
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
'Content-Type': 'application/json'
}
});
}
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Error fetching units of measure:', error);
return new Response(
JSON.stringify({ error: 'Failed to fetch units of measure' }),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
};