feat(line_items): update part number and component part number fields to use integers; enhance schemas and services for better data handling

This commit is contained in:
Galindo97
2026-01-22 09:52:21 -06:00
parent ce8712436e
commit 9b32c019ef
12 changed files with 777 additions and 36 deletions

View File

@@ -40,11 +40,11 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
line_number: Mapped[int] = mapped_column(Integer) # LINEAIMPO/LINEAEXPO/LINEA
# Part identification
part_number: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")
part_number: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("a76.parts.id")
) # NUMPARTE
component_part_number: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")
component_part_number: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("a76.parts.id")
) # NUMPARTECOM
class_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("a76.classes.id")

View File

@@ -44,12 +44,16 @@ from api.v1.modules.a24.fa.fa_item_lines.dto import (
class LineItemBase(BaseModel):
"""Base schema for line items"""
model_config = ConfigDict(populate_by_name=True)
line_number: int = Field(..., description="Line number")
# Part identification
part_number_id: Optional[int] = Field(None, description="Part number")
part_number_id: Optional[int] = Field(
None, description="Part number", alias="part_number", serialization_alias="part_number_id"
)
component_part_number_id: Optional[int] = Field(
None, description="Component part number"
None, description="Component part number", alias="component_part_number", serialization_alias="component_part_number_id"
)
class_id: Optional[int] = Field(None, description="Class code")
@@ -257,6 +261,12 @@ class LineItemResponse(LineItemBase):
if hasattr(data, key):
result[key] = getattr(data, key)
# Map model field names to schema field names for aliased fields
if hasattr(data, "part_number"):
result["part_number_id"] = data.part_number
if hasattr(data, "component_part_number"):
result["component_part_number_id"] = data.component_part_number
# Extract class info
if hasattr(data, "class_info") and data.class_info is not None:
result["class_code"] = data.class_info.class_code

View File

@@ -269,6 +269,12 @@ class ItemService:
line_dict["tenant_id"] = tenant_id
line_dict["company_id"] = company_id
# Map schema field names to model field names
if "part_number_id" in line_dict:
line_dict["part_number"] = line_dict.pop("part_number_id")
if "component_part_number_id" in line_dict:
line_dict["component_part_number"] = line_dict.pop("component_part_number_id")
# Create line item
db_line = LineItem(**line_dict)
db.add(db_line)
@@ -485,6 +491,12 @@ class ItemService:
line_dict["tenant_id"] = tenant_id
line_dict["company_id"] = company_id
# Map schema field names to model field names
if "part_number_id" in line_dict:
line_dict["part_number"] = line_dict.pop("part_number_id")
if "component_part_number_id" in line_dict:
line_dict["component_part_number"] = line_dict.pop("component_part_number_id")
db_line = LineItem(**line_dict)
db.add(db_line)
db.flush()

View File

@@ -2,6 +2,9 @@
import * as RadioGroup from '$lib/components/ui/radio-group';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { Folder } from 'lucide-svelte';
import PartNumberDialog from './part-number-dialog.svelte';
import type { LineItem, LineDescriptions } from '$lib/api/dashboard/a76/items';
let {
@@ -12,6 +15,8 @@
descriptions: LineDescriptions;
} = $props();
let showPartDialog = $state(false);
// Initialize fa_data for fixed asset system
if (!lineItem.fa_data) {
lineItem.fa_data = {};
@@ -29,8 +34,18 @@
if (!lineItem.fa_data) lineItem.fa_data = {};
lineItem.fa_data.contains_subitems = val === 'si';
}
function handlePartSelect(part: any) {
lineItem.part_number_id = part.id;
// Store part number for display
(lineItem as any).part_number = part.part_number;
(lineItem as any).part_description_es = part.description_spanish;
(lineItem as any).part_description_en = part.description_english;
}
</script>
<PartNumberDialog bind:open={showPartDialog} onSelect={handlePartSelect} />
<div class="lg:col-span-5 space-y-3">
<!-- Is Item/Subitem and Contains Sub-Items -->
<div class="grid grid-cols-2 gap-2">
@@ -72,11 +87,32 @@
<!-- 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 ID: <span class="text-red-500">*</span></Label>
<Label for="num_parte" class="text-xs">Número de Parte: <span class="text-red-500">*</span></Label>
<div class="flex gap-1">
<Input id="num_parte" type="number" bind:value={lineItem.part_number_id} class="h-7 text-xs" placeholder="ID de número de parte" />
<Input
id="num_parte"
type="text"
value={(lineItem as any).part_number || ''}
readonly
class="h-7 text-xs flex-1 bg-muted cursor-pointer"
placeholder="Selecciona número de parte"
onclick={() => (showPartDialog = true)}
/>
<Button
variant="outline"
size="icon"
class="h-7 w-7 shrink-0"
onclick={() => (showPartDialog = true)}
>
<Folder class="w-3.5 h-3.5" />
</Button>
</div>
<p class="text-xs text-muted-foreground">ID de número de parte existente en catálogo</p>
{#if (lineItem as any).part_description_es}
<p class="text-xs text-muted-foreground truncate">{(lineItem as any).part_description_es}</p>
{/if}
{#if lineItem.part_number_id}
<p class="text-xs text-muted-foreground italic">ID: {lineItem.part_number_id}</p>
{/if}
</div>
<div class="space-y-1">

View File

@@ -35,20 +35,27 @@
function handleClassSelect(classItem: any) {
lineItem.class_id = classItem.id;
// Store the code in the lineItem for display
// Store the unit of measure and description for display
(lineItem as any).class_unit_of_measure = classItem.unit_of_measure;
(lineItem as any).class_code = classItem.class_code;
(lineItem as any).class_description = classItem.description_es || classItem.description_en;
}
function handleUnitSelect(unit: any) {
lineItem.unit_of_measure = unit.id;
// Store unit code for display
(lineItem as any).unit_code = unit.code;
(lineItem as any).unit_description = unit.description || unit.description_en;
}
function handleCountrySelect(country: any) {
customs.origin_country = country.mex_key || country.m3_key;
customs.origin_country = country.m3_key || country.mex_key;
(customs as any).origin_country_name = country.description || country.description_en;
}
function handleFractionSelect(fraction: any) {
customs.fraction = fraction.fraction;
(customs as any).fraction_description = fraction.description;
}
</script>
@@ -63,14 +70,16 @@
<div class="grid grid-cols-2 gap-4">
<!-- Class - Full Width -->
<div class="col-span-2 space-y-1">
<Label for="clase" class="text-xs font-medium">Clase (ID): <span class="text-red-500">*</span></Label>
<Label for="clase" class="text-xs font-medium">Clase: <span class="text-red-500">*</span></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)"
type="text"
value={(lineItem as any).class_code || ''}
readonly
class="h-8 text-xs flex-1 bg-muted cursor-pointer"
placeholder="Selecciona una clase"
onclick={() => (showClassDialog = true)}
/>
<Button
variant="outline"
@@ -81,8 +90,11 @@
<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 (lineItem as any).class_description}
<p class="text-xs text-muted-foreground">{(lineItem as any).class_description}</p>
{/if}
{#if lineItem.class_id}
<p class="text-xs text-muted-foreground italic">ID: {lineItem.class_id}</p>
{/if}
</div>
@@ -95,7 +107,15 @@
<div class="space-y-1">
<Label class="text-xs font-medium">U.M.: <span class="text-red-500">*</span></Label>
<div class="flex gap-1">
<Input id="um" type="number" bind:value={lineItem.unit_of_measure} class="h-8 text-xs flex-1" placeholder="ID de U.M." />
<Input
id="um"
type="text"
value={(lineItem as any).unit_code || ''}
readonly
class="h-8 text-xs flex-1 bg-muted cursor-pointer"
placeholder="Selecciona U.M."
onclick={() => (showUnitDialog = true)}
/>
<Button
variant="outline"
size="icon"
@@ -105,6 +125,9 @@
<Folder class="w-4 h-4" />
</Button>
</div>
{#if (lineItem as any).unit_description}
<p class="text-xs text-muted-foreground truncate">{(lineItem as any).unit_description}</p>
{/if}
</div>
<!-- Unit Cost and Fraction -->
@@ -117,9 +140,16 @@
</div>
<div class="space-y-1">
<Label for="fraccion" class="text-xs font-medium">Fraction:</Label>
<Label for="fraccion" class="text-xs font-medium">Fracción:</Label>
<div class="flex gap-1">
<Input id="fraccion" bind:value={customs.fraction} class="h-8 text-xs text-center flex-1" />
<Input
id="fraccion"
value={customs.fraction || ''}
readonly
class="h-8 text-xs text-center flex-1 bg-muted cursor-pointer"
placeholder="Selecciona fracción"
onclick={() => (showFractionDialog = true)}
/>
<Button
variant="outline"
size="icon"
@@ -129,13 +159,23 @@
<Folder class="w-4 h-4" />
</Button>
</div>
{#if (customs as any).fraction_description}
<p class="text-xs text-muted-foreground truncate">{(customs as any).fraction_description}</p>
{/if}
</div>
<!-- Origin Country and Tariff Type -->
<div class="space-y-1">
<Label for="pais_origen" class="text-xs font-medium">Origin Country: <span class="text-red-500">*</span></Label>
<Label for="pais_origen" class="text-xs font-medium">País de Origen: <span class="text-red-500">*</span></Label>
<div class="flex gap-1">
<Input id="pais_origen" bind:value={customs.origin_country} class="h-8 text-xs flex-1" />
<Input
id="pais_origen"
value={customs.origin_country || ''}
readonly
class="h-8 text-xs flex-1 bg-muted cursor-pointer"
placeholder="Selecciona país"
onclick={() => (showCountryDialog = true)}
/>
<Button
variant="outline"
size="icon"
@@ -145,6 +185,9 @@
<Folder class="w-4 h-4" />
</Button>
</div>
{#if (customs as any).origin_country_name}
<p class="text-xs text-muted-foreground truncate">{(customs as any).origin_country_name}</p>
{/if}
</div>
<div class="flex items-end gap-6">

View File

@@ -0,0 +1,175 @@
<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?: (part: any) => void;
} = $props();
let searchQuery = $state('');
let isSearching = $state(false);
let parts = $state<any[]>([]);
let displayedParts = $state<any[]>([]);
let currentPage = $state(1);
let itemsPerPage = 10;
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
);
$effect(() => {
if (open) {
searchParts();
}
});
$effect(() => {
currentPage = 1;
loadMoreParts();
});
async function searchParts() {
const activeCompanyId = companyStore?.activeCompany?.id;
if (!activeCompanyId) {
toast.error('No hay compañía activa');
return;
}
isSearching = true;
try {
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');
}
const data = await response.json();
parts = data.items || [];
loadMoreParts();
} catch (error) {
console.error('Error searching parts:', error);
toast.error('Error al buscar números de parte');
parts = [];
} finally {
isSearching = false;
}
}
function loadMoreParts() {
const start = 0;
const end = currentPage * itemsPerPage;
displayedParts = filteredParts.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 && displayedParts.length < filteredParts.length) {
currentPage++;
loadMoreParts();
}
}
function handleSelect(part: any) {
if (onSelect) {
onSelect(part);
}
open = false;
}
</script>
<Dialog.Root bind:open>
<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.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 número de parte 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-[150px]">Número de Parte</Table.Head>
<Table.Head>Descripción (ES)</Table.Head>
<Table.Head>Descripción (EN)</Table.Head>
<Table.Head class="w-[100px]">Clase</Table.Head>
<Table.Head class="w-[100px]"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if displayedParts.length === 0}
<Table.Row>
<Table.Cell colspan={5} class="text-center py-8 text-muted-foreground">
No se encontraron números de parte
</Table.Cell>
</Table.Row>
{:else}
{#each displayedParts as 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.part_class || '-'}</Table.Cell>
<Table.Cell>
<Button variant="ghost" size="sm" class="h-8">
Seleccionar
</Button>
</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
{/if}
</div>
<Dialog.Footer class="mt-4">
<p class="text-xs text-muted-foreground">
Mostrando {displayedParts.length} de {filteredParts.length} resultados
</p>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -212,9 +212,109 @@
selectedItem = lineData.full_item;
// Deep clone and normalize numeric values
editingItem = normalizeItemData({ ...lineData.full_item });
// Enrich with descriptive data
enrichItemData(editingItem);
showItemSheet = true;
}
// Enrich item with descriptive data for display
async function enrichItemData(item: Partial<Item>) {
if (!item.lines || item.lines.length === 0 || !activeCompanyId) return;
const line = item.lines[0];
// Load class data
if (line.class_id) {
try {
const response = await fetch(
`/api-sveltekit/classes/${line.class_id}?company_id=${activeCompanyId}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
const classData = await response.json();
(line as any).class_code = classData.class_code;
(line as any).class_unit_of_measure = classData.unit_of_measure;
(line as any).class_description = classData.description_es || classData.description_en;
}
} catch (error) {
console.error('Error loading class data:', error);
}
}
// Load part number data
if (line.part_number_id) {
try {
const response = await fetch(
`/api-sveltekit/parts/${line.part_number_id}?company_id=${activeCompanyId}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
const partData = await response.json();
(line as any).part_number = partData.part_number;
(line as any).part_description_es = partData.description_spanish;
(line as any).part_description_en = partData.description_english;
}
} catch (error) {
console.error('Error loading part data:', error);
}
}
// Load unit of measure data
if (line.unit_of_measure) {
try {
const response = await fetch(
`/api-sveltekit/units-of-measure/${line.unit_of_measure}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
const unitData = await response.json();
(line as any).unit_code = unitData.code;
(line as any).unit_description = unitData.description || unitData.description_en;
}
} catch (error) {
console.error('Error loading unit data:', error);
}
}
// Load country data (if needed)
if (line.customs?.origin_country) {
try {
const response = await fetch(
`/api-sveltekit/countries?search=${line.customs.origin_country}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
const data = await response.json();
if (data.items && data.items.length > 0) {
const country = data.items[0];
(line.customs as any).origin_country_name = country.description || country.description_en;
}
}
} catch (error) {
console.error('Error loading country data:', error);
}
}
// Load fraction data (if needed)
if (line.customs?.fraction) {
try {
const response = await fetch(
`/api-sveltekit/tariff-fractions?search=${line.customs.fraction}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
const data = await response.json();
if (data.items && data.items.length > 0) {
const fraction = data.items[0];
(line.customs as any).fraction_description = fraction.description;
}
}
} catch (error) {
console.error('Error loading fraction data:', error);
}
}
}
// Normalize numeric values from strings to numbers
function normalizeItemData(item: Partial<Item>): Partial<Item> {
if (item.lines && item.lines.length > 0) {
@@ -300,6 +400,22 @@
cleaned.unit_of_measure = toNumberOrUndefined(cleaned.unit_of_measure);
cleaned.alternate_unit = toNumberOrUndefined(cleaned.alternate_unit);
// Remove display-only fields
delete cleaned.class_code;
delete cleaned.class_unit_of_measure;
delete cleaned.class_description;
delete cleaned.part_number;
delete cleaned.part_description_es;
delete cleaned.part_description_en;
delete cleaned.unit_code;
delete cleaned.unit_description;
// Remove display-only fields from nested objects
if (cleaned.customs) {
delete cleaned.customs.origin_country_name;
delete cleaned.customs.fraction_description;
}
// Remove empty nested objects
if (!hasValues(cleaned.financial)) delete cleaned.financial;
if (!hasValues(cleaned.quantity)) delete cleaned.quantity;
@@ -325,7 +441,7 @@
order: editingItem.order,
warehouse: editingItem.warehouse,
location: editingItem.location,
lines: editingItem.lines || []
lines: cleanedLines
});
// Verificar si hay errores de validación
@@ -384,12 +500,15 @@
isSaving = true;
try {
// Clean lines data before sending
const cleanedLines = (editingItem.lines || []).map(cleanLineData);
const response = 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
});
// Verificar si hay errores de validación

View File

@@ -1,7 +1,6 @@
import type { ColumnDef } from "@tanstack/table-core";
import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js";
import { createRawSnippet } from "svelte";
import DataTableActions from "./data-table-actions.svelte";
import type { Pedimento } from "$lib/api/dashboard/a76/pedimentos";
/**
@@ -201,7 +200,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
return renderSnippet(dateSnippet, { date: formatDate(row.original.pedimento_dates?.payment_date) });
}
},
{
/*{
accessorKey: "pedimento_config_update_rectification.pediment_rectifed_18",
header: "Pedimento 18",
cell: ({ row }) => {
@@ -214,8 +213,8 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
});
return renderSnippet(ped18Snippet, { value: row.original.pedimento_config_update_rectification?.pediment_rectifed_18 });
}
},
{
},*/
/*{
accessorKey: "pedimento_config_update_rectification.r1",
header: "Pedimento R1",
cell: ({ row }) => {
@@ -228,7 +227,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
});
return renderSnippet(r1Snippet, { value: row.original.pedimento_config_update_rectification?.r1 });
}
},
},*/
{
accessorKey: "pedimento_validation.electronic_signature",
header: "Acuse Electrónico",
@@ -375,13 +374,8 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
});
return renderSnippet(dateSnippet, { date: formatDate(row.original.created_at) });
}
},
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
}
}
// Columna de acciones eliminada - ahora usamos botones en el footer
];
}

View File

@@ -0,0 +1,87 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ cookies, url, params }) => {
const token = cookies.get('access_token');
const { id } = params;
// Obtener company_id de la cookie o query params
const companyId = cookies.get('active_company_id') || url.searchParams.get('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}/`;
try {
const fetchUrl = `${baseUrl}v1/a76/classes/${id}?company_id=${companyId}`;
console.log('Fetching class 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 class',
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 class API route:', error);
return new Response(
JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
};

View File

@@ -0,0 +1,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/parts?${queryString}`;
console.log('Fetching parts 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 parts',
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 parts API route:', error);
return new Response(
JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
};

View File

@@ -0,0 +1,87 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ cookies, url, params }) => {
const token = cookies.get('access_token');
const { id } = params;
// Obtener company_id de la cookie o query params
const companyId = cookies.get('active_company_id') || url.searchParams.get('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}/`;
try {
const fetchUrl = `${baseUrl}v1/a76/parts/${id}?company_id=${companyId}`;
console.log('Fetching part 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 part',
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 part API route:', error);
return new Response(
JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
};

View File

@@ -0,0 +1,87 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ cookies, params, url }) => {
const token = cookies.get('access_token');
const { id } = params;
// Obtener company_id de la cookie o query params
const companyId = cookies.get('active_company_id') || url.searchParams.get('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}/`;
try {
const fetchUrl = `${baseUrl}v1/a76/units-of-measure/${id}?company_id=${companyId}`;
console.log('Fetching unit of measure 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 unit of measure',
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 unit of measure 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'
}
}
);
}
};