feat: Add new fields for invoice logistics and line items, enhance UI for class selection

This commit is contained in:
Galindo97
2026-01-15 08:12:00 -06:00
parent 98c7fe3bb5
commit 7e9a1acd30
15 changed files with 465 additions and 43 deletions

View File

@@ -674,6 +674,32 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
Boolean, default=False
) # SETRATAPROCESOCTM / Se trata de proceso CTM
# Continuation Tab Fields
equipment_reviewed: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False
) # Fue revisado el equipo
is_subdivision: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False
) # Sub división
acts_as_cd: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False
) # Funge como CD
pedimento_arrived: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False
) # Llegó el pedimento
green_light_mx: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False
) # Semáforo verde México
green_light_us: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False
) # Semáforo verde USA
red_light_mx: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False
) # Semáforo rojo México
red_light_us: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False
) # Semáforo rojo USA
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="logistics")

View File

@@ -345,6 +345,15 @@ class InvoiceLogisticsBase(BaseModel):
None, max_length=20, description="Payment receipt number"
)
is_ctm_process: Optional[bool] = Field(False, description="Is CTM process")
# Continuation Tab Fields
equipment_reviewed: Optional[bool] = Field(False, description="Equipment reviewed")
is_subdivision: Optional[bool] = Field(False, description="Is subdivision")
acts_as_cd: Optional[bool] = Field(False, description="Acts as CD")
pedimento_arrived: Optional[bool] = Field(False, description="Pedimento arrived")
green_light_mx: Optional[bool] = Field(False, description="Green light Mexico")
green_light_us: Optional[bool] = Field(False, description="Green light USA")
red_light_mx: Optional[bool] = Field(False, description="Red light Mexico")
red_light_us: Optional[bool] = Field(False, description="Red light USA")
class InvoiceSalesDetailsBase(BaseModel):

View File

@@ -1,5 +1,5 @@
from typing import Optional, TYPE_CHECKING
from sqlalchemy import Boolean, String, Text, ForeignKey
from sqlalchemy import Boolean, String, Text, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
@@ -39,5 +39,13 @@ class LineDescription(Base):
lot: Mapped[Optional[str]] = mapped_column(String(254)) # LOTE
entry_number: Mapped[Optional[str]] = mapped_column(String(50)) # NUMENTRADA/NUMERODEENTRADA
# Eighth rule and A31 fields
eighth_rule_fraction: Mapped[Optional[str]] = mapped_column(String(20)) # Eighth Rule Fraction
eighth_rule_line: Mapped[Optional[int]] = mapped_column(Integer) # Eighth Rule Line
consider_a31: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # Consider in A31
# Machinery location
machinery_location: Mapped[Optional[str]] = mapped_column(String(200)) # Machinery and equipment location
# Relationship (one-to-one)
line: Mapped["LineItem"] = relationship(back_populates="description")

View File

@@ -26,6 +26,14 @@ class LineDescriptionBase(BaseModel):
# Lot and entry tracking
lot: Optional[str] = Field(None, max_length=254, description="Lot (LOTE)")
entry_number: Optional[str] = Field(None, max_length=50, description="Entry number (NUMENTRADA/NUMERODEENTRADA)")
# Eighth rule and A31 fields
eighth_rule_fraction: Optional[str] = Field(None, max_length=20, description="Eighth Rule Fraction")
eighth_rule_line: Optional[int] = Field(None, description="Eighth Rule Line")
consider_a31: Optional[bool] = Field(False, description="Consider in A31")
# Machinery location
machinery_location: Optional[str] = Field(None, max_length=200, description="Machinery and equipment location")
class LineDescriptionCreate(LineDescriptionBase):

View File

@@ -41,15 +41,15 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
component_part_number_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")
) # NUMPARTECOM
class_id: Mapped[Optional[str]] = mapped_column(
class_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("a76.classes.id")
) # CLASE
# Unit of measure
unit_of_measure: Mapped[Optional[str]] = mapped_column(
unit_of_measure: Mapped[Optional[int]] = mapped_column(
ForeignKey("a76.units_of_measure.id")
) # UNIDADMEDIDA/UNIMED
alternate_unit: Mapped[Optional[str]] = mapped_column(
alternate_unit: Mapped[Optional[int]] = mapped_column(
ForeignKey("a76.units_of_measure.id")
) # UNIMEDALTERNA
uma_key: Mapped[Optional[str]] = mapped_column(String(2)) # CLAVEUMA
@@ -173,11 +173,15 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
back_populates="line", cascade="all, delete-orphan", uselist=False
)
class_info: Mapped[Optional["Class"]] = relationship(
foreign_keys=[class_id], viewonly=True
"api.v1.modules.a76.classes.models.Class",
foreign_keys=[class_id],
viewonly=True
)
unit_of_measure_info: Mapped[Optional["UnitOfMeasure"]] = relationship(
foreign_keys=[unit_of_measure], viewonly=True
"api.v1.modules.a76.general_catalogs.units_of_measure.models.UnitOfMeasure",
foreign_keys=[unit_of_measure],
viewonly=True
)
fa_data: Mapped[Optional["FaLineItem"]] = relationship(
"FaLineItem", back_populates="master_info", uselist=False
"FaLineItem", back_populates="master_info", uselist=False, cascade="all, delete"
)

View File

@@ -53,24 +53,12 @@ class LineItemBase(BaseModel):
)
class_id: Optional[int] = Field(None, description="Class code")
@field_validator(
"unit_of_measure",
"alternate_unit",
mode="before",
)
@classmethod
def convert_to_string(cls, v):
"""Convert integers to strings for FK fields"""
if v is not None and not isinstance(v, str):
return str(v)
return v
# Unit of measure
unit_of_measure: Optional[str] = Field(
None, max_length=10, description="Unit of measure"
unit_of_measure: Optional[int] = Field(
None, description="Unit of measure"
)
alternate_unit: Optional[str] = Field(
None, max_length=10, description="Alternate unit"
alternate_unit: Optional[int] = Field(
None, description="Alternate unit"
)
uma_key: Optional[str] = Field(None, max_length=2, description="UMA key")
auxiliary_unit: Optional[str] = Field(

View File

@@ -70,6 +70,10 @@ export interface LineDescriptions {
has_serial?: boolean;
lot?: string;
entry_number?: string;
eighth_rule_fraction?: string;
eighth_rule_line?: number;
consider_a31?: boolean;
machinery_location?: string;
}
export interface LineReferences {
@@ -127,8 +131,8 @@ export interface LineItem {
identifier?: string;
// Unit of Measure
unit_of_measure?: string;
alternate_unit?: string;
unit_of_measure?: number;
alternate_unit?: number;
// Permits
permit_number?: string;
@@ -141,10 +145,14 @@ export interface LineItem {
is_subitem?: boolean;
includes_subitems?: boolean;
tax_payment?: boolean;
is_military_mcia?: boolean;
// Payment
payment_method?: string;
igi_amount?: number;
// Additional notes
wildcard_field?: string;
// Computed fields from class_info relation
class_code?: string;

View File

@@ -0,0 +1,170 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import * as Table from '$lib/components/ui/table';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import { Search, Loader2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
onSelect
}: {
open?: boolean;
onSelect?: (classItem: any) => void;
} = $props();
let searchQuery = $state('');
let isSearching = $state(false);
let classes = $state<any[]>([]);
let displayedClasses = $state<any[]>([]);
let currentPage = $state(1);
let itemsPerPage = 10;
const filteredClasses = $derived(
searchQuery
? classes.filter(c =>
c.class_code?.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.description_es?.toLowerCase().includes(searchQuery.toLowerCase())
)
: classes
);
$effect(() => {
if (open) {
searchClasses();
}
});
$effect(() => {
currentPage = 1;
loadMoreClasses();
});
async function searchClasses() {
const activeCompanyId = companyStore?.activeCompany?.id;
if (!activeCompanyId) {
toast.error('No hay compañía activa');
return;
}
isSearching = true;
try {
const response = await fetch(
`/api-sveltekit/classes?company_id=${activeCompanyId}&limit=100`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error('Error al buscar clases');
}
const data = await response.json();
classes = data.data?.items || [];
loadMoreClasses();
} catch (error) {
console.error('Error searching classes:', error);
toast.error('Error al buscar clases');
classes = [];
} finally {
isSearching = false;
}
}
function loadMoreClasses() {
const start = 0;
const end = currentPage * itemsPerPage;
displayedClasses = filteredClasses.slice(start, end);
}
function handleScroll(e: Event) {
const target = e.target as HTMLDivElement;
const threshold = 100;
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
if (scrolledToBottom && displayedClasses.length < filteredClasses.length) {
currentPage++;
loadMoreClasses();
}
}
function handleSelect(classItem: any) {
if (onSelect) {
onSelect(classItem);
}
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-4xl max-h-[80vh] flex flex-col">
<Dialog.Header>
<Dialog.Title>Seleccionar Clase</Dialog.Title>
<Dialog.Description>
Busca y selecciona una clase para la partida
</Dialog.Description>
</Dialog.Header>
<div class="flex gap-2 mb-4">
<div class="relative flex-1">
<Search class="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por código o descripción..."
bind:value={searchQuery}
class="pl-8"
/>
</div>
</div>
<div class="flex-1 overflow-auto border rounded-md" onscroll={handleScroll}>
{#if isSearching}
<div class="flex items-center justify-center py-8">
<Loader2 class="w-6 h-6 animate-spin text-muted-foreground" />
</div>
{:else}
<Table.Root>
<Table.Header class="sticky top-0 bg-background z-10">
<Table.Row>
<Table.Head class="w-[120px]">Código</Table.Head>
<Table.Head>Descripción</Table.Head>
<Table.Head class="w-[100px]">U.M.</Table.Head>
<Table.Head class="w-[100px]"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if displayedClasses.length === 0}
<Table.Row>
<Table.Cell colspan={4} class="text-center py-8 text-muted-foreground">
No se encontraron clases
</Table.Cell>
</Table.Row>
{:else}
{#each displayedClasses as classItem}
<Table.Row class="cursor-pointer hover:bg-muted/50" onclick={() => handleSelect(classItem)}>
<Table.Cell class="font-medium">{classItem.class_code}</Table.Cell>
<Table.Cell>{classItem.description_es || classItem.description_en || '-'}</Table.Cell>
<Table.Cell>{classItem.unit_of_measure || '-'}</Table.Cell>
<Table.Cell>
<Button size="sm" variant="ghost" onclick={() => handleSelect(classItem)}>
Seleccionar
</Button>
</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
{/if}
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -72,10 +72,11 @@
<!-- Descriptions -->
<fieldset class="border rounded-md p-2 space-y-2">
<div class="space-y-1">
<Label for="num_parte" class="text-xs">Part Number:</Label>
<Label for="num_parte" class="text-xs">Part Number ID (opcional):</Label>
<div class="flex gap-1">
<Input id="num_parte" bind:value={lineItem.part_number_id} class="h-7 text-xs" />
<Input id="num_parte" type="number" bind:value={lineItem.part_number_id} class="h-7 text-xs" placeholder="Dejar vacío si no aplica" />
</div>
<p class="text-xs text-muted-foreground">ID de número de parte existente en catálogo</p>
</div>
<div class="space-y-1">

View File

@@ -6,6 +6,7 @@
import UnitOfMeasureDialog from './unit-of-measure-dialog.svelte';
import CountryDialog from './country-dialog.svelte';
import TariffFractionDialog from './tariff-fraction-dialog.svelte';
import ClassDialog from './class-dialog.svelte';
import type { LineItem, LineQuantities, LineFinancials, LineCustoms } from '$lib/api/dashboard/a76/items';
let {
@@ -20,12 +21,26 @@
customs: LineCustoms;
} = $props();
let showClassDialog = $state(false);
let showUnitDialog = $state(false);
let showCountryDialog = $state(false);
let showFractionDialog = $state(false);
// Initialize from existing data
$effect(() => {
if (lineItem.class_code) {
// Do nothing, it's already set
}
});
function handleClassSelect(classItem: any) {
lineItem.class_id = classItem.id;
// Store the code in the lineItem for display
(lineItem as any).class_code = classItem.class_code;
}
function handleUnitSelect(unit: any) {
lineItem.unit_of_measure = unit.code;
lineItem.unit_of_measure = unit.id;
}
function handleCountrySelect(country: any) {
@@ -37,6 +52,7 @@
}
</script>
<ClassDialog bind:open={showClassDialog} onSelect={handleClassSelect} />
<UnitOfMeasureDialog bind:open={showUnitDialog} onSelect={handleUnitSelect} />
<CountryDialog bind:open={showCountryDialog} onSelect={handleCountrySelect} />
<TariffFractionDialog bind:open={showFractionDialog} onSelect={handleFractionSelect} />
@@ -47,8 +63,27 @@
<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" />
<Label for="clase" class="text-xs font-medium">* Clase (ID):</Label>
<div class="flex gap-1">
<Input
id="clase"
type="number"
bind:value={lineItem.class_id}
class="h-8 text-xs flex-1"
placeholder="ID de clase (1-6)"
/>
<Button
variant="outline"
size="icon"
class="h-8 w-8 shrink-0"
onclick={() => (showClassDialog = true)}
>
<Folder class="w-4 h-4" />
</Button>
</div>
{#if (lineItem as any).class_code}
<p class="text-xs text-muted-foreground">Código: {(lineItem as any).class_code}</p>
{/if}
</div>
<!-- Quantity and U.M. on the same row -->
@@ -60,7 +95,7 @@
<div class="space-y-1">
<Label class="text-xs font-medium">U.M.:</Label>
<div class="flex gap-1">
<Input id="um" bind:value={lineItem.unit_of_measure} class="h-8 text-xs flex-1" placeholder="U.M." />
<Input id="um" type="number" bind:value={lineItem.unit_of_measure} class="h-8 text-xs flex-1" placeholder="ID de U.M." />
<Button
variant="outline"
size="icon"
@@ -117,7 +152,9 @@
<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>
<option value="PROSEC">PROSEC</option>
<option value="ALADI">ALADI</option>
<option value="TLCS">TLCS</option>
</select>
</div>

View File

@@ -94,7 +94,7 @@
<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-6 text-xs" />
<Input id="localizacion_maquinaria" bind:value={descriptions.machinery_location} class="h-6 text-xs" />
</div>
<Label for="localizacion_maquinaria" class="text-xs">Location variable</Label>
</div>
@@ -102,7 +102,7 @@
<!-- Military Equipment -->
<div class="flex items-center space-x-1.5 ">
<Checkbox id="equipo_militar" />
<Checkbox id="equipo_militar" bind:checked={lineItem.is_military_mcia} />
<Label for="equipo_militar" class="text-xs font-normal">Enable if Item Contains Military Equipment</Label>
</div>
@@ -132,18 +132,18 @@
<div class="grid grid-cols-4 gap-2 items-end">
<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-6 text-xs" />
<Input id="fraccion_regla_octava" bind:value={descriptions.eighth_rule_fraction} class="h-6 text-xs" />
</div>
<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-6 text-xs text-right" />
<Input id="linea_regla" type="number" min="0" bind:value={descriptions.eighth_rule_line} class="h-6 text-xs text-right" />
</div>
</div>
</div>
<!-- Consider in a31 -->
<div class="flex items-center space-x-1.5 ">
<Checkbox id="a31" />
<Checkbox id="a31" bind:checked={descriptions.consider_a31} />
<Label for="a31" class="text-xs font-normal">Consider in A31</Label>
</div>

View File

@@ -18,6 +18,7 @@
<Label for="notas_identificadores" class="text-xs">Additional Identifiers / Notes:</Label>
<textarea
id="notas_identificadores"
bind:value={lineItem.wildcard_field}
class="flex min-h-[120px] w-full rounded-md border-2 border-input dark:border-zinc-600 bg-background dark:text-white px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 dark:focus-visible:border-zinc-400 dark:focus-visible:ring-zinc-400/50"
placeholder="Additional identifiers or notes..."
></textarea>

View File

@@ -83,6 +83,8 @@
if (response.data) {
items = response.data.items || [];
currentPage = 1;
// Wait for derived state to update before loading items
await new Promise(resolve => setTimeout(resolve, 0));
loadMoreItems();
}
} catch (error: any) {
@@ -150,6 +152,8 @@
tax_payment: false,
payment_method: undefined,
igi_amount: undefined,
is_military_mcia: false,
wildcard_field: undefined,
// Nested relations
financial: {
unit_cost_usd: undefined,
@@ -191,6 +195,10 @@
brand: undefined,
model: undefined,
has_serial: false,
eighth_rule_fraction: undefined,
eighth_rule_line: undefined,
consider_a31: false,
machinery_location: undefined,
},
reference: {
serie_id: undefined,
@@ -263,18 +271,61 @@
showDeleteDialog = true;
}
// Helper function to check if an object has any meaningful values
function hasValues(obj: any): boolean {
if (!obj || typeof obj !== 'object') return false;
return Object.values(obj).some(val =>
val !== undefined && val !== null && val !== '' &&
!(typeof val === 'object' && !hasValues(val))
);
}
// Clean nested data before sending to API
function cleanLineData(line: any) {
const cleaned: any = { ...line };
// Helper function to convert to number or undefined
const toNumberOrUndefined = (value: any): number | undefined => {
if (value === undefined || value === null || value === '') {
return undefined;
}
const numValue = Number(value);
return (!isNaN(numValue) && isFinite(numValue)) ? numValue : undefined;
};
// Convert integer fields
cleaned.part_number_id = toNumberOrUndefined(cleaned.part_number_id);
cleaned.component_part_number_id = toNumberOrUndefined(cleaned.component_part_number_id);
cleaned.class_id = toNumberOrUndefined(cleaned.class_id);
cleaned.unit_of_measure = toNumberOrUndefined(cleaned.unit_of_measure);
cleaned.alternate_unit = toNumberOrUndefined(cleaned.alternate_unit);
// Remove empty nested objects
if (!hasValues(cleaned.financial)) delete cleaned.financial;
if (!hasValues(cleaned.quantity)) delete cleaned.quantity;
if (!hasValues(cleaned.customs)) delete cleaned.customs;
if (!hasValues(cleaned.description)) delete cleaned.description;
if (!hasValues(cleaned.reference)) delete cleaned.reference;
if (!hasValues(cleaned.fa_data)) delete cleaned.fa_data;
return cleaned;
}
async function saveNewItem() {
if (!invoice?.id || !activeCompanyId) return;
isSaving = true;
try {
// Clean lines data before sending
const cleanedLines = (editingItem.lines || []).map(cleanLineData);
const response = await itemsApi.create(activeCompanyId, {
invoice_id: invoice.id,
reference_number: editingItem.reference_number,
order: editingItem.order,
warehouse: editingItem.warehouse,
location: editingItem.location,
lines: editingItem.lines || []
lines: cleanedLines
});
// Recargar items
@@ -300,12 +351,15 @@
isSaving = true;
try {
// Clean lines data before sending
const cleanedLines = (editingItem.lines || []).map(cleanLineData);
await itemsApi.update(selectedItem.id, activeCompanyId, {
reference_number: editingItem.reference_number,
order: editingItem.order,
warehouse: editingItem.warehouse,
location: editingItem.location,
lines: editingItem.lines || []
lines: cleanedLines
});
// Recargar items

View File

@@ -158,7 +158,7 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
}
// Logistics
payload.logistics = buildLogisticsData(generalFormData, observationFormData);
payload.logistics = buildLogisticsData(generalFormData, observationFormData, continuationFormData);
// Eliminar campos undefined para no enviarlos
Object.keys(payload).forEach(key => {
@@ -220,13 +220,30 @@ function buildFinancialsData(generalFormData: any, observationFormData: any, oth
};
}
function buildLogisticsData(generalFormData: any, observationFormData: any) {
// Retornar logistics como objeto único
function buildLogisticsData(generalFormData: any, observationFormData: any, continuationFormData: any) {
// Retornar logistics como objeto único con datos de continuación
return {
carrier_id: generalFormData?.carrier_id || null,
transport_type: generalFormData?.transport_type || 'none',
driver_name: generalFormData?.driver_name || null,
vehicle_num: generalFormData?.transport_num || null,
vehicle_num: generalFormData?.transport_num || continuationFormData?.numero_tipo_transporte || null,
incoterm: observationFormData?.incoterm || null,
// Campos de continuación mapeados a logistics
transport_num: continuationFormData?.numero_tipo_transporte || null,
is_rail: continuationFormData?.es_ferrocarril === 'si' ? true : false,
bill_number: continuationFormData?.numero_bl || null,
guide_number: continuationFormData?.cantidad_guias_embarque ? String(continuationFormData.cantidad_guias_embarque) : null,
destination_location: continuationFormData?.destino_origen || null,
origin_location: continuationFormData?.puerto_entrada || null,
// Checkboxes de continuación
equipment_reviewed: continuationFormData?.fue_revisado_equipo || false,
is_subdivision: continuationFormData?.sub_division || false,
acts_as_cd: continuationFormData?.funge_como_cd || false,
pedimento_arrived: continuationFormData?.llego_pedimento || false,
// Semáforos de continuación
green_light_mx: continuationFormData?.semaforo_verde_aduana_mexicana || false,
green_light_us: continuationFormData?.semaforo_verde_aduana_americana || false,
red_light_mx: continuationFormData?.semaforo_rojo_aduana_mexicana || false,
red_light_us: continuationFormData?.semaforo_rojo_aduana_americana || false,
};
}

View File

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