|
|
|
|
@ -4,122 +4,16 @@
|
|
|
|
|
tz: string;
|
|
|
|
|
next: string | null;
|
|
|
|
|
};
|
|
|
|
|
export let form: any;
|
|
|
|
|
|
|
|
|
|
let freq: 'off' | 'daily' | 'weekly' | 'weekdays' = data.pref.freq;
|
|
|
|
|
let time: string = data.pref.time ?? '08:30';
|
|
|
|
|
|
|
|
|
|
let saving = false;
|
|
|
|
|
let errorMsg: string | null = null;
|
|
|
|
|
let successMsg: string | null = null;
|
|
|
|
|
let serverNext: string | null = data.next ?? null;
|
|
|
|
|
|
|
|
|
|
function normalizeTime(input: string): string | null {
|
|
|
|
|
const m = /^\s*(\d{1,2}):(\d{1,2})\s*$/.exec(input || '');
|
|
|
|
|
if (!m) return null;
|
|
|
|
|
const h = Number(m[1]);
|
|
|
|
|
const min = Number(m[2]);
|
|
|
|
|
if (!Number.isFinite(h) || !Number.isFinite(min)) return null;
|
|
|
|
|
if (h < 0 || h > 23 || min < 0 || min > 59) return null;
|
|
|
|
|
const hh = String(h).padStart(2, '0');
|
|
|
|
|
const mm = String(min).padStart(2, '0');
|
|
|
|
|
return `${hh}:${mm}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ymdInTZ(d: Date, tz: string): string {
|
|
|
|
|
const parts = new Intl.DateTimeFormat('en-GB', {
|
|
|
|
|
timeZone: tz,
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
day: '2-digit'
|
|
|
|
|
}).formatToParts(d);
|
|
|
|
|
const get = (t: string) => parts.find((p) => p.type === t)?.value || '';
|
|
|
|
|
return `${get('year')}-${get('month')}-${get('day')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hmInTZ(d: Date, tz: string): string {
|
|
|
|
|
const parts = new Intl.DateTimeFormat('en-GB', {
|
|
|
|
|
timeZone: tz,
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
hour12: false
|
|
|
|
|
}).formatToParts(d);
|
|
|
|
|
const get = (t: string) => parts.find((p) => p.type === t)?.value || '';
|
|
|
|
|
return `${get('hour')}:${get('minute')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function weekdayShortInTZ(d: Date, tz: string): string {
|
|
|
|
|
return new Intl.DateTimeFormat('en-GB', { timeZone: tz, weekday: 'short' }).format(d);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function computeNext(freq: 'off' | 'daily' | 'weekly' | 'weekdays', timeStr: string | null, tz: string): string | null {
|
|
|
|
|
if (freq === 'off' || !timeStr) return null;
|
|
|
|
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const nowHM = hmInTZ(now, tz);
|
|
|
|
|
const [nowH, nowM] = String(nowHM).split(':');
|
|
|
|
|
const [cfgH, cfgM] = String(timeStr).split(':');
|
|
|
|
|
const nowMin = (parseInt(nowH || '0', 10) || 0) * 60 + (parseInt(nowM || '0', 10) || 0);
|
|
|
|
|
const cfgMin = (parseInt(cfgH || '0', 10) || 0) * 60 + (parseInt(cfgM || '0', 10) || 0);
|
|
|
|
|
|
|
|
|
|
const allowDay = (w: string) => {
|
|
|
|
|
if (freq === 'daily') return true;
|
|
|
|
|
if (freq === 'weekly') return w === 'Mon';
|
|
|
|
|
return w !== 'Sat' && w !== 'Sun';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (let offset = 0; offset < 14; offset++) {
|
|
|
|
|
const cand = new Date(now.getTime() + offset * 24 * 60 * 60 * 1000);
|
|
|
|
|
const wd = weekdayShortInTZ(cand, tz);
|
|
|
|
|
if (!allowDay(wd)) continue;
|
|
|
|
|
if (offset === 0 && nowMin >= cfgMin) continue;
|
|
|
|
|
|
|
|
|
|
return `${ymdInTZ(cand, tz)} ${normalizeTime(timeStr)}`;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$: clientNext = computeNext(freq, time, data.tz);
|
|
|
|
|
|
|
|
|
|
async function onSubmit(e: SubmitEvent) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
errorMsg = null;
|
|
|
|
|
successMsg = null;
|
|
|
|
|
saving = true;
|
|
|
|
|
try {
|
|
|
|
|
const body: any = { freq };
|
|
|
|
|
if (freq !== 'off') {
|
|
|
|
|
body.time = time && time.trim() ? time : '08:30';
|
|
|
|
|
} else if (time && time.trim()) {
|
|
|
|
|
// Permitimos conservar/actualizar hora estando en off
|
|
|
|
|
body.time = time.trim();
|
|
|
|
|
}
|
|
|
|
|
const res = await fetch('/api/me/preferences', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body)
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
freq = json.freq;
|
|
|
|
|
time = json.time;
|
|
|
|
|
serverNext = computeNext(freq, time, data.tz);
|
|
|
|
|
successMsg = 'Preferencias guardadas.';
|
|
|
|
|
} else {
|
|
|
|
|
const txt = await res.text();
|
|
|
|
|
errorMsg = txt || 'No se pudieron guardar las preferencias.';
|
|
|
|
|
}
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
errorMsg = e?.message || 'Error de red al guardar.';
|
|
|
|
|
} finally {
|
|
|
|
|
saving = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<section style="max-width: 720px; margin: 2rem auto; padding: 0 1rem;">
|
|
|
|
|
<h1 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 1rem;">Preferencias de recordatorios</h1>
|
|
|
|
|
|
|
|
|
|
<form on:submit|preventDefault={onSubmit} style="display: grid; gap: 1rem;">
|
|
|
|
|
<form method="POST" style="display: grid; gap: 1rem;">
|
|
|
|
|
<div>
|
|
|
|
|
<label for="freq" style="display:block; font-weight:600; margin-bottom: 0.25rem;">Frecuencia</label>
|
|
|
|
|
<select id="freq" name="freq" bind:value={freq}>
|
|
|
|
|
@ -139,23 +33,20 @@
|
|
|
|
|
<p style="font-size: 0.9rem; color: #555; margin-top: 0.25rem;">Zona horaria: {data.tz}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if errorMsg}
|
|
|
|
|
<div style="color: #b00020;">{errorMsg}</div>
|
|
|
|
|
{#if form?.error}
|
|
|
|
|
<div style="color: #b00020;">{form.error}</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if successMsg}
|
|
|
|
|
<div style="color: #007e33;">{successMsg}</div>
|
|
|
|
|
{#if form?.success}
|
|
|
|
|
<div style="color: #007e33;">Preferencias guardadas.</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<button type="submit" disabled={saving} style="padding: 0.5rem 1rem;">
|
|
|
|
|
{saving ? 'Guardando…' : 'Guardar'}
|
|
|
|
|
</button>
|
|
|
|
|
<button type="submit" style="padding: 0.5rem 1rem;">Guardar</button>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<div style="margin-top: 2rem;">
|
|
|
|
|
<h2 style="font-size: 1.2rem; font-weight: 600;">Próximo recordatorio</h2>
|
|
|
|
|
<ul>
|
|
|
|
|
<li>Servidor: {serverNext ?? '—'}</li>
|
|
|
|
|
<li>Calculado ahora: {clientNext ?? '—'}</li>
|
|
|
|
|
<li>Servidor: {data.next ?? '—'}</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|