Merge branch 'development' into feature/fraction-API

This commit is contained in:
Galindo97
2026-02-16 12:04:56 -06:00
48 changed files with 697 additions and 461 deletions

View File

@@ -58,7 +58,7 @@ def validate_update(
# Columna A: Pedimento (si no viene en CSV, usar el existente)
if invoice_data.compliance_mx.pedimento_id:
invoice_data.compliance_mx.pedimento_id = clean_str(invoice_data.compliance_mx.pedimento_id)
invoice_data.compliance_mx.pedimento_id = invoice_data.compliance_mx.pedimento_id
else:
invoice_data.compliance_mx.pedimento_id = existing_invoice.compliance_mx.pedimento_id if existing_invoice.compliance_mx else None

View File

@@ -3,36 +3,31 @@ from core.exceptions import ErrorCollector
from ..line_items import models
from sqlalchemy.orm import Session
def item_exists(
db: Session,
item_line: int,
tenant_id: int,
company_id: int
):
def item_exists(db: Session, item_line: int, tenant_id: int, company_id: int):
item_exists = (
db.query(models.LineItem.id)
db.query(models.LineItem)
.filter(
models.LineItem.line_number == item_line,
models.LineItem.tenant_id == tenant_id,
models.LineItem.company_id == company_id,
)
.first()
)
return item_exists
def count_items(db: Session, invoice_id: int, tenant_id: int, company_id: int):
count = (
db.query(func.count())
.select_from(models.Item)
.filter(
models.Item.invoice_id == invoice_id,
models.Item.tenant_id == tenant_id,
models.Item.company_id == company_id,
)
.scalar()
)
if item_exists:
return item_exists
return None
def count_items(
db: Session,
invoice_id: int,
tenant_id: int,
company_id: int
):
count = db.query(func.count()).select_from(models.Item).filter(
models.Item.invoice_id == invoice_id,
models.Item.tenant_id == tenant_id,
models.Item.company_id == company_id,
).scalar()
return count
return count

View File

@@ -42,9 +42,7 @@ def validate_common(
invoice: InvoiceHeader = invoice_exists_by_id(
db, invoice_id, tenant_id, company_id, errors
)
line_item: LineItem = item_exists(
db, line.line_number, tenant_id, company_id
)
line_item: LineItem = item_exists(db, line.line_number, tenant_id, company_id)
fecha_factura = invoice.invoice_date if invoice else None
fraction = None
@@ -181,13 +179,15 @@ def validate_common(
fraction = line.customs.fraction if line.customs.fraction else fraction
country = line.customs.origin_country
if line_item:
if line_item and line_item.customs:
country = (
line_item.customs.fraction if line_item.customs.origin_country else country
line_item.customs.origin_country
if line_item.customs.origin_country
else country
)
fraction_type = line.customs.fraction_type.upper()
if line_item:
if line_item and line_item.customs:
fraction_type = (
line_item.customs.fraction_type
if line_item.customs.fraction_type
@@ -195,7 +195,7 @@ def validate_common(
)
sector = line.customs.sector
if line_item:
if line_item and line_item.customs:
sector = line_item.customs.sector if line_item.customs.sector else sector
country_m3 = db.query(Country.m3_key).filter(Country.m3_key == country).scalar()

View File

@@ -206,8 +206,8 @@ def validate_create(
net_weight_input = line.quantity.net_weight or Decimal("0")
# Determinar si la unidad de medida es de peso
unit_is_kgs = line.unit_of_measure and line.unit_of_measure.upper() == "KGS"
unit_is_lbs = line.unit_of_measure and line.unit_of_measure.upper() == "LB"
unit_is_kgs = line.unit_of_measure and line.unit_of_measure == "24" #KGS
unit_is_lbs = line.unit_of_measure and line.unit_of_measure == "25" #LBS
# Calcular peso neto en kilogramos (estándar interno)
if unit_is_kgs:
@@ -237,11 +237,11 @@ def validate_create(
package_weight_unit = Decimal("0")
# Obtener peso unitario del bulto si existe
if line.quantity.package_key:
if line.quantity.package_id:
package: Package = (
db.query(Package)
.filter(
Package.key == line.quantity.package_key,
Package.id == line.quantity.package_id,
Package.tenant_id == tenant_id,
Package.company_id == company_id,
)
@@ -278,22 +278,22 @@ def validate_create(
# ==========================================
# ASIGNAR DESCRIPCIÓN DE BULTOS
# ==========================================
if package_quantity and package_quantity > 0 and line.quantity.package_key:
if package_quantity and package_quantity > 0 and line.quantity.package_id:
package: Package = (
db.query(Package)
.filter(
Package.key == line.quantity.package_key,
Package.id == line.quantity.package_id,
Package.tenant_id == tenant_id,
Package.company_id == company_id,
)
.first()
)
if package:
line.quantity.package_description = package.description_es
line.description.package_description = package.description_es
else:
line.quantity.package_quantity = 0
line.quantity.package_key = None
line.quantity.package_description = None
line.quantity.package_id = None
line.description.package_description = None
# ==========================================
# ASIGNAR FRACCIÓN AMERICANA POR DEFECTO

View File

@@ -24,7 +24,8 @@ class LineDescription(Base):
description_english: Mapped[Optional[str]] = mapped_column(String(4999)) # DESCRIPCIONI
extra_description: Mapped[Optional[str]] = mapped_column(Text) # DESCRIPCIONEEXTRA
part_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONPARTE
class_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONCLASE
class_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONCLASE
package_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONBULTO
# Product attributes
brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCA

View File

@@ -13,6 +13,7 @@ class LineDescriptionBase(BaseModel):
extra_description: Optional[str] = Field(None, description="Extra description (DESCRIPCIONEEXTRA)")
part_description: Optional[str] = Field(None, max_length=500, description="Part description (DESCRIPCIONPARTE)")
class_description: Optional[str] = Field(None, max_length=500, description="Class description (DESCRIPCIONCLASE)")
package_description: Optional[str] = Field(None, max_length=500, description="Package description (DESCRIPCIONBULTO)")
# Product attributes
brand: Optional[str] = Field(None, max_length=50, description="Brand (MARCA)")

View File

@@ -17,6 +17,7 @@ from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSec
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
from .dtos.pedimentos import PedimentosResponse
from .schemas import PedimentoCatalogsResponse, PedimentoCreationResponse, PedimentoEditionResponse
@@ -111,10 +112,13 @@ class PedimentoCatalogService:
if not pedimento:
return None
# Convert SQLAlchemy object to Pydantic DTO
pedimento_dto = PedimentosResponse.model_validate(pedimento)
return PedimentoEditionResponse(
**catalogs.model_dump(),
is_create=False,
pedimento=pedimento,
pedimento=pedimento_dto,
pedimento_id=pedimento_id
)

View File

@@ -2,7 +2,7 @@
Consolidated schemas for Pedimento catalog responses
"""
from typing import List, Optional, Any
from typing import List, Optional
from pydantic import BaseModel
# Import DTOs for catalog items
@@ -11,6 +11,7 @@ from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSec
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
from .dtos.pedimentos import PedimentosResponse
class PedimentoCatalogsResponse(BaseModel):
@@ -33,5 +34,5 @@ class PedimentoEditionResponse(PedimentoCatalogsResponse):
"""Response for editing an existing pedimento (catalogs + pedimento data)"""
is_create: bool = False
pedimento: Optional[Any] = None # Will be PedimentosResponse but avoiding circular import
pedimento: Optional[PedimentosResponse] = None
pedimento_id: Optional[int] = None

View File

@@ -29,7 +29,9 @@ from api.v1.modules.a76.parts.models import Part
from api.v1.modules.a24.fa.fa_parts.models import FaPart
from api.v1.modules.a24.inv.inv_parts.models import InvPart
from api.v1.modules.a76.manifests.manifest.models import Manifest
from api.v1.modules.a76.manifests.concept_manifestation.models import ConceptManifestation
from api.v1.modules.a76.manifests.concept_manifestation.models import (
ConceptManifestation,
)
from api.v1.modules.a76.manifests.value_manifestation.models import ValueManifestation
# Configurar logging
@@ -93,7 +95,6 @@ async def on_startup():
run_migrations()
logger.info("Base de datos inicializada correctamente.")
# Configurar CORS
app.add_middleware(
CORSMiddleware,
@@ -112,6 +113,7 @@ app.add_middleware(TenantMiddleware)
# Middleware de Contexto de Usuario (Audit Log)
from api.v1.modules.a76.audit_log.middleware import UserContextMiddleware
app.add_middleware(UserContextMiddleware)
# Importar modelos para Audit Log
@@ -130,100 +132,121 @@ from api.v1.modules.a76.general_catalogs.company.models import Company
from api.v1.modules.public.reference_data.countries.models import Country
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection
from api.v1.modules.public.reference_data.customs_warehouses.models import CustomsWarehouse
from api.v1.modules.public.reference_data.customs_warehouses.models import (
CustomsWarehouse,
)
from api.v1.modules.public.reference_data.incoterms.models import Incoterm
from api.v1.modules.public.reference_data.invoice_types.models import InvoiceType
from api.v1.modules.public.reference_data.material_types.models import MaterialType
from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod
from api.v1.modules.public.reference_data.pedimento_codes.models import PedimentoCode
from api.v1.modules.public.reference_data.pedimento_regimens.models import RegimenPedimento
from api.v1.modules.public.reference_data.pedimento_regimens.models import (
RegimenPedimento,
)
from api.v1.modules.public.reference_data.sectors.models import Sector
from api.v1.modules.public.reference_data.states.models import State
from api.v1.modules.public.reference_data.transport_modes.models import TransportMode
from api.v1.modules.public.reference_data.transport_types.models import TransportType
from api.v1.modules.public.reference_data.valuation_methods.models import ValuationMethod
from api.v1.modules.public.reference_data.valuation_methods.models import (
ValuationMethod,
)
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
from api.v1.modules.a76.general_catalogs.identifiers.models import Identifier
from api.v1.modules.a76.classes.models import Class
from api.v1.modules.a76.general_catalogs.classification_concepts.models import ClassificationConcept
from api.v1.modules.a76.general_catalogs.classification_concepts.models import (
ClassificationConcept,
)
from api.v1.modules.a76.general_catalogs.concepts.models import Concept
from api.v1.modules.a76.general_catalogs.customs_broker_concepts.models import CustomsBrokerConcept
from api.v1.modules.a76.general_catalogs.depreciation_catalog.models import DepreciationCatalog
from api.v1.modules.a76.general_catalogs.customs_broker_concepts.models import (
CustomsBrokerConcept,
)
from api.v1.modules.a76.general_catalogs.depreciation_catalog.models import (
DepreciationCatalog,
)
from api.v1.modules.a76.general_catalogs.doda.models import Doda
from api.v1.modules.a76.general_catalogs.electronic_notices.models import ElectronicNotice
from api.v1.modules.a76.general_catalogs.electronic_notices.models import (
ElectronicNotice,
)
from api.v1.modules.a76.general_catalogs.equivalencies.models import Equivalency
from api.v1.modules.a76.general_catalogs.error_catalogs.models import ErrorCatalog
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDACatalog
from api.v1.modules.a76.general_catalogs.inpc.models import INPC
from api.v1.modules.a76.general_catalogs.legends.models import Legend
from api.v1.modules.a76.general_catalogs.multi_currency_types.models import MultiCurrencyType
from api.v1.modules.a76.general_catalogs.multi_currency_types.models import (
MultiCurrencyType,
)
from api.v1.modules.a76.general_catalogs.packages.models import Package
from api.v1.modules.a76.general_catalogs.ports.models import Port
from api.v1.modules.a76.general_catalogs.prevalidators.models import Prevalidator
from api.v1.modules.a76.general_catalogs.seal.models import Seal
from api.v1.modules.a76.general_catalogs.signatures.models import Signature
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import (
TariffFraction,
)
from api.v1.modules.a76.general_catalogs.unit_conversions.models import UnitConversion
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
USTariffFraction,
)
# Registrar Listeners de Auditoría
@app.on_event("startup")
def register_audit():
register_audit_listeners([
# Core Transactions
Pedimentos,
InvoiceHeader,
InvoiceSalesDetails,
Item,
# Sidebar Core Modules
ClientProvider,
CustomsBroker,
Part,
Company,
# Reference Data
Country,
CurrencyType,
CustomsSection,
CustomsWarehouse,
Incoterm,
InvoiceType,
MaterialType,
PaymentMethod,
PedimentoCode,
RegimenPedimento,
Sector,
State,
TransportMode,
TransportType,
ValuationMethod,
UnitOfMeasure,
ExchangeRate,
Identifier,
Class,
ClassificationConcept,
Concept,
CustomsBrokerConcept,
DepreciationCatalog,
Doda,
ElectronicNotice,
Equivalency,
ErrorCatalog,
FDACatalog,
INPC,
Legend,
MultiCurrencyType,
Package,
Port,
Prevalidator,
Seal,
Signature,
TariffFraction,
UnitConversion,
USTariffFraction
])
register_audit_listeners(
[
# Core Transactions
Pedimentos,
InvoiceHeader,
InvoiceSalesDetails,
Item,
# Sidebar Core Modules
ClientProvider,
CustomsBroker,
Part,
Company,
# Reference Data
Country,
CurrencyType,
CustomsSection,
CustomsWarehouse,
Incoterm,
InvoiceType,
MaterialType,
PaymentMethod,
PedimentoCode,
RegimenPedimento,
Sector,
State,
TransportMode,
TransportType,
ValuationMethod,
UnitOfMeasure,
ExchangeRate,
Identifier,
Class,
ClassificationConcept,
Concept,
CustomsBrokerConcept,
DepreciationCatalog,
Doda,
ElectronicNotice,
Equivalency,
ErrorCatalog,
FDACatalog,
INPC,
Legend,
MultiCurrencyType,
Package,
Port,
Prevalidator,
Seal,
Signature,
TariffFraction,
UnitConversion,
USTariffFraction,
]
)
# Crear directorio de uploads si no existe y montar archivos estáticos

View File

@@ -122,7 +122,7 @@
id="broker_key"
bind:value={formData.broker_key}
placeholder="Ej: 01001"
maxlength="5"
maxlength={5}
disabled={isEdit}
/>
</div>
@@ -133,7 +133,7 @@
id="concept"
bind:value={formData.concept}
placeholder="Ej: 001"
maxlength="15"
maxlength={15}
disabled={isEdit}
/>
</div>

View File

@@ -5,6 +5,11 @@
import { Label } from '$lib/components/ui/label';
import { companyStore } from '$lib/stores/company.svelte';
import { obtenerAtajosFormularioIdentificadores } from '$lib/config/shortcuts/dashboard/general_catalogs/identifiers/edit';
import {
createIdentifier,
updateIdentifier,
type Identifier
} from '$lib/api/dashboard/a76/general_catalogs/identifiers';
let {
open = $bindable(false),

View File

@@ -26,7 +26,7 @@
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
const data = await response.json();
if (Array.isArray(data)) {
countries = data;
} else if (data.items && Array.isArray(data.items)) {
@@ -80,7 +80,7 @@
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-4xl w-full max-h-[85vh] p-0 flex flex-col">
<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>
@@ -88,7 +88,11 @@
<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" />
<Input
bind:value={searchTerm}
placeholder="Buscando..."
class="flex-1 h-9"
/>
</div>
</div>
@@ -106,7 +110,9 @@
<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 M3</th
>
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
>Clave Mexicana</th
>
@@ -125,11 +131,11 @@
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>
<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}
@@ -143,9 +149,7 @@
</table>
</div>
<div
class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400"
>
<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;

View File

@@ -1,13 +1,11 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
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';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosPestanasItemFa } from '$lib/config/shortcuts/dashboard/invoices/item/fixed_asset';
// Child components
import MainData from './main-data.svelte';
@@ -26,7 +24,6 @@
invoice,
onSave,
onCancel,
isTargetingPreset = false,
isSaving = false
}: {
@@ -36,87 +33,39 @@
invoice: Invoice | null;
onSave: () => void;
onCancel?: () => void;
isTargetingPreset?: boolean;
isSaving?: boolean;
} = $props();
// Acceso directo a la primera línea para evitar repeticiones en el HTML
let line = $derived(editingItem.lines?.[0]);
let activeTab = $state('generales');
const tabMapping: Record<string, string> = {
'tab1': 'generales',
'tab2': 'continuacion',
'tab3': 'series',
'tab4': 'etiquetado',
'tab5': 'identificadores'
};
useShortcuts(
'Invoice Item Form (Fixed Asset)',
obtenerAtajosPestanasItemFa({
cambiarPestana: (target) => {
const tab = tabMapping[target];
if (tab) activeTab = tab;
},
manejarGuardar: onSave,
manejarCancelar: () => onCancel?.()
})
);
</script>
<Dialog.Root bind:open={open}>
<Dialog.Content class="sm:max-w-6xl max-h-[90vh] p-0 overflow-hidden z-[100] [&>button]:hidden">
<div class="bg-white dark:bg-zinc-950 border-b dark:border-zinc-800 px-4 py-3 shadow-sm flex items-start justify-between gap-3">
<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>
<Dialog.Title class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 leading-tight flex items-center gap-2">
{#if isTargetingPreset}
{isEditMode ? 'Editar Item de Plantilla' : 'Nuevo Item para Plantilla'}
{:else}
<Sheet.Root bind:open={open}>
<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'}
{/if}
{#if editingItem.lines && editingItem.lines.length > 1}
<span class="px-1.5 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/30 text-[10px] text-blue-700 dark:text-blue-300 font-bold border border-blue-200 dark:border-blue-800">
{editingItem.lines.length} lines
</span>
{/if}
</Dialog.Title>
{#if !isTargetingPreset}
</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>
{:else}
<p class="text-[10px] text-muted-foreground flex items-center gap-1.5">
<span class="font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wider">Modo Plantilla</span>
</p>
{/if}
</div>
</div>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="sm" onclick={() => onCancel?.()} disabled={isSaving} class="h-7 text-xs px-2">
Cancelar
<Button variant="ghost" size="icon" onclick={() => onCancel?.()} 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>
<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}
</Button>
</div>
</div>
</header>
<div class="p-3 overflow-y-auto max-h-[calc(90vh-88px)] bg-slate-50/70 dark:bg-black">
<div class="flex-1 overflow-y-auto px-2 py-1.5">
<div class="space-y-2">
{#if line}
@@ -152,7 +101,7 @@
</div>
</div>
<Tabs.Root bind:value={activeTab} class="w-full">
<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
@@ -216,5 +165,23 @@
{/if}
</div>
</div>
</Dialog.Content>
</Dialog.Root>
<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={() => onCancel?.()} 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}
</Button>
</div>
</footer>
</Sheet.Content>
</Sheet.Root>

View File

@@ -24,12 +24,11 @@
const filteredParts = $derived(
searchQuery
? parts.filter(
(p) =>
p.part_number?.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.description_spanish?.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.description_english?.toLowerCase().includes(searchQuery.toLowerCase())
)
? parts.filter(p =>
p.part_number?.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.description_spanish?.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.description_english?.toLowerCase().includes(searchQuery.toLowerCase())
)
: parts
);
@@ -53,12 +52,15 @@
isSearching = true;
try {
const response = await fetch(`/api-sveltekit/parts?company_id=${activeCompanyId}&limit=100`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
const response = await fetch(
`/api-sveltekit/parts?company_id=${activeCompanyId}&limit=100`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
});
);
if (!response.ok) {
throw new Error('Error al buscar números de parte');
@@ -85,9 +87,8 @@
function handleScroll(e: Event) {
const target = e.target as HTMLDivElement;
const threshold = 100;
const scrolledToBottom =
target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
if (scrolledToBottom && displayedParts.length < filteredParts.length) {
currentPage++;
loadMoreParts();
@@ -103,10 +104,12 @@
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-5xl w-full max-h-[90vh] p-0 flex flex-col">
<Dialog.Content class="!max-w-[50vw] w-[50vw] max-h-[80vh] flex flex-col">
<Dialog.Header>
<Dialog.Title>Seleccionar Número de Parte</Dialog.Title>
<Dialog.Description>Busca y selecciona un número de parte para la partida</Dialog.Description>
<Dialog.Description>
Busca y selecciona un número de parte para la partida
</Dialog.Description>
</Dialog.Header>
<div class="flex gap-2 mb-4">
@@ -145,18 +148,15 @@
</Table.Row>
{:else}
{#each displayedParts as part}
<Table.Row
class="cursor-pointer hover:bg-muted/50"
onclick={() => handleSelect(part)}
>
<Table.Row class="cursor-pointer hover:bg-muted/50" onclick={() => handleSelect(part)}>
<Table.Cell class="font-medium">{part.part_number}</Table.Cell>
<Table.Cell>{part.description_spanish || '-'}</Table.Cell>
<Table.Cell class="text-muted-foreground"
>{part.description_english || '-'}</Table.Cell
>
<Table.Cell class="text-muted-foreground">{part.description_english || '-'}</Table.Cell>
<Table.Cell class="text-muted-foreground">{part.part_class || '-'}</Table.Cell>
<Table.Cell>
<Button variant="ghost" size="sm" class="h-8">Seleccionar</Button>
<Button variant="ghost" size="sm" class="h-8">
Seleccionar
</Button>
</Table.Cell>
</Table.Row>
{/each}

View File

@@ -2,6 +2,12 @@
import type { LineFinancials, LineQuantities } from '$lib/api/dashboard/a76/items';
let { financials = $bindable(), quantities = $bindable() }: { financials: LineFinancials; quantities: LineQuantities } = $props();
// Helper function to safely format numbers
function formatNumber(value: any, decimals: number = 8): string {
const num = Number(value);
return isNaN(num) ? '0.00000000' : num.toFixed(decimals);
}
</script>
<div>
@@ -10,18 +16,18 @@
<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-gray-900 dark:text-gray-100">{quantities.quantity_temp_export?.toFixed(8) || '0.00000000'}</span></div>
<div>Temporary: <span class="text-gray-900 dark:text-gray-100">{formatNumber(quantities.quantity_temp_export)}</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>Definitive: <span class="text-gray-900 dark:text-gray-100">{formatNumber(quantities.quantity_returned)}</span></div>
<div>Returned Values: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.value_returned_usd)}</span></div>
<div class="col-span-2">Returned Values: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.value_returned_mxn)}</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>Net: <span class="text-gray-900 dark:text-gray-100">{formatNumber(quantities.net_weight)}</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>Whole: <span class="text-gray-900 dark:text-gray-100">{formatNumber(quantities.gross_weight)}</span></div>
<div><span class="text-gray-900 dark:text-gray-100">0.00000000</span></div>
</div>
</fieldset>
@@ -33,13 +39,13 @@
<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>Cost: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.unit_cost_usd)}</span></div>
<div><span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.unit_cost_mxn)}</span></div>
<div>Value: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.value_usd)}</span></div>
<div><span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.value_mxn)}</span></div>
<div class="text-xs">Capture Cost: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.unit_cost_capture)}</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">{formatNumber(financials.value_usd)}</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">{formatNumber(financials.customs_value_usd)}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
</div>
</fieldset>
</div>

View File

@@ -83,7 +83,7 @@
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-4xl w-full max-h-[90vh] p-0 flex flex-col">
<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>
@@ -91,7 +91,11 @@
<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" />
<Input
bind:value={searchTerm}
placeholder="Buscando..."
class="flex-1 h-9"
/>
</div>
</div>
@@ -109,7 +113,9 @@
<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"
>U.M.</th
>
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
>Descripción Español</th
>
@@ -150,9 +156,7 @@
</table>
</div>
<div
class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400"
>
<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;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import * as Sheet from '$lib/components/ui/sheet';
import * as Tabs from '$lib/components/ui/tabs';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
@@ -60,25 +60,29 @@
// Initialize missing nested objects if they don't exist
$effect(() => {
if (open && editingItem) {
if (!editingItem.lines) editingItem.lines = [{}];
if (!editingItem.lines[0].quantity) editingItem.lines[0].quantity = {};
if (!editingItem.lines[0].financial) editingItem.lines[0].financial = {};
if (!editingItem.lines[0].customs) editingItem.lines[0].customs = {};
if (!editingItem.lines[0].description) editingItem.lines[0].description = {};
if (!editingItem.lines) editingItem.lines = [{ line_number: 1 } as any];
if (editingItem.lines[0] && !editingItem.lines[0].quantity)
editingItem.lines[0].quantity = {} as any;
if (editingItem.lines[0] && !editingItem.lines[0].financial)
editingItem.lines[0].financial = {} as any;
if (editingItem.lines[0] && !editingItem.lines[0].customs)
editingItem.lines[0].customs = {} as any;
if (editingItem.lines[0] && !editingItem.lines[0].description)
editingItem.lines[0].description = {} as any;
}
});
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-5xl max-h-[90vh] p-0 overflow-hidden z-[100] [&>button]:hidden">
<Sheet.Root bind:open>
<Sheet.Content side="right" class="w-[50vw] overflow-hidden p-0 sm:max-w-none">
<div
class="px-6 py-4 border-b bg-white dark:bg-zinc-950 flex items-start justify-between gap-3"
class="flex items-start justify-between gap-3 border-b bg-white px-6 py-4 dark:bg-zinc-950"
>
<div class="space-y-1">
<Dialog.Title class="text-lg font-semibold">
<Sheet.Title class="text-lg font-semibold">
{isEditMode ? 'Editar Item' : 'Agregar Nuevo Item'} - SCAII (Inventario)
</Dialog.Title>
<Dialog.Description class="text-sm text-muted-foreground">
</Sheet.Title>
<Sheet.Description class="text-sm text-muted-foreground">
{isEditMode
? 'Modifica los campos del inventario y guarda los cambios.'
: 'Completa la información del nuevo item de inventario.'}
@@ -87,13 +91,13 @@
{editingItem.lines.length} items en esta partida
</Badge>
{/if}
</Dialog.Description>
</Sheet.Description>
</div>
<div class="flex gap-2">
<Button variant="outline" onclick={() => onCancel?.()} disabled={isSaving}>Cancelar</Button>
<Button onclick={onSave} disabled={isSaving}>
{#if isSaving}
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
{isEditMode ? 'Guardar Cambios' : 'Agregar Item'}
@@ -102,7 +106,7 @@
</div>
</div>
<div class="p-6 overflow-auto max-h-[calc(90vh-96px)] bg-slate-50/60 dark:bg-black">
<div class="max-h-[calc(90vh-96px)] overflow-auto bg-slate-50/60 p-6 dark:bg-black">
<Tabs.Root bind:value={activeTab} class="mt-0">
<Tabs.List class="grid w-full grid-cols-4">
<Tabs.Trigger value="general">General</Tabs.Trigger>
@@ -112,13 +116,13 @@
</Tabs.List>
<!-- Tab: General -->
<Tabs.Content value="general" class="space-y-4 mt-4">
<Tabs.Content value="general" class="mt-4 space-y-4">
<!-- Información de la Factura (Solo lectura) -->
{#if !isTargetingPreset}
<div class="rounded-lg border bg-muted/50 p-4 space-y-3">
<div class="space-y-3 rounded-lg border bg-muted/50 p-4">
<h4 class="text-sm font-medium">Información de la Factura (SCAII - Inventario)</h4>
{#if !invoice?.id}
<div class="text-sm text-amber-600 bg-amber-50 dark:bg-amber-950/20 p-3 rounded">
<div class="rounded bg-amber-50 p-3 text-sm text-amber-600 dark:bg-amber-950/20">
⚠️ Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la
factura.
</div>
@@ -139,7 +143,7 @@
</div>
<div class="col-span-2">
<span class="text-muted-foreground">Sistema:</span>
<span class="ml-2 font-medium bg-blue-100 dark:bg-blue-900/30 px-2 py-1 rounded"
<span class="ml-2 rounded bg-blue-100 px-2 py-1 font-medium dark:bg-blue-900/30"
>SCAII (Inventory)</span
>
</div>
@@ -207,7 +211,7 @@
</Tabs.Content>
<!-- Tab: Clasificación -->
<Tabs.Content value="clasificacion" class="space-y-4 mt-4">
<Tabs.Content value="clasificacion" class="mt-4 space-y-4">
<div class="space-y-4">
<div class="space-y-2">
<Label for="tariff_fraction">Fracción Arancelaria</Label>
@@ -223,21 +227,21 @@
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="product_type">Tipo de Producto</Label>
{#if line}
{#if line?.description}
<Input
id="product_type"
placeholder="Materia prima, producto terminado, etc."
bind:value={line.description.extra_description_2}
bind:value={(line.description as any).extra_description_2}
/>
{/if}
</div>
<div class="space-y-2">
<Label for="material_type">Tipo de Material</Label>
{#if line}
{#if line?.description}
<Input
id="material_type"
placeholder="Metal, plástico, etc."
bind:value={line.description.extra_description_3}
bind:value={(line.description as any).extra_description_3}
/>
{/if}
</div>
@@ -262,7 +266,7 @@
</Tabs.Content>
<!-- Tab: Cantidades -->
<Tabs.Content value="cantidades" class="space-y-4 mt-4">
<Tabs.Content value="cantidades" class="mt-4 space-y-4">
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
@@ -350,7 +354,7 @@
id="packages"
type="number"
placeholder="0"
bind:value={line.quantity.packages}
bind:value={(line.quantity as any).packages}
/>
{/if}
</div>
@@ -360,7 +364,7 @@
<Input
id="package_type"
placeholder="Caja, pallet, etc."
bind:value={line.quantity.package_type}
bind:value={(line.quantity as any).package_type}
/>
{/if}
</div>
@@ -374,7 +378,7 @@
id="imported_quantity"
type="number"
placeholder="0"
bind:value={line.quantity.quantity_imported}
bind:value={(line.quantity as any).quantity_imported}
/>
{/if}
</div>
@@ -385,7 +389,8 @@
id="remaining_quantity"
type="number"
placeholder="0"
value={(line.quantity.quantity || 0) - (line.quantity.quantity_imported || 0)}
value={(line.quantity.quantity || 0) -
((line.quantity as any).quantity_imported || 0)}
disabled
/>
{/if}
@@ -395,7 +400,7 @@
</Tabs.Content>
<!-- Tab: Otros -->
<Tabs.Content value="otros" class="space-y-4 mt-4">
<Tabs.Content value="otros" class="mt-4 space-y-4">
<div class="space-y-4">
<div class="space-y-2">
<Label for="brand">Marca</Label>
@@ -435,7 +440,7 @@
{#if line?.description}
<textarea
id="observations"
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 disabled:cursor-not-allowed disabled:opacity-50"
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:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Notas adicionales sobre el inventario..."
bind:value={line.description.extra_description}
></textarea>
@@ -445,5 +450,5 @@
</Tabs.Content>
</Tabs.Root>
</div>
</Dialog.Content>
</Dialog.Root>
</Sheet.Content>
</Sheet.Root>

View File

@@ -1,123 +1,106 @@
<script lang="ts" generics="TData, TValue">
import { onMount } from 'svelte';
import {
type ColumnDef,
getCoreRowModel
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
loading: boolean;
hasMore: boolean;
loadMore: () => void;
pageCount: number;
totalItems: number;
};
let {
data,
columns,
loading,
hasMore,
loadMore
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel()
get data() { return data; },
get columns() { return columns; },
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
get pageCount() { return pageCount; },
});
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url, { keepFocus: true, noScroll: true });
}
// Intersection Observer para detectar cuando el usuario llega al final
onMount(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading) {
loadMore();
}
},
{
root: scrollContainer,
threshold: 0.1
}
);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
return () => {
observer.disconnect();
};
});
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="bg-background">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
content={header.column.columnDef.header}
context={header.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
</Table.Row>
{/each}
<!-- Loading Trigger - Se activa cuando es visible -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4 px-2">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} registros
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= pageCount}
>
Siguiente
</Button>
</div>
</div>

View File

@@ -120,7 +120,7 @@ class CompanyStore {
* @param company - La compañía a establecer como activa
* @param silent - Si es true, no dispara el evento companyChanged (para inicialización)
*/
setActiveCompany(company: Company, silent: boolean = false) {
async setActiveCompany(company: Company, silent: boolean = false) {
const previousCompanyId = this._activeCompany?.id;
this._activeCompany = company;
@@ -130,8 +130,20 @@ class CompanyStore {
}
// Guardar en cookie para acceso desde el servidor (SSR)
if (typeof document !== 'undefined') {
document.cookie = `active_company_id=${company.id}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
// Usar el endpoint del servidor para garantizar que la cookie esté disponible en SSR
if (browser) {
try {
await fetch('/api-sveltekit/company/set-active', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ companyId: company.id }),
credentials: 'include'
});
} catch (error) {
console.error('Error setting active company cookie:', error);
}
}
// Despachar evento personalizado solo si:

View File

@@ -36,7 +36,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
try {
const fetchUrl = `${baseUrl}v1/a76/classes?${queryString}`;
console.log('Fetching classes from:', fetchUrl);
const response = await fetch(
fetchUrl,

View File

@@ -32,7 +32,6 @@ export const GET: RequestHandler = async ({ cookies, url, params }) => {
try {
const fetchUrl = `${baseUrl}v1/a76/classes/${id}?company_id=${companyId}`;
console.log('Fetching class from:', fetchUrl);
const response = await fetch(
fetchUrl,

View File

@@ -0,0 +1,29 @@
/**
* API route para establecer la compañía activa en una cookie
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ cookies, request }) => {
try {
const { companyId } = await request.json();
if (!companyId || typeof companyId !== 'number') {
return json({ error: 'Invalid company ID' }, { status: 400 });
}
// Establecer la cookie desde el servidor
cookies.set('active_company_id', companyId.toString(), {
path: '/',
maxAge: 60 * 60 * 24 * 30, // 30 días
sameSite: 'lax',
httpOnly: false, // Permitir acceso desde JavaScript
secure: process.env.NODE_ENV === 'production'
});
return json({ success: true, companyId });
} catch (error) {
console.error('Error setting active company:', error);
return json({ error: 'Internal server error' }, { status: 500 });
}
};

View File

@@ -35,8 +35,7 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
const queryString = searchParams.toString();
try {
const fetchUrl = `${baseUrl}v1/a76/parts?${queryString}`;
console.log('Fetching parts from:', fetchUrl);
const fetchUrl = `${baseUrl}v1/a76/parts?${queryString}`;
const response = await fetch(
fetchUrl,

View File

@@ -31,8 +31,7 @@ export const GET: RequestHandler = async ({ cookies, url, params }) => {
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
try {
const fetchUrl = `${baseUrl}v1/a76/parts/${id}?company_id=${companyId}`;
console.log('Fetching part from:', fetchUrl);
const fetchUrl = `${baseUrl}v1/a76/parts/${id}?company_id=${companyId}`;
const response = await fetch(
fetchUrl,

View File

@@ -20,8 +20,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
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,
@@ -35,8 +33,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
);
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), {

View File

@@ -36,8 +36,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
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,
@@ -51,8 +49,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
);
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), {

View File

@@ -31,8 +31,7 @@ export const GET: RequestHandler = async ({ cookies, params, url }) => {
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
try {
const fetchUrl = `${baseUrl}v1/a76/units-of-measure/${id}?company_id=${companyId}`;
console.log('Fetching unit of measure from:', fetchUrl);
const fetchUrl = `${baseUrl}v1/a76/units-of-measure/${id}?company_id=${companyId}`;
const response = await fetch(
fetchUrl,

View File

@@ -39,7 +39,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/classification-concepts?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
const response = await authenticatedFetch(`v1/a76/classification-concepts/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', classifications: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };

View File

@@ -89,7 +89,6 @@
/>
<CreateDialog
bind:open={dialogOpen}
title="Crear Nueva Clasificación de Concepto"
onSuccess={handleSuccess}
/>
</div>

View File

@@ -59,7 +59,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
});
const response = await authenticatedFetch(
`v1/a76/concepts?${queryParams.toString()}`,
`v1/a76/concepts/?${queryParams.toString()}`,
{ method: 'GET' },
cookies,
fetch

View File

@@ -2,20 +2,25 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
return { error: 'No authenticated', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }, activeCompanyId: null };
return { error: 'No authenticated', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return { error: 'No company selected', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }, activeCompanyId: null };
return { error: 'No company selected', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const filters: Record<string, string> = {};
@@ -28,22 +33,22 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId,
company_id: companyId.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/customs-broker-concepts?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
const response = await authenticatedFetch(`v1/a76/customs-broker-concepts/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', concepts: { items: [], total: 0, page, page_size: pageSize, pages: 0 }, activeCompanyId: companyId };
return { error: 'Failed to load', concepts: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
const data = await response.json();
// Calculate pages if not provided by API
const pages = data.pages || Math.ceil(data.total / pageSize);
return { concepts: { ...data, pages }, activeCompanyId: parseInt(companyId) };
return { concepts: { ...data, pages } };
} catch (error) {
console.error('Error loading customs broker concepts:', error);
return { error: 'Error loading', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }, activeCompanyId: null };
return { error: 'Error loading', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -10,8 +10,11 @@
import { browser } from '$app/environment';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaConceptosAA } from '$lib/config/shortcuts/dashboard/general_catalogs/customs_broker_concepts/list';
import { companyStore } from '$lib/stores/company.svelte';
let { data } = $props();
const activeCompanyId = $derived(companyStore.activeCompany?.id);
let dialogOpen = $state(false);
// Atajos
@@ -92,9 +95,11 @@
/>
</div>
<CreateEditDialog
bind:open={dialogOpen}
companyId={data.activeCompanyId}
onSuccess={handleSuccess}
/>
{#if activeCompanyId}
<CreateEditDialog
bind:open={dialogOpen}
companyId={activeCompanyId}
onSuccess={handleSuccess}
/>
{/if}
</div>

View File

@@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
@@ -12,7 +12,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return { error: 'No company selected', dodas: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
@@ -26,7 +31,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId,
company_id: companyId.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/doda?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);

View File

@@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
@@ -12,7 +12,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return { error: 'No company selected', notices: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
@@ -28,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId,
company_id: companyId.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/electronic-notices?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);

View File

@@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
@@ -12,7 +12,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return { error: 'No company selected', equivalencies: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
@@ -28,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId,
company_id: companyId.toString(),
...filters
});

View File

@@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
@@ -12,7 +12,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return { error: 'No company selected', identifiers: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
@@ -28,11 +33,11 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId,
company_id: companyId.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/identifiers?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
const response = await authenticatedFetch(`v1/a76/identifiers/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', identifiers: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };

View File

@@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
@@ -12,7 +12,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return { error: 'No company selected', inpc: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
@@ -28,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId,
company_id: companyId.toString(),
...filters
});

View File

@@ -59,7 +59,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
});
const response = await authenticatedFetch(
`v1/a76/packages?${queryParams.toString()}`,
`v1/a76/packages/?${queryParams.toString()}`,
{ method: 'GET' },
cookies,
fetch

View File

@@ -2,7 +2,8 @@ import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
@@ -10,7 +11,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
@@ -27,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const query = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId
company_id: companyId.toString()
});
const endpoint = `${apiUrl}v1/a76/ports?${query.toString()}`;

View File

@@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
@@ -12,7 +12,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return { error: 'No company selected', prevalidators: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
@@ -28,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId,
company_id: companyId.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/prevalidators?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);

View File

@@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
@@ -12,7 +12,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return { error: 'No company selected', signatures: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
@@ -23,7 +28,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
if (code) filters.code = code;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), company_id: companyId, ...filters });
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), company_id: companyId.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/signatures?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {

View File

@@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
@@ -17,9 +17,11 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
// Obtener company_id
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId ? parseInt(cookieCompanyId) : undefined;
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {

View File

@@ -187,8 +187,7 @@
selectedInvoiceId = null;
} else {
selectedInvoiceId = invoice.id;
}
console.log('Selected Invoice ID:', selectedInvoiceId);
}
}
const selectedInvoice = $derived(

View File

@@ -222,29 +222,199 @@
return { ...skeleton, ...filtered };
}
// Función para mapear la factura existente a los formData
function mapInvoiceToTopFields(invoice: any) {
if (!invoice) return topFieldsSkeleton;
let operationType: string | null = null;
if (invoice.operation_type) {
operationType = invoice.operation_type;
} else if (data.filters?.operation_type !== undefined) {
operationType = data.filters.operation_type ?? null;
}
return {
is_pedimento_pending: invoice.compliance_mx?.is_pedimento_pending || false,
pedimento_id: invoice.compliance_mx?.pedimento_id || '',
remesa: invoice.compliance_mx?.remesa || '',
invoice_number: invoice.invoice_number || '',
invoice_date: invoice.invoice_date || new Date().toISOString().split('T')[0],
emission_date: invoice.emission_date || new Date().toISOString().split('T')[0],
operation_type: operationType,
invoice_type: invoice.invoice_type || (data.filters?.invoice_type ?? ''),
fecha_pedimento_del: '',
fecha_pedimento_al: '',
clave_pedimento: '',
regimen_pedimento: ''
};
}
function mapInvoiceToGeneral(invoice: any) {
if (!invoice) return generalSkeleton;
return {
provider_header: invoice.compliance_mx?.provider_header || 'proveedor',
provider_id: invoice.compliance_mx?.provider_id || null,
sold_to_header: invoice.compliance_mx?.sold_to_header || 'consignado_a',
sold_to_id: invoice.compliance_mx?.sold_to_id || null,
shipped_to_header: invoice.compliance_mx?.shipped_to_header || 'enviado_a',
shipped_to_id: invoice.compliance_mx?.shipped_to_id || null,
customs_broker_id: invoice.compliance_mx?.customs_broker_id || null,
customs_broker_us_id: invoice.compliance_mx?.customs_broker_us_id || null,
currency_type: invoice.financials?.currency_type || '',
currency: invoice.financials?.currency || 'foreign',
exchange_rate: invoice.financials?.exchange_rate || null,
weight_type: 'kgs',
iva_factor: invoice.financials?.iva_factor || null,
carrier_id: invoice.logistics?.carrier_id || null,
transport_id: invoice.logistics?.transport_id || '',
driver_name: invoice.logistics?.driver_name || '',
transport_type: invoice.logistics?.transport_type || '',
transport_num: invoice.logistics?.vehicle_num || '',
aduana: invoice.compliance_mx?.aduana || '',
document_type: invoice.document_type || ''
};
}
function mapInvoiceToObservations(invoice: any) {
if (!invoice) return observationSkeleton;
return {
observation_es: invoice.observation_es || '',
observation_en: invoice.observation_en || '',
freight: invoice.financials?.freight || null,
insurance_value: invoice.financials?.insurance_value || null,
insurance: invoice.financials?.insurance || null,
packaging: invoice.financials?.packaging || null,
other_increments: invoice.financials?.other_increments || null,
total_increments_mn: invoice.financials?.total_increments_mn || null,
total_increments_me: invoice.financials?.total_increments_me || null,
incoterm: invoice.logistics?.incoterm || null,
enclosure: invoice.compliance_mx?.enclosure || null,
num_seals: null,
movement_type: invoice.compliance_mx?.movement_type || '',
alternate_invoice: invoice.alternate_invoice || '',
valuation_method: invoice.compliance_mx?.value_method || null
};
}
function mapInvoiceToItems(invoice: any) {
if (!invoice) return ensureItemsFormData(null);
return {
items: invoice.items || []
};
}
function mapInvoiceToOthers(invoice: any) {
if (!invoice) return othersSkeleton;
return {
comments_status: invoice.comments_status || '',
transport_mode: invoice.logistics?.transport_mode || 'TRUCK',
is_mixed: invoice.compliance_mx?.is_mixed || false,
print_stamp: invoice.print_stamp || false,
rule_3121_parties_ii: invoice.compliance_mx?.rule_3121_parties_ii || false,
related_doc_id: invoice.related_doc_id || null,
code_signature: invoice.compliance_mx?.code_signature || '',
electronic_signature: invoice.compliance_mx?.electronic_signature || '',
mandatory_person: invoice.compliance_mx?.mandatory_person || '',
contingency_mode: invoice.compliance_mx?.contingency_mode || false,
cove: invoice.compliance_mx?.cove || '',
operation_num: invoice.compliance_mx?.operation_num || '',
adendas: invoice.compliance_mx?.adendas || '',
observations_vu: invoice.compliance_mx?.observations_vu || '',
certified_number: invoice.compliance_mx?.certified_number || '',
bill_number: invoice.logistics?.bill_number || '',
guide_number: invoice.logistics?.guide_number || '',
shipment_number: invoice.logistics?.shipment_number || '',
option_iv18: invoice.compliance_mx?.option_iv18 || '',
delivered_status: invoice.delivered_status || false,
received_by: invoice.received_by || '',
delivery_date: invoice.delivery_date || ''
};
}
function mapInvoiceToContinuation(invoice: any) {
if (!invoice) return continuationSkeleton;
return {
numero_tipo_transporte: invoice.logistics?.numero_tipo_transporte || '',
es_ferrocarril: invoice.logistics?.es_ferrocarril || 'no',
numero_bl: invoice.logistics?.numero_bl || '',
cantidad_guias_embarque: invoice.logistics?.cantidad_guias_embarque || null,
destino_origen: invoice.logistics?.destino_origen || '',
puerto_entrada: invoice.logistics?.puerto_entrada || '',
vehicle_data: invoice.logistics?.vehicle_data || '',
fue_revisado_equipo: invoice.logistics?.fue_revisado_equipo || false,
sub_division: invoice.compliance_mx?.subdivision || false,
funge_como_cd: invoice.logistics?.acts_as_cd || false,
llego_pedimento: invoice.compliance_mx?.llego_pedimento || false,
errores_facturacion: invoice.errores_facturacion || [],
semaforo_verde_aduana_mexicana: invoice.compliance_mx?.semaforo_verde_aduana_mexicana || false,
semaforo_verde_aduana_americana: invoice.compliance_mx?.semaforo_verde_aduana_americana || false,
semaforo_rojo_aduana_mexicana: invoice.compliance_mx?.semaforo_rojo_aduana_mexicana || false,
semaforo_rojo_aduana_americana: invoice.compliance_mx?.semaforo_rojo_aduana_americana || false,
is_mixed: invoice.compliance_mx?.is_mixed || false,
reason_export: invoice.compliance_mx?.reason_export || '1',
purchase_order: invoice.purchase_order || '',
payment_terms: invoice.payment_terms || '',
handling_fees: invoice.financials?.handling_fees || 0,
cfdi_uuid: invoice.cfdi_uuid || '',
path_pdf: invoice.path_pdf || '',
path_xml: invoice.path_xml || ''
};
}
// Referencias a los componentes de formulario para obtener sus datos
// Si estamos en modo edición (!data.isCreate) y tenemos una factura, usarla
// Si estamos en modo creación, usar defaultSettings
let InvoiceTopFieldsFormData = $state<any>(
mergeDefaults(topFieldsSkeleton, data.defaultSettings?.InvoiceTopFieldsFormData)
!data.isCreate && data.invoice
? mapInvoiceToTopFields(data.invoice)
: mergeDefaults(topFieldsSkeleton, data.defaultSettings?.InvoiceTopFieldsFormData)
);
let generalFormData = $state<any>(
mergeDefaults(generalSkeleton, data.defaultSettings?.generalFormData)
!data.isCreate && data.invoice
? mapInvoiceToGeneral(data.invoice)
: mergeDefaults(generalSkeleton, data.defaultSettings?.generalFormData)
);
let observationFormData = $state<any>(
mergeDefaults(observationSkeleton, data.defaultSettings?.observationFormData)
!data.isCreate && data.invoice
? mapInvoiceToObservations(data.invoice)
: mergeDefaults(observationSkeleton, data.defaultSettings?.observationFormData)
);
let itemsFormData = $state<any>(
!data.isCreate && data.invoice
? mapInvoiceToItems(data.invoice)
: ensureItemsFormData(data.defaultSettings?.itemsFormData)
);
let itemsFormData = $state<any>(ensureItemsFormData(data.defaultSettings?.itemsFormData));
let othersFormData = $state<any>(
mergeDefaults(othersSkeleton, data.defaultSettings?.othersFormData)
!data.isCreate && data.invoice
? mapInvoiceToOthers(data.invoice)
: mergeDefaults(othersSkeleton, data.defaultSettings?.othersFormData)
);
let continuationFormData = $state<any>(
mergeDefaults(continuationSkeleton, data.defaultSettings?.continuationFormData)
!data.isCreate && data.invoice
? mapInvoiceToContinuation(data.invoice)
: mergeDefaults(continuationSkeleton, data.defaultSettings?.continuationFormData)
);
// Estados para saber si existen datos previos
let observationExists = $state(!!data.defaultSettings?.observationFormData);
let itemsExists = $state(!!data.defaultSettings?.itemsFormData?.items?.length);
let othersExists = $state(!!data.defaultSettings?.othersFormData);
let continuationExists = $state(!!data.defaultSettings?.continuationFormData);
let observationExists = $state(
!data.isCreate ? !!data.invoice : !!data.defaultSettings?.observationFormData
);
let itemsExists = $state(
!data.isCreate
? !!(data.invoice?.items && data.invoice.items.length > 0)
: !!data.defaultSettings?.itemsFormData?.items?.length
);
let othersExists = $state(
!data.isCreate ? !!data.invoice : !!data.defaultSettings?.othersFormData
);
let continuationExists = $state(
!data.isCreate ? !!data.invoice : !!data.defaultSettings?.continuationFormData
);
let calculatedExchangeRate = $state<number | null>(
data.invoice?.financials?.exchange_rate ?? null
@@ -314,8 +484,7 @@
const actualResponse = response as any;
const items = actualResponse.data?.items || [];
if (items.length === 0) {
console.log('No exchange rate found for', date);
if (items.length === 0) {
if (!uiStore.isExchangeRateDialogOpen) {
missingExchangeRateDate = date;
showExchangeRateDialog = true;

View File

@@ -496,7 +496,7 @@
onclick={() => handleDeleteItem(i)}
class="h-8 w-8 text-zinc-500 hover:text-destructive"
>
<Trash2 class="h-3.5 h-3.5" />
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div>
</Table.Cell>

View File

@@ -24,7 +24,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/public/reference_data/incoterms?page=${page}&page_size=${pageSize}`,
`v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`,
{},
cookies,
fetch

View File

@@ -2,7 +2,7 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/reference_data/incoterms/columns.js';
import { createColumns } from '$lib/components/dashboard/reference_data/incoterms/columns';
import DataTable from '$lib/components/dashboard/reference_data/incoterms/data-table.svelte';
import CreateEditDialog from '$lib/components/dashboard/reference_data/incoterms/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
@@ -59,11 +59,7 @@
<p class="text-muted-foreground">
Catálogo de Incoterms
</p>
</div>
<Button onclick={() => createDialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Incoterm
</Button>
</div>
</div>
<div class="flex gap-4 items-end">

View File

@@ -1,7 +0,0 @@
import { Dialog } from "bits-ui";
console.log("Dialog is:", Dialog);
try {
console.log("Dialog.Root is:", Dialog.Root);
} catch (e) {
console.log("Error accessing Dialog.Root:", e.message);
}