Se normalizo la tabla de partes para anexo 76 y 24, ademas se parametrizo el formulario
This commit is contained in:
@@ -1,9 +1,57 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface FaData {
|
||||
origin_country?: string | null;
|
||||
sector?: string | null;
|
||||
fraction_type?: string | null;
|
||||
}
|
||||
|
||||
export interface InvData {
|
||||
part_type?: string | null;
|
||||
material_type?: string | null;
|
||||
reference_number?: string | null;
|
||||
flex_reference_number?: string | null;
|
||||
equivalent_uom?: string | null;
|
||||
conversion_factor?: number | null;
|
||||
stock_uom?: string | null;
|
||||
alternate_uom?: string | null;
|
||||
conversion_uom?: string | null;
|
||||
added_value?: number | null;
|
||||
added_value_type?: string | null;
|
||||
assigned_client?: string | null;
|
||||
supplier_code?: string | null;
|
||||
is_textile?: string | null;
|
||||
bom_version?: number | null;
|
||||
is_repair?: string | null;
|
||||
is_hazardous?: string | null;
|
||||
emergency_number?: string | null;
|
||||
danger_class?: string | null;
|
||||
packaging_group?: string | null;
|
||||
width?: string | null;
|
||||
thickness?: string | null;
|
||||
specification?: string | null;
|
||||
total_value?: number | null;
|
||||
direct_labor?: number | null;
|
||||
general_expenses?: number | null;
|
||||
total_expenses?: number | null;
|
||||
depreciation?: number | null;
|
||||
tooling?: number | null;
|
||||
material_consumed?: number | null;
|
||||
profit?: number | null;
|
||||
us_fraction_alt?: string | null;
|
||||
ca_fraction?: string | null;
|
||||
ad_valorem_us?: number | null;
|
||||
nafta_result?: string | null;
|
||||
nafta_percentage?: number | null;
|
||||
dta?: string | null;
|
||||
dtb?: string | null;
|
||||
dtg?: string | null;
|
||||
}
|
||||
|
||||
|
||||
export interface Part {
|
||||
id: number;
|
||||
// Llaves foráneas y IDs
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
client_id: number;
|
||||
@@ -11,99 +59,76 @@ export interface Part {
|
||||
// Identificación
|
||||
part_number: string;
|
||||
commercial_part_number: string | null;
|
||||
part_class: string | null;
|
||||
material_type?: string | null;
|
||||
|
||||
// Descripciones
|
||||
// Descripciones y Clase
|
||||
description_spanish: string | null;
|
||||
description_english: string | null;
|
||||
part_class: string | null;
|
||||
unit_of_measure: string | null;
|
||||
|
||||
// Físico y Origen
|
||||
unit_of_measure: string;
|
||||
alternate_unit_measure: string | null;
|
||||
country_of_origin: string;
|
||||
// Costos y Pesos
|
||||
unit_cost: number | null;
|
||||
currency_key: string | null;
|
||||
currency_type: string | null;
|
||||
unit_weight: number | null;
|
||||
weight_type: string | null;
|
||||
part_photo: string | null;
|
||||
|
||||
// Clasificación Arancelaria
|
||||
// Regulatorio
|
||||
fraction: string | null;
|
||||
us_fraction: string | null;
|
||||
|
||||
// Costos y Valores
|
||||
unit_cost: number | null;
|
||||
currency_key: string | null; // currency_type en DB a veces es redundante, usamos key
|
||||
added_value: number | null;
|
||||
|
||||
// Regulatorio y Proveedores
|
||||
supplier: string | null;
|
||||
fda_key: string | null;
|
||||
fcc_key: string | null;
|
||||
eccn: string | null;
|
||||
license_code: string | null;
|
||||
eccn: string | null;
|
||||
export_code: string | null;
|
||||
exclusion_symbol: string | null;
|
||||
|
||||
// Estado
|
||||
// Estado y Media
|
||||
is_active: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
part_photo: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
|
||||
fa_data?: FaData | null;
|
||||
inv_data?: InvData | null;
|
||||
}
|
||||
|
||||
export interface PartCreate {
|
||||
company_id: number;
|
||||
client_id: number;
|
||||
part_number: string;
|
||||
|
||||
// Opcionales
|
||||
description_spanish?: string | null;
|
||||
description_english?: string | null;
|
||||
commercial_part_number?: string | null;
|
||||
part_class?: string | null;
|
||||
material_type?: string | null;
|
||||
|
||||
unit_of_measure: string;
|
||||
alternate_unit_measure?: string | null;
|
||||
country_of_origin?: string;
|
||||
unit_weight?: number | null;
|
||||
weight_type?: string | null;
|
||||
|
||||
fraction?: string | null;
|
||||
us_fraction?: string | null;
|
||||
|
||||
unit_cost?: number | null;
|
||||
currency_key?: string | null;
|
||||
added_value?: number | null;
|
||||
|
||||
supplier?: string | null;
|
||||
fda_key?: string | null;
|
||||
fcc_key?: string | null;
|
||||
eccn?: string | null;
|
||||
license_code?: string | null;
|
||||
export_code?: string | null;
|
||||
exclusion_symbol?: string | null;
|
||||
|
||||
part_photo?: string | null;
|
||||
is_active?: boolean;
|
||||
|
||||
export interface PartCreate extends Omit<Part, 'id' | 'tenant_id' | 'created_at' | 'updated_at'> {
|
||||
}
|
||||
|
||||
|
||||
export interface PartUpdate extends Partial<PartCreate> {}
|
||||
|
||||
|
||||
export interface PartListResponse {
|
||||
items: Part[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export const partsApi = {
|
||||
list: (params: { company_id: number; page?: number; page_size?: number; q?: string }) => {
|
||||
|
||||
list: (params: {
|
||||
company_id: number;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
q?: string
|
||||
}) => {
|
||||
const { company_id, page = 1, page_size = 50, q = '' } = params;
|
||||
const skip = (page - 1) * page_size;
|
||||
|
||||
return api.get<PartListResponse>(
|
||||
`/v1/a76/parts/?company_id=${company_id}&skip=${skip}&limit=${page_size}&description=${q}`
|
||||
);
|
||||
const query = new URLSearchParams({
|
||||
company_id: company_id.toString(),
|
||||
skip: skip.toString(),
|
||||
limit: page_size.toString(),
|
||||
description: q
|
||||
});
|
||||
|
||||
return api.get<PartListResponse>(`/v1/a76/parts/?${query.toString()}`);
|
||||
},
|
||||
|
||||
get: (id: number, company_id: number) => {
|
||||
@@ -118,6 +143,7 @@ export const partsApi = {
|
||||
return api.put<Part>(`/v1/a76/parts/${id}?company_id=${company_id}`, data);
|
||||
},
|
||||
|
||||
|
||||
delete: (id: number, company_id: number) => {
|
||||
return api.delete<void>(`/v1/a76/parts/${id}?company_id=${company_id}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,711 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
// UI Components
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
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";
|
||||
|
||||
// Iconos
|
||||
import {
|
||||
ArrowLeft, LoaderCircle, Save, Package, DollarSign,
|
||||
FileText, Settings, Image as ImageIcon, FolderSearch,
|
||||
UserCheck, CheckCircle2, XCircle, Tag, Layers, Scale, Info, Briefcase, ShieldCheck
|
||||
} from 'lucide-svelte';
|
||||
|
||||
// Stores & APIs
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { partsApi } from '$lib/api/dashboard/a76/parts';
|
||||
import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers';
|
||||
import { classesApi } from '$lib/api/dashboard/a76/classes';
|
||||
import { materialTypesApi } from '$lib/api/dashboard/a76/material-types';
|
||||
|
||||
// Modales
|
||||
import ClientSelectorDialog from '$lib/components/dashboard/goods/modales/client-selector-dialog.svelte';
|
||||
import ClassSelectorDialog from '$lib/components/dashboard/goods/parts/class-selector-dialog.svelte';
|
||||
import MaterialTypeSelectorDialog from '$lib/components/dashboard/goods/modales/material-type-selector-dialog.svelte';
|
||||
import UnitMeasureSelectorDialog from '$lib/components/dashboard/goods/modales/unit-measure-dialog.svelte';
|
||||
|
||||
// --- PROPS ---
|
||||
let { partId = null, formType = 'inv' }: { partId?: number | null, formType?: 'inv' | 'fa' } = $props();
|
||||
|
||||
// --- LÓGICA DE ESTADO ---
|
||||
let isEdit = $derived(!!partId);
|
||||
let title = $derived(isEdit ? "Editar Parte" : "Nueva Parte");
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Estado Modales
|
||||
let showClientModal = $state(false);
|
||||
let showClassModal = $state(false);
|
||||
let showMaterialModal = $state(false);
|
||||
let showUOMModal = $state(false);
|
||||
let showAltUOMModal = $state(false);
|
||||
|
||||
// Descripciones Visuales
|
||||
let selectedClientName = $state("");
|
||||
let selectedClientStatus = $state(true);
|
||||
let selectedClassDesc = $state("");
|
||||
let selectedMaterialDesc = $state("");
|
||||
|
||||
// Estado Formulario
|
||||
let formData = $state({
|
||||
client_id: 0,
|
||||
part_number: '',
|
||||
description_spanish: '',
|
||||
description_english: '',
|
||||
part_class: '',
|
||||
material_type: '',
|
||||
unit_of_measure: 'PZ',
|
||||
unit_weight: 0,
|
||||
weight_type: 'KG',
|
||||
unit_cost: 0,
|
||||
currency_key: 'USD',
|
||||
added_value: 0,
|
||||
value_added_type: 'USD',
|
||||
us_fraction: '',
|
||||
supplier: '',
|
||||
fcc_key: '',
|
||||
part_photo: '',
|
||||
fda_key: '',
|
||||
commercial_part_number: '',
|
||||
alternate_unit_measure: '',
|
||||
fraction: '',
|
||||
eccn: '',
|
||||
license_code: '',
|
||||
export_code: '',
|
||||
exclusion_symbol: '',
|
||||
is_active: true,
|
||||
// --- CAMPOS ESPECIFICOS DE FA (Activos Fijos) ---
|
||||
origin_country: 'MEX',
|
||||
sector: '',
|
||||
fraction_type: ''
|
||||
});
|
||||
|
||||
// --- CARGA DE DATOS ---
|
||||
$effect(() => {
|
||||
if (companyStore.activeCompany?.id && partId) {
|
||||
loadPartData(partId, companyStore.activeCompany.id);
|
||||
}
|
||||
});
|
||||
|
||||
async function loadPartData(id: number, companyId: number) {
|
||||
loading = true;
|
||||
try {
|
||||
const response = await partsApi.get(id, companyId);
|
||||
if (response.data) {
|
||||
const d = response.data;
|
||||
formData = {
|
||||
client_id: d.client_id,
|
||||
part_number: d.part_number,
|
||||
description_spanish: d.description_spanish || '',
|
||||
description_english: d.description_english || '',
|
||||
part_class: d.part_class || '',
|
||||
material_type: (d as any).material_type || '',
|
||||
origin_country: d.origin_country || 'MEX',
|
||||
unit_of_measure: d.unit_of_measure || 'PZ',
|
||||
fraction: d.fraction || '',
|
||||
us_fraction: d.us_fraction || '',
|
||||
unit_weight: Number(d.unit_weight) || 0,
|
||||
weight_type: d.weight_type || 'KG',
|
||||
supplier: d.supplier || '',
|
||||
fda_key: d.fda_key || '',
|
||||
fcc_key: d.fcc_key || '',
|
||||
eccn: d.eccn || '',
|
||||
license_code: d.license_code || '',
|
||||
export_code: d.export_code || '',
|
||||
exclusion_symbol: d.exclusion_symbol || '',
|
||||
unit_cost: Number(d.unit_cost) || 0,
|
||||
currency_key: d.currency_key || 'USD',
|
||||
added_value: Number(d.added_value) || 0,
|
||||
value_added_type: 'USD',
|
||||
commercial_part_number: d.commercial_part_number || '',
|
||||
alternate_unit_measure: d.alternate_unit_measure || '',
|
||||
part_photo: d.part_photo || '',
|
||||
is_active: d.is_active ?? true,
|
||||
// Cargar datos FA si existen
|
||||
sector: d.fa_data?.sector || '',
|
||||
fraction_type: d.fa_data?.fraction_type || ''
|
||||
};
|
||||
if (d.client_id) await fetchClientName(d.client_id, companyId);
|
||||
if (d.part_class) await fetchClassDesc(d.part_class, companyId);
|
||||
if ((d as any).material_type) await fetchMaterialName((d as any).material_type);
|
||||
}
|
||||
} catch (e) { console.error(e); } finally { loading = false; }
|
||||
}
|
||||
|
||||
// --- HELPERS VISUALES ---
|
||||
async function fetchClientName(clientId: number, companyId: number) {
|
||||
try {
|
||||
const res = await clientsProvidersApi.get(clientId, companyId);
|
||||
const clientData = (res as any).data || res;
|
||||
if (clientData) {
|
||||
selectedClientName = clientData.name;
|
||||
selectedClientStatus = clientData.is_active ?? true;
|
||||
}
|
||||
} catch (e) { console.log("Error visual cliente", e); }
|
||||
}
|
||||
|
||||
async function fetchClassDesc(code: string, companyId: number) {
|
||||
try {
|
||||
const res = await classesApi.list({ company_id: companyId, class_code: code });
|
||||
const data = (res as any).data || res;
|
||||
const list = data.items || data.classes || [];
|
||||
if (list.length > 0) {
|
||||
const found = list.find((i: any) => i.class_code === code) || list[0];
|
||||
selectedClassDesc = found.description_es || found.description_en || "";
|
||||
}
|
||||
} catch (e) { console.log("Error visual clase", e); }
|
||||
}
|
||||
|
||||
async function fetchMaterialName(key: string) {
|
||||
try {
|
||||
const res = await materialTypesApi.list(1, 100);
|
||||
const data = (res as any).data || res;
|
||||
const list = data.items || [];
|
||||
const found = list.find((m: any) => m.key === key);
|
||||
if (found) selectedMaterialDesc = found.description;
|
||||
} catch (e) { console.log("Error visual material", e); }
|
||||
}
|
||||
|
||||
// --- HANDLERS ---
|
||||
function handleClientSelect(client: any) { formData.client_id = client.id; selectedClientName = client.name; selectedClientStatus = client.is_active ?? true; }
|
||||
function handleClassSelect(item: any) { formData.part_class = item.class_code; selectedClassDesc = item.description_es || item.description_en || ""; }
|
||||
function handleMaterialSelect(item: any) { formData.material_type = item.key; selectedMaterialDesc = item.description; }
|
||||
function handleUOMSelect(item: any) { formData.unit_of_measure = item.code; }
|
||||
function handleAltUOMSelect(item: any) { formData.alternate_unit_measure = item.code; }
|
||||
|
||||
// --- SUBMIT ---
|
||||
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 = 'Debe seleccionar un Cliente'; return; }
|
||||
if (!formData.part_number.trim()) { error = 'Número de Parte requerido'; return; }
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
// Construimos el payload.
|
||||
// Si es FA, inyectamos el objeto fa_data anidado.
|
||||
let commonData: any = { ...formData };
|
||||
|
||||
if (formType === 'fa') {
|
||||
commonData.fa_data = {
|
||||
sector: formData.sector,
|
||||
fraction_type: formData.fraction_type,
|
||||
origin_country: formData.origin_country
|
||||
};
|
||||
}
|
||||
|
||||
if (isEdit && partId) {
|
||||
await partsApi.update(partId, commonData, activeCompanyId);
|
||||
} else {
|
||||
await partsApi.create({ ...commonData, company_id: activeCompanyId }, activeCompanyId);
|
||||
}
|
||||
goto('/dashboard/goods/parts');
|
||||
} catch (e: any) { error = e.message || 'Error 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 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">
|
||||
{formType === 'fa' ? 'Gestión de Activos Fijos (Q-Partes)' : 'Gestión detallada de números de parte (S-Partes).'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if formType === 'fa'}
|
||||
<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-[500px]">
|
||||
|
||||
<Tabs.Content value="general" class="space-y-6 pt-4 animate-in fade-in duration-300">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_pn" class="text-base font-semibold required">Número de Parte</Label>
|
||||
<Input id="fa_pn" bind:value={formData.part_number} disabled={isEdit} maxlength={50} class="text-lg font-mono" placeholder="Ej: MAQ-001"/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_client" class="required">Cliente Asignado</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input id="fa_client" bind:value={formData.client_id} readonly onclick={() => showClientModal = true} class="cursor-pointer font-mono" placeholder="Seleccione..."/>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showClientModal = true}><FolderSearch class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
{#if selectedClientName}<div class="text-xs font-semibold text-primary">{selectedClientName}</div>{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_desc_es">Descripción Español</Label>
|
||||
<Textarea id="fa_desc_es" bind:value={formData.description_spanish} rows={2}/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_desc_en">Descripción Inglés</Label>
|
||||
<Textarea id="fa_desc_en" bind:value={formData.description_english} rows={2}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 border rounded-lg bg-slate-50 dark:bg-slate-900/30">
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_class" class="required">Clase</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input id="fa_class" bind:value={formData.part_class} readonly onclick={() => showClassModal = true} class="cursor-pointer" placeholder="Seleccione..."/>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showClassModal = true}><FolderSearch class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
{#if selectedClassDesc}<div class="text-xs text-muted-foreground">{selectedClassDesc}</div>{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_uom" class="required">Unidad Medida</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input id="fa_uom" bind:value={formData.unit_of_measure} readonly onclick={() => showUOMModal = true} class="cursor-pointer" placeholder="PZ"/>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showUOMModal = true}><FolderSearch class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_origin">País Origen (ISO)</Label>
|
||||
<Input id="fa_origin" bind:value={formData.origin_country} maxlength={3} placeholder="MEX"/>
|
||||
</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="fa_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="fa_cost" bind:value={formData.unit_cost} class="pl-7" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_currency">Moneda</Label>
|
||||
<Select.Root type="single" bind:value={formData.currency_key}>
|
||||
<Select.Trigger id="fa_currency">{formData.currency_key}</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="USD">Extranjera (USD)</Select.Item>
|
||||
<Select.Item value="MXP">Nacional (MXP)</Select.Item>
|
||||
<Select.Item value="EUR">Euros (EUR)</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_weight">Peso Unitario</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input type="number" step="0.0001" id="fa_weight" bind:value={formData.unit_weight} placeholder="0.0000" />
|
||||
<Select.Root type="single" bind:value={formData.weight_type}>
|
||||
<Select.Trigger class="w-[80px]">{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="space-y-4 p-4 border rounded-lg">
|
||||
<h3 class="font-medium text-sm text-muted-foreground flex items-center gap-2"><Briefcase class="h-4 w-4"/> Datos Aduaneros</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_fraction">Fracción UMT (MX)</Label>
|
||||
<Input id="fa_fraction" bind:value={formData.fraction} maxlength={10} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_sector">Sector</Label>
|
||||
<Input id="fa_sector" bind:value={formData.sector} placeholder="Ej: Automotriz"/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_fraction_type">Tipo Tarifa</Label>
|
||||
<Input id="fa_fraction_type" bind:value={formData.fraction_type} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_us_fraction">Fracción Americana</Label>
|
||||
<Input id="fa_us_fraction" bind:value={formData.us_fraction} maxlength={10} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<Switch id="fa_is_active" bind:checked={formData.is_active} />
|
||||
<Label for="fa_is_active">Activo en sistema</Label>
|
||||
</div>
|
||||
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="cont1" class="space-y-6 pt-4 animate-in fade-in duration-300">
|
||||
<div class="p-6 border rounded-lg space-y-4">
|
||||
<div class="grid gap-4 max-w-lg mx-auto">
|
||||
<Label for="fa_photo" class="text-left flex items-center gap-2">
|
||||
<ImageIcon class="h-4 w-4"/> URL Imagen de la parte
|
||||
</Label>
|
||||
<Input id="fa_photo" bind:value={formData.part_photo} maxlength={255} placeholder="https://ejemplo.com/imagen.jpg" />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="cont2" class="space-y-6 pt-4 animate-in fade-in duration-300">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4 p-4 border rounded-lg">
|
||||
<h3 class="font-medium text-sm text-muted-foreground flex items-center gap-2"><ShieldCheck class="h-4 w-4"/> Control de Exportación</h3>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_lic">License Code</Label>
|
||||
<Input id="fa_lic" bind:value={formData.license_code} maxlength={3} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_exp">Export Code</Label>
|
||||
<Input id="fa_exp" bind:value={formData.export_code} maxlength={2} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_eccn">ECCN</Label>
|
||||
<Input id="fa_eccn" bind:value={formData.eccn} maxlength={20} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_exc">Símbolo Excepción Licencia</Label>
|
||||
<Input id="fa_exc" bind:value={formData.exclusion_symbol} maxlength={19} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 p-4 border rounded-lg">
|
||||
<h3 class="font-medium text-sm text-muted-foreground flex items-center gap-2"><Info class="h-4 w-4"/> Regulaciones Adicionales</h3>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_fda">Clave FDA</Label>
|
||||
<Input id="fa_fda" bind:value={formData.fda_key} maxlength={20} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="fa_fcc">Clave FCC</Label>
|
||||
<Input id="fa_fcc" bind:value={formData.fcc_key} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
</div>
|
||||
|
||||
<Tabs.List class="grid grid-cols-3 fixed bottom-24 left-1/2 -translate-x-1/2 w-[95%] max-w-xl z-40 shadow-2xl bg-background border p-1 rounded-xl">
|
||||
<Tabs.Trigger value="general" class="flex gap-2 items-center justify-center">Generales</Tabs.Trigger>
|
||||
<Tabs.Trigger value="cont1" class="flex gap-2 items-center justify-center">Continuación 1</Tabs.Trigger>
|
||||
<Tabs.Trigger value="cont2" class="flex gap-2 items-center justify-center">Continuación 2</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">
|
||||
<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] bg-amber-600 hover:bg-amber-700">
|
||||
{#if loading}<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />{:else}<Save class="mr-2 h-4 w-4" />{/if}
|
||||
{isEdit ? 'Actualizar Activo' : 'Guardar Activo'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{:else}
|
||||
<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-[500px]">
|
||||
|
||||
<Tabs.Content value="general" class="space-y-6 pt-4 animate-in fade-in duration-300">
|
||||
|
||||
<div class="grid gap-2">
|
||||
<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="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="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="desc_en">Inglés</Label>
|
||||
<Textarea id="desc_en" bind:value={formData.description_english} maxlength={500} rows={3}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="part_class_client" class="required">Clase (Anexo 24 - Cliente)</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute left-3 top-2.5 text-muted-foreground">
|
||||
<Package class="h-4 w-4" />
|
||||
</div>
|
||||
<Input id="part_class_client" bind:value={formData.part_class} maxlength={8} placeholder="Seleccione Clase..." class="pl-9 font-mono cursor-pointer" readonly onclick={() => showClassModal = true}/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showClassModal = true}><FolderSearch class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
{#if selectedClassDesc}<div class="text-xs text-primary font-medium px-1 flex items-center gap-1"><Tag class="h-3 w-3" />{selectedClassDesc}</div>{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="part_class_material">Tipo Material (Catálogo General)</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute left-3 top-2.5 text-muted-foreground">
|
||||
<Layers class="h-4 w-4" />
|
||||
</div>
|
||||
<Input id="part_class_material" bind:value={formData.material_type} maxlength={8} placeholder="Seleccione Material..." class="pl-9 font-mono cursor-pointer" readonly onclick={() => showMaterialModal = true}/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showMaterialModal = true}><FolderSearch class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
{#if selectedMaterialDesc}<div class="text-xs text-blue-600 font-medium px-1 flex items-center gap-1"><Layers class="h-3 w-3" />{selectedMaterialDesc}</div>{/if}
|
||||
<p class="text-[10px] text-muted-foreground">Opcional: Clasificación adicional por tipo de material.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="country">País Origen (ISO)</Label>
|
||||
<Input id="country" bind:value={formData.origin_country} maxlength={3} placeholder="MEX"/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="uom" class="required">Unidad de Medida (TIGIE)</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute left-3 top-2.5 text-muted-foreground">
|
||||
<Scale class="h-4 w-4" />
|
||||
</div>
|
||||
<Input id="uom" bind:value={formData.unit_of_measure} placeholder="Seleccione..." class="pl-9 font-mono cursor-pointer" readonly onclick={() => showUOMModal = true}/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showUOMModal = true}><FolderSearch class="h-4 w-4" /></Button>
|
||||
</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, valores y peso unitario</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="currency">Tipos de Moneda</Label>
|
||||
<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ólares (USD)</Select.Item>
|
||||
<Select.Item value="MXP">Pesos (MXP)</Select.Item>
|
||||
<Select.Item value="EUR">Euros (EUR)</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<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} placeholder="0.0000" />
|
||||
<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="space-y-3 p-4 border rounded-lg">
|
||||
<Label class="font-semibold">Tipo Valor Agregado</Label>
|
||||
<div class="flex flex-wrap gap-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="radio" id="va_ext" name="va_type" value="USD" bind:group={formData.value_added_type} class="accent-primary h-4 w-4 cursor-pointer" />
|
||||
<Label for="va_ext" class="font-normal cursor-pointer">Extranjera (Dls)</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="radio" id="va_nac" name="va_type" value="MXP" bind:group={formData.value_added_type} class="accent-primary h-4 w-4 cursor-pointer" />
|
||||
<Label for="va_nac" class="font-normal cursor-pointer">Nacional (Pesos)</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="radio" id="va_pct" name="va_type" value="PERCENT" bind:group={formData.value_added_type} class="accent-primary h-4 w-4 cursor-pointer" />
|
||||
<Label for="va_pct" class="font-normal cursor-pointer">Porcentaje</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 mt-2">
|
||||
<div class="relative max-w-xs">
|
||||
{#if formData.value_added_type === 'PERCENT'}
|
||||
<span class="absolute right-3 top-2.5 text-muted-foreground">%</span>
|
||||
{:else}
|
||||
<span class="absolute left-3 top-2.5 text-muted-foreground">$</span>
|
||||
{/if}
|
||||
<Input type="number" step="0.0001" id="added_value" bind:value={formData.added_value} class={formData.value_added_type === 'PERCENT' ? 'pr-7' : 'pl-7'} placeholder="0.00"/>
|
||||
</div>
|
||||
</div>
|
||||
</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="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="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</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">Cliente Asignado</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<UserCheck class="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input type="number" id="client_id" bind:value={formData.client_id} class="pl-9 font-mono" placeholder="Seleccione un cliente..." readonly onclick={() => showClientModal = true}/>
|
||||
</div>
|
||||
<Button variant="outline" class="shrink-0" type="button" onclick={() => showClientModal = true}><FolderSearch class="h-4 w-4 mr-2" /> Buscar</Button>
|
||||
</div>
|
||||
{#if selectedClientName}
|
||||
<div class="flex items-center gap-2 mt-1 px-3 py-2 bg-slate-50 dark:bg-slate-900/50 border rounded-md text-sm">
|
||||
<span class="font-semibold text-primary">{selectedClientName}</span>
|
||||
{#if selectedClientStatus}<span class="text-green-600 flex items-center gap-1 text-xs font-medium"><CheckCircle2 class="h-3 w-3"/> Activo</span>{:else}<span class="text-red-600 flex items-center gap-1 text-xs font-medium"><XCircle class="h-3 w-3"/> Inactivo</span>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</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">
|
||||
<Label for="alt_um">UM Conversión (Alterna)</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute left-3 top-2.5 text-muted-foreground"><Scale class="h-4 w-4" /></div>
|
||||
<Input id="alt_um" bind:value={formData.alternate_unit_measure} placeholder="Seleccione..." class="pl-9 font-mono cursor-pointer" readonly onclick={() => showAltUOMModal = true}/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showAltUOMModal = true}><FolderSearch class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
</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</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} />
|
||||
<Label for="is_active">Parte Activa en Sistema</Label>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</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">
|
||||
<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" />{:else}<Save class="mr-2 h-4 w-4" />{/if}
|
||||
{isEdit ? 'Actualizar' : 'Guardar'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ClientSelectorDialog bind:open={showClientModal} onSelect={handleClientSelect} />
|
||||
<ClassSelectorDialog bind:open={showClassModal} onSelect={handleClassSelect} />
|
||||
<MaterialTypeSelectorDialog bind:open={showMaterialModal} onSelect={handleMaterialSelect} />
|
||||
<UnitMeasureSelectorDialog bind:open={showUOMModal} onSelect={handleUOMSelect} />
|
||||
<UnitMeasureSelectorDialog bind:open={showAltUOMModal} onSelect={handleAltUOMSelect} />
|
||||
|
||||
<style>
|
||||
:global(.required::after) {
|
||||
content: " *";
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
</style>
|
||||
@@ -1,686 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// UI Components
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
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";
|
||||
|
||||
// Iconos
|
||||
import {
|
||||
ArrowLeft, LoaderCircle, Save, Package, DollarSign,
|
||||
FileText, Settings, Image as ImageIcon, Search,
|
||||
UserCheck, CheckCircle2, XCircle, Tag, Layers, Scale
|
||||
} from 'lucide-svelte';
|
||||
import PartForm from '$lib/components/dashboard/goods/parts/partForm.svelte';
|
||||
|
||||
// Stores & APIs
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { partsApi, type PartCreate } from '$lib/api/dashboard/a76/parts';
|
||||
import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers';
|
||||
import { classesApi } from '$lib/api/dashboard/a76/classes';
|
||||
import { materialTypesApi } from '$lib/api/dashboard/a76/material-types';
|
||||
|
||||
// Modales
|
||||
import ClientSelectorDialog from '$lib/components/dashboard/goods/modales/client-selector-dialog.svelte';
|
||||
import ClassSelectorDialog from '$lib/components/dashboard/goods/parts/class-selector-dialog.svelte';
|
||||
import MaterialTypeSelectorDialog from '$lib/components/dashboard/goods/modales/material-type-selector-dialog.svelte';
|
||||
import UnitMeasureSelectorDialog from '$lib/components/dashboard/goods/modales/unit-measure-dialog.svelte';
|
||||
|
||||
// --- 1. IDENTIFICACIÓN ---
|
||||
let id = $derived($page.params.id === 'new' ? null : Number($page.params.id));
|
||||
let isEdit = $derived(!!id);
|
||||
let title = $derived(isEdit ? "Editar Parte" : "Nueva Parte");
|
||||
|
||||
// --- 2. ESTADOS ---
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Estado Modales
|
||||
let showClientModal = $state(false);
|
||||
let showClassModal = $state(false);
|
||||
let showMaterialModal = $state(false);
|
||||
let showUOMModal = $state(false);
|
||||
let showAltUOMModal = $state(false);
|
||||
|
||||
// Descripciones Visuales
|
||||
let selectedClientName = $state("");
|
||||
let selectedClientStatus = $state(true);
|
||||
let selectedClassDesc = $state("");
|
||||
let selectedMaterialDesc = $state("");
|
||||
|
||||
// Estado Formulario
|
||||
let formData = $state({
|
||||
client_id: 0,
|
||||
part_number: '',
|
||||
|
||||
// General
|
||||
description_spanish: '',
|
||||
description_english: '',
|
||||
part_class: '',
|
||||
material_type: '',
|
||||
country_of_origin: 'MEX',
|
||||
unit_of_measure: 'PZ', // U.M. TIGIE
|
||||
|
||||
// Costos y Pesos
|
||||
unit_weight: 0,
|
||||
weight_type: 'KG',
|
||||
unit_cost: 0,
|
||||
currency_key: 'USD',
|
||||
added_value: 0,
|
||||
value_added_type: 'USD',
|
||||
us_fraction: '',
|
||||
|
||||
// Opciones
|
||||
supplier: '',
|
||||
fcc_key: '',
|
||||
part_photo: '',
|
||||
|
||||
// Opcionales 2
|
||||
fda_key: '',
|
||||
|
||||
// Otros (Comerciales)
|
||||
commercial_part_number: '',
|
||||
alternate_unit_measure: '', // U.M. Comercial
|
||||
|
||||
// Regulatorios
|
||||
fraction: '',
|
||||
eccn: '',
|
||||
license_code: '',
|
||||
export_code: '',
|
||||
exclusion_symbol: '',
|
||||
|
||||
is_active: true
|
||||
});
|
||||
|
||||
// --- 3. CARGA INICIAL ---
|
||||
onMount(async () => {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
if (id) {
|
||||
await loadPartData(id, companyId);
|
||||
}
|
||||
});
|
||||
|
||||
async function loadPartData(partId: number, companyId: number) {
|
||||
loading = true;
|
||||
try {
|
||||
const response = await partsApi.get(partId, companyId);
|
||||
|
||||
if (response.error) {
|
||||
error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
const d = response.data;
|
||||
// Mapeo de datos
|
||||
formData = {
|
||||
client_id: d.client_id,
|
||||
part_number: d.part_number,
|
||||
description_spanish: d.description_spanish || '',
|
||||
description_english: d.description_english || '',
|
||||
|
||||
part_class: d.part_class || '',
|
||||
material_type: d.material_type || '', // Corregido
|
||||
|
||||
country_of_origin: d.country_of_origin || 'MEX',
|
||||
unit_of_measure: d.unit_of_measure || 'PZ',
|
||||
fraction: d.fraction || '',
|
||||
us_fraction: d.us_fraction || '',
|
||||
unit_weight: Number(d.unit_weight) || 0,
|
||||
weight_type: d.weight_type || 'KG',
|
||||
supplier: d.supplier || '',
|
||||
fda_key: d.fda_key || '',
|
||||
fcc_key: d.fcc_key || '',
|
||||
eccn: d.eccn || '',
|
||||
license_code: d.license_code || '',
|
||||
export_code: d.export_code || '',
|
||||
exclusion_symbol: d.exclusion_symbol || '',
|
||||
unit_cost: Number(d.unit_cost) || 0,
|
||||
currency_key: d.currency_key || 'USD',
|
||||
added_value: Number(d.added_value) || 0,
|
||||
value_added_type: 'USD',
|
||||
commercial_part_number: d.commercial_part_number || '',
|
||||
alternate_unit_measure: d.alternate_unit_measure || '',
|
||||
part_photo: d.part_photo || '',
|
||||
is_active: d.is_active ?? true
|
||||
};
|
||||
|
||||
// Cargar datos visuales
|
||||
if (d.client_id) await fetchClientName(d.client_id, companyId);
|
||||
if (d.part_class) await fetchClassDesc(d.part_class, companyId);
|
||||
if (d.material_type) await fetchMaterialName(d.material_type);
|
||||
}
|
||||
} catch (e) {
|
||||
error = "Error al cargar la parte";
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- HELPERS VISUALES ---
|
||||
async function fetchClientName(clientId: number, companyId: number) {
|
||||
try {
|
||||
const res = await clientsProvidersApi.get(clientId, companyId);
|
||||
const clientData = (res as any).data || res;
|
||||
if (clientData) {
|
||||
selectedClientName = clientData.name;
|
||||
selectedClientStatus = clientData.is_active ?? true;
|
||||
}
|
||||
} catch (e) { console.log("Error visual cliente", e); }
|
||||
}
|
||||
|
||||
async function fetchClassDesc(code: string, companyId: number) {
|
||||
try {
|
||||
const res = await classesApi.list({ company_id: companyId, class_code: code });
|
||||
const data = (res as any).data || res;
|
||||
const list = data.items || data.classes || [];
|
||||
if (list.length > 0) {
|
||||
const found = list.find((i: any) => i.class_code === code) || list[0];
|
||||
selectedClassDesc = found.description_es || found.description_en || "";
|
||||
}
|
||||
} catch (e) { console.log("Error visual clase", e); }
|
||||
}
|
||||
|
||||
async function fetchMaterialName(key: string) {
|
||||
try {
|
||||
const res = await materialTypesApi.list(1, 100);
|
||||
const data = (res as any).data || res;
|
||||
const list = data.items || [];
|
||||
const found = list.find((m: any) => m.key === key);
|
||||
if (found) selectedMaterialDesc = found.description;
|
||||
} catch (e) { console.log("Error visual material", e); }
|
||||
}
|
||||
|
||||
// --- HANDLERS ---
|
||||
function handleClientSelect(client: any) {
|
||||
formData.client_id = client.id;
|
||||
selectedClientName = client.name;
|
||||
selectedClientStatus = client.is_active ?? true;
|
||||
}
|
||||
|
||||
function handleClassSelect(item: any) {
|
||||
formData.part_class = item.class_code;
|
||||
selectedClassDesc = item.description_es || item.description_en || "";
|
||||
}
|
||||
|
||||
function handleMaterialSelect(item: any) {
|
||||
formData.material_type = item.key;
|
||||
selectedMaterialDesc = item.description;
|
||||
}
|
||||
|
||||
function handleUOMSelect(item: any) {
|
||||
formData.unit_of_measure = item.code;
|
||||
}
|
||||
|
||||
function handleAltUOMSelect(item: any) {
|
||||
formData.alternate_unit_measure = item.code;
|
||||
}
|
||||
|
||||
// --- SUBMIT ---
|
||||
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 = 'Debe seleccionar un Cliente (Pestaña Otros)'; return; }
|
||||
if (!formData.part_number.trim()) { error = 'Número de Parte requerido'; return; }
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const commonData = {
|
||||
description_spanish: formData.description_spanish || null,
|
||||
description_english: formData.description_english || null,
|
||||
|
||||
part_class: formData.part_class || null,
|
||||
material_type: formData.material_type || null, // Corregido
|
||||
|
||||
country_of_origin: formData.country_of_origin || 'MEX',
|
||||
unit_of_measure: formData.unit_of_measure,
|
||||
fraction: formData.fraction || null,
|
||||
us_fraction: formData.us_fraction || null,
|
||||
unit_weight: Number(formData.unit_weight) || 0,
|
||||
weight_type: formData.weight_type || 'KG',
|
||||
supplier: formData.supplier || null,
|
||||
fda_key: formData.fda_key || null,
|
||||
fcc_key: formData.fcc_key || null,
|
||||
eccn: formData.eccn || null,
|
||||
license_code: formData.license_code || null,
|
||||
export_code: formData.export_code || null,
|
||||
exclusion_symbol: formData.exclusion_symbol || null,
|
||||
alternate_unit_measure: formData.alternate_unit_measure || null,
|
||||
part_photo: formData.part_photo || null,
|
||||
added_value: Number(formData.added_value) || 0,
|
||||
unit_cost: Number(formData.unit_cost) || 0,
|
||||
currency_key: formData.currency_key || 'USD',
|
||||
commercial_part_number: formData.commercial_part_number || null,
|
||||
is_active: formData.is_active
|
||||
};
|
||||
|
||||
if (isEdit && id) {
|
||||
const response = await partsApi.update(id, commonData, activeCompanyId);
|
||||
if (response.error) throw new Error(response.error);
|
||||
} 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);
|
||||
}
|
||||
|
||||
goto('/dashboard/goods/parts');
|
||||
|
||||
} catch (e: any) {
|
||||
console.error("Error en el guardado:", e);
|
||||
error = e.message || 'Error inesperado al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
let type = 'fa';
|
||||
</script>
|
||||
|
||||
<div class="w-full mx-auto max-w-6xl py-6 px-4 space-y-6 pb-48">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<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-[500px]">
|
||||
|
||||
<Tabs.Content value="general" class="space-y-6 pt-4 animate-in fade-in duration-300">
|
||||
|
||||
<div class="grid gap-2">
|
||||
<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="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="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="desc_en">Inglés</Label>
|
||||
<Textarea id="desc_en" bind:value={formData.description_english} maxlength={500} rows={3}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="part_class_client" class="required">Clase (Anexo 24 - Cliente)</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute left-3 top-2.5 text-muted-foreground">
|
||||
<Package class="h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
id="part_class_client"
|
||||
bind:value={formData.part_class}
|
||||
maxlength={8}
|
||||
placeholder="Seleccione Clase..."
|
||||
class="pl-9 font-mono cursor-pointer"
|
||||
readonly
|
||||
onclick={() => showClassModal = true}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showClassModal = true}>
|
||||
<Search class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if selectedClassDesc}
|
||||
<div class="text-xs text-primary font-medium px-1 animate-in fade-in flex items-center gap-1">
|
||||
<Tag class="h-3 w-3" />
|
||||
{selectedClassDesc}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="part_class_material">Tipo Material (Catálogo General)</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute left-3 top-2.5 text-muted-foreground">
|
||||
<Layers class="h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
id="part_class_material"
|
||||
bind:value={formData.material_type}
|
||||
maxlength={8}
|
||||
placeholder="Seleccione Material..."
|
||||
class="pl-9 font-mono cursor-pointer"
|
||||
readonly
|
||||
onclick={() => showMaterialModal = true}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showMaterialModal = true}>
|
||||
<Search class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if selectedMaterialDesc}
|
||||
<div class="text-xs text-blue-600 font-medium px-1 animate-in fade-in flex items-center gap-1">
|
||||
<Layers class="h-3 w-3" />
|
||||
{selectedMaterialDesc}
|
||||
</div>
|
||||
{/if}
|
||||
<p class="text-[10px] text-muted-foreground">Opcional: Clasificación adicional por tipo de material.</p>
|
||||
</div>
|
||||
|
||||
<!-- <div class="grid gap-2">
|
||||
<Label for="country">País Origen (ISO)</Label>
|
||||
<Input id="country" bind:value={formData.country_of_origin} maxlength={3} placeholder="MEX" class="font-mono"/>
|
||||
</div> -->
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="uom" class="required">Unidad de Medida (TIGIE)</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute left-3 top-2.5 text-muted-foreground">
|
||||
<Scale class="h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
id="uom"
|
||||
bind:value={formData.unit_of_measure}
|
||||
placeholder="Seleccione..."
|
||||
class="pl-9 font-mono cursor-pointer"
|
||||
readonly
|
||||
onclick={() => showUOMModal = true}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showUOMModal = true}>
|
||||
<Search class="h-4 w-4" />
|
||||
</Button>
|
||||
</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, valores y peso unitario
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="currency">Tipos de Moneda</Label>
|
||||
<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ólares (USD)</Select.Item>
|
||||
<Select.Item value="MXP">Pesos (MXP)</Select.Item>
|
||||
<Select.Item value="EUR">Euros (EUR)</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<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} placeholder="0.0000" />
|
||||
<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="space-y-3 p-4 border rounded-lg">
|
||||
<Label class="font-semibold">Tipo Valor Agregado</Label>
|
||||
<div class="flex flex-wrap gap-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="radio" id="va_ext" name="va_type" value="USD" bind:group={formData.value_added_type} class="accent-primary h-4 w-4 cursor-pointer" />
|
||||
<Label for="va_ext" class="font-normal cursor-pointer">Extranjera (Dls)</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="radio" id="va_nac" name="va_type" value="MXP" bind:group={formData.value_added_type} class="accent-primary h-4 w-4 cursor-pointer" />
|
||||
<Label for="va_nac" class="font-normal cursor-pointer">Nacional (Pesos)</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="radio" id="va_pct" name="va_type" value="PERCENT" bind:group={formData.value_added_type} class="accent-primary h-4 w-4 cursor-pointer" />
|
||||
<Label for="va_pct" class="font-normal cursor-pointer">Porcentaje</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 mt-2">
|
||||
<div class="relative max-w-xs">
|
||||
{#if formData.value_added_type === 'PERCENT'}
|
||||
<span class="absolute right-3 top-2.5 text-muted-foreground">%</span>
|
||||
{:else}
|
||||
<span class="absolute left-3 top-2.5 text-muted-foreground">$</span>
|
||||
{/if}
|
||||
<Input type="number" step="0.0001" id="added_value" bind:value={formData.added_value} class={formData.value_added_type === 'PERCENT' ? 'pr-7' : 'pl-7'} placeholder="0.00"/>
|
||||
</div>
|
||||
</div>
|
||||
</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="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="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</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">Cliente Asignado</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<UserCheck class="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="number"
|
||||
id="client_id"
|
||||
bind:value={formData.client_id}
|
||||
class="pl-9 font-mono"
|
||||
placeholder="Seleccione un cliente..."
|
||||
readonly
|
||||
onclick={() => showClientModal = true}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" class="shrink-0" type="button" onclick={() => showClientModal = true}>
|
||||
<Search class="h-4 w-4 mr-2" /> Buscar
|
||||
</Button>
|
||||
</div>
|
||||
{#if selectedClientName}
|
||||
<div class="flex items-center gap-2 mt-1 px-3 py-2 bg-slate-50 dark:bg-slate-900/50 border rounded-md text-sm">
|
||||
<span class="font-semibold text-primary">{selectedClientName}</span>
|
||||
{#if selectedClientStatus}
|
||||
<span class="text-green-600 flex items-center gap-1 text-xs font-medium"><CheckCircle2 class="h-3 w-3"/> Activo</span>
|
||||
{:else}
|
||||
<span class="text-red-600 flex items-center gap-1 text-xs font-medium"><XCircle class="h-3 w-3"/> Inactivo</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<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"/> Datos Comerciales (Factura)
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<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} placeholder="Código en factura" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="uom" class="required">Unidad de medida alternativa</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute left-3 top-2.5 text-muted-foreground">
|
||||
<Scale class="h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
id="uom"
|
||||
bind:value={formData.alternate_unit_measure}
|
||||
placeholder="Seleccione..."
|
||||
class="pl-9 font-mono cursor-pointer"
|
||||
readonly
|
||||
onclick={() => showAltUOMModal = true}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" type="button" onclick={() => showAltUOMModal = true}>
|
||||
<Search class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border rounded-lg space-y-4">
|
||||
<h3 class="font-medium text-sm text-muted-foreground">Datos Regulatorios</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} />
|
||||
<Label for="is_active">Parte Activa en Sistema</Label>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</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">
|
||||
<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" />{:else}<Save class="mr-2 h-4 w-4" />{/if}
|
||||
{isEdit ? 'Actualizar' : 'Guardar'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ClientSelectorDialog
|
||||
bind:open={showClientModal}
|
||||
onSelect={handleClientSelect}
|
||||
/>
|
||||
|
||||
<ClassSelectorDialog
|
||||
bind:open={showClassModal}
|
||||
onSelect={handleClassSelect}
|
||||
/>
|
||||
|
||||
<MaterialTypeSelectorDialog
|
||||
bind:open={showMaterialModal}
|
||||
onSelect={handleMaterialSelect}
|
||||
/>
|
||||
|
||||
<UnitMeasureSelectorDialog
|
||||
bind:open={showUOMModal}
|
||||
onSelect={handleUOMSelect}
|
||||
/>
|
||||
|
||||
<UnitMeasureSelectorDialog
|
||||
bind:open={showAltUOMModal}
|
||||
onSelect={handleAltUOMSelect}
|
||||
/>
|
||||
|
||||
<style>
|
||||
:global(.required::after) {
|
||||
content: " *";
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
</style>
|
||||
<PartForm partId={id} formType={type} />
|
||||
Reference in New Issue
Block a user