feat: agregar acciones del servidor para preferencias y simplificar UI

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
borja 2 weeks ago
parent f6b6ab7e6c
commit 24b29aac18

@ -1,6 +1,6 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad, Actions } from './$types';
import { getDb } from '$lib/server/db'; import { getDb } from '$lib/server/db';
import { redirect } from '@sveltejs/kit'; import { redirect, fail } from '@sveltejs/kit';
function ymdInTZ(d: Date, tz: string): string { function ymdInTZ(d: Date, tz: string): string {
const parts = new Intl.DateTimeFormat('en-GB', { const parts = new Intl.DateTimeFormat('en-GB', {
@ -106,3 +106,73 @@ export const load: PageServerLoad = async ({ locals }) => {
next next
}; };
}; };
export const actions: Actions = {
default: async ({ locals, request }) => {
const userId = locals.userId ?? null;
if (!userId) {
throw redirect(302, '/login');
}
const form = await request.formData();
const freqRaw = String(form.get('freq') || '').trim().toLowerCase();
const timeRaw = form.has('time') ? String(form.get('time') || '').trim() : null;
const allowed = new Set(['off', 'daily', 'weekly', 'weekdays']);
if (!allowed.has(freqRaw)) {
return fail(400, { error: 'freq inválida' });
}
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}`;
}
const db = await getDb();
let timeToSave: string | null = null;
if (freqRaw === 'off') {
if (timeRaw && timeRaw.length > 0) {
const norm = normalizeTime(timeRaw);
if (!norm) return fail(400, { error: 'hora inválida' });
timeToSave = norm;
} else {
const row = db
.prepare(
`SELECT reminder_time AS time
FROM user_preferences
WHERE user_id = ?
LIMIT 1`
)
.get(userId) as any;
timeToSave = row?.time ? String(row.time) : '08:30';
}
} else {
if (!timeRaw || timeRaw.length === 0) {
timeToSave = '08:30';
} else {
const norm = normalizeTime(timeRaw);
if (!norm) return fail(400, { error: 'hora inválida' });
timeToSave = norm;
}
}
db.prepare(
`INSERT INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at)
VALUES (?, ?, ?, (SELECT last_reminded_on FROM user_preferences WHERE user_id = ?), strftime('%Y-%m-%d %H:%M:%f','now'))
ON CONFLICT(user_id) DO UPDATE SET
reminder_freq = excluded.reminder_freq,
reminder_time = excluded.reminder_time,
updated_at = excluded.updated_at`
).run(userId, freqRaw, timeToSave, userId);
return { success: true, pref: { freq: freqRaw, time: timeToSave } };
}
};

@ -4,122 +4,16 @@
tz: string; tz: string;
next: string | null; next: string | null;
}; };
export let form: any;
let freq: 'off' | 'daily' | 'weekly' | 'weekdays' = data.pref.freq; let freq: 'off' | 'daily' | 'weekly' | 'weekdays' = data.pref.freq;
let time: string = data.pref.time ?? '08:30'; 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> </script>
<section style="max-width: 720px; margin: 2rem auto; padding: 0 1rem;"> <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> <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> <div>
<label for="freq" style="display:block; font-weight:600; margin-bottom: 0.25rem;">Frecuencia</label> <label for="freq" style="display:block; font-weight:600; margin-bottom: 0.25rem;">Frecuencia</label>
<select id="freq" name="freq" bind:value={freq}> <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> <p style="font-size: 0.9rem; color: #555; margin-top: 0.25rem;">Zona horaria: {data.tz}</p>
</div> </div>
{#if errorMsg} {#if form?.error}
<div style="color: #b00020;">{errorMsg}</div> <div style="color: #b00020;">{form.error}</div>
{/if} {/if}
{#if successMsg} {#if form?.success}
<div style="color: #007e33;">{successMsg}</div> <div style="color: #007e33;">Preferencias guardadas.</div>
{/if} {/if}
<button type="submit" disabled={saving} style="padding: 0.5rem 1rem;"> <button type="submit" style="padding: 0.5rem 1rem;">Guardar</button>
{saving ? 'Guardando…' : 'Guardar'}
</button>
</form> </form>
<div style="margin-top: 2rem;"> <div style="margin-top: 2rem;">
<h2 style="font-size: 1.2rem; font-weight: 600;">Próximo recordatorio</h2> <h2 style="font-size: 1.2rem; font-weight: 600;">Próximo recordatorio</h2>
<ul> <ul>
<li>Servidor: {serverNext ?? '—'}</li> <li>Servidor: {data.next ?? '—'}</li>
<li>Calculado ahora: {clientNext ?? '—'}</li>
</ul> </ul>
</div> </div>
</section> </section>

Loading…
Cancel
Save