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