feat: permitir múltiples IDs en /t x y /t tomar (espacios o comas; máx 10)

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
pull/1/head
brobert 1 month ago
parent ac0c5ff064
commit 9b57662a6b

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

Loading…
Cancel
Save