Modulo de tipo de cambio inteligente

This commit is contained in:
2026-02-09 11:30:08 -06:00
parent 33a53d95b4
commit b76bd71265
3 changed files with 272 additions and 235 deletions

View File

@@ -1,59 +1,56 @@
<script lang="ts">
import { onMount } from 'svelte';
import { companyStore } from '$lib/stores/company.svelte';
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import CreateEditDialog from './create-edit-dialog.svelte';
import { onMount } from 'svelte';
import { companyStore } from '$lib/stores/company.svelte';
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import { page } from '$app/state';
import CreateEditDialog from './create-edit-dialog.svelte';
let open = $state(false);
let checked = $state(false);
let open = $state(false);
let checked = $state(false);
async function checkExchangeRate() {
console.log('[ExchangeRateGuard] Checking...', companyStore.activeCompany);
if (!companyStore.activeCompany?.id) {
console.log('[ExchangeRateGuard] No active company');
return;
}
async function checkExchangeRate() {
console.log('[ExchangeRateGuard] Checking...', companyStore.activeCompany);
if (!companyStore.activeCompany?.id) {
console.log('[ExchangeRateGuard] No active company');
return;
}
// Use local date instead of UTC
const today = new Date().toLocaleDateString('fr-CA'); // YYYY-MM-DD
console.log('[ExchangeRateGuard] Date:', today);
try {
const response = await getExchangeRates(companyStore.activeCompany.id, {
date: today,
page_size: 1
});
console.log('[ExchangeRateGuard] Response (stringified):', JSON.stringify(response, null, 2));
// Use local date instead of UTC
const today = new Date().toLocaleDateString('fr-CA'); // YYYY-MM-DD
console.log('[ExchangeRateGuard] Date:', today);
// api.get returns { data: ..., status: ... } and types now reflect that
const items = response.data?.items || [];
if (items.length === 0) {
console.log('[ExchangeRateGuard] No rate found, opening modal');
open = true;
}
} catch (error) {
console.error('[ExchangeRateGuard] Error checking exchange rate:', error);
} finally {
checked = true;
}
}
try {
const response = await getExchangeRates(companyStore.activeCompany.id, {
date: today,
page_size: 1
});
console.log('[ExchangeRateGuard] Response (stringified):', JSON.stringify(response, null, 2));
$effect(() => {
if (companyStore.activeCompany?.id && !checked) {
checkExchangeRate();
}
});
// api.get returns { data: ..., status: ... } and types now reflect that
const items = response.data?.items || [];
function handleSuccess() {
console.log('Exchange rate created successfully via guard');
checked = true;
}
if (items.length === 0) {
console.log('[ExchangeRateGuard] No rate found, opening modal');
open = true;
}
} catch (error) {
console.error('[ExchangeRateGuard] Error checking exchange rate:', error);
} finally {
checked = true;
}
}
$effect(() => {
const isDashboard = page.url.pathname === '/dashboard' || page.url.pathname === '/dashboard/';
if (companyStore.activeCompany?.id && !checked && isDashboard) {
checkExchangeRate();
}
});
function handleSuccess() {
console.log('Exchange rate created successfully via guard');
checked = true;
}
</script>
<CreateEditDialog
bind:open
onSuccess={handleSuccess}
overlayClass="bg-black/20"
/>
<CreateEditDialog bind:open onSuccess={handleSuccess} overlayClass="bg-black/20" />

View File

@@ -1,196 +1,214 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import { Switch } from '$lib/components/ui/switch';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import { Switch } from '$lib/components/ui/switch';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
let {
invoice,
formData = $bindable(),
invoiceTypes = [],
pedimentos = [],
defaultOperationType = undefined,
defaultInvoiceType = undefined,
}: {
invoice: Invoice | null;
formData?: any;
invoiceTypes?: InvoiceType[];
pedimentos?: Pedimento[];
defaultOperationType?: string | null;
defaultInvoiceType?: string | null;
} = $props();
let {
invoice,
formData = $bindable(),
invoiceTypes = [],
pedimentos = [],
defaultOperationType = undefined,
defaultInvoiceType = undefined
}: {
invoice: Invoice | null;
formData?: any;
invoiceTypes?: InvoiceType[];
pedimentos?: Pedimento[];
defaultOperationType?: string | null;
defaultInvoiceType?: string | null;
} = $props();
function handlePedimentoChange(pedimentoId: string) {
if (!pedimentoId) return;
const selectedPedimento = pedimentos.find(p => p.id === parseInt(pedimentoId));
if (!selectedPedimento) return;
function handlePedimentoChange(pedimentoId: string) {
if (!pedimentoId) return;
// Actualizar los campos del pedimento en formData
formData.fecha_pedimento_del = selectedPedimento.pedimento_dates?.start_date || '';
formData.fecha_pedimento_al = selectedPedimento.pedimento_dates?.end_date || '';
formData.clave_pedimento = selectedPedimento.pedimento_code || '';
formData.regimen_pedimento = selectedPedimento.regime || '';
// Construir el número de pedimento completo
const pedimentoNumber = `${selectedPedimento.customs_office?.slice(0,2) || ''}-${selectedPedimento.license || ''}-${selectedPedimento.pedimento_number || ''}`.replace(/^-+|-+$/g, '');
formData.pedimento = pedimentoNumber;
}
const selectedPedimento = pedimentos.find((p) => p.id === parseInt(pedimentoId));
if (!selectedPedimento) return;
$effect(() => {
if (formData?.pedimento_id && pedimentos.length > 0 && !formData.pedimento) {
handlePedimentoChange(formData.pedimento_id);
}
});
// Actualizar los campos del pedimento en formData
formData.fecha_pedimento_del = selectedPedimento.pedimento_dates?.start_date || '';
formData.fecha_pedimento_al = selectedPedimento.pedimento_dates?.end_date || '';
formData.clave_pedimento = selectedPedimento.pedimento_code || '';
formData.regimen_pedimento = selectedPedimento.regime || '';
// Efecto para actualizar operation_type cuando cambia defaultOperationType
$effect(() => {
if (formData && defaultOperationType !== undefined && defaultOperationType !== null) {
// Si operation_type está vacío, null, o undefined, actualizarlo con defaultOperationType
if (!formData.operation_type) {
formData.operation_type = defaultOperationType;
}
}
});
// Construir el número de pedimento completo
const pedimentoNumber =
`${selectedPedimento.customs_office?.slice(0, 2) || ''}-${selectedPedimento.license || ''}-${selectedPedimento.pedimento_number || ''}`.replace(
/^-+|-+$/g,
''
);
formData.pedimento = pedimentoNumber;
}
if (!formData) {
let operationType: string | null = null;
if (invoice?.operation_type) {
operationType = invoice.operation_type;
} else if (defaultOperationType !== undefined) {
operationType = defaultOperationType ?? null;
}
$effect(() => {
if (formData?.pedimento_id && pedimentos.length > 0 && !formData.pedimento) {
handlePedimentoChange(formData.pedimento_id);
}
});
formData = {
is_pedimento_pending: false,
pedimento_id: invoice?.compliance_mx?.pedimento_id || '',
remesa: invoice?.compliance_mx?.remesa || '',
invoice_number: invoice?.invoice_number || '',
invoice_date: invoice?.invoice_date || new Date().toISOString().split('T')[0],
emission_date: new Date().toISOString().split('T')[0],
operation_type: operationType,
invoice_type: invoice?.invoice_type || (defaultInvoiceType ?? ''),
// Campos del pedimento (se llenarán al seleccionar un pedimento)
fecha_pedimento_del: '',
fecha_pedimento_al: '',
clave_pedimento: '',
regimen_pedimento: '',
};
} else {
// Si formData ya existe pero operation_type está vacío, usar defaultOperationType
if (!formData.operation_type && defaultOperationType !== undefined && defaultOperationType !== null) {
formData.operation_type = defaultOperationType;
}
}
// Efecto para actualizar operation_type cuando cambia defaultOperationType
$effect(() => {
if (formData && defaultOperationType !== undefined && defaultOperationType !== null) {
// Si operation_type está vacío, null, o undefined, actualizarlo con defaultOperationType
if (!formData.operation_type) {
formData.operation_type = defaultOperationType;
}
}
});
if (!formData) {
let operationType: string | null = null;
if (invoice?.operation_type) {
operationType = invoice.operation_type;
} else if (defaultOperationType !== undefined) {
operationType = defaultOperationType ?? null;
}
formData = {
is_pedimento_pending: false,
pedimento_id: invoice?.compliance_mx?.pedimento_id || '',
remesa: invoice?.compliance_mx?.remesa || '',
invoice_number: invoice?.invoice_number || '',
invoice_date:
invoice?.invoice_date || (invoice ? '' : new Date().toISOString().split('T')[0]),
emission_date: invoice?.emission_date || new Date().toISOString().split('T')[0],
operation_type: operationType,
invoice_type: invoice?.invoice_type || (defaultInvoiceType ?? ''),
// Campos del pedimento (se llenarán al seleccionar un pedimento)
fecha_pedimento_del: '',
fecha_pedimento_al: '',
clave_pedimento: '',
regimen_pedimento: ''
};
} else {
// Si formData ya existe pero operation_type está vacío, usar defaultOperationType
if (
!formData.operation_type &&
defaultOperationType !== undefined &&
defaultOperationType !== null
) {
formData.operation_type = defaultOperationType;
}
}
</script>
<!-- Datos Principales en una fila compacta (reusable across tabs) -->
<div class="grid grid-cols-12 gap-3 items-end pb-3">
<div class="col-span-1 space-y-1">
<Label for="operation_type" class="text-xs">Tipo de Operación<span class="text-red-500">*</span></Label>
<Select.Root
type="single"
value={formData.operation_type || ''}
onValueChange={(v) => {
formData.operation_type = v;
}}
>
<Select.Trigger id="operation_type" class="h-8 text-sm">
<span class="truncate">
{formData.operation_type
? (formData.operation_type === 'exp' ? 'Exp' : 'Imp')
: '...'}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="exp">Exportación</Select.Item>
<Select.Item value="imp">Importación</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="col-span-1 space-y-1">
<Label for="operation_type" class="text-xs">Tipo de factura <span class="text-red-500">*</span></Label>
<Select.Root
type="single"
value={formData.invoice_type || ''}
onValueChange={(v) => {
formData.invoice_type = v ?? '';
}}
>
<Select.Trigger id="invoice_type" class="h-7 text-xs">
<span class="truncate">
{formData.invoice_type
? `${formData.invoice_type}`
: '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each invoiceTypes as type}
<Select.Item value={type.key}>
{type.key} - {type.description}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="grid grid-cols-12 items-end gap-3 pb-3">
<div class="col-span-1 space-y-1">
<Label for="operation_type" class="text-xs"
>Tipo de Operación<span class="text-red-500">*</span></Label
>
<Select.Root
type="single"
value={formData.operation_type || ''}
onValueChange={(v) => {
formData.operation_type = v;
}}
>
<Select.Trigger id="operation_type" class="h-8 text-sm">
<span class="truncate">
{formData.operation_type ? (formData.operation_type === 'exp' ? 'Exp' : 'Imp') : '...'}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="exp">Exportación</Select.Item>
<Select.Item value="imp">Importación</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="col-span-1 space-y-1">
<Label for="operation_type" class="text-xs"
>Tipo de factura <span class="text-red-500">*</span></Label
>
<Select.Root
type="single"
value={formData.invoice_type || ''}
onValueChange={(v) => {
formData.invoice_type = v ?? '';
}}
>
<Select.Trigger id="invoice_type" class="h-7 text-xs">
<span class="truncate">
{formData.invoice_type ? `${formData.invoice_type}` : '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each invoiceTypes as type}
<Select.Item value={type.key}>
{type.key} - {type.description}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="col-span-1 space-y-1 pb-1 items-center flex flex-col">
<Label for="is_pedimento_pending" class="text-xs">Pedimento Pendiente?</Label>
<Switch
id="is_pedimento_pending"
checked={formData.is_pedimento_pending}
onCheckedChange={(checked) => {
formData.is_pedimento_pending = checked;
}}
/>
</div>
<div class="col-span-2 space-y-1">
<Label for="pedimento" class="text-xs">Pedimento</Label>
<Select.Root
type="single"
value={formData.pedimento_id ? String(formData.pedimento_id) : ''}
onValueChange={(v) => {
formData.pedimento_id = v ? parseInt(v) : null;
if (v) {
handlePedimentoChange(v);
}
}}
>
<Select.Trigger id="pedimento" class="h-8 text-sm">
<span class="truncate">
{formData.pedimento || 'Selecciona pedimento...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each pedimentos as pedimento}
<Select.Item value={String(pedimento.id)}>
{pedimento.customs_office?.slice(0, 2)}-{pedimento.license}-{pedimento.pedimento_number}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="col-span-1 flex flex-col items-center space-y-1 pb-1">
<Label for="is_pedimento_pending" class="text-xs">Pedimento Pendiente?</Label>
<Switch
id="is_pedimento_pending"
checked={formData.is_pedimento_pending}
onCheckedChange={(checked) => {
formData.is_pedimento_pending = checked;
}}
/>
</div>
<div class="col-span-2 space-y-1">
<Label for="pedimento" class="text-xs">Pedimento</Label>
<Select.Root
type="single"
value={formData.pedimento_id ? String(formData.pedimento_id) : ''}
onValueChange={(v) => {
formData.pedimento_id = v ? parseInt(v) : null;
if (v) {
handlePedimentoChange(v);
}
}}
>
<Select.Trigger id="pedimento" class="h-8 text-sm">
<span class="truncate">
{formData.pedimento || 'Selecciona pedimento...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each pedimentos as pedimento}
<Select.Item value={String(pedimento.id)}>
{pedimento.customs_office?.slice(0, 2)}-{pedimento.license}-{pedimento.pedimento_number}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="col-span-1 space-y-1">
<Label for="remesa" class="text-xs">Remesa</Label>
<Input id="remesa" bind:value={formData.remesa} class="h-8 text-sm" />
</div>
<div class="col-span-1 space-y-1">
<Label for="remesa" class="text-xs">Remesa</Label>
<Input id="remesa" bind:value={formData.remesa} class="h-8 text-sm" />
</div>
<div class="col-span-2 space-y-1">
<Label for="invoice_number" class="text-xs">Núm. Factura <span class="text-red-500">*</span></Label>
<Input id="invoice_number" bind:value={formData.invoice_number} class="h-8 text-sm font-medium" required />
</div>
<div class="col-span-2 space-y-1">
<Label for="invoice_number" class="text-xs"
>Núm. Factura <span class="text-red-500">*</span></Label
>
<Input
id="invoice_number"
bind:value={formData.invoice_number}
class="h-8 text-sm font-medium"
required
/>
</div>
<div class="col-span-2 space-y-1">
<Label for="invoice_date" class="text-xs">Fecha Factura <span class="text-red-500">*</span></Label>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} class="h-8 text-sm" />
</div>
<div class="col-span-2 space-y-1">
<Label for="invoice_date" class="text-xs"
>Fecha Factura <span class="text-red-500">*</span></Label
>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} class="h-8 text-sm" />
</div>
<div class="col-span-2 space-y-1">
<Label for="emission_date" class="text-xs">Fecha Emisión</Label>
<Input id="emission_date" type="date" bind:value={formData.emission_date} class="h-8 text-sm" />
</div>
<div class="col-span-2 space-y-1">
<Label for="emission_date" class="text-xs">Fecha Emisión</Label>
<Input id="emission_date" type="date" bind:value={formData.emission_date} class="h-8 text-sm" />
</div>
</div>

View File

@@ -109,6 +109,9 @@
data.invoice?.financials?.exchange_rate ?? null
);
// Guardar la fecha inicial para detectar cambios manuales
let initialInvoiceDate = data.invoice?.invoice_date || '';
let showExchangeRateDialog = $state(false);
let missingExchangeRateDate = $state('');
@@ -149,13 +152,32 @@
// Efecto para actualizar el tipo de cambio cuando cambia la fecha de factura
$effect(() => {
if (mounted && companyStore?.activeCompany?.id && InvoiceTopFieldsFormData?.invoice_date) {
getExchangeRateByDate(InvoiceTopFieldsFormData.invoice_date, companyStore.activeCompany.id)
const currentDate = InvoiceTopFieldsFormData?.invoice_date;
// Solo proceder si:
// 1. Tenemos datos cargados y compañía activa
// 2. Es una factura nueva (isCreate)
// 3. O la fecha es distinta a la original (el usuario la cambió)
// 4. O no tenemos ningún tipo de cambio todavía
const shouldFetch =
mounted &&
companyStore?.activeCompany?.id &&
currentDate &&
(data.isCreate || currentDate !== initialInvoiceDate || !calculatedExchangeRate);
if (shouldFetch) {
getExchangeRateByDate(currentDate, companyStore.activeCompany.id)
.then((rate) => {
if (rate) {
calculatedExchangeRate = rate.value;
} else {
calculatedExchangeRate = 0;
// Si no hay en catálogo, pero YA teníamos uno en la factura (y no cambió fecha), NO poner 0
if (currentDate === initialInvoiceDate && data.invoice?.financials?.exchange_rate) {
calculatedExchangeRate = data.invoice.financials.exchange_rate;
} else {
calculatedExchangeRate = 0;
}
}
})
.catch((err) => console.error('Error auto-updating exchange rate:', err));
@@ -293,7 +315,7 @@
: null}
Nueva Factura
{#if invoiceTypeInfo}
<span class="text-muted-foreground font-normal text-2xl">
<span class="text-2xl font-normal text-muted-foreground">
- {invoiceTypeInfo.description}
</span>
{/if}
@@ -420,9 +442,9 @@
}}
/>
<div
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
>
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
<div class="mx-auto max-w-[1400px] space-y-4 px-4 py-4">
<!-- Tabs Navigation -->
<Tabs.Root bind:value={activeTab}>
<div class="w-full overflow-x-auto pb-2">