Files
plantillas-proyectos/frontend/src/routes/dashboard/invoices/settings/+page.svelte

701 lines
24 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { page } from '$app/stores';
import * as Tabs from '$lib/components/ui/tabs';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import { Input } from '$lib/components/ui/input';
import { Badge } from '$lib/components/ui/badge';
import { Separator } from '$lib/components/ui/separator';
import {
LoaderCircle, Save, FileText, Eye, DollarSign,
Truck, Package, ArrowLeft
} from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import { api } from '$lib/api';
import { goto } from '$app/navigation';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/components/ui/select';
// Import Form Components
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
import ObservationsTabForm from '$lib/components/dashboard/invoices/edit/observations-tab-form.svelte';
import ItemsTabForm from '$lib/components/dashboard/invoices/edit/items/items-tab-form.svelte';
import OthersTabForm from '$lib/components/dashboard/invoices/edit/others-tab-form.svelte';
import InvoiceTopFields from '$lib/components/dashboard/invoices/edit/invoice-top-fields.svelte';
import ContinuationTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte';
import SsimpSettingsForm from '$lib/components/dashboard/invoices/settings/ssimp-settings-form.svelte';
import SsisdefSettingsForm from '$lib/components/dashboard/invoices/settings/ssisdef-settings-form.svelte';
import SsicmSettingsForm from '$lib/components/dashboard/invoices/settings/ssicm-settings-form.svelte';
import SscrSettingsForm from '$lib/components/dashboard/invoices/settings/sscr-settings-form.svelte';
// Props
let { data } = $props();
// State
let selectedInvoiceType = $state<string>('');
let selectedOperationType = $state<string>('');
let isLoading = $state(false);
let isSaving = $state(false);
let activeTab = $state('general');
let companyStore: any = $state(undefined);
// Form Data State - Initialize as null to let components self-initialize
let InvoiceTopFieldsFormData = $state<any>(null);
let generalFormData = $state<any>(null);
let observationFormData = $state<any>(null);
let itemsFormData = $state<any>(null);
let othersFormData = $state<any>(null);
let continuationFormData = $state<any>(null);
let ssimpFormData = $state<any>(null);
// Derived object for components to consume context
// We use a getter to avoid circular reactivity issues during initialization
let invoice = $derived.by(() => ({
id: 0,
tenant_id: 0,
company_id: companyStore?.activeCompany?.id || 0,
capture_date: new Date().toISOString(),
operation_type: selectedOperationType,
invoice_type: selectedInvoiceType,
financials: { exchange_rate: 0 },
compliance_mx: {
pedimento_id: InvoiceTopFieldsFormData?.pedimento_id || '',
remesa: InvoiceTopFieldsFormData?.remesa || ''
},
invoice_number: InvoiceTopFieldsFormData?.invoice_number || '',
invoice_date: InvoiceTopFieldsFormData?.invoice_date || '',
alternate_invoice: observationFormData?.alternate_invoice || ''
}) as any);
// Existence flags (not strictly needed for settings but required by components)
let observationExists = $state(false);
let itemsExists = $state(false);
let othersExists = $state(false);
let continuationExists = $state(false);
const operationTypes = [
{ value: 'imp', label: 'Importación' },
{ value: 'exp', label: 'Exportación' }
];
onMount(async () => {
try {
const companyStoreModule = await import('$lib/stores/company.svelte');
companyStore = companyStoreModule.companyStore;
} catch (err) {
console.error('Error loading company store:', err);
}
});
async function loadSettings() {
if (!selectedInvoiceType || !selectedOperationType || !companyStore?.activeCompany?.id) return;
isLoading = true;
try {
const res = await api.get(
`/v1/a76/invoice-settings/${selectedInvoiceType}?operation_type=${selectedOperationType}&company_id=${companyStore.activeCompany.id}`
);
if (res.status === 200 || res.data) {
const settingsData = res.data;
if (settingsData && settingsData.settings) {
applySettings(settingsData.settings);
} else {
resetForms();
}
} else {
resetForms();
}
} catch (error) {
console.error('Error loading settings', error);
resetForms();
} finally {
isLoading = false;
}
}
function applySettings(settings: any) {
InvoiceTopFieldsFormData = {
is_pedimento_pending: false,
pedimento_id: '',
remesa: '',
invoice_number: '',
invoice_date: new Date().toISOString().split('T')[0],
emission_date: new Date().toISOString().split('T')[0],
operation_type: selectedOperationType,
invoice_type: selectedInvoiceType,
fecha_pedimento_del: '',
fecha_pedimento_al: '',
clave_pedimento: '',
regimen_pedimento: '',
...(settings.InvoiceTopFieldsFormData || {})
};
generalFormData = {
provider_header: 'proveedor',
provider_id: null,
sold_to_header: 'consignado_a',
sold_to_id: null,
shipped_to_header: 'enviado_a',
shipped_to_id: null,
customs_broker_id: null,
customs_broker_us_id: null,
currency_type: '',
currency: 'foreign',
exchange_rate: null,
weight_type: 'kgs',
iva_factor: null,
carrier_id: null,
transport_id: '',
driver_name: '',
transport_type: '',
transport_num: '',
aduana: '',
document_type: '',
...(settings.generalFormData || {})
};
observationFormData = {
observation_es: '',
observation_en: '',
freight: null,
insurance_value: null,
insurance: null,
packaging: null,
other_increments: null,
total_increments_mn: null,
total_increments_me: null,
incoterm: null,
enclosure: null,
num_seals: null,
movement_type: '',
alternate_invoice: '',
valuation_method: null,
...(settings.observationFormData || {})
};
itemsFormData = settings.itemsFormData || {};
ssimpFormData = settings.ssimpFormData || {};
othersFormData = {
comments_status: '',
transport_mode: 'TRUCK',
is_mixed: null,
print_stamp: false,
rule_3121_parties_ii: false,
related_doc_id: null,
code_signature: '',
electronic_signature: '',
mandatory_person: '',
contingency_mode: false,
cove: '',
operation_num: '',
adendas: '',
observations_vu: '',
certified_number: '',
...(settings.othersFormData || {})
};
continuationFormData = {
numero_tipo_transporte: '',
es_ferrocarril: 'no',
numero_bl: '',
cantidad_guias_embarque: null,
destino_origen: '',
puerto_entrada: '',
fue_revisado_equipo: false,
sub_division: false,
funge_como_cd: false,
llego_pedimento: false,
errores_facturacion: [],
semaforo_verde_aduana_mexicana: false,
semaforo_verde_aduana_americana: false,
semaforo_rojo_aduana_mexicana: false,
semaforo_rojo_aduana_americana: false,
...(settings.continuationFormData || {})
};
// The derived 'invoice' will update automatically because it spread InvoiceTopFieldsFormData
}
function resetForms() {
InvoiceTopFieldsFormData = {
is_pedimento_pending: false,
pedimento_id: '',
remesa: '',
invoice_number: '',
invoice_date: new Date().toISOString().split('T')[0],
emission_date: new Date().toISOString().split('T')[0],
operation_type: selectedOperationType,
invoice_type: selectedInvoiceType,
fecha_pedimento_del: '',
fecha_pedimento_al: '',
clave_pedimento: '',
regimen_pedimento: ''
};
generalFormData = {
provider_header: 'proveedor',
provider_id: null,
sold_to_header: 'consignado_a',
sold_to_id: null,
shipped_to_header: 'enviado_a',
shipped_to_id: null,
customs_broker_id: null,
customs_broker_us_id: null,
currency_type: '',
currency: 'foreign',
exchange_rate: null,
weight_type: 'kgs',
iva_factor: null,
carrier_id: null,
transport_id: '',
driver_name: '',
transport_type: '',
transport_num: '',
aduana: '',
document_type: ''
};
observationFormData = {
observation_es: '',
observation_en: '',
freight: null,
insurance_value: null,
insurance: null,
packaging: null,
other_increments: null,
total_increments_mn: null,
total_increments_me: null,
incoterm: null,
enclosure: null,
num_seals: null,
movement_type: '',
alternate_invoice: '',
valuation_method: null
};
itemsFormData = {};
ssimpFormData = {};
othersFormData = {
comments_status: '',
transport_mode: 'TRUCK',
is_mixed: null,
print_stamp: false,
rule_3121_parties_ii: false,
related_doc_id: null,
code_signature: '',
electronic_signature: '',
mandatory_person: '',
contingency_mode: false,
cove: '',
operation_num: '',
adendas: '',
observations_vu: '',
certified_number: ''
};
continuationFormData = {
numero_tipo_transporte: '',
es_ferrocarril: 'no',
numero_bl: '',
cantidad_guias_embarque: null,
destino_origen: '',
puerto_entrada: '',
fue_revisado_equipo: false,
sub_division: false,
funge_como_cd: false,
llego_pedimento: false,
errores_facturacion: [],
semaforo_verde_aduana_mexicana: false,
semaforo_verde_aduana_americana: false,
semaforo_rojo_aduana_mexicana: false,
semaforo_rojo_aduana_americana: false
};
// The derived 'invoice' handles itself
}
function cleanObject(obj: any): any {
if (Array.isArray(obj)) {
return obj.map(cleanObject).filter((v) => v !== null && v !== undefined && v !== '');
}
if (obj !== null && typeof obj === 'object') {
return Object.entries(obj).reduce((acc: any, [key, value]) => {
const cleaned = cleanObject(value);
if (
cleaned !== null &&
cleaned !== undefined &&
cleaned !== '' &&
!(typeof cleaned === 'object' && Object.keys(cleaned).length === 0)
) {
acc[key] = cleaned;
}
return acc;
}, {});
}
return obj;
}
function handleBack() {
goto('/dashboard/invoices');
}
let operationTypeText = $derived.by(() => {
if (selectedOperationType === 'exp') return 'EXPORTACIÓN';
if (selectedOperationType === 'imp') return 'IMPORTACIÓN';
return 'TIPO NO DEFINIDO';
});
let operationColorClass = $derived.by(() => {
if (selectedOperationType === 'exp') return 'bg-blue-500/10 text-blue-600 border-blue-500/20';
if (selectedOperationType === 'imp') return 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20';
return 'bg-muted text-muted-foreground border-border';
});
// Contexto reactivo para componentes
let invoiceType = $derived(selectedInvoiceType || 'TEM');
let operationTypeNumeric = $derived(selectedOperationType === 'exp' ? 1 : 2);
async function handleSaveSettings() {
if (!selectedInvoiceType || !selectedOperationType || !companyStore?.activeCompany?.id) {
toast.error('Por favor selecciona tipo de factura y operación');
return;
}
isSaving = true;
// Use snaphot to get clean data from Svelte 5 proxies
const rawSettings = {
InvoiceTopFieldsFormData: $state.snapshot(InvoiceTopFieldsFormData),
generalFormData: $state.snapshot(generalFormData),
observationFormData: $state.snapshot(observationFormData),
itemsFormData: $state.snapshot(itemsFormData),
ssimpFormData: $state.snapshot(ssimpFormData),
othersFormData: $state.snapshot(othersFormData),
continuationFormData: $state.snapshot(continuationFormData)
};
// Clean the settings to only save what's configured
const cleanedSettings = cleanObject(rawSettings);
const settingsPayload = {
invoice_type: selectedInvoiceType,
operation_type: selectedOperationType,
settings: cleanedSettings
};
try {
const response = await api.put(
`/v1/a76/invoice-settings/?company_id=${companyStore.activeCompany.id}`,
settingsPayload
);
if (response.status === 200 || response.data) {
toast.success('Configuración guardada correctamente.');
} else {
console.error('Save error details:', response.error || response.validationErrors);
toast.error('Error al guardar la configuración.');
}
} catch (error) {
console.error('Error saving settings:', error);
toast.error('Ocurrió un error al guardar.');
} finally {
isSaving = false;
}
}
// Watch for selection changes
// We'll call loadSettings manually when selection changes to avoid reactive loops
$effect(() => {
if (browser && !InvoiceTopFieldsFormData && companyStore?.activeCompany?.id) {
loadSettings();
}
});
// Handle explicit changes
function handleOperationTypeChange(v: string) {
selectedOperationType = v;
loadSettings();
}
function handleInvoiceTypeChange(v: string) {
selectedInvoiceType = v;
loadSettings();
}
</script>
<Tabs.Root bind:value={activeTab}>
<div class="space-y-4">
<!-- Header -->
<div class="flex items-center justify-between">
<div class="space-y-1">
<div class="flex items-center gap-3">
<Button variant="ghost" size="icon" onclick={handleBack}>
<ArrowLeft size={20} />
</Button>
<h1 class="text-3xl font-bold tracking-tight">
Configuración de Facturas
</h1>
{#if selectedOperationType}
<Badge variant="outline" class="px-3 py-1 text-sm font-bold {operationColorClass}">
{operationTypeText}
</Badge>
{/if}
{#if selectedInvoiceType}
<Badge variant="secondary">
{data.invoiceTypes.find((t: any) => t.key === selectedInvoiceType)?.description || selectedInvoiceType}
</Badge>
{/if}
</div>
<p class="text-muted-foreground">
Define los valores predeterminados para la creación de facturas.
</p>
</div>
</div>
<Separator />
<div class="pb-48">
<Card.Root class="mb-8 border-dashed bg-muted/30">
<Card.Header class="py-4">
<Card.Title class="text-lg">Contexto de Configuración</Card.Title>
<Card.Description
>Selecciona el contexto para editar sus valores por defecto.</Card.Description
>
</Card.Header>
<Card.Content class="flex gap-6 py-2">
<div class="space-y-2">
<Label class="text-xs uppercase text-muted-foreground font-bold tracking-wider">Tipo de Operación</Label>
<Select type="single" value={selectedOperationType} onValueChange={handleOperationTypeChange}>
<SelectTrigger class="w-[200px] h-10 bg-background">
{operationTypes.find((t: any) => t.value === selectedOperationType)?.label ||
'Seleccionar...'}
</SelectTrigger>
<SelectContent>
{#each operationTypes as type}
<SelectItem value={type.value}>{type.label}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label class="text-xs uppercase text-muted-foreground font-bold tracking-wider">Tipo de Factura</Label>
<Select type="single" value={selectedInvoiceType} onValueChange={handleInvoiceTypeChange}>
<SelectTrigger class="w-[300px] h-10 bg-background">
{@const type = data.invoiceTypes.find((t: any) => t.key === selectedInvoiceType)}
{type ? `${type.key} - ${type.description}` : 'Seleccionar...'}
</SelectTrigger>
<SelectContent class="max-h-[300px]">
{#each data.invoiceTypes as type}
<SelectItem value={type.key}>{type.key} - {type.description}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
</Card.Content>
</Card.Root>
{#if selectedInvoiceType && selectedOperationType}
{#if isLoading}
<div class="flex justify-center py-24">
<div class="flex flex-col items-center gap-4">
<LoaderCircle class="animate-spin h-10 w-10 text-primary" />
<p class="text-sm text-muted-foreground animate-pulse">Cargando configuración...</p>
</div>
</div>
{:else}
<div class="grid gap-6">
<!-- Top Fields (Header) -->
<div class="bg-background rounded-xl border-2 shadow-sm p-6">
<div class="flex items-center gap-2 mb-6 border-b pb-4">
<div class="p-1.5 bg-primary/10 rounded-md">
<FileText class="w-5 h-5 text-primary" />
</div>
<h3 class="text-lg font-bold">Encabezado Predeterminado</h3>
</div>
<InvoiceTopFields
{invoice}
bind:formData={InvoiceTopFieldsFormData}
invoiceTypes={data.invoiceTypes || []}
pedimentos={data.pedimentos || []}
defaultOperationType={selectedOperationType}
defaultInvoiceType={selectedInvoiceType}
{invoiceType}
isSettings={true}
/>
</div>
<div class="space-y-4">
<Tabs.Content value="general">
<GeneralTabForm
{invoice}
bind:formData={generalFormData}
invoiceTypes={data.invoiceTypes || []}
customsBrokers={data.customsBrokers || []}
clients={data.clients || []}
providers={data.providers || []}
currencyTypes={data.currencyTypes || []}
transportTypes={data.transportTypes || []}
transporters={data.transporters || []}
vehicles={data.vehicles || []}
drivers={data.drivers || []}
trailers={data.trailers || []}
customsSections={data.customsSections || []}
codePedimentoRegimens={data.codePedimentoRegimens || []}
operationType={operationTypeNumeric}
{invoiceType}
exchangeRate={0}
isSettings={true}
/>
</Tabs.Content>
<Tabs.Content value="observations">
<ObservationsTabForm
{invoice}
bind:formData={observationFormData}
bind:exists={observationExists}
seals={[]}
incoterms={data.incoterms || []}
enclosure={[]}
operationType={operationTypeNumeric}
{invoiceType}
isSettings={true}
/>
</Tabs.Content>
<Tabs.Content value="items">
<div class="p-12 border-2 border-dashed rounded-xl bg-muted/20 text-center flex flex-col items-center gap-4">
<div class="p-4 bg-muted rounded-full">
<DollarSign class="w-8 h-8 text-muted-foreground opacity-50" />
</div>
<div class="max-w-md">
<h4 class="font-bold text-lg">Configuración de Partidas</h4>
<p class="text-sm text-muted-foreground mt-1">
La configuración predeterminada de partidas es limitada desde esta sección. Los valores se heredarán de los maestros de productos cuando sea posible.
</p>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="others">
<OthersTabForm
{invoice}
bind:formData={othersFormData}
bind:exists={othersExists}
transportModes={data.transportModes || []}
operationType={operationTypeNumeric}
{invoiceType}
isSettings={true}
/>
</Tabs.Content>
<Tabs.Content value="continuation">
<div class="space-y-6">
{#if invoiceType !== 'MEX'}
<div class="bg-background rounded-xl border p-6">
<h4 class="text-sm font-semibold uppercase text-muted-foreground mb-4">Captura de Valores por Defecto</h4>
<ContinuationTabForm
{invoice}
bind:formData={continuationFormData}
bind:exists={continuationExists}
operationType={operationTypeNumeric}
{invoiceType}
isSettings={true}
/>
</div>
{/if}
</div>
</Tabs.Content>
<Tabs.Content value="ssimp">
<div class="space-y-6">
<div class="bg-background rounded-xl border p-6">
<h4 class="text-sm font-semibold uppercase text-muted-foreground mb-4">
{selectedOperationType === 'exp'
? `Parámetros de Exportación ${data.invoiceTypes?.find((t: any) => t.key === invoiceType)?.description || invoiceType}`
: invoiceType === 'DEF'
? 'Parámetros Adicionales SSisDef'
: invoiceType === 'MEX'
? 'Parámetros Adicionales SSICM'
: 'Parámetros Adicionales SSimp'}
</h4>
{#if selectedOperationType === 'exp'}
<SscrSettingsForm bind:formData={ssimpFormData} />
{:else if invoiceType === 'DEF'}
<SsisdefSettingsForm bind:formData={ssimpFormData} />
{:else if invoiceType === 'MEX'}
<SsicmSettingsForm bind:formData={ssimpFormData} drivers={data.drivers || []} />
{:else}
<SsimpSettingsForm bind:formData={ssimpFormData} />
{/if}
</div>
</div>
</Tabs.Content>
</div>
</div>
{/if}
{:else}
<div
class="flex flex-col items-center justify-center border-2 border-dashed rounded-xl p-24 text-muted-foreground bg-muted/10 mt-12"
>
<div class="p-6 bg-muted rounded-full mb-6">
<FileText class="h-16 w-16 opacity-20" />
</div>
<h3 class="text-xl font-bold text-foreground">Sin Contexto de Edición</h3>
<p class="max-w-xs text-center mt-2">
Selecciona un tipo de operación y factura para comenzar a editar los valores globales por defecto.
</p>
</div>
{/if}
</div>
</div>
<!-- Sticky Footer -->
<div
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur border-t shadow-lg z-[10] transition-all duration-300 md:ml-[calc(var(--sidebar-width))] group-has-data-[state=collapsed]/sidebar-wrapper:md:ml-[calc(var(--sidebar-width-icon))]"
>
<div class="container mx-auto max-w-[1400px] space-y-4 p-4">
<!-- Tabs Navigation (Only shown if context selected) -->
{#if selectedInvoiceType && selectedOperationType}
<div class="w-full overflow-x-auto pb-2">
<Tabs.List class="inline-flex md:grid md:w-full md:grid-cols-6 bg-muted rounded-md p-1 h-auto flex-wrap">
<Tabs.Trigger value="general" class="whitespace-nowrap">
<FileText size={16} class="mr-2" />
General
</Tabs.Trigger>
<Tabs.Trigger value="observations" class="whitespace-nowrap">
<Eye size={16} class="mr-2" />
Observaciones
</Tabs.Trigger>
<Tabs.Trigger value="items" class="whitespace-nowrap">
<DollarSign size={16} class="mr-2" />
Partidas
</Tabs.Trigger>
<Tabs.Trigger value="others" class="whitespace-nowrap">
<Truck size={16} class="mr-2" />
Otros
</Tabs.Trigger>
<Tabs.Trigger value="continuation" class="whitespace-nowrap">
<Package size={16} class="mr-2" />
Cont.
</Tabs.Trigger>
<Tabs.Trigger value="ssimp" class="whitespace-nowrap">
<Package size={16} class="mr-2" />
Continuación
</Tabs.Trigger>
</Tabs.List>
</div>
{/if}
<div class="flex justify-end gap-3">
<Button variant="outline" onclick={resetForms} disabled={isSaving || !selectedInvoiceType}>
Restablecer
</Button>
<Button onclick={handleSaveSettings} disabled={isSaving || !selectedInvoiceType}>
{#if isSaving}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
<Save class="mr-2 h-4 w-4" />
Guardar Configuración
{/if}
</Button>
</div>
</div>
</div>
</Tabs.Root>