diff --git a/src/services/command.ts b/src/services/command.ts index d1b369b..84a352a 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -167,8 +167,8 @@ export class CommandService { '- 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', + ' · Completar: x, hecho, completar, done (acepta varios IDs: "x 14 19 24" o "x 14,19,24"; máximo 10)', + ' · Tomar: tomar, claim (acepta varios IDs: "tomar 12 19 50" o "tomar 12,19,50"; máximo 10)', ' · Soltar: soltar, unassign', '- Preferencias:', ' · `/t configurar daily|l-v|weekly|off [HH:MM]` (por defecto 08:30; semanal: lunes; l-v: lunes a viernes)', @@ -189,7 +189,8 @@ export class CommandService { '- Ver grupo: `/t ver` (en el grupo)', '- Ver mis tareas: `/t ver mis` (por DM)', '- Ver todos: `/t ver todos`', - '- Completar: `/t x 123`', + '- Completar: `/t x 123` (también varias: `/t x 14 19 24` o `/t x 14,19,24` — máx. 10)', + '- Tomar varias: `/t tomar 12 19 50` o `/t tomar 12,19,50` — máx. 10', '- Configurar recordatorios: `/t configurar daily|l-v|weekly|off [HH:MM]`' ].join('\n'); return [{ @@ -490,106 +491,252 @@ export class CommandService { // Completar tarea (con validación opcional de membresía) if (action === 'completar') { - const idToken = tokens[2]; - const id = idToken ? parseInt(idToken, 10) : NaN; - if (!id || Number.isNaN(id)) { + // Soportar múltiples IDs separados por espacios y/o comas + const rawIds = (tokens.slice(2).join(' ') || '').trim(); + const parsedList = Array.from(new Set( + rawIds + .split(/[,\s]+/) + .map(t => t.trim()) + .filter(Boolean) + .map(t => parseInt(t, 10)) + .filter(n => Number.isFinite(n) && n > 0) + )); + const MAX_IDS = 10; + const truncated = parsedList.length > MAX_IDS; + const ids = parsedList.slice(0, MAX_IDS); + + // Sin IDs: ayuda de uso + if (ids.length === 0) { return [{ recipient: context.sender, - message: 'ℹ️ Uso: `/t x 26`' + message: 'ℹ️ Uso: `/t x 26` o múltiples: `/t x 14 19 24` o `/t x 14,19,24` (máx. 10)' }]; } - const task = TaskService.getTaskById(id); - if (!task) { + // Caso de 1 ID: mantener comportamiento actual + if (ids.length === 1) { + const id = ids[0]; + + const task = TaskService.getTaskById(id); + if (!task) { + return [{ + recipient: context.sender, + message: `⚠️ Tarea ${codeId(id)} no encontrada.` + }]; + } + const enforce = String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true'; + if (task.group_id && GroupSyncService.isSnapshotFresh(task.group_id) && enforce && !GroupSyncService.isUserActiveInGroup(context.sender, task.group_id)) { + return [{ + recipient: context.sender, + message: 'No puedes completar esta tarea porque no apareces como miembro activo del grupo.' + }]; + } + + const res = TaskService.completeTask(id, context.sender); + const who = (await ContactsService.getDisplayName(context.sender)) || context.sender; + if (res.status === 'not_found') { + return [{ + recipient: context.sender, + message: `⚠️ Tarea ${codeId(id)} no encontrada.` + }]; + } + if (res.status === 'already') { + const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; + return [{ + recipient: context.sender, + message: `ℹ️ ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}` + }]; + } + + const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; return [{ recipient: context.sender, - message: `⚠️ Tarea ${codeId(id)} no encontrada.` + message: `${ICONS.complete} ${codeId(id)} completada — ${res.task?.description || '(sin descripción)'}${due}` }]; } + + // Modo múltiple const enforce = String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true'; - if (task.group_id && GroupSyncService.isSnapshotFresh(task.group_id) && enforce && !GroupSyncService.isUserActiveInGroup(context.sender, task.group_id)) { - return [{ - recipient: context.sender, - message: 'No puedes completar esta tarea porque no apareces como miembro activo del grupo.' - }]; - } + let cntUpdated = 0, cntAlready = 0, cntNotFound = 0, cntBlocked = 0; + const lines: string[] = []; - const res = TaskService.completeTask(id, context.sender); - const who = (await ContactsService.getDisplayName(context.sender)) || context.sender; - if (res.status === 'not_found') { - return [{ - recipient: context.sender, - message: `⚠️ Tarea ${codeId(id)} no encontrada.` - }]; + if (truncated) { + lines.push('⚠️ Se procesarán solo los primeros 10 IDs.'); } - if (res.status === 'already') { + + for (const id of ids) { + const task = TaskService.getTaskById(id); + if (!task) { + lines.push(`⚠️ ${codeId(id)} no encontrada.`); + cntNotFound++; + continue; + } + + if (task.group_id && GroupSyncService.isSnapshotFresh(task.group_id) && enforce && !GroupSyncService.isUserActiveInGroup(context.sender, task.group_id)) { + lines.push(`🚫 ${codeId(id)} — no permitido (no eres miembro activo).`); + cntBlocked++; + continue; + } + + const res = TaskService.completeTask(id, context.sender); const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; - return [{ - recipient: context.sender, - message: `ℹ️ ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}` - }]; + if (res.status === 'already') { + lines.push(`ℹ️ ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}`); + cntAlready++; + } else if (res.status === 'updated') { + lines.push(`${ICONS.complete} ${codeId(id)} completada — ${res.task?.description || '(sin descripción)'}${due}`); + cntUpdated++; + } else if (res.status === 'not_found') { + lines.push(`⚠️ ${codeId(id)} no encontrada.`); + cntNotFound++; + } + } + + // Resumen final + const summary: string[] = []; + if (cntUpdated) summary.push(`completadas ${cntUpdated}`); + if (cntAlready) summary.push(`ya estaban ${cntAlready}`); + if (cntNotFound) summary.push(`no encontradas ${cntNotFound}`); + if (cntBlocked) summary.push(`bloqueadas ${cntBlocked}`); + if (summary.length) { + lines.push(''); + lines.push(`Resumen: ${summary.join(', ')}.`); } - const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; return [{ recipient: context.sender, - message: `${ICONS.complete} ${codeId(id)} completada — ${res.task?.description || '(sin descripción)'}${due}` + message: lines.join('\n') }]; } // Tomar tarea (con validación opcional de membresía) if (action === 'tomar') { - const idToken = tokens[2]; - const id = idToken ? parseInt(idToken, 10) : NaN; - if (!id || Number.isNaN(id)) { + // Soportar múltiples IDs separados por espacios y/o comas + const rawIds = (tokens.slice(2).join(' ') || '').trim(); + const parsedList = Array.from(new Set( + rawIds + .split(/[,\s]+/) + .map(t => t.trim()) + .filter(Boolean) + .map(t => parseInt(t, 10)) + .filter(n => Number.isFinite(n) && n > 0) + )); + const MAX_IDS = 10; + const truncated = parsedList.length > MAX_IDS; + const ids = parsedList.slice(0, MAX_IDS); + + // Sin IDs: ayuda de uso + if (ids.length === 0) { return [{ recipient: context.sender, - message: 'ℹ️ Uso: `/t tomar 26`' + message: 'ℹ️ Uso: `/t tomar 26` o múltiples: `/t tomar 12 19 50` o `/t tomar 12,19,50` (máx. 10)' }]; } - const task = TaskService.getTaskById(id); - if (!task) { + // Caso de 1 ID: mantener comportamiento actual + if (ids.length === 1) { + const id = ids[0]; + + const task = TaskService.getTaskById(id); + if (!task) { + return [{ + recipient: context.sender, + message: `⚠️ Tarea ${codeId(id)} no encontrada.` + }]; + } + const enforce = String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true'; + if (task.group_id && GroupSyncService.isSnapshotFresh(task.group_id) && enforce && !GroupSyncService.isUserActiveInGroup(context.sender, task.group_id)) { + return [{ + recipient: context.sender, + message: 'No puedes tomar esta tarea: no apareces como miembro activo del grupo. Pide acceso a un admin si crees que es un error.' + }]; + } + + const res = TaskService.claimTask(id, context.sender); + const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; + + if (res.status === 'not_found') { + return [{ + recipient: context.sender, + message: `⚠️ Tarea ${codeId(id)} no encontrada.` + }]; + } + if (res.status === 'completed') { + return [{ + recipient: context.sender, + message: `ℹ️ ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}` + }]; + } + if (res.status === 'already') { + return [{ + recipient: context.sender, + message: `ℹ️ ${codeId(id)} ya la tenías — ${res.task?.description || '(sin descripción)'}${due}` + }]; + } + + const lines = [ + italic(`${ICONS.take} Has tomado ${codeId(id)}`), + `${res.task?.description || '(sin descripción)'}`, + res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '' + ].filter(Boolean); return [{ recipient: context.sender, - message: `⚠️ Tarea ${codeId(id)} no encontrada.` + message: lines.join('\n') }]; } + + // Modo múltiple const enforce = String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true'; - if (task.group_id && GroupSyncService.isSnapshotFresh(task.group_id) && enforce && !GroupSyncService.isUserActiveInGroup(context.sender, task.group_id)) { - return [{ - recipient: context.sender, - message: 'No puedes tomar esta tarea: no apareces como miembro activo del grupo. Pide acceso a un admin si crees que es un error.' - }]; + let cntClaimed = 0, cntAlready = 0, cntCompleted = 0, cntNotFound = 0, cntBlocked = 0; + const lines: string[] = []; + + if (truncated) { + lines.push('⚠️ Se procesarán solo los primeros 10 IDs.'); } - const res = TaskService.claimTask(id, context.sender); - const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; + for (const id of ids) { + const task = TaskService.getTaskById(id); + if (!task) { + lines.push(`⚠️ ${codeId(id)} no encontrada.`); + cntNotFound++; + continue; + } - if (res.status === 'not_found') { - return [{ - recipient: context.sender, - message: `⚠️ Tarea ${codeId(id)} no encontrada.` - }]; - } - if (res.status === 'completed') { - return [{ - recipient: context.sender, - message: `ℹ️ ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}` - }]; + if (task.group_id && GroupSyncService.isSnapshotFresh(task.group_id) && enforce && !GroupSyncService.isUserActiveInGroup(context.sender, task.group_id)) { + lines.push(`🚫 ${codeId(id)} — no permitido (no eres miembro activo).`); + cntBlocked++; + continue; + } + + const res = TaskService.claimTask(id, context.sender); + const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; + if (res.status === 'already') { + lines.push(`ℹ️ ${codeId(id)} ya la tenías — ${res.task?.description || '(sin descripción)'}${due}`); + cntAlready++; + } else if (res.status === 'claimed') { + lines.push(`${ICONS.take} ${codeId(id)} tomada — ${res.task?.description || '(sin descripción)'}${due}`); + cntClaimed++; + } else if (res.status === 'completed') { + lines.push(`ℹ️ ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}`); + cntCompleted++; + } else if (res.status === 'not_found') { + lines.push(`⚠️ ${codeId(id)} no encontrada.`); + cntNotFound++; + } } - if (res.status === 'already') { - return [{ - recipient: context.sender, - message: `ℹ️ ${codeId(id)} ya la tenías — ${res.task?.description || '(sin descripción)'}${due}` - }]; + + // Resumen final + const summary: string[] = []; + if (cntClaimed) summary.push(`tomadas ${cntClaimed}`); + if (cntAlready) summary.push(`ya las tenías ${cntAlready}`); + if (cntCompleted) summary.push(`ya completadas ${cntCompleted}`); + if (cntNotFound) summary.push(`no encontradas ${cntNotFound}`); + if (cntBlocked) summary.push(`bloqueadas ${cntBlocked}`); + if (summary.length) { + lines.push(''); + lines.push(`Resumen: ${summary.join(', ')}.`); } - const lines = [ - italic(`${ICONS.take} Has tomado ${codeId(id)}`), - `${res.task?.description || '(sin descripción)'}`, - res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '' - ].filter(Boolean); return [{ recipient: context.sender, message: lines.join('\n')