feat: centralizar iconos en ICONS y actualizar mensajes a nuevos iconos

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
pull/1/head
borja 2 months ago
parent d4f25efaff
commit f22fff887c

@ -4,6 +4,7 @@ import { normalizeWhatsAppId, isGroupId } from '../utils/whatsapp';
import { TaskService } from '../tasks/service'; import { TaskService } from '../tasks/service';
import { GroupSyncService } from './group-sync'; import { GroupSyncService } from './group-sync';
import { ContactsService } from './contacts'; import { ContactsService } from './contacts';
import { ICONS } from '../utils/icons';
type CommandContext = { type CommandContext = {
sender: string; // normalized user id (digits only), but accept raw too sender: string; // normalized user id (digits only), but accept raw too
@ -151,6 +152,20 @@ export class CommandService {
return String(ymd); 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') { if (!action || action === 'ayuda') {
const help = [ const help = [
'Guía rápida:', 'Guía rápida:',
@ -196,8 +211,9 @@ export class CommandService {
} }
const rendered = items.map((t) => { const rendered = items.map((t) => {
const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : ''; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — 👥 sin dueño`; 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); const total = TaskService.countGroupUnassigned(context.groupId);
@ -242,9 +258,10 @@ export class CommandService {
); );
const owner = const owner =
(t.assignees?.length || 0) === 0 (t.assignees?.length || 0) === 0
? '👥 sin dueño' ? `${ICONS.unassigned} sin dueño`
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${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}`; return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart}${owner}`;
})); }));
sections.push(...rendered); sections.push(...rendered);
@ -268,8 +285,9 @@ export class CommandService {
if (unassigned.length > 0) { if (unassigned.length > 0) {
sections.push(`${groupName} — Sin dueño`); sections.push(`${groupName} — Sin dueño`);
const renderedUnassigned = unassigned.map((t) => { const renderedUnassigned = unassigned.map((t) => {
const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : ''; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — 👥 sin dueño`; 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); sections.push(...renderedUnassigned);
@ -322,9 +340,10 @@ export class CommandService {
); );
const owner = const owner =
(t.assignees?.length || 0) === 0 (t.assignees?.length || 0) === 0
? '👥 sin dueño' ? `${ICONS.unassigned} sin dueño`
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${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}`; return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart}${owner}`;
})); }));
@ -372,9 +391,10 @@ export class CommandService {
); );
const owner = const owner =
(t.assignees?.length || 0) === 0 (t.assignees?.length || 0) === 0
? '👥 sin dueño' ? `${ICONS.unassigned} sin dueño`
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${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}`; return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart}${owner}`;
})); }));
sections.push(...rendered); sections.push(...rendered);
@ -421,7 +441,7 @@ export class CommandService {
const due = res.task?.due_date ? ` — 📅 ${formatDDMM(res.task?.due_date)}` : ''; const due = res.task?.due_date ? ` — 📅 ${formatDDMM(res.task?.due_date)}` : '';
return [{ return [{
recipient: context.sender, 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 [{ return [{
recipient: context.sender, 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) { if (res.now_unassigned) {
return [{ return [{
recipient: context.sender, 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 [{ return [{
recipient: context.sender, 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[] = []; const responses: CommandResponse[] = [];
// 1) Ack al creador con formato compacto // 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 ackLines: string[] = [ackHeader];
const dueFmt = formatDDMM(dueDate); const dueFmt = formatDDMM(dueDate);
if (dueFmt) ackLines.push(`📅 ${dueFmt}`); if (dueFmt) ackLines.push(`${ICONS.date} ${dueFmt}`);
if (assignmentUserIds.length === 0) { if (assignmentUserIds.length === 0) {
ackLines.push(`👥 sin dueño${groupName ? ` (${groupName})` : ''}`); ackLines.push(`${ICONS.unassigned} sin dueño${groupName ? ` (${groupName})` : ''}`);
} else { } else {
const assigneesList = assignedDisplayNames.join(', '); const assigneesList = assignedDisplayNames.join(', ');
ackLines.push(`${assignmentUserIds.length > 1 ? '👥' : '👤'} ${assigneesList}`); ackLines.push(`${assignmentUserIds.length > 1 ? '👥' : '👤'} ${assigneesList}`);
@ -663,7 +683,7 @@ export class CommandService {
responses.push({ responses.push({
recipient: uid, recipient: uid,
message: [ message: [
`🔔 ${taskId}${formatDDMM(dueDate) ? `📅 ${formatDDMM(dueDate)}` : ''}`, `${ICONS.assignNotice} Tarea ${taskId}${formatDDMM(dueDate) ? `${ICONS.date} ${formatDDMM(dueDate)}` : ''}`,
`“*${description || '(sin descripción)'}*”`, `“*${description || '(sin descripción)'}*”`,
groupName ? `Grupo: ${groupName}` : null, groupName ? `Grupo: ${groupName}` : null,
`Completar: /t x ${taskId}` `Completar: /t x ${taskId}`

@ -4,6 +4,7 @@ import { TaskService } from '../tasks/service';
import { ResponseQueue } from './response-queue'; import { ResponseQueue } from './response-queue';
import { ContactsService } from './contacts'; import { ContactsService } from './contacts';
import { GroupSyncService } from './group-sync'; import { GroupSyncService } from './group-sync';
import { ICONS } from '../utils/icons';
type UserPreference = { type UserPreference = {
user_id: string; user_id: string;
@ -124,7 +125,7 @@ export class RemindersService {
} }
const sections: string[] = []; 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()) { for (const [groupId, arr] of byGroup.entries()) {
const groupName = const groupName =
@ -138,9 +139,10 @@ export class RemindersService {
); );
const owner = const owner =
(t.assignees?.length || 0) === 0 (t.assignees?.length || 0) === 0
? '👥 sin dueño' ? `${ICONS.unassigned} sin dueño`
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${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}`; return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart}${owner}`;
})); }));
sections.push(...rendered); sections.push(...rendered);

@ -0,0 +1,14 @@
export const ICONS = {
create: '📝',
complete: '✅',
assignNotice: '📬',
reminder: '⏰',
date: '📅',
unassigned: '🚫👤',
take: '✋',
unassign: '↩️',
info: '',
warn: '⚠️',
person: '👤',
people: '👥',
} as const;

@ -122,7 +122,7 @@ test('completar tarea: camino feliz, ya completada y no encontrada', async () =>
}); });
expect(responses.length).toBe(1); expect(responses.length).toBe(1);
expect(responses[0].recipient).toBe('1234567890'); expect(responses[0].recipient).toBe('1234567890');
expect(responses[0].message).toContain(`✔️ ${taskId} completada`); expect(responses[0].message).toContain(` ${taskId} completada`);
// 2) Ya completada // 2) Ya completada
responses = await CommandService.handle({ responses = await CommandService.handle({
@ -309,8 +309,8 @@ describe('CommandService', () => {
expect(responses.length).toBe(1); expect(responses.length).toBe(1);
expect(responses[0].recipient).toBe('1234567890'); expect(responses[0].recipient).toBe('1234567890');
// Debe empezar con " <id> " // Debe empezar con "📝 <id> "
expect(responses[0].message).toMatch(/^ \d+ /); expect(responses[0].message).toMatch(/^📝 \d+ /);
// Debe contener la descripción en negrita compacta // Debe contener la descripción en negrita compacta
expect(responses[0].message).toContain('*Test task*'); expect(responses[0].message).toContain('*Test task*');
// No debe usar el texto antiguo "Tarea <id> creada" // No debe usar el texto antiguo "Tarea <id> creada"

Loading…
Cancel
Save