Merge fix/invoices_tipo_cambio - resolved conflicts
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
17
frontend/src/lib/stores/ui.svelte.ts
Normal file
17
frontend/src/lib/stores/ui.svelte.ts
Normal 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();
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user