Merge branch 'fix/id-items' into development
This commit is contained in:
@@ -115,3 +115,21 @@ def validate_create(
|
||||
solution="Proporciona una descripción del item en español",
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
# 8. Validar customs.origin_country
|
||||
if not line.customs or not line.customs.origin_country:
|
||||
errors.add_error(
|
||||
field="customs.origin_country",
|
||||
message="País de Origen es obligatorio",
|
||||
solution="Selecciona el país de origen del item",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# 9. Validar customs.fraction_type
|
||||
if not line.customs or not line.customs.fraction_type:
|
||||
errors.add_error(
|
||||
field="customs.fraction_type",
|
||||
message="Tipo de Tarifa es obligatorio",
|
||||
solution="Selecciona el tipo de tarifa (GENERAL, PROSEC, ALADI, TLCS)",
|
||||
code="REQUIRED"
|
||||
)
|
||||
@@ -130,15 +130,26 @@ def validate_update(
|
||||
|
||||
# 8. Validar datos aduanales si se proporcionan
|
||||
if line.customs:
|
||||
# Validar país de origen
|
||||
if line.customs.origin_country is not None and not line.customs.origin_country:
|
||||
errors.add_error(
|
||||
field="customs.origin_country",
|
||||
message="Origin Country no puede estar vacío",
|
||||
solution="Selecciona el país de origen del item",
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
# Validar país de origen (OBLIGATORIO)
|
||||
if line.customs.origin_country is not None:
|
||||
if not line.customs.origin_country:
|
||||
errors.add_error(
|
||||
field="customs.origin_country",
|
||||
message="País de Origen es obligatorio",
|
||||
solution="Selecciona el país de origen del item",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# Validar tipo de tarifa (OBLIGATORIO)
|
||||
if line.customs.fraction_type is not None:
|
||||
if not line.customs.fraction_type:
|
||||
errors.add_error(
|
||||
field="customs.fraction_type",
|
||||
message="Tipo de Tarifa es obligatorio",
|
||||
solution="Selecciona el tipo de tarifa (GENERAL, PROSEC, ALADI, TLCS)",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# Validar preferencia arancelaria
|
||||
if line.customs.preference is not None and not line.customs.preference:
|
||||
errors.add_error(
|
||||
@@ -181,20 +192,18 @@ def validate_update(
|
||||
solution="Selecciona una forma de pago válida del catálogo",
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
|
||||
# 9. Validar descripción en español si se proporciona
|
||||
if line.description and hasattr(line.description, "description_spanish"):
|
||||
if (
|
||||
line.description.description_spanish is not None
|
||||
and not line.description.description_spanish
|
||||
):
|
||||
errors.add_error(
|
||||
field="description.description_spanish",
|
||||
message="La descripción en español no puede estar vacía",
|
||||
solution="Proporciona una descripción del item en español",
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# 9. Validar descripción en español (OBLIGATORIA)
|
||||
if line.description and hasattr(line.description, 'description_spanish'):
|
||||
if line.description.description_spanish is not None:
|
||||
if not line.description.description_spanish.strip():
|
||||
errors.add_error(
|
||||
field="description.description_spanish",
|
||||
message="Descripción en Español es obligatoria",
|
||||
solution="Proporciona una descripción del item en español",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# 10. Validar subpartidas si se actualizan
|
||||
if line.fa_data and line.fa_data.is_subitem:
|
||||
# Es subpartida, debe tener partida principal
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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: Optional[int] = Field(None, description="Part number")
|
||||
component_part_number: Optional[int] = Field(
|
||||
None, description="Component 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", alias="component_part_number", serialization_alias="component_part_number_id"
|
||||
)
|
||||
class_id: Optional[int] = Field(None, description="Class code")
|
||||
|
||||
@@ -253,6 +257,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
|
||||
|
||||
@@ -276,6 +276,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)
|
||||
@@ -504,6 +510,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()
|
||||
|
||||
@@ -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:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="num_parte" type="number" bind:value={lineItem.part_number} 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">
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
editingItem = $bindable(),
|
||||
invoice,
|
||||
onSave,
|
||||
onCancel,
|
||||
isSaving = false
|
||||
}: {
|
||||
open: boolean;
|
||||
@@ -30,6 +31,7 @@
|
||||
editingItem: Partial<Item>;
|
||||
invoice: Invoice | null;
|
||||
onSave: () => void;
|
||||
onCancel?: () => void;
|
||||
isSaving?: boolean;
|
||||
} = $props();
|
||||
|
||||
@@ -55,7 +57,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onclick={() => open = false} class="h-7 w-7 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<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>
|
||||
</div>
|
||||
@@ -164,7 +166,7 @@
|
||||
|
||||
<footer class="bg-white dark:bg-zinc-950 border-t border-zinc-200 dark:border-zinc-800 px-3 py-1.5 shadow-sm shrink-0">
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<Button variant="outline" size="sm" onclick={() => open = false} disabled={isSaving} class="h-7 text-xs px-2">
|
||||
<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">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
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';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let {
|
||||
lineItem = $bindable(),
|
||||
@@ -26,6 +27,9 @@
|
||||
let showCountryDialog = $state(false);
|
||||
let showFractionDialog = $state(false);
|
||||
|
||||
// Track previous class_id to detect changes
|
||||
let previousClassId = $state<number | undefined>(undefined);
|
||||
|
||||
// Initialize from existing data
|
||||
$effect(() => {
|
||||
if (lineItem.class_code) {
|
||||
@@ -33,22 +37,83 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for class_id changes and update descriptions automatically
|
||||
$effect(() => {
|
||||
const currentClassId = lineItem.class_id;
|
||||
const activeCompanyId = companyStore?.activeCompany?.id;
|
||||
|
||||
// Only fetch if class_id changed, is valid, and we have a company
|
||||
if (currentClassId && currentClassId !== previousClassId && activeCompanyId) {
|
||||
previousClassId = currentClassId;
|
||||
|
||||
// Fetch all classes and find the one with matching ID
|
||||
fetch(`/api-sveltekit/classes?company_id=${activeCompanyId}&limit=100`)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error('Failed to fetch classes');
|
||||
})
|
||||
.then(data => {
|
||||
const classes = data.items || [];
|
||||
const classItem = classes.find((c: any) => c.id === currentClassId);
|
||||
|
||||
if (classItem) {
|
||||
// Store the code and description in the lineItem for display
|
||||
(lineItem as any).class_code = classItem.class_code;
|
||||
(lineItem as any).class_unit_of_measure = classItem.unit_of_measure;
|
||||
(lineItem as any).class_description = classItem.description_es || classItem.description_en;
|
||||
|
||||
// Update description fields if description object exists
|
||||
if ((lineItem as any).description) {
|
||||
if (classItem.description_es) {
|
||||
(lineItem as any).description.description_spanish = classItem.description_es;
|
||||
}
|
||||
if (classItem.description_en) {
|
||||
(lineItem as any).description.description_english = classItem.description_en;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching class data:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
// Update description fields if description object exists
|
||||
if ((lineItem as any).description) {
|
||||
if (classItem.description_es) {
|
||||
(lineItem as any).description.description_spanish = classItem.description_es;
|
||||
}
|
||||
if (classItem.description_en) {
|
||||
(lineItem as any).description.description_english = 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 +128,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 +148,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 +165,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 +183,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 +198,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 +217,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,12 +243,16 @@
|
||||
<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">
|
||||
<div class="space-y-1">
|
||||
<Label for="tipo_tarifa" class="text-xs font-medium">Tariff Type: <span class="text-red-500">*</span></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={undefined}>Selecciona...</option>
|
||||
<option value="GENERAL">GENERAL</option>
|
||||
<option value="PROSEC">PROSEC</option>
|
||||
<option value="ALADI">ALADI</option>
|
||||
|
||||
@@ -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>
|
||||
@@ -14,6 +14,7 @@
|
||||
editingItem = $bindable(),
|
||||
invoice,
|
||||
onSave,
|
||||
onCancel,
|
||||
isSaving = false
|
||||
}: {
|
||||
open: boolean;
|
||||
@@ -21,6 +22,7 @@
|
||||
editingItem: Partial<Item>;
|
||||
invoice: Invoice | null;
|
||||
onSave: () => void;
|
||||
onCancel?: () => void;
|
||||
isSaving?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
@@ -258,7 +260,7 @@
|
||||
</Tabs.Root>
|
||||
|
||||
<Sheet.Footer class="mt-6 gap-2">
|
||||
<Button variant="outline" onclick={() => open = false} disabled={isSaving}>
|
||||
<Button variant="outline" onclick={() => onCancel?.()} disabled={isSaving}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onclick={onSave} disabled={isSaving}>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
let isEditMode = $state(false);
|
||||
let showDeleteDialog = $state(false);
|
||||
let selectedItem = $state<Item | null>(null);
|
||||
let originalItemData = $state<Partial<Item> | null>(null); // Guardar estado original para cancelar
|
||||
let editingItem = $state<Partial<Item>>({
|
||||
invoice_id: undefined,
|
||||
reference_number: '',
|
||||
@@ -179,7 +180,7 @@
|
||||
},
|
||||
customs: {
|
||||
fraction: undefined,
|
||||
fraction_type: 'GENERAL',
|
||||
fraction_type: undefined,
|
||||
american_fraction: undefined,
|
||||
origin_country: undefined,
|
||||
destination_country: undefined,
|
||||
@@ -211,10 +212,112 @@
|
||||
isEditMode = true;
|
||||
selectedItem = lineData.full_item;
|
||||
// Deep clone and normalize numeric values
|
||||
editingItem = normalizeItemData({ ...lineData.full_item });
|
||||
editingItem = normalizeItemData(JSON.parse(JSON.stringify(lineData.full_item)));
|
||||
// Guardar una copia del estado original para restaurar al cancelar
|
||||
originalItemData = JSON.parse(JSON.stringify(editingItem));
|
||||
// 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 +403,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 +444,7 @@
|
||||
order: editingItem.order,
|
||||
warehouse: editingItem.warehouse,
|
||||
location: editingItem.location,
|
||||
lines: editingItem.lines || []
|
||||
lines: cleanedLines
|
||||
});
|
||||
|
||||
// Verificar si hay errores de validación
|
||||
@@ -384,12 +503,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
|
||||
@@ -444,6 +566,65 @@
|
||||
}
|
||||
|
||||
function saveItem() {
|
||||
// Validar campos obligatorios antes de guardar
|
||||
const line = editingItem.lines?.[0];
|
||||
const missingFields: string[] = [];
|
||||
|
||||
if (!line) {
|
||||
toast.warning('Error de datos', {
|
||||
description: 'No se encontró información de la línea del item'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Clase
|
||||
if (!line.class_id) {
|
||||
missingFields.push('Clase');
|
||||
}
|
||||
|
||||
// 2. Cantidad
|
||||
if (!line.quantity?.quantity || line.quantity.quantity <= 0) {
|
||||
missingFields.push('Cantidad');
|
||||
}
|
||||
|
||||
// 3. Unidad de Medida
|
||||
if (!line.unit_of_measure) {
|
||||
missingFields.push('U.M. (Unidad de Medida)');
|
||||
}
|
||||
|
||||
// 4. Costo Unitario (al menos uno debe estar presente)
|
||||
const hasCost = line.financial?.unit_cost_usd ||
|
||||
line.financial?.unit_cost_mxn ||
|
||||
line.financial?.unit_cost_capture;
|
||||
if (!hasCost) {
|
||||
missingFields.push('Costo Unitario (USD, MXN o Captura)');
|
||||
}
|
||||
|
||||
// 5. País de Origen
|
||||
if (!line.customs?.origin_country) {
|
||||
missingFields.push('País de Origen');
|
||||
}
|
||||
|
||||
// 6. Tipo de Tarifa
|
||||
if (!line.customs?.fraction_type) {
|
||||
missingFields.push('Tipo de Tarifa');
|
||||
}
|
||||
|
||||
// 7. Descripción en Español
|
||||
if (!line.description?.description_spanish?.trim()) {
|
||||
missingFields.push('Descripción en Español');
|
||||
}
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
const fieldsList = missingFields.join('\n• ');
|
||||
toast.warning('Completa los campos obligatorios', {
|
||||
description: `Faltan los siguientes campos:\n• ${fieldsList}`,
|
||||
duration: 10000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Si pasa la validación, continuar con el guardado
|
||||
if (isEditMode) {
|
||||
saveEditedItem();
|
||||
} else {
|
||||
@@ -475,6 +656,15 @@
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelEdit() {
|
||||
// Restaurar los datos originales si estamos editando
|
||||
if (isEditMode && originalItemData) {
|
||||
editingItem = JSON.parse(JSON.stringify(originalItemData));
|
||||
}
|
||||
// Cerrar el sheet
|
||||
showItemSheet = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-4 grid-rows-1 gap-3">
|
||||
@@ -596,6 +786,7 @@
|
||||
bind:editingItem={editingItem}
|
||||
{invoice}
|
||||
onSave={saveItem}
|
||||
onCancel={handleCancelEdit}
|
||||
{isSaving}
|
||||
/>
|
||||
{:else}
|
||||
@@ -605,6 +796,7 @@
|
||||
bind:editingItem={editingItem}
|
||||
{invoice}
|
||||
onSave={saveItem}
|
||||
onCancel={handleCancelEdit}
|
||||
{isSaving}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
87
frontend/src/routes/api-sveltekit/classes/[id]/+server.ts
Normal file
87
frontend/src/routes/api-sveltekit/classes/[id]/+server.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
91
frontend/src/routes/api-sveltekit/parts/+server.ts
Normal file
91
frontend/src/routes/api-sveltekit/parts/+server.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
87
frontend/src/routes/api-sveltekit/parts/[id]/+server.ts
Normal file
87
frontend/src/routes/api-sveltekit/parts/[id]/+server.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user