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 { 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}`

@ -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);

@ -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[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 " <id> "
expect(responses[0].message).toMatch(/^ \d+ /);
// Debe empezar con "📝 <id> "
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 <id> creada"

Loading…
Cancel
Save