Merge fix/invoices_tipo_cambio - resolved conflicts

This commit is contained in:
2026-02-10 09:16:06 -06:00
7 changed files with 1003 additions and 835 deletions

View File

@@ -22,7 +22,6 @@
} from 'lucide-svelte';
import { fly, scale } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { obtenerAtajosFormularioTiposCambio } from '$lib/config/shortcuts/dashboard/general_catalogs/exchange_rate/edit';
let {
open = $bindable(false),
@@ -38,8 +37,6 @@
initialDate?: string;
} = $props();
// Atajos
const isEdit = $derived(!!item);
const isMissingRateContext = $derived(!isEdit && initialDate);
const title = $derived(
@@ -169,25 +166,25 @@
<Dialog.Overlay class="fixed inset-0 z-[9999] {overlayClass}" />
<Dialog.Content
class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[480px] translate-x-[-50%] translate-y-[-50%] border-0 bg-transparent shadow-2xl p-0 sm:rounded-xl overflow-hidden"
class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[480px] translate-x-[-50%] translate-y-[-50%] overflow-hidden border-0 bg-transparent p-0 shadow-2xl sm:rounded-xl"
>
<div
class="bg-background flex flex-col h-full rounded-xl overflow-hidden border border-border"
class="flex h-full flex-col overflow-hidden rounded-xl border border-border bg-background"
>
{#if isMissingRateContext}
<div
class="bg-amber-50 dark:bg-amber-950/40 p-5 flex gap-4 border-b border-amber-100 dark:border-amber-900/50"
class="flex gap-4 border-b border-amber-100 bg-amber-50 p-5 dark:border-amber-900/50 dark:bg-amber-950/40"
>
<div
class="bg-amber-100 dark:bg-amber-900/60 p-2.5 rounded-full h-fit shadow-sm shrink-0"
class="h-fit shrink-0 rounded-full bg-amber-100 p-2.5 shadow-sm dark:bg-amber-900/60"
>
<AlertCircle class="text-amber-600 dark:text-amber-400" size={24} />
</div>
<div class="space-y-1">
<h3 class="font-semibold text-amber-900 dark:text-amber-100 text-lg leading-tight">
<h3 class="text-lg font-semibold leading-tight text-amber-900 dark:text-amber-100">
{title}
</h3>
<p class="text-sm text-amber-800/80 dark:text-amber-200/80 leading-relaxed">
<p class="text-sm leading-relaxed text-amber-800/80 dark:text-amber-200/80">
Para continuar con el guardado, es necesario registrar el tipo de cambio oficial
para esta fecha.
</p>
@@ -207,11 +204,11 @@
e.preventDefault();
handleSubmit();
}}
class="p-6 pt-4 space-y-6"
class="space-y-6 p-6 pt-4"
>
{#if error}
<div
class="rounded-lg bg-destructive/10 border border-destructive/20 p-3 flex gap-3 text-sm text-destructive"
class="flex gap-3 rounded-lg border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive"
transition:fly={{ y: -10 }}
>
<AlertCircle size={16} class="mt-0.5 shrink-0" />
@@ -221,7 +218,7 @@
<div class="grid gap-5">
<div class="grid gap-2">
<Label for="date" class="text-sm font-medium text-muted-foreground ml-1"
<Label for="date" class="ml-1 text-sm font-medium text-muted-foreground"
>Fecha Aplicable</Label
>
<div class="relative">
@@ -231,11 +228,11 @@
bind:value={formData.date}
disabled={loading}
required
class="pl-3 h-11 text-base bg-muted/30 focus:bg-background transition-colors"
class="h-11 bg-muted/30 pl-3 text-base transition-colors focus:bg-background"
/>
{#if isMissingRateContext}
<div
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-amber-600 font-medium bg-amber-100 px-2 py-0.5 rounded-full"
class="absolute right-3 top-1/2 -translate-y-1/2 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-600"
>
Requerida
</div>
@@ -245,27 +242,27 @@
<div class="grid gap-2">
<div class="flex items-center justify-between">
<Label for="value" class="text-sm font-medium text-muted-foreground ml-1"
<Label for="value" class="ml-1 text-sm font-medium text-muted-foreground"
>Tipo de Cambio (MXN/USD)</Label
>
<Button
type="button"
variant="ghost"
size="sm"
class="h-6 text-xs text-primary hover:text-primary/80 px-2 -mr-2"
class="-mr-2 h-6 px-2 text-xs text-primary hover:text-primary/80"
disabled={scraping || loading || !formData.date}
onclick={() => fetchFromDof(formData.date)}
>
{#if scraping}
<span class="animate-spin mr-1.5"></span> Consultando...
<span class="mr-1.5 animate-spin"></span> Consultando...
{:else}
<CloudDownload size={14} class="mr-1.5" /> Consultar DOF
{/if}
</Button>
</div>
<div class="relative group">
<div class="group relative">
<div
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground font-semibold"
class="absolute left-3 top-1/2 -translate-y-1/2 font-semibold text-muted-foreground"
>
$
</div>
@@ -277,11 +274,11 @@
disabled={loading}
required
placeholder="0.0000"
class="pl-7 h-11 text-lg font-mono tracking-wide focus:ring-2 ring-primary/20 transition-all group-hover:border-primary/50"
class="h-11 pl-7 font-mono text-lg tracking-wide ring-primary/20 transition-all group-hover:border-primary/50 focus:ring-2"
autofocus
/>
</div>
<p class="text-[11px] text-muted-foreground text-right px-1">Ej. 24.1234</p>
<p class="px-1 text-right text-[11px] text-muted-foreground">Ej. 24.1234</p>
</div>
</div>
@@ -299,9 +296,9 @@
>
Cancelar
</Button>
<Button type="submit" disabled={loading} class="w-full shadow-sm font-medium">
<Button type="submit" disabled={loading} class="w-full font-medium shadow-sm">
{#if loading}
<span class="animate-spin mr-2"></span>
<span class="mr-2 animate-spin"></span>
{:else}
Ok
{/if}

View File

@@ -1,52 +1,82 @@
<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 { uiStore } from '$lib/stores/ui.svelte';
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import CreateEditDialog from './create-edit-dialog.svelte';
let open = $state(false);
let checked = $state(false);
let {
overlayClass = 'bg-black/5'
}: {
overlayClass?: string;
} = $props();
async function checkExchangeRate() {
if (!companyStore.activeCompany?.id) {
return;
}
let open = $state(false);
let checked = $state(false);
// Use local date instead of UTC
const today = new Date().toLocaleDateString('fr-CA'); // YYYY-MM-DD
try {
const response = await getExchangeRates(companyStore.activeCompany.id, {
date: today,
page_size: 1
});
async function checkExchangeRate() {
console.log('[ExchangeRateGuard] Checking...', companyStore.activeCompany);
if (!companyStore.activeCompany?.id) {
console.log('[ExchangeRateGuard] No active company');
return;
}
// api.get returns { data: ..., status: ... } and types now reflect that
const items = response.data?.items || [];
if (items.length === 0) {
open = true;
}
} catch (error) {
console.error('[ExchangeRateGuard] Error checking exchange rate:', error);
} finally {
checked = true;
}
}
// Use local date instead of UTC
const today = new Date().toLocaleDateString('fr-CA'); // YYYY-MM-DD
console.log('[ExchangeRateGuard] Date:', today);
$effect(() => {
if (companyStore.activeCompany?.id && !checked) {
checkExchangeRate();
}
});
try {
const response = await getExchangeRates(companyStore.activeCompany.id, {
date: today,
page_size: 1
});
console.log('[ExchangeRateGuard] Response (stringified):', JSON.stringify(response, null, 2));
function handleSuccess() {
checked = true;
}
// 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, attempting to open modal');
if (!uiStore.isExchangeRateDialogOpen) {
uiStore.isExchangeRateDialogOpen = true;
open = true;
} else {
console.log('[ExchangeRateGuard] Modal already open, skipping');
}
}
} catch (error) {
console.error('[ExchangeRateGuard] Error checking exchange rate:', error);
} finally {
checked = true;
}
}
$effect(() => {
if (companyStore.activeCompany?.id && !checked) {
checkExchangeRate();
}
});
function handleSuccess() {
console.log('Exchange rate created successfully via guard');
uiStore.isExchangeRateDialogOpen = false;
checked = true;
}
$effect(() => {
// Sincronizar el estado global con el estado local
if (open) {
uiStore.isExchangeRateDialogOpen = true;
} else if (checked) {
// Solo limpiar si este componente ya hizo su check inicial
// y el estado local es cerrado.
// Usamos una pequeña comprobación para no pisar a otros si fuera necesario,
// pero como son excluyentes, esto debería bastar.
if (uiStore.isExchangeRateDialogOpen && !open) {
uiStore.isExchangeRateDialogOpen = false;
}
}
});
</script>
<CreateEditDialog
bind:open
onSuccess={handleSuccess}
overlayClass="bg-black/20"
/>
<CreateEditDialog bind:open onSuccess={handleSuccess} {overlayClass} />

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/reference_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/reference_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

@@ -0,0 +1,17 @@
/**
* UI Store for managing global UI states
*/
class UIStore {
private _isExchangeRateDialogOpen = $state(false);
get isExchangeRateDialogOpen() {
return this._isExchangeRateDialogOpen;
}
set isExchangeRateDialogOpen(value: boolean) {
this._isExchangeRateDialogOpen = value;
}
}
export const uiStore = new UIStore();

View File

@@ -2,18 +2,18 @@
import { setContext, onMount } from 'svelte';
import { invalidateAll } from '$app/navigation';
import type { LayoutData } from './$types';
import AppSidebar from "$lib/components/sidebar/app-sidebar.svelte";
import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js";
import { Separator } from "$lib/components/ui/separator/index.js";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import { companyStore } from "$lib/stores/company.svelte";
import ExchangeRateGuard from "$lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte";
import AppSidebar from '$lib/components/sidebar/app-sidebar.svelte';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import { Separator } from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { companyStore } from '$lib/stores/company.svelte';
import ExchangeRateGuard from '$lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte';
let { data, children }: { data: LayoutData; children: any } = $props();
// Hacer disponible el usuario en el contexto para los componentes hijos
setContext('user', data.user);
// Inicializar el store con las compañías pre-cargadas desde el servidor
onMount(() => {
if (data.companies) {
@@ -36,7 +36,7 @@
<AppSidebar />
<Sidebar.Inset class="overflow-x-hidden">
<header
class="group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear"
class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12"
>
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1" />
@@ -56,11 +56,11 @@
-->
</div>
</header>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0 overflow-x-hidden">
<div class="flex flex-1 flex-col gap-4 overflow-x-hidden p-4 pt-0">
<!-- Contenido de cada página -->
{@render children()}
</div>
</Sidebar.Inset>
</Sidebar.Provider>
<ExchangeRateGuard />
<ExchangeRateGuard overlayClass="bg-black/5" />

View File

@@ -8,6 +8,7 @@
import { Badge } from '$lib/components/ui/badge';
import { Separator } from '$lib/components/ui/separator';
import { toast } from 'svelte-sonner';
import { uiStore } from '$lib/stores/ui.svelte';
import {
ArrowLeft,
FileText,
@@ -110,7 +111,21 @@
);
let showExchangeRateDialog = $state(false);
$effect(() => {
if (showExchangeRateDialog) {
uiStore.isExchangeRateDialogOpen = true;
} else {
// Solo limpiar si este componente tenía el modal abierto
if (uiStore.isExchangeRateDialogOpen && !showExchangeRateDialog) {
uiStore.isExchangeRateDialogOpen = false;
}
}
});
let missingExchangeRateDate = $state('');
let lastFetchedDate = $state('');
let originalInvoiceDate = $state(data.invoice?.invoice_date || '');
async function checkExchangeRate(date: string): Promise<boolean> {
if (!date || !companyStore?.activeCompany?.id) return true;
@@ -130,8 +145,13 @@
if (items.length === 0) {
console.log('No exchange rate found for', date);
missingExchangeRateDate = date;
showExchangeRateDialog = true;
if (!uiStore.isExchangeRateDialogOpen) {
missingExchangeRateDate = date;
showExchangeRateDialog = true;
uiStore.isExchangeRateDialogOpen = true;
return false;
}
// Si ya hay un modal abierto, no abrir este pero retornar false para indicar que falta
return false;
}
return true;
@@ -149,13 +169,41 @@
// 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;
if (mounted && companyStore?.activeCompany?.id && currentDate) {
// No buscar si:
// 1. Ya tenemos un valor y la fecha es la original (evita sobreescribir al cargar)
const currentRate = calculatedExchangeRate;
if (
!data.isCreate &&
currentDate === originalInvoiceDate &&
typeof currentRate === 'number' &&
currentRate > 0
) {
return;
}
// 2. Ya buscamos para esta fecha recientemente
if (currentDate === lastFetchedDate) {
return;
}
getExchangeRateByDate(currentDate, companyStore.activeCompany.id)
.then((rate) => {
lastFetchedDate = currentDate;
if (rate) {
calculatedExchangeRate = rate.value;
} else {
calculatedExchangeRate = 0;
// Solo abrir modal si NO es la fecha de hoy (el guard global la maneja)
// Y si no hay otro modal de TC abierto ya
const today = new Date().toISOString().split('T')[0];
if (currentDate !== today && !uiStore.isExchangeRateDialogOpen) {
missingExchangeRateDate = currentDate;
showExchangeRateDialog = true;
uiStore.isExchangeRateDialogOpen = true;
}
}
})
.catch((err) => console.error('Error auto-updating exchange rate:', err));
@@ -293,7 +341,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}
@@ -407,7 +455,14 @@
<ExchangeRateDialog
bind:open={showExchangeRateDialog}
initialDate={missingExchangeRateDate}
overlayClass="bg-black/5"
onSuccess={() => {
uiStore.isExchangeRateDialogOpen = false;
// Si es factura nueva y no tiene fecha, poner la de hoy
if (data.isCreate && !InvoiceTopFieldsFormData.invoice_date) {
InvoiceTopFieldsFormData.invoice_date = new Date().toISOString().split('T')[0];
}
// Actualizar el tipo de cambio mostrado
if (InvoiceTopFieldsFormData.invoice_date && companyStore?.activeCompany?.id) {
getExchangeRateByDate(
@@ -420,9 +475,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">