From f22fff887c6f9dfdc908db2411e61a1e6ae026d7 Mon Sep 17 00:00:00 2001 From: borja Date: Sun, 7 Sep 2025 01:48:26 +0200 Subject: [PATCH] feat: centralizar iconos en ICONS y actualizar mensajes a nuevos iconos Co-authored-by: aider (openrouter/openai/gpt-5) --- src/services/command.ts | 56 +++++++++++++++++++---------- src/services/reminders.ts | 8 +++-- src/utils/icons.ts | 14 ++++++++ tests/unit/services/command.test.ts | 6 ++-- 4 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 src/utils/icons.ts diff --git a/src/services/command.ts b/src/services/command.ts index aa5b30d..f628c55 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -4,6 +4,7 @@ import { normalizeWhatsAppId, isGroupId } from '../utils/whatsapp'; import { TaskService } from '../tasks/service'; import { GroupSyncService } from './group-sync'; import { ContactsService } from './contacts'; +import { ICONS } from '../utils/icons'; type CommandContext = { sender: string; // normalized user id (digits only), but accept raw too @@ -151,6 +152,20 @@ export class CommandService { return String(ymd); }; + // TZ y "hoy" en TZ para marcar vencidas en listados + const TZ = process.env.TZ && process.env.TZ.trim() ? process.env.TZ : 'Europe/Madrid'; + const ymdInTZ = (d: Date): 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')}`; + }; + const todayYMD = ymdInTZ(new Date()); + if (!action || action === 'ayuda') { const help = [ 'Guía rápida:', @@ -196,8 +211,9 @@ export class CommandService { } const rendered = items.map((t) => { - const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : ''; - return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — 👥 sin dueño`; + const isOverdue = t.due_date ? t.due_date < todayYMD : false; + const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; + return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${ICONS.unassigned} sin dueño`; }); const total = TaskService.countGroupUnassigned(context.groupId); @@ -242,9 +258,10 @@ export class CommandService { ); const owner = (t.assignees?.length || 0) === 0 - ? '👥 sin dueño' + ? `${ICONS.unassigned} sin dueño` : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; - const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : ''; + const isOverdue = t.due_date ? t.due_date < todayYMD : false; + const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`; })); sections.push(...rendered); @@ -268,8 +285,9 @@ export class CommandService { if (unassigned.length > 0) { sections.push(`${groupName} — Sin dueño`); const renderedUnassigned = unassigned.map((t) => { - const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : ''; - return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — 👥 sin dueño`; + const isOverdue = t.due_date ? t.due_date < todayYMD : false; + const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; + return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${ICONS.unassigned} sin dueño`; }); sections.push(...renderedUnassigned); @@ -322,9 +340,10 @@ export class CommandService { ); const owner = (t.assignees?.length || 0) === 0 - ? '👥 sin dueño' + ? `${ICONS.unassigned} sin dueño` : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; - const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : ''; + const isOverdue = t.due_date ? t.due_date < todayYMD : false; + const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`; })); @@ -372,9 +391,10 @@ export class CommandService { ); const owner = (t.assignees?.length || 0) === 0 - ? '👥 sin dueño' + ? `${ICONS.unassigned} sin dueño` : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; - const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : ''; + const isOverdue = t.due_date ? t.due_date < todayYMD : false; + const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`; })); sections.push(...rendered); @@ -421,7 +441,7 @@ export class CommandService { const due = res.task?.due_date ? ` — 📅 ${formatDDMM(res.task?.due_date)}` : ''; return [{ recipient: context.sender, - message: `✔️ ${id} completada — “*${res.task?.description || '(sin descripción)'}*”${due}\nGracias, ${who}.` + message: `${ICONS.complete} ${id} completada — “*${res.task?.description || '(sin descripción)'}*”${due}\nGracias, ${who}.` }]; } @@ -460,7 +480,7 @@ export class CommandService { return [{ recipient: context.sender, - message: `👤 Has tomado ${id} — “*${res.task?.description || '(sin descripción)'}*”${due}` + message: `${ICONS.take} Has tomado ${id} — “*${res.task?.description || '(sin descripción)'}*”${due}` }]; } @@ -500,13 +520,13 @@ export class CommandService { if (res.now_unassigned) { return [{ recipient: context.sender, - message: `👥 ${id} queda sin dueño — “*${res.task?.description || '(sin descripción)'}*”${due}` + message: `${ICONS.unassigned} ${id} queda sin dueño — “*${res.task?.description || '(sin descripción)'}*”${due}` }]; } return [{ recipient: context.sender, - message: `👤 Has soltado ${id} — “*${res.task?.description || '(sin descripción)'}*”${due}` + message: `${ICONS.unassign} Has soltado ${id} — “*${res.task?.description || '(sin descripción)'}*”${due}` }]; } @@ -641,12 +661,12 @@ export class CommandService { const responses: CommandResponse[] = []; // 1) Ack al creador con formato compacto - const ackHeader = `✅ ${taskId} “*${description || '(sin descripción)'}*”`; + const ackHeader = `${ICONS.create} ${taskId} “*${description || '(sin descripción)'}*”`; const ackLines: string[] = [ackHeader]; const dueFmt = formatDDMM(dueDate); - if (dueFmt) ackLines.push(`📅 ${dueFmt}`); + if (dueFmt) ackLines.push(`${ICONS.date} ${dueFmt}`); if (assignmentUserIds.length === 0) { - ackLines.push(`👥 sin dueño${groupName ? ` (${groupName})` : ''}`); + ackLines.push(`${ICONS.unassigned} sin dueño${groupName ? ` (${groupName})` : ''}`); } else { const assigneesList = assignedDisplayNames.join(', '); ackLines.push(`${assignmentUserIds.length > 1 ? '👥' : '👤'} ${assigneesList}`); @@ -663,7 +683,7 @@ export class CommandService { responses.push({ recipient: uid, message: [ - `🔔 ${taskId}${formatDDMM(dueDate) ? ` — 📅 ${formatDDMM(dueDate)}` : ''}`, + `${ICONS.assignNotice} Tarea ${taskId}${formatDDMM(dueDate) ? ` — ${ICONS.date} ${formatDDMM(dueDate)}` : ''}`, `“*${description || '(sin descripción)'}*”`, groupName ? `Grupo: ${groupName}` : null, `Completar: /t x ${taskId}` diff --git a/src/services/reminders.ts b/src/services/reminders.ts index 0026013..d063851 100644 --- a/src/services/reminders.ts +++ b/src/services/reminders.ts @@ -4,6 +4,7 @@ import { TaskService } from '../tasks/service'; import { ResponseQueue } from './response-queue'; import { ContactsService } from './contacts'; import { GroupSyncService } from './group-sync'; +import { ICONS } from '../utils/icons'; type UserPreference = { user_id: string; @@ -124,7 +125,7 @@ export class RemindersService { } const sections: string[] = []; - sections.push(pref.reminder_freq === 'weekly' ? 'Resumen semanal — tus tareas' : 'Resumen diario — tus tareas'); + sections.push(pref.reminder_freq === 'weekly' ? `${ICONS.reminder} Recordatorio semanal — tus tareas` : `${ICONS.reminder} Recordatorio diario — tus tareas`); for (const [groupId, arr] of byGroup.entries()) { const groupName = @@ -138,9 +139,10 @@ export class RemindersService { ); const owner = (t.assignees?.length || 0) === 0 - ? '👥 sin dueño' + ? `${ICONS.unassigned} sin dueño` : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; - const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : ''; + const isOverdue = t.due_date ? t.due_date < todayYMD : false; + const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`; })); sections.push(...rendered); diff --git a/src/utils/icons.ts b/src/utils/icons.ts new file mode 100644 index 0000000..71743fd --- /dev/null +++ b/src/utils/icons.ts @@ -0,0 +1,14 @@ +export const ICONS = { + create: '📝', + complete: '✅', + assignNotice: '📬', + reminder: '⏰', + date: '📅', + unassigned: '🚫👤', + take: '✋', + unassign: '↩️', + info: 'ℹ️', + warn: '⚠️', + person: '👤', + people: '👥', +} as const; diff --git a/tests/unit/services/command.test.ts b/tests/unit/services/command.test.ts index e8714ac..815dc20 100644 --- a/tests/unit/services/command.test.ts +++ b/tests/unit/services/command.test.ts @@ -122,7 +122,7 @@ test('completar tarea: camino feliz, ya completada y no encontrada', async () => }); expect(responses.length).toBe(1); expect(responses[0].recipient).toBe('1234567890'); - expect(responses[0].message).toContain(`✔️ ${taskId} completada`); + expect(responses[0].message).toContain(`✅ ${taskId} completada`); // 2) Ya completada responses = await CommandService.handle({ @@ -309,8 +309,8 @@ describe('CommandService', () => { expect(responses.length).toBe(1); expect(responses[0].recipient).toBe('1234567890'); - // Debe empezar con "✅ " - expect(responses[0].message).toMatch(/^✅ \d+ /); + // Debe empezar con "📝 " + expect(responses[0].message).toMatch(/^📝 \d+ /); // Debe contener la descripción en negrita compacta expect(responses[0].message).toContain('*Test task*'); // No debe usar el texto antiguo "Tarea creada"