feat: añadir modo weekdays en recordatorios con hora configurable

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
pull/1/head
brobert 1 month ago
parent a8448fa4dd
commit 098e135b11

@ -198,5 +198,49 @@ export const migrations: Migration[] = [
ON user_aliases (user_id);
`);
}
},
{
version: 7,
name: 'user-preferences-weekdays',
checksum: 'v7-user-preferences-weekdays-2025-09-20',
up: (db: Database) => {
// Re-crear tabla user_preferences para ampliar CHECK con 'weekdays'
try { db.exec(`PRAGMA foreign_keys = OFF;`); } catch {}
db.exec(`
CREATE TABLE IF NOT EXISTS user_preferences_new (
user_id TEXT PRIMARY KEY,
reminder_freq TEXT NOT NULL CHECK (reminder_freq IN ('off','daily','weekly','weekdays')),
reminder_time TEXT NOT NULL DEFAULT '08:30',
last_reminded_on TEXT NULL,
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`);
// Copiar datos existentes si la tabla anterior existe
try {
db.exec(`
INSERT INTO user_preferences_new (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at)
SELECT user_id,
CASE WHEN reminder_freq IN ('off','daily','weekly') THEN reminder_freq ELSE 'off' END,
COALESCE(reminder_time, '08:30'),
last_reminded_on,
COALESCE(updated_at, strftime('%Y-%m-%d %H:%M:%f', 'now'))
FROM user_preferences;
`);
db.exec(`DROP TABLE user_preferences;`);
} catch {
// nada que copiar
}
db.exec(`ALTER TABLE user_preferences_new RENAME TO user_preferences;`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_user_prefs_freq_time
ON user_preferences (reminder_freq, reminder_time);
`);
try { db.exec(`PRAGMA foreign_keys = ON;`); } catch {}
}
}
];

@ -171,7 +171,7 @@ export class CommandService {
' · Tomar: tomar, claim',
' · Soltar: soltar, unassign',
'- Preferencias:',
' · `/t configurar daily|weekly|off` (hora por defecto 08:30; semanal: lunes 08:30)',
' · `/t configurar daily|l-v|weekly|off [HH:MM]` (por defecto 08:30; semanal: lunes; l-v: lunes a viernes)',
'- Notas:',
' · En grupos, el bot responde por DM (no publica en el grupo).',
' · Si creas en grupo y no mencionas a nadie → “sin responsable”; en DM → se asigna a quien la cree.',
@ -190,7 +190,7 @@ export class CommandService {
'- Ver mis tareas: `/t ver mis` (por DM)',
'- Ver todos: `/t ver todos`',
'- Completar: `/t x 123`',
'- Configurar recordatorios: `/t configurar daily|weekly|off`'
'- Configurar recordatorios: `/t configurar daily|l-v|weekly|off [HH:MM]`'
].join('\n');
return [{
recipient: context.sender,
@ -670,10 +670,14 @@ export class CommandService {
if (action === 'configurar') {
const optRaw = (tokens[2] || '').toLowerCase();
const map: Record<string, 'daily' | 'weekly' | 'off'> = {
const map: Record<string, 'daily' | 'weekly' | 'off' | 'weekdays'> = {
'daily': 'daily',
'diario': 'daily',
'diaria': 'daily',
'l-v': 'weekdays',
'lv': 'weekdays',
'laborables': 'weekdays',
'weekdays': 'weekdays',
'semanal': 'weekly',
'weekly': 'weekly',
'off': 'off',
@ -681,10 +685,26 @@ export class CommandService {
'ninguno': 'off'
};
const freq = map[optRaw];
// Hora opcional HH:MM
const timeRaw = tokens[3] || '';
let timeNorm: string | null = null;
if (timeRaw) {
const m = /^(\d{1,2}):([0-5]\d)$/.exec(timeRaw);
if (!m) {
return [{
recipient: context.sender,
message: 'Uso: `/t configurar daily|l-v|weekly|off [HH:MM]`'
}];
}
const hh = Math.max(0, Math.min(23, parseInt(m[1], 10)));
timeNorm = `${String(hh).padStart(2, '0')}:${m[2]}`;
}
if (!freq) {
return [{
recipient: context.sender,
message: 'Uso: `/t configurar daily|weekly|off`'
message: 'Uso: `/t configurar daily|l-v|weekly|off [HH:MM]`'
}];
}
const ensured = ensureUserExists(context.sender, this.dbInstance);
@ -693,12 +713,24 @@ export class CommandService {
}
this.dbInstance.prepare(`
INSERT INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at)
VALUES (?, ?, COALESCE((SELECT reminder_time FROM user_preferences WHERE user_id = ?), '08:30'), NULL, strftime('%Y-%m-%d %H:%M:%f', 'now'))
VALUES (?, ?, COALESCE(?, COALESCE((SELECT reminder_time FROM user_preferences WHERE user_id = ?), '08:30')), NULL, strftime('%Y-%m-%d %H:%M:%f', 'now'))
ON CONFLICT(user_id) DO UPDATE SET
reminder_freq = excluded.reminder_freq,
reminder_time = CASE WHEN ? IS NOT NULL THEN excluded.reminder_time ELSE reminder_time END,
updated_at = excluded.updated_at
`).run(ensured, freq, ensured);
const label = freq === 'daily' ? 'diario' : freq === 'weekly' ? 'semanal (lunes 08:30)' : 'apagado';
`).run(ensured, freq, timeNorm, ensured, timeNorm);
let label: string;
if (freq === 'daily') {
label = timeNorm ? `diario (${timeNorm})` : 'diario';
} else if (freq === 'weekdays') {
label = timeNorm ? `laborables (lunes a viernes ${timeNorm})` : 'laborables (lunes a viernes)';
} else if (freq === 'weekly') {
label = timeNorm ? `semanal (lunes ${timeNorm})` : 'semanal (lunes 08:30)';
} else {
label = 'apagado';
}
return [{
recipient: context.sender,
message: `✅ Recordatorios: ${label}`

@ -9,7 +9,7 @@ import { codeId, formatDDMM, bold, italic } from '../utils/formatting';
type UserPreference = {
user_id: string;
reminder_freq: 'daily' | 'weekly' | 'off';
reminder_freq: 'daily' | 'weekly' | 'weekdays' | 'off';
reminder_time: string; // 'HH:MM'
last_reminded_on: string | null; // 'YYYY-MM-DD'
};
@ -85,7 +85,7 @@ export class RemindersService {
const rows = this.dbInstance.prepare(`
SELECT user_id, reminder_freq, reminder_time, last_reminded_on
FROM user_preferences
WHERE reminder_freq IN ('daily', 'weekly')
WHERE reminder_freq IN ('daily', 'weekly', 'weekdays')
`).all() as UserPreference[];
for (const pref of rows) {
@ -95,6 +95,9 @@ export class RemindersService {
// Verificar hora alcanzada
if (!pref.reminder_time || nowHM < pref.reminder_time) continue;
// Laborables: solo de lunes a viernes
if (pref.reminder_freq === 'weekdays' && (weekday === 'Sat' || weekday === 'Sun')) continue;
// Semanal: solo lunes (Mon)
if (pref.reminder_freq === 'weekly' && weekday !== 'Mon') continue;

@ -73,7 +73,7 @@ describe('CommandService - configurar recordatorios', () => {
it('configurar con opción inválida devuelve uso correcto y no escribe en DB', async () => {
const res = await runCmd('/t configurar foo');
expect(res).toHaveLength(1);
expect(res[0].message).toContain('Uso: `/t configurar daily|weekly|off`');
expect(res[0].message).toContain('Uso: `/t configurar daily|l-v|weekly|off [HH:MM]`');
const pref = getPref();
expect(pref).toBeNull();
@ -88,4 +88,16 @@ describe('CommandService - configurar recordatorios', () => {
pref = getPref();
expect(pref!.freq).toBe('off');
});
it('configurar l-v con hora guarda weekdays y respeta hora', async () => {
const res = await runCmd('/t configurar l-v 8:00');
expect(res).toHaveLength(1);
expect(res[0].recipient).toBe(SENDER);
expect(res[0].message).toContain('laborables');
const pref = getPref();
expect(pref).not.toBeNull();
expect(pref!.freq).toBe('weekdays');
expect(pref!.time).toBe('08:00');
});
});

@ -40,7 +40,7 @@ describe('RemindersService', () => {
`);
});
function insertPref(freq: 'daily' | 'weekly' | 'off', time: string = '08:30', last: string | null = null) {
function insertPref(freq: 'daily' | 'weekly' | 'weekdays' | 'off', time: string = '08:30', last: string | null = null) {
memdb.prepare(`
INSERT INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at)
VALUES (?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%f', 'now'))
@ -164,4 +164,35 @@ describe('RemindersService', () => {
const msg: string = String(row?.message || '');
expect(msg.includes('… y 2 más')).toBe(true);
});
it('weekdays: envía en martes a la hora configurada', async () => {
insertPref('weekdays', '08:00', null);
// Martes 2025-09-09 08:05 Europe/Madrid ≈ 06:05Z
const now = new Date('2025-09-09T06:05:00.000Z');
// Crear 1 tarea asignada al usuario
TaskService.createTask(
{ description: 'Tarea LV', due_date: '2025-09-10', group_id: null, created_by: USER },
[{ user_id: USER, assigned_by: USER }]
);
await RemindersService.runOnce(now);
expect(countQueued()).toBe(1);
expect(getLastReminded()).toBe('2025-09-09');
});
it('weekdays: no envía en sábado', async () => {
insertPref('weekdays', '08:00', null);
// Sábado 2025-09-13 08:05 Europe/Madrid ≈ 06:05Z
const now = new Date('2025-09-13T06:05:00.000Z');
TaskService.createTask(
{ description: 'Tarea LV2', due_date: '2025-09-14', group_id: null, created_by: USER },
[{ user_id: USER, assigned_by: USER }]
);
await RemindersService.runOnce(now);
expect(countQueued()).toBe(0);
expect(getLastReminded()).toBeNull();
});
});

Loading…
Cancel
Save