feat: añadir formatting.ts para IDs 4 dígitos y fechas

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

@ -5,6 +5,7 @@ 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'; import { ICONS } from '../utils/icons';
import { padTaskId, codeId, formatDDMM, bold, italic } from '../utils/formatting';
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
@ -141,16 +142,7 @@ export class CommandService {
}; };
const action = ACTION_ALIASES[rawAction] || rawAction; const action = ACTION_ALIASES[rawAction] || rawAction;
// Helper para fechas dd/MM // Usar formatDDMM desde utils/formatting
const formatDDMM = (ymd?: string | null): string | null => {
if (!ymd) return null;
const parts = String(ymd).split('-');
if (parts.length >= 3) {
const [Y, M, D] = parts;
if (D && M) return `${D}/${M}`;
}
return String(ymd);
};
// TZ y "hoy" en TZ para marcar vencidas en listados // 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 TZ = process.env.TZ && process.env.TZ.trim() ? process.env.TZ : 'Europe/Madrid';
@ -167,14 +159,36 @@ export class CommandService {
const todayYMD = ymdInTZ(new Date()); const todayYMD = ymdInTZ(new Date());
if (!action || action === 'ayuda') { if (!action || action === 'ayuda') {
const isAdvanced = (tokens[2] || '').toLowerCase() === 'avanzada';
if (isAdvanced) {
const adv = [
'Ayuda avanzada:',
'- Comandos y alias:',
' · Crear: n, nueva, crear, +',
' · Ver: ver, mostrar, listar, ls (scopes: grupo | mis | todos | sin)',
' · Completar: x, hecho, completar, done',
' · Tomar: tomar, claim',
' · Soltar: soltar, unassign',
'- Preferencias:',
' · /t configurar daily|weekly|off (hora por defecto 08:30; semanal: lunes 08:30)',
'- Notas:',
' · En grupos, el bot responde por DM al autor (no publica en el grupo).',
' · Si creas en grupo y no mencionas a nadie → “sin responsable”; en DM → se asigna al creador.',
' · Fechas dd/MM con ⚠️ si está vencida.',
' · Mostramos los IDs con 4 dígitos, pero puedes escribirlos sin ceros (p. ej., 26).',
].join('\n');
return [{
recipient: context.sender,
message: adv
}];
}
const help = [ const help = [
'Guía rápida:', 'Guía rápida:',
'- Crear: /t n Descripción mañana @Ana', '- Crear: /t n Descripción @600123456',
'- Ver grupo: /t ver grupo', '- Ver grupo: /t ver grupo | tus tareas: /t ver mis',
'- Ver mis: /t ver mis', '- Completar: /t x 26 | Tomar: /t tomar 26 | Soltar: /t soltar 26',
'- Ver todos: /t ver todos', '- Recordatorios: /t configurar daily|weekly|off',
'- Completar: /t x 123', '- Más: /t ayuda avanzada'
'- Configurar recordatorios: /t configurar daily|weekly|off'
].join('\n'); ].join('\n');
return [{ return [{
recipient: context.sender, recipient: context.sender,
@ -224,17 +238,17 @@ export class CommandService {
const rendered = items.map((t) => { const rendered = items.map((t) => {
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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 responsable`; return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart}${ICONS.unassigned} sin responsable`;
}); });
const total = TaskService.countGroupUnassigned(context.groupId); const total = TaskService.countGroupUnassigned(context.groupId);
if (total > items.length) { if (total > items.length) {
rendered.push(`… y ${total - items.length} más`); rendered.push(italic(`… y ${total - items.length} más`));
} }
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: [`${groupName} — Sin responsable`, ...rendered].join('\n') message: [bold(`${groupName} — Sin responsable`), ...rendered].join('\n')
}]; }];
} }
@ -243,7 +257,7 @@ export class CommandService {
const sections: string[] = []; const sections: string[] = [];
// Encabezado fijo para la sección de tareas del usuario // Encabezado fijo para la sección de tareas del usuario
sections.push('Tus tareas'); sections.push(bold('Tus tareas'));
// Tus tareas (mis) // Tus tareas (mis)
const myItems = TaskService.listUserPending(context.sender, LIMIT); const myItems = TaskService.listUserPending(context.sender, LIMIT);
@ -262,7 +276,7 @@ export class CommandService {
(groupId && GroupSyncService.activeGroupsCache.get(groupId)) || (groupId && GroupSyncService.activeGroupsCache.get(groupId)) ||
(groupId && groupId !== '(sin grupo)' ? groupId : 'Sin grupo'); (groupId && groupId !== '(sin grupo)' ? groupId : 'Sin grupo');
sections.push(groupName); sections.push(bold(groupName));
const rendered = await Promise.all(arr.map(async (t) => { const rendered = await Promise.all(arr.map(async (t) => {
const names = await Promise.all( const names = await Promise.all(
(t.assignees || []).map(async (uid) => (await ContactsService.getDisplayName(uid)) || uid) (t.assignees || []).map(async (uid) => (await ContactsService.getDisplayName(uid)) || uid)
@ -273,14 +287,14 @@ export class CommandService {
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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 `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart}${owner}`;
})); }));
sections.push(...rendered); sections.push(...rendered);
} }
const totalMy = TaskService.countUserPending(context.sender); const totalMy = TaskService.countUserPending(context.sender);
if (totalMy > myItems.length) { if (totalMy > myItems.length) {
sections.push(`… y ${totalMy - myItems.length} más`); sections.push(italic(`… y ${totalMy - myItems.length} más`));
} }
} else { } else {
sections.push('No tienes tareas pendientes.'); sections.push('No tienes tareas pendientes.');
@ -294,17 +308,17 @@ export class CommandService {
const groupName = GroupSyncService.activeGroupsCache.get(context.groupId) || context.groupId; const groupName = GroupSyncService.activeGroupsCache.get(context.groupId) || context.groupId;
const unassigned = TaskService.listGroupUnassigned(context.groupId, LIMIT); const unassigned = TaskService.listGroupUnassigned(context.groupId, LIMIT);
if (unassigned.length > 0) { if (unassigned.length > 0) {
sections.push(`${groupName} — Sin responsable`); sections.push(bold(`${groupName} — Sin responsable`));
const renderedUnassigned = unassigned.map((t) => { const renderedUnassigned = unassigned.map((t) => {
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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 responsable`; return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart}${ICONS.unassigned} sin responsable`;
}); });
sections.push(...renderedUnassigned); sections.push(...renderedUnassigned);
const totalUnassigned = TaskService.countGroupUnassigned(context.groupId); const totalUnassigned = TaskService.countGroupUnassigned(context.groupId);
if (totalUnassigned > unassigned.length) { if (totalUnassigned > unassigned.length) {
sections.push(`… y ${totalUnassigned - unassigned.length} más`); sections.push(italic(`… y ${totalUnassigned - unassigned.length} más`));
} }
} else { } else {
sections.push(`${groupName} — Sin responsable\n(no hay tareas sin responsable)`); sections.push(`${groupName} — Sin responsable\n(no hay tareas sin responsable)`);
@ -322,17 +336,17 @@ export class CommandService {
gid; gid;
if (unassigned.length > 0) { if (unassigned.length > 0) {
sections.push(`${groupName} — Sin responsable`); sections.push(bold(`${groupName} — Sin responsable`));
const renderedUnassigned = unassigned.map((t) => { const renderedUnassigned = unassigned.map((t) => {
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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 responsable`; return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart}${ICONS.unassigned} sin responsable`;
}); });
sections.push(...renderedUnassigned); sections.push(...renderedUnassigned);
const totalUnassigned = TaskService.countGroupUnassigned(gid); const totalUnassigned = TaskService.countGroupUnassigned(gid);
if (totalUnassigned > unassigned.length) { if (totalUnassigned > unassigned.length) {
sections.push(`… y ${totalUnassigned - unassigned.length} más`); sections.push(italic(`… y ${totalUnassigned - unassigned.length} más`));
} }
} }
} }
@ -392,7 +406,7 @@ export class CommandService {
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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 `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart}${owner}`;
})); }));
const total = TaskService.countGroupPending(context.groupId); const total = TaskService.countGroupPending(context.groupId);
@ -402,7 +416,7 @@ export class CommandService {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: [groupName, ...rendered].join('\n') message: [bold(groupName), ...rendered].join('\n')
}]; }];
} }
@ -432,7 +446,7 @@ export class CommandService {
(groupId && GroupSyncService.activeGroupsCache.get(groupId)) || (groupId && GroupSyncService.activeGroupsCache.get(groupId)) ||
(groupId && groupId !== '(sin grupo)' ? groupId : 'Sin grupo'); (groupId && groupId !== '(sin grupo)' ? groupId : 'Sin grupo');
sections.push(groupName); sections.push(bold(groupName));
const rendered = await Promise.all(arr.map(async (t) => { const rendered = await Promise.all(arr.map(async (t) => {
const names = await Promise.all( const names = await Promise.all(
(t.assignees || []).map(async (uid) => (await ContactsService.getDisplayName(uid)) || uid) (t.assignees || []).map(async (uid) => (await ContactsService.getDisplayName(uid)) || uid)
@ -443,13 +457,13 @@ export class CommandService {
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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 `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart}${owner}`;
})); }));
sections.push(...rendered); sections.push(...rendered);
} }
if (total > items.length) { if (total > items.length) {
sections.push(`… y ${total - items.length} más`); sections.push(italic(`… y ${total - items.length} más`));
} }
return [{ return [{
@ -465,7 +479,7 @@ export class CommandService {
if (!id || Number.isNaN(id)) { if (!id || Number.isNaN(id)) {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: 'Uso: /t x <id>' message: ' Uso: /t x 26'
}]; }];
} }
@ -473,7 +487,7 @@ export class CommandService {
if (!task) { if (!task) {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: `⚠️ Tarea ${id} no encontrada.` message: `⚠️ Tarea ${codeId(id)} no encontrada.`
}]; }];
} }
const enforce = String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true'; const enforce = String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true';
@ -489,21 +503,27 @@ export class CommandService {
if (res.status === 'not_found') { if (res.status === 'not_found') {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: `⚠️ Tarea ${id} no encontrada.` message: `⚠️ Tarea ${codeId(id)} no encontrada.`
}]; }];
} }
if (res.status === 'already') { if (res.status === 'already') {
const due = res.task?.due_date ? `📅 ${formatDDMM(res.task?.due_date)}` : ''; const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: ` ${id} ya estaba completada — _${res.task?.description || '(sin descripción)'}_${due}` message: ` ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}`
}]; }];
} }
const due = res.task?.due_date ? ` — 📅 ${formatDDMM(res.task?.due_date)}` : ''; const dueLine = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
const lines = [
`${ICONS.complete} ${codeId(id)}`,
`${res.task?.description || '(sin descripción)'}`,
dueLine,
italic(`Gracias, ${who}.`)
].filter(Boolean);
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: `${ICONS.complete} ${id} completada — _${res.task?.description || '(sin descripción)'}_${due}\nGracias, ${who}.` message: lines.join('\n')
}]; }];
} }
@ -514,7 +534,7 @@ export class CommandService {
if (!id || Number.isNaN(id)) { if (!id || Number.isNaN(id)) {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: 'Uso: /t tomar <id>' message: ' Uso: /t tomar 26'
}]; }];
} }
@ -522,7 +542,7 @@ export class CommandService {
if (!task) { if (!task) {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: `⚠️ Tarea ${id} no encontrada.` message: `⚠️ Tarea ${codeId(id)} no encontrada.`
}]; }];
} }
const enforce = String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true'; const enforce = String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true';
@ -534,7 +554,7 @@ export class CommandService {
} }
const res = TaskService.claimTask(id, context.sender); const res = TaskService.claimTask(id, context.sender);
const due = res.task?.due_date ? `📅 ${formatDDMM(res.task?.due_date)}` : ''; const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
if (res.status === 'not_found') { if (res.status === 'not_found') {
return [{ return [{
@ -545,19 +565,24 @@ export class CommandService {
if (res.status === 'completed') { if (res.status === 'completed') {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: ` ${id} ya estaba completada — _${res.task?.description || '(sin descripción)'}_${due}` message: ` ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}`
}]; }];
} }
if (res.status === 'already') { if (res.status === 'already') {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: ` ${id} ya la tenías — _${res.task?.description || '(sin descripción)'}_${due}` message: ` ${codeId(id)} ya la tenías — ${res.task?.description || '(sin descripción)'}${due}`
}]; }];
} }
const lines = [
`${ICONS.take} ${codeId(id)}`,
`${res.task?.description || '(sin descripción)'}`,
res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''
].filter(Boolean);
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: `${ICONS.take} Has tomado ${id} — _${res.task?.description || '(sin descripción)'}_${due}` message: lines.join('\n')
}]; }];
} }
@ -568,7 +593,7 @@ export class CommandService {
if (!id || Number.isNaN(id)) { if (!id || Number.isNaN(id)) {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: 'Uso: /t soltar <id>' message: ' Uso: /t soltar 26'
}]; }];
} }
@ -588,7 +613,7 @@ export class CommandService {
} }
const res = TaskService.unassignTask(id, context.sender); const res = TaskService.unassignTask(id, context.sender);
const due = res.task?.due_date ? `📅 ${formatDDMM(res.task?.due_date)}` : ''; const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
if (res.status === 'not_found') { if (res.status === 'not_found') {
return [{ return [{
@ -599,26 +624,37 @@ export class CommandService {
if (res.status === 'completed') { if (res.status === 'completed') {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: ` ${id} ya estaba completada — _${res.task?.description || '(sin descripción)'}_${due}` message: ` ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}`
}]; }];
} }
if (res.status === 'not_assigned') { if (res.status === 'not_assigned') {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: ` ${id} no la tenías asignada — _${res.task?.description || '(sin descripción)'}_${due}` message: ` ${codeId(id)} no la tenías asignada — ${res.task?.description || '(sin descripción)'}${due}`
}]; }];
} }
if (res.now_unassigned) { if (res.now_unassigned) {
const lines = [
`${ICONS.unassigned} ${codeId(id)}`,
`${res.task?.description || '(sin descripción)'}`,
res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '',
italic('Queda sin responsable.')
].filter(Boolean);
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: `${ICONS.unassigned} ${id} queda sin responsable — _${res.task?.description || '(sin descripción)'}_${due}` message: lines.join('\n')
}]; }];
} }
const lines = [
`${ICONS.unassign} ${codeId(id)}`,
`${res.task?.description || '(sin descripción)'}`,
res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''
].filter(Boolean);
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: `${ICONS.unassign} Has soltado ${id} — _${res.task?.description || '(sin descripción)'}_${due}` message: lines.join('\n')
}]; }];
} }
@ -753,8 +789,8 @@ 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 = `${ICONS.create} ${taskId} _${description || '(sin descripción)'}_`; const ackHeader = `${ICONS.create} ${codeId(taskId)}`;
const ackLines: string[] = [ackHeader]; const ackLines: string[] = [ackHeader, `${description || '(sin descripción)'}`];
const dueFmt = formatDDMM(dueDate); const dueFmt = formatDDMM(dueDate);
if (dueFmt) ackLines.push(`${ICONS.date} ${dueFmt}`); if (dueFmt) ackLines.push(`${ICONS.date} ${dueFmt}`);
if (assignmentUserIds.length === 0) { if (assignmentUserIds.length === 0) {
@ -775,10 +811,11 @@ export class CommandService {
responses.push({ responses.push({
recipient: uid, recipient: uid,
message: [ message: [
`${ICONS.assignNotice} Tarea ${taskId}${formatDDMM(dueDate) ? `${ICONS.date} ${formatDDMM(dueDate)}` : ''}`, `${ICONS.assignNotice} ${codeId(taskId)}`,
`_${description || '(sin descripción)'}_`, `${description || '(sin descripción)'}`,
formatDDMM(dueDate) ? `${ICONS.date} ${formatDDMM(dueDate)}` : null,
groupName ? `Grupo: ${groupName}` : null, groupName ? `Grupo: ${groupName}` : null,
`Completar: /t x ${taskId}` italic(`Acciones: Completar → /t x ${taskId} · Soltar → /t soltar ${taskId}`)
].filter(Boolean).join('\n'), ].filter(Boolean).join('\n'),
mentions: [creatorJid] mentions: [creatorJid]
}); });

@ -5,6 +5,7 @@ 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'; import { ICONS } from '../utils/icons';
import { codeId, formatDDMM, bold, italic } from '../utils/formatting';
type UserPreference = { type UserPreference = {
user_id: string; user_id: string;
@ -132,7 +133,7 @@ export class RemindersService {
(groupId && GroupSyncService.activeGroupsCache.get(groupId)) || (groupId && GroupSyncService.activeGroupsCache.get(groupId)) ||
(groupId && groupId !== '(sin grupo)' ? groupId : 'Sin grupo'); (groupId && groupId !== '(sin grupo)' ? groupId : 'Sin grupo');
sections.push(groupName); sections.push(bold(groupName));
const rendered = await Promise.all(arr.map(async (t) => { const rendered = await Promise.all(arr.map(async (t) => {
const names = await Promise.all( const names = await Promise.all(
(t.assignees || []).map(async (uid) => (await ContactsService.getDisplayName(uid)) || uid) (t.assignees || []).map(async (uid) => (await ContactsService.getDisplayName(uid)) || uid)
@ -143,13 +144,13 @@ export class RemindersService {
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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 `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart}${owner}`;
})); }));
sections.push(...rendered); sections.push(...rendered);
} }
if (total > items.length) { if (total > items.length) {
sections.push(`… y ${total - items.length} más`); sections.push(italic(`… y ${total - items.length} más`));
} }
// (Etapa 3) Sección opcional de "sin responsable" filtrada por membresía activa + snapshot fresca. // (Etapa 3) Sección opcional de "sin responsable" filtrada por membresía activa + snapshot fresca.
@ -162,17 +163,17 @@ export class RemindersService {
const groupName = const groupName =
(gid && GroupSyncService.activeGroupsCache.get(gid)) || (gid && GroupSyncService.activeGroupsCache.get(gid)) ||
gid; gid;
sections.push(`${groupName} — Sin responsable`); sections.push(bold(`${groupName} — Sin responsable`));
const renderedUnassigned = unassigned.map((t) => { const renderedUnassigned = unassigned.map((t) => {
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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 responsable`; return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart}${ICONS.unassigned} sin responsable`;
}); });
sections.push(...renderedUnassigned); sections.push(...renderedUnassigned);
const totalUnassigned = TaskService.countGroupUnassigned(gid); const totalUnassigned = TaskService.countGroupUnassigned(gid);
if (totalUnassigned > unassigned.length) { if (totalUnassigned > unassigned.length) {
sections.push(`… y ${totalUnassigned - unassigned.length} más`); sections.push(italic(`… y ${totalUnassigned - unassigned.length} más`));
} }
} }
} }

@ -0,0 +1,31 @@
/**
* Utilidades de formato de mensajes (IDs, fechas y estilos WhatsApp).
*/
export function padTaskId(id: number, width: number = 4): string {
const s = String(Math.max(0, Math.floor(id)));
if (s.length >= width) return s;
return '0'.repeat(width - s.length) + s;
}
export function codeId(id: number): string {
return '`' + padTaskId(id) + '`';
}
export function formatDDMM(ymd?: string | null): string | null {
if (!ymd) return null;
const parts = String(ymd).split('-');
if (parts.length >= 3) {
const [Y, M, D] = parts;
if (D && M) return `${D}/${M}`;
}
return String(ymd);
}
export function bold(s: string): string {
return `*${s}*`;
}
export function italic(s: string): string {
return `_${s}_`;
}

@ -47,7 +47,7 @@ describe('CommandService - /t tomar y /t soltar', () => {
const res = await CommandService.handle(ctx('111', '/t tomar')); const res = await CommandService.handle(ctx('111', '/t tomar'));
expect(res).toHaveLength(1); expect(res).toHaveLength(1);
expect(res[0].recipient).toBe('111'); expect(res[0].recipient).toBe('111');
expect(res[0].message).toContain('Uso: /t tomar <id>'); expect(res[0].message).toContain('Uso: /t tomar 26');
}); });
it('tomar: not_found', async () => { it('tomar: not_found', async () => {
@ -78,7 +78,7 @@ describe('CommandService - /t tomar y /t soltar', () => {
it('soltar: uso inválido (sin id)', async () => { it('soltar: uso inválido (sin id)', async () => {
const res = await CommandService.handle(ctx('111', '/t soltar')); const res = await CommandService.handle(ctx('111', '/t soltar'));
expect(res[0].message).toContain('Uso: /t soltar <id>'); expect(res[0].message).toContain('Uso: /t soltar 26');
}); });
it('soltar: not_found', async () => { it('soltar: not_found', async () => {

Loading…
Cancel
Save