Se mejoro el formulario de partes

This commit is contained in:
2026-01-06 13:29:46 -06:00
parent 28427adefa
commit 7158967b17

View File

@@ -10,8 +10,9 @@
import { Textarea } from '$lib/components/ui/textarea';
import * as Tabs from '$lib/components/ui/tabs';
import * as Card from '$lib/components/ui/card';
import * as Select from "$lib/components/ui/select";
import { Switch } from "$lib/components/ui/switch";
import { ArrowLeft, LoaderCircle, Save, CheckCircle2, XCircle, Package, DollarSign } from 'lucide-svelte';
import { ArrowLeft, LoaderCircle, Save, Package, DollarSign, FileText, Settings, Image as ImageIcon } from 'lucide-svelte';
import { companyStore } from '$lib/stores/company.svelte';
import { partsApi, type PartCreate } from '$lib/api/dashboard/a76/parts';
@@ -35,27 +36,30 @@
part_class: '',
country_of_origin: 'MEX',
unit_of_measure: 'PZ',
// Opcionales 1
fraction: '',
us_fraction: '',
// Costos y Pesos
unit_weight: 0,
weight_type: 'KG',
// Opcionales 2
unit_cost: 0,
currency_key: 'USD',
added_value: 0,
us_fraction: '',
// Opciones
supplier: '',
fda_key: '',
fcc_key: '',
part_photo: '',
// Opcionales 2
fda_key: '',
// Otros
commercial_part_number: '',
fraction: '', // Fraccion MX
eccn: '',
license_code: '',
export_code: '',
exclusion_symbol: '',
// Costos
unit_cost: 0,
currency_key: 'USD',
added_value: 0,
commercial_part_number: '',
// Otros
alternate_unit_measure: '',
part_photo: '',
is_active: true
});
@@ -121,18 +125,15 @@
}
}
async function handleSubmit() {
error = null;
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) { error = 'No hay una compañía activa seleccionada'; return; }
if (!formData.client_id) { error = 'ID Cliente requerido'; return; }
if (!formData.client_id) { error = 'ID Cliente requerido (Pestaña Otros)'; return; }
if (!formData.part_number.trim()) { error = 'Número de Parte requerido'; return; }
if (!formData.fraction.trim()) { error = 'Fracción MX requerida'; return; }
loading = true;
try {
const commonData = {
description_spanish: formData.description_spanish || null,
description_english: formData.description_english || null,
@@ -165,60 +166,44 @@
if (isEdit && id) {
const response = await partsApi.update(id, commonData, activeCompanyId);
if (response.error) throw new Error(response.error);
console.log("Registro actualizado con éxito");
} else {
const createData: PartCreate = {
...commonData,
company_id: activeCompanyId,
client_id: Number(formData.client_id),
part_number: formData.part_number
};
const response = await partsApi.create(createData, activeCompanyId);
if (response.error) throw new Error(response.error);
console.log("Registro creado con éxito");
}
goto('/dashboard/goods/parts');
} catch (e: any) {
console.error("Error en el guardado:", e);
if (e.message?.includes('already exists')) {
error = `El número de parte ${formData.part_number} ya existe para este cliente.`;
} else if (e.message?.includes('foreign key constraint')) {
error = `Error: Uno de los datos (País, Moneda o Unidad) no existe en el sistema.`;
} else if (e.message?.includes('foreign key')) {
error = `Error: Uno de los catálogos (País, Moneda, UM) no es válido.`;
} else {
error = e.message || 'Ocurrió un error inesperado al guardar';
error = e.message || 'Error inesperado al guardar';
}
} finally {
loading = false;
}
}
</script>
<div class="w-full mx-auto max-w-6xl py-6 px-4 space-y-6 pb-48">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<Button variant="outline" size="icon" href="/dashboard/goods/parts">
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
<p class="text-xs text-muted-foreground">{isEdit ? 'Modificando registro existente' : 'Creando nuevo registro'}</p>
</div>
<div class="flex items-center gap-4">
<Button variant="outline" size="icon" href="/dashboard/goods/parts">
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
<p class="text-muted-foreground">Gestión detallada de números de parte.</p>
</div>
{#if isEdit}
<div class="flex items-center gap-2 px-3 py-1 rounded-full border text-xs font-medium {formData.is_active ? 'bg-green-100 text-green-700 border-green-200' : 'bg-red-100 text-red-700 border-red-200'}">
{#if formData.is_active} <CheckCircle2 class="w-3 h-3 mr-1"/> Activo {:else} <XCircle class="w-3 h-3 mr-1"/> Inactivo {/if}
</div>
{/if}
</div>
{#if error}
@@ -227,210 +212,253 @@
</div>
{/if}
{#key id}
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-6">
<Card.Root>
<Card.Content class="p-6">
<Tabs.Root value="general" class="w-full">
<div class="min-h-[450px]">
<div class="min-h-[500px]">
<Tabs.Content value="general" class="space-y-4 pt-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div class="grid gap-2">
<Label for="client_id" class="required">ID Cliente</Label>
<Input type="number" id="client_id" bind:value={formData.client_id} placeholder="Ej. 11" />
</div>
<div class="grid gap-2">
<Label for="part_number" class="required">Número de Parte</Label>
<Input id="part_number" bind:value={formData.part_number} disabled={isEdit} maxlength={50} class="font-mono" />
</div>
<div class="grid gap-2">
<Label for="part_class">Clase / Familia</Label>
<Input id="part_class" bind:value={formData.part_class} maxlength={8} />
</div>
</div>
<div class="space-y-4">
<div class="grid gap-2">
<Label for="country">País de Origen</Label>
<Input id="country" bind:value={formData.country_of_origin} maxlength={3} placeholder="MEX" />
</div>
<div class="grid gap-2">
<Label for="uom" class="required">Unidad Medida (UM)</Label>
<select id="uom" bind:value={formData.unit_of_measure} class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
<option value="PZ">Pieza (PZ)</option>
<option value="KG">Kilogramo (KG)</option>
<option value="EA">Elemento (EA)</option>
<option value="L">Litro (L)</option>
</select>
</div>
</div>
</div>
<Tabs.Content value="general" class="space-y-6 pt-4 animate-in fade-in duration-300">
<div class="grid gap-2">
<Label for="desc_es">Descripción (Español)</Label>
<Textarea id="desc_es" bind:value={formData.description_spanish} maxlength={500} />
<Label for="part_number" class="text-base font-semibold required">Número de Parte</Label>
<Input id="part_number" bind:value={formData.part_number} disabled={isEdit} maxlength={50} class="text-lg font-mono" placeholder="Ej: 123-ABC-456"/>
</div>
<div class="grid gap-2">
<Label for="desc_en">Descripción (Inglés)</Label>
<Textarea id="desc_en" bind:value={formData.description_english} maxlength={500} />
</div>
</Tabs.Content>
<Tabs.Content value="opt1" class="space-y-4 pt-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-4 border rounded-lg bg-slate-50 dark:bg-slate-900/50 space-y-4">
<h3 class="font-semibold text-sm">Clasificación Arancelaria</h3>
<div class="space-y-3 p-4 border rounded-lg bg-slate-50 dark:bg-slate-900/30">
<h3 class="font-medium text-sm text-muted-foreground flex items-center gap-2">
<FileText class="h-4 w-4"/> Descripción
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="frac_mx" class="required">Fracción MX</Label>
<Input id="frac_mx" bind:value={formData.fraction} maxlength={10} placeholder="Ej: 85011001" />
<Label for="desc_es">Español</Label>
<Textarea id="desc_es" bind:value={formData.description_spanish} maxlength={500} rows={3}/>
</div>
<div class="grid gap-2">
<Label for="frac_us">Fracción US (HTS)</Label>
<Input id="frac_us" bind:value={formData.us_fraction} maxlength={16} placeholder="Ej: 8501.10.00" />
</div>
</div>
<div class="p-4 border rounded-lg bg-slate-50 dark:bg-slate-900/50 space-y-4">
<h3 class="font-semibold text-sm flex items-center gap-2">
<Package class="h-4 w-4"/> Dimensiones Físicas
</h3>
<div class="grid gap-2">
<Label for="weight">Peso Unitario</Label>
<Input type="number" step="0.0001" id="weight" bind:value={formData.unit_weight} />
</div>
<div class="grid gap-2">
<Label for="weight_type">Tipo Peso</Label>
<select id="weight_type" bind:value={formData.weight_type} class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
<option value="KG">Kilogramos (KG)</option>
<option value="LB">Libras (LB)</option>
</select>
<Label for="desc_en">Inglés</Label>
<Textarea id="desc_en" bind:value={formData.description_english} maxlength={500} rows={3}/>
</div>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="opt2" class="space-y-4 pt-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="supplier">Proveedor</Label>
<Input id="supplier" bind:value={formData.supplier} maxlength={14} />
<Label for="part_class">Clase</Label>
<Input id="part_class" bind:value={formData.part_class} maxlength={8} placeholder="Código de clase"/>
</div>
<div class="grid gap-2">
<Label for="eccn">ECCN (Export Control)</Label>
<Input id="eccn" bind:value={formData.eccn} maxlength={20} />
</div>
<div class="grid gap-2">
<Label for="fda">FDA Key</Label>
<Input id="fda" bind:value={formData.fda_key} maxlength={20} />
</div>
<div class="grid gap-2">
<Label for="fcc">FCC Key</Label>
<Input id="fcc" bind:value={formData.fcc_key} maxlength={30} />
</div>
<div class="grid gap-2">
<Label for="lic_code">License Code</Label>
<Input id="lic_code" bind:value={formData.license_code} maxlength={3} />
</div>
<div class="grid gap-2">
<Label for="exp_code">Export Code</Label>
<Input id="exp_code" bind:value={formData.export_code} maxlength={2} />
</div>
<div class="grid gap-2 md:col-span-2">
<Label for="exc_sym">Exclusion Symbol</Label>
<Input id="exc_sym" bind:value={formData.exclusion_symbol} maxlength={19} />
<Label for="country">País Origen (ISO)</Label>
<Input id="country" bind:value={formData.country_of_origin} maxlength={3} placeholder="MEX"/>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="costs" class="space-y-4 pt-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-4 border rounded-lg bg-green-50/50 dark:bg-green-900/10 space-y-4">
<h3 class="font-semibold text-sm text-green-800 dark:text-green-300 flex items-center gap-2">
<DollarSign class="h-4 w-4"/> Costos y Moneda
</h3>
<div class="p-4 border rounded-lg bg-slate-50 dark:bg-slate-900/30 space-y-4">
<h3 class="font-medium text-sm text-muted-foreground flex items-center gap-2">
<Package class="h-4 w-4"/> Unidades de Medida
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="unit_cost">Costo Unitario</Label>
<div class="relative">
<span class="absolute left-3 top-2.5 text-muted-foreground">$</span>
<Input type="number" step="0.0001" id="unit_cost" bind:value={formData.unit_cost} class="pl-7" />
</div>
<Label for="uom">UM Comercial</Label>
<Select.Root type="single" bind:value={formData.unit_of_measure}>
<Select.Trigger id="uom">
{formData.unit_of_measure || "Seleccione"}
</Select.Trigger>
<Select.Content>
<Select.Item value="PZ">Pieza (PZ)</Select.Item>
<Select.Item value="KG">Kilogramo (KG)</Select.Item>
<Select.Item value="EA">Elemento (EA)</Select.Item>
<Select.Item value="L">Litro (L)</Select.Item>
<Select.Item value="M">Metro (M)</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
<div class="p-4 border rounded-lg bg-green-50/50 dark:bg-green-900/10 space-y-4">
<h3 class="font-medium text-sm text-green-800 dark:text-green-300 flex items-center gap-2">
<DollarSign class="h-4 w-4"/> Costos y Pesos
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid gap-2">
<Label for="currency">Moneda</Label>
<select id="currency" bind:value={formData.currency_key} class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
<option value="USD">USD - Dólar Americano</option>
<option value="MXP">MXP - Peso Mexicano</option>
<option value="EUR">EUR - Euro</option>
</select>
<Select.Root type="single" bind:value={formData.currency_key}>
<Select.Trigger id="currency">{formData.currency_key}</Select.Trigger>
<Select.Content>
<Select.Item value="USD">Dólar (USD)</Select.Item>
<Select.Item value="MXP">Peso (MXP)</Select.Item>
<Select.Item value="EUR">Euro (EUR)</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="grid gap-2">
<Label for="added_value">Valor Agregado</Label>
<Input type="number" step="0.0001" id="added_value" bind:value={formData.added_value} />
<Label for="unit_cost">Costo Unitario</Label>
<Input type="number" step="0.0001" id="unit_cost" bind:value={formData.unit_cost} />
</div>
</div>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2 border-t border-green-200 dark:border-green-800/30">
<div class="grid gap-2">
<Label for="comm_pn">No. Parte Comercial</Label>
<Input id="comm_pn" bind:value={formData.commercial_part_number} maxlength={70} placeholder="Código usado en factura..." />
<p class="text-xs text-muted-foreground">Si difiere del número de parte interno.</p>
<Label for="unit_weight">Peso Unitario</Label>
<div class="flex gap-2">
<Input type="number" step="0.0001" id="unit_weight" bind:value={formData.unit_weight} />
<Select.Root type="single" bind:value={formData.weight_type}>
<Select.Trigger class="w-[100px]">{formData.weight_type}</Select.Trigger>
<Select.Content>
<Select.Item value="KG">KG</Select.Item>
<Select.Item value="LB">LB</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
</div>
<div class="grid gap-2">
<Label for="added_value">Valor Agregado (Monto)</Label>
<Input type="number" step="0.0001" id="added_value" bind:value={formData.added_value} />
<p class="text-xs text-muted-foreground">Ingrese el monto numérico del valor agregado.</p>
</div>
<div class="grid gap-2 pt-4 border-t">
<Label for="frac_us">Fracción Americana (HTS)</Label>
<Input id="frac_us" bind:value={formData.us_fraction} maxlength={16} placeholder="Ej: 8501.10.00" />
</div>
</Tabs.Content>
<Tabs.Content value="others" class="space-y-4 pt-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<Tabs.Content value="opciones" class="space-y-6 pt-4 animate-in fade-in duration-300">
<div class="space-y-4 p-4 border rounded-lg bg-card">
<h3 class="font-medium text-sm text-muted-foreground">Configuración General</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="grid gap-2">
<Label for="fcc">Clave FCC</Label>
<Input id="fcc" bind:value={formData.fcc_key} maxlength={30} />
</div>
<div class="grid gap-2">
<Label for="supplier">Proveedor (Textil)</Label>
<Input id="supplier" bind:value={formData.supplier} maxlength={14} />
</div>
</div>
<div class="grid gap-2">
<Label for="alt_um">UM Alterna</Label>
<Label for="photo" class="flex items-center gap-2"><ImageIcon class="h-4 w-4"/> URL Imagen de la parte</Label>
<Input id="photo" bind:value={formData.part_photo} maxlength={255} placeholder="https://..." />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="opcionales2" class="space-y-6 pt-4 animate-in fade-in duration-300">
<div class="p-4 border rounded-lg space-y-4">
<h3 class="font-medium text-sm text-muted-foreground">FDA (Food and Drug Administration)</h3>
<div class="grid gap-2">
<Label for="fda">Clave FDA</Label>
<Input id="fda" bind:value={formData.fda_key} maxlength={20} />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="otros" class="space-y-6 pt-4 animate-in fade-in duration-300">
<div class="grid gap-2">
<Label for="client_id" class="required">Catálogo de Clientes (ID)</Label>
<Input type="number" id="client_id" bind:value={formData.client_id} placeholder="ID del Cliente" />
</div>
<div class="grid gap-2">
<Label for="comm_pn">Número de Parte Comercial</Label>
<Input id="comm_pn" bind:value={formData.commercial_part_number} maxlength={70} />
</div>
<div class="p-4 border rounded-lg space-y-4">
<h3 class="font-medium text-sm text-muted-foreground">Factor Conversión</h3>
<div class="grid gap-2">
<Label for="alt_um">UM Conversión (Alterna)</Label>
<Input id="alt_um" bind:value={formData.alternate_unit_measure} maxlength={14} />
</div>
<div class="grid gap-2">
<Label for="photo">URL Foto</Label>
<Input id="photo" bind:value={formData.part_photo} maxlength={255} />
</div>
<div class="flex items-center space-x-2 pt-4">
<Switch id="active-mode" bind:checked={formData.is_active} />
<Label for="active-mode">Parte Activa en Sistema</Label>
</div>
<div class="p-4 border rounded-lg space-y-4 bg-slate-50 dark:bg-slate-900/30">
<h3 class="font-medium text-sm text-muted-foreground">Datos Regulatorios Adicionales</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="fraction_mx" class="required">Fracción Arancelaria (MX)</Label>
<Input id="fraction_mx" bind:value={formData.fraction} maxlength={10} />
</div>
<div class="grid gap-2">
<Label for="eccn">ECCN</Label>
<Input id="eccn" bind:value={formData.eccn} maxlength={20} />
</div>
<div class="grid gap-2">
<Label for="lic_code">License Code</Label>
<Input id="lic_code" bind:value={formData.license_code} maxlength={3} />
</div>
<div class="grid gap-2">
<Label for="exp_code">Export Code</Label>
<Input id="exp_code" bind:value={formData.export_code} maxlength={2} />
</div>
<div class="grid gap-2">
<Label for="exc_sym">Símbolo de Exclusión</Label>
<Input id="exc_sym" bind:value={formData.exclusion_symbol} maxlength={19} />
</div>
</div>
</div>
<div class="flex items-center gap-3 p-4 border rounded-lg bg-card">
<Switch id="is_active" bind:checked={formData.is_active} disabled={loading} />
<div class="grid gap-0.5">
<Label for="is_active">Parte Activa en Sistema</Label>
<p class="text-xs text-muted-foreground">Habilitar o deshabilitar</p>
</div>
</div>
</Tabs.Content>
</div>
<div class="fixed bottom-20 left-0 right-0 z-40 flex justify-center pointer-events-none">
<Tabs.List class="pointer-events-auto grid grid-cols-5 w-[95%] max-w-4xl shadow-2xl bg-background border p-1 rounded-xl">
<Tabs.Trigger value="general" class="text-xs md:text-sm">General</Tabs.Trigger>
<Tabs.Trigger value="opt1" class="text-xs md:text-sm">Op. 1</Tabs.Trigger>
<Tabs.Trigger value="opt2" class="text-xs md:text-sm">Op. 2</Tabs.Trigger>
<Tabs.Trigger value="costs" class="text-xs md:text-sm">Costos</Tabs.Trigger>
<Tabs.Trigger value="others" class="text-xs md:text-sm">Otros</Tabs.Trigger>
</Tabs.List>
</div>
<Tabs.List class="grid grid-cols-4 fixed bottom-24 left-1/2 -translate-x-1/2 w-[95%] max-w-2xl z-40 shadow-2xl bg-background border p-1 rounded-xl">
<Tabs.Trigger value="general" class="flex gap-2 items-center justify-center">
<Package class="h-4 w-4 hidden sm:block" /> General
</Tabs.Trigger>
<Tabs.Trigger value="opciones" class="flex gap-2 items-center justify-center">
<Settings class="h-4 w-4 hidden sm:block" /> Opciones
</Tabs.Trigger>
<Tabs.Trigger value="opcionales2" class="flex gap-2 items-center justify-center">
<FileText class="h-4 w-4 hidden sm:block" /> Opcionales 2
</Tabs.Trigger>
<Tabs.Trigger value="otros" class="flex gap-2 items-center justify-center">
<Settings class="h-4 w-4 hidden sm:block" /> Otros
</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</Card.Content>
</Card.Root>
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-inner">
<div class="max-w-6xl mx-auto flex justify-end gap-4 px-4 w-full">
<Button type="button" variant="ghost" href="/dashboard/goods/parts" disabled={loading}>
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50">
<div class="max-w-6xl mx-auto flex justify-end gap-4">
<Button variant="ghost" href="/dashboard/goods/parts" disabled={loading}>
Cancelar
</Button>
<Button type="submit" disabled={loading} class="min-w-[140px]">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
<Save class="mr-2 h-4 w-4" />
{isEdit ? 'Actualizar' : 'Guardar'}
{/if}
{isEdit ? 'Actualizar' : 'Guardar'}
</Button>
</div>
</div>
</form>
{/key}
</div>
<style>