Merge branch 'development' into fix/modulo-pedimento
This commit is contained in:
@@ -119,7 +119,14 @@
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
if (!formData.date) throw new Error('La fecha es requerida');
|
||||
if (formData.value === null) throw new Error('El valor es requerido');
|
||||
if (
|
||||
formData.value === null ||
|
||||
formData.value === undefined ||
|
||||
String(formData.value).trim() === ''
|
||||
)
|
||||
throw new Error('El tipo de cambio es requerido');
|
||||
if (Number(formData.value) <= 0)
|
||||
throw new Error('El tipo de cambio debe ser un valor mayor a 0');
|
||||
|
||||
showConfirmation = true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -181,6 +181,20 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar tipo de cambio
|
||||
if (
|
||||
formData.exchange_rate === null ||
|
||||
formData.exchange_rate === undefined ||
|
||||
String(formData.exchange_rate).trim() === ''
|
||||
) {
|
||||
error = 'El tipo de cambio es requerido (pestaña Financieros)';
|
||||
return;
|
||||
}
|
||||
if (Number(formData.exchange_rate) <= 0) {
|
||||
error = 'El tipo de cambio debe ser mayor a 0 (pestaña Financieros)';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
|
||||
128
frontend/src/lib/components/session-timeout-warning.svelte
Normal file
128
frontend/src/lib/components/session-timeout-warning.svelte
Normal file
@@ -0,0 +1,128 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import {
|
||||
SESSION_WARNING_EVENT,
|
||||
SESSION_EXPIRED_EVENT,
|
||||
SESSION_EXTENDED_EVENT,
|
||||
getSessionManager
|
||||
} from '$lib/session-manager';
|
||||
import type { SessionWarningDetail } from '$lib/session-manager';
|
||||
|
||||
// ─── State ────────────────────────────────────────────────────────────────
|
||||
let open = $state(false);
|
||||
let remainingSeconds = $state(300);
|
||||
let countdownId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||
function formatTime(secs: number): string {
|
||||
const m = Math.floor(secs / 60);
|
||||
const s = secs % 60;
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function clearCountdown() {
|
||||
if (countdownId !== null) {
|
||||
clearInterval(countdownId);
|
||||
countdownId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
clearCountdown();
|
||||
countdownId = setInterval(() => {
|
||||
remainingSeconds = Math.max(0, remainingSeconds - 1);
|
||||
if (remainingSeconds === 0) clearCountdown();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ─── Event handlers ───────────────────────────────────────────────────────
|
||||
function onWarning(e: Event) {
|
||||
const { remainingMs } = (e as CustomEvent<SessionWarningDetail>).detail;
|
||||
remainingSeconds = Math.floor(remainingMs / 1000);
|
||||
open = true;
|
||||
startCountdown();
|
||||
}
|
||||
|
||||
function onExpired() {
|
||||
open = false;
|
||||
clearCountdown();
|
||||
}
|
||||
|
||||
function onExtended() {
|
||||
open = false;
|
||||
clearCountdown();
|
||||
}
|
||||
|
||||
// ─── User actions ─────────────────────────────────────────────────────────
|
||||
function continueSession() {
|
||||
const mgr = getSessionManager();
|
||||
mgr?.extendSession();
|
||||
open = false;
|
||||
clearCountdown();
|
||||
}
|
||||
|
||||
function logoutNow() {
|
||||
open = false;
|
||||
clearCountdown();
|
||||
// Dispara el evento de sesión expirada para que el layout gestione el logout
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(SESSION_EXPIRED_EVENT, { detail: { reason: 'manual' } })
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
||||
onMount(() => {
|
||||
if (!browser) return;
|
||||
window.addEventListener(SESSION_WARNING_EVENT, onWarning);
|
||||
window.addEventListener(SESSION_EXPIRED_EVENT, onExpired);
|
||||
window.addEventListener(SESSION_EXTENDED_EVENT, onExtended);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (!browser) return;
|
||||
clearCountdown();
|
||||
window.removeEventListener(SESSION_WARNING_EVENT, onWarning);
|
||||
window.removeEventListener(SESSION_EXPIRED_EVENT, onExpired);
|
||||
window.removeEventListener(SESSION_EXTENDED_EVENT, onExtended);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
session-timeout-warning.svelte
|
||||
Diálogo que avisa al usuario cuando su sesión está a punto de expirar
|
||||
por inactividad. Se controla completamente a través de eventos DOM.
|
||||
-->
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm" />
|
||||
<Dialog.Content
|
||||
class="fixed left-1/2 top-1/2 z-[9999] w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg bg-background p-6 shadow-xl"
|
||||
>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="flex items-center gap-2 text-lg font-semibold">
|
||||
⚠️ Sesión por expirar
|
||||
</Dialog.Title>
|
||||
<Dialog.Description class="mt-2 text-sm text-muted-foreground">
|
||||
Tu sesión cerrará automáticamente por inactividad en
|
||||
<span class="font-mono font-bold text-foreground">
|
||||
{formatTime(remainingSeconds)}
|
||||
</span>.
|
||||
<br />
|
||||
¿Deseas continuar trabajando?
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Dialog.Footer class="mt-6 flex gap-3">
|
||||
<Button variant="outline" class="flex-1" onclick={logoutNow}>
|
||||
Cerrar sesión
|
||||
</Button>
|
||||
<Button class="flex-1" onclick={continueSession}>
|
||||
Continuar sesión
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
Reference in New Issue
Block a user