feat: Enhance invoice editing components with new dialogs and improved UI

- Added country selection dialog with search functionality.
- Introduced tariff fraction selection dialog with infinite scrolling and search.
- Implemented unit of measure selection dialog with search capabilities.
- Updated invoice editing UI to include new fields for line descriptions and identifiers.
- Improved layout and spacing for better user experience in invoice editing forms.
- Added API endpoints for fetching tariff fractions and units of measure with proper error handling.
This commit is contained in:
Galindo97
2026-01-13 14:09:22 -06:00
parent 802ab8dc01
commit 98c7fe3bb5
18 changed files with 1163 additions and 353 deletions

View File

@@ -38,11 +38,9 @@ class TariffFractionUpdateDTO(BaseModel):
class TariffFractionResponseDTO(BaseModel):
"""DTO para respuesta de fracción arancelaria"""
"""DTO para respuesta de fracción arancelaria (catálogo global)"""
id: int
tenant_id: int
company_id: int
code: str
fraction: str
description: Optional[str] = None
@@ -50,8 +48,6 @@ class TariffFractionResponseDTO(BaseModel):
umt: Optional[str] = None
adv_impo: Optional[str] = None
adv_expo: Optional[str] = None
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)

View File

@@ -4,15 +4,15 @@ Modelos ORM para fracciones arancelarias (SITAR-SCAII)
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import Integer, PrimaryKeyConstraint, String, Numeric
from sqlalchemy.orm import Mapped, mapped_column
class TariffFraction(Base, TenantScopedMixin, TimestampMixin):
class TariffFraction(Base):
"""
Modelo para fracciones arancelarias mexicanas (SITAR-SCAII)
Catálogo de referencia global (no tenant-scoped)
Corresponde a la tabla sFracciones
"""
@@ -22,10 +22,10 @@ class TariffFraction(Base, TenantScopedMixin, TimestampMixin):
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# Código completo de la fracción (ej: 01012101)
code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
code: Mapped[str] = mapped_column(String(10), unique=True, index=True, nullable=False)
# Fracción formateada (ej: 0101.21.01)
fraction: Mapped[str] = mapped_column(String(15), index=True)

View File

@@ -1,5 +1,6 @@
"""
Endpoints API para fracciones arancelarias
Catálogo de referencia global (no tenant-scoped)
"""
from typing import Any, Dict, Optional
@@ -8,7 +9,6 @@ from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
from .dto import (
TariffFractionCreateDTO,
@@ -17,22 +17,6 @@ from .dto import (
)
from .service import TariffFractionService
# Create base router with generic CRUD routes (disabled list because we'll create a custom one)
base_router = TenantCRUDRoutes(
service=TariffFractionService,
create_schema=TariffFractionCreateDTO,
update_schema=TariffFractionUpdateDTO,
response_schema=TariffFractionResponseDTO,
prefix="/tariff-fractions",
tags=["a76 / general catalogs / tariff fractions"],
resource_name="TariffFraction",
id_name="tariff_fraction_id",
enable_list=False, # Disable default list, we'll add custom one
enable_filters=False,
default_page_size=50,
max_page_size=10000,
)
router = APIRouter(prefix="/tariff-fractions", tags=["a76 / general catalogs / tariff fractions"])
# Custom list endpoint with search filter
@@ -40,25 +24,22 @@ router = APIRouter(prefix="/tariff-fractions", tags=["a76 / general catalogs / t
"/",
response_model=Dict[str, Any],
summary="List Tariff Fractions",
description="Get paginated list of Tariff Fractions with optional search filter",
description="Get paginated list of Tariff Fractions with optional search filter (global catalog)",
)
async def list_tariff_fractions(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=10000, description="Page size"),
search: Optional[str] = Query(None, description="Search in code, fraction, description, nico, or umt"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
skip = (page - 1) * page_size
filters = {}
if search:
filters["search"] = search
items, total = TariffFractionService.get_all(
db, tenant_id, company_id, skip, page_size, filters
db, skip, page_size, filters
)
return {
@@ -69,6 +50,73 @@ async def list_tariff_fractions(
"pages": (total + page_size - 1) // page_size,
}
# Include other CRUD routes from base router
router.include_router(base_router.router)
@router.get(
"/{tariff_fraction_id}",
response_model=TariffFractionResponseDTO,
summary="Get Tariff Fraction by ID",
description="Get a specific tariff fraction by ID",
)
async def get_tariff_fraction(
tariff_fraction_id: int,
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
item = TariffFractionService.get_by_id(db, tariff_fraction_id)
if not item:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Tariff fraction not found")
return TariffFractionResponseDTO.model_validate(item)
@router.post(
"/",
response_model=TariffFractionResponseDTO,
summary="Create Tariff Fraction",
description="Create a new tariff fraction (admin only)",
status_code=201,
)
async def create_tariff_fraction(
data: TariffFractionCreateDTO,
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
item = TariffFractionService.create(db, data)
return TariffFractionResponseDTO.model_validate(item)
@router.put(
"/{tariff_fraction_id}",
response_model=TariffFractionResponseDTO,
summary="Update Tariff Fraction",
description="Update an existing tariff fraction (admin only)",
)
async def update_tariff_fraction(
tariff_fraction_id: int,
data: TariffFractionUpdateDTO,
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
item = TariffFractionService.update(db, tariff_fraction_id, data)
if not item:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Tariff fraction not found")
return TariffFractionResponseDTO.model_validate(item)
@router.delete(
"/{tariff_fraction_id}",
summary="Delete Tariff Fraction",
description="Delete a tariff fraction (admin only)",
status_code=204,
)
async def delete_tariff_fraction(
tariff_fraction_id: int,
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
success = TariffFractionService.delete(db, tariff_fraction_id)
if not success:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Tariff fraction not found")

View File

@@ -1,5 +1,6 @@
"""
Service para fracciones arancelarias
Catálogo de referencia global (no tenant-scoped)
"""
from typing import List, Optional, Tuple, Dict, Any
@@ -15,23 +16,18 @@ logger = logging.getLogger(__name__)
class TariffFractionService:
"""Service para gestionar fracciones arancelarias"""
"""Service para gestionar fracciones arancelarias (catálogo global)"""
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[TariffFraction], int]:
"""Obtiene todas las fracciones arancelarias con filtros opcionales"""
query = db.query(TariffFraction).filter(
TariffFraction.tenant_id == tenant_id,
TariffFraction.company_id == company_id,
)
query = db.query(TariffFraction)
# Aplicar filtros
if filters:
@@ -67,18 +63,12 @@ class TariffFractionService:
def get_by_id(
db: Session,
tariff_fraction_id: int,
tenant_id: int,
company_id: int,
) -> Optional[TariffFraction]:
"""Obtiene una fracción arancelaria por ID"""
return (
db.query(TariffFraction)
.filter(
TariffFraction.id == tariff_fraction_id,
TariffFraction.tenant_id == tenant_id,
TariffFraction.company_id == company_id,
)
.filter(TariffFraction.id == tariff_fraction_id)
.first()
)
@@ -86,18 +76,12 @@ class TariffFractionService:
def get_by_code(
db: Session,
code: str,
tenant_id: int,
company_id: int,
) -> Optional[TariffFraction]:
"""Obtiene una fracción arancelaria por código"""
return (
db.query(TariffFraction)
.filter(
TariffFraction.code == code,
TariffFraction.tenant_id == tenant_id,
TariffFraction.company_id == company_id,
)
.filter(TariffFraction.code == code)
.first()
)
@@ -105,16 +89,12 @@ class TariffFractionService:
def create(
db: Session,
tariff_fraction_data: TariffFractionCreateDTO,
tenant_id: int,
company_id: int,
) -> TariffFraction:
"""Crea una nueva fracción arancelaria"""
try:
tariff_fraction = TariffFraction(
**tariff_fraction_data.model_dump(),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(tariff_fraction)
db.commit()
@@ -133,13 +113,11 @@ class TariffFractionService:
db: Session,
tariff_fraction_id: int,
tariff_fraction_data: TariffFractionUpdateDTO,
tenant_id: int,
company_id: int,
) -> Optional[TariffFraction]:
"""Actualiza una fracción arancelaria existente"""
tariff_fraction = TariffFractionService.get_by_id(
db, tariff_fraction_id, tenant_id, company_id
db, tariff_fraction_id
)
if not tariff_fraction:
@@ -165,13 +143,11 @@ class TariffFractionService:
def delete(
db: Session,
tariff_fraction_id: int,
tenant_id: int,
company_id: int,
) -> bool:
"""Elimina una fracción arancelaria"""
tariff_fraction = TariffFractionService.get_by_id(
db, tariff_fraction_id, tenant_id, company_id
db, tariff_fraction_id
)
if not tariff_fraction:

View File

@@ -68,6 +68,8 @@ export interface LineDescriptions {
brand?: string;
model?: string;
has_serial?: boolean;
lot?: string;
entry_number?: string;
}
export interface LineReferences {
@@ -133,6 +135,7 @@ export interface LineItem {
page_line?: string;
has_certificate?: boolean;
certificate_number?: string;
octave_permit?: string;
// Flags
is_subitem?: boolean;

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}

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,11 @@
<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 type { LineItem, LineQuantities, LineFinancials, LineCustoms } from '$lib/api/dashboard/a76/items';
let {
@@ -14,73 +19,114 @@
financials: LineFinancials;
customs: LineCustoms;
} = $props();
let showUnitDialog = $state(false);
let showCountryDialog = $state(false);
let showFractionDialog = $state(false);
function handleUnitSelect(unit: any) {
lineItem.unit_of_measure = unit.code;
}
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>
<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">* Class:</Label>
<Input id="clase" bind:value={lineItem.class_id} class="h-8 text-xs" />
</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" bind:value={lineItem.unit_of_measure} class="h-8 text-xs flex-1" placeholder="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="PREFERENCIAL">PREFERENCIAL</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" 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 ">
<div class="flex items-center space-x-1.5 ">
<Checkbox id="equipo_militar" />
<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" value="0000.00.00" 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" value="0" class="h-6 text-xs text-right" />
</div>
</div>
</div>
<!-- Consider in a31 -->
<div class="flex items-center space-x-2 ">
<div class="flex items-center space-x-1.5 ">
<Checkbox id="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,17 @@
<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..."
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

@@ -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'
}
}
);
}
};