From 137e0d2d07b0f9f5d5d114d28e16f1fc61955487 Mon Sep 17 00:00:00 2001 From: borja Date: Sat, 6 Sep 2025 20:38:14 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1ade=20alias=20para=20tomar/solta?= =?UTF-8?q?r=20y=20ver=20sin/todos=20con=20consultas=20por=20grupo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- src/services/command.ts | 124 ++++++++++++++++++++++++++++++++++++++++ src/tasks/service.ts | 50 ++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/src/services/command.ts b/src/services/command.ts index 3a09208..358f76d 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -108,8 +108,13 @@ export class CommandService { 'done': 'completar', 'tomar': 'tomar', 'claim': 'tomar', + 'asumir': 'tomar', + 'asumo': 'tomar', 'soltar': 'soltar', 'unassign': 'soltar', + 'dejar': 'soltar', + 'liberar': 'soltar', + 'renunciar': 'soltar', 'ayuda': 'ayuda', 'help': 'ayuda', '?': 'ayuda', @@ -148,6 +153,125 @@ export class CommandService { const scope = (tokens[2] || '').toLowerCase() || (isGroupId(context.groupId) ? 'grupo' : 'mis'); const LIMIT = 10; + // Ver sin dueño del grupo actual + if (scope === 'sin') { + if (!isGroupId(context.groupId)) { + return [{ + recipient: context.sender, + message: 'Este comando se usa en grupos. Prueba: /t ver mis' + }]; + } + if (!GroupSyncService.isGroupActive(context.groupId)) { + return [{ + recipient: context.sender, + message: '⚠️ Este grupo no está activo.' + }]; + } + const items = TaskService.listGroupUnassigned(context.groupId, LIMIT); + const groupName = GroupSyncService.activeGroupsCache.get(context.groupId) || context.groupId; + + if (items.length === 0) { + return [{ + recipient: context.sender, + message: `No hay tareas sin dueño en ${groupName}.` + }]; + } + + 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 total = TaskService.countGroupUnassigned(context.groupId); + if (total > items.length) { + rendered.push(`… y ${total - items.length} más`); + } + + return [{ + recipient: context.sender, + message: [`${groupName} — Sin dueño`, ...rendered].join('\n') + }]; + } + + // Ver todos: "tus tareas" + "sin dueño (grupo actual)" si estás en un grupo + if (scope === 'todos') { + const sections: string[] = []; + + // Tus tareas (mis) + const myItems = TaskService.listUserPending(context.sender, LIMIT); + if (myItems.length > 0) { + // Agrupar por grupo como en "ver mis" + const byGroup = new Map(); + for (const t of myItems) { + const key = t.group_id || '(sin grupo)'; + const arr = byGroup.get(key) || []; + arr.push(t); + byGroup.set(key, arr); + } + + sections.push('Tus tareas'); + for (const [groupId, arr] of byGroup.entries()) { + const groupName = + (groupId && GroupSyncService.activeGroupsCache.get(groupId)) || + (groupId && groupId !== '(sin grupo)' ? groupId : 'Sin grupo'); + + sections.push(groupName); + const rendered = await Promise.all(arr.map(async (t) => { + const names = await Promise.all( + (t.assignees || []).map(async (uid) => (await ContactsService.getDisplayName(uid)) || uid) + ); + const owner = + (t.assignees?.length || 0) === 0 + ? '👥 sin dueño' + : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; + const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : ''; + return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`; + })); + sections.push(...rendered); + } + + const totalMy = TaskService.countUserPending(context.sender); + if (totalMy > myItems.length) { + sections.push(`… y ${totalMy - myItems.length} más`); + } + } else { + sections.push('No tienes tareas pendientes.'); + } + + // Si se invoca en un grupo activo, añadir "sin dueño" de ese grupo + if (isGroupId(context.groupId)) { + if (!GroupSyncService.isGroupActive(context.groupId)) { + sections.push('⚠️ Este grupo no está activo.'); + } else { + const groupName = GroupSyncService.activeGroupsCache.get(context.groupId) || context.groupId; + const unassigned = TaskService.listGroupUnassigned(context.groupId, LIMIT); + 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`; + }); + sections.push(...renderedUnassigned); + + const totalUnassigned = TaskService.countGroupUnassigned(context.groupId); + if (totalUnassigned > unassigned.length) { + sections.push(`… y ${totalUnassigned - unassigned.length} más`); + } + } else { + sections.push(`${groupName} — Sin dueño\n(no hay tareas sin dueño)`); + } + } + } else { + // En DM: nota instructiva + sections.push('ℹ️ Para ver tareas sin dueño de un grupo, usa “/t ver sin” desde ese grupo.'); + } + + return [{ + recipient: context.sender, + message: sections.join('\n') + }]; + } + // Ver grupo if (scope === 'grupo') { if (!isGroupId(context.groupId)) { diff --git a/src/tasks/service.ts b/src/tasks/service.ts index a1a1d6f..53fc5ba 100644 --- a/src/tasks/service.ts +++ b/src/tasks/service.ts @@ -222,4 +222,54 @@ export class TaskService { }, }; } + + // Listar pendientes sin dueño del grupo (limite por defecto 10) + static listGroupUnassigned(groupId: string, limit: number = 10): Array<{ + id: number; + description: string; + due_date: string | null; + group_id: string | null; + assignees: string[]; + }> { + const rows = this.dbInstance + .prepare(` + SELECT id, description, due_date, group_id + FROM tasks + WHERE group_id = ? + AND (completed = 0 OR completed_at IS NULL) + AND NOT EXISTS ( + SELECT 1 FROM task_assignments ta WHERE ta.task_id = tasks.id + ) + ORDER BY + CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, + due_date ASC, + id ASC + LIMIT ? + `) + .all(groupId, limit) as any[]; + + return rows.map((r) => ({ + id: Number(r.id), + description: String(r.description || ''), + due_date: r.due_date ? String(r.due_date) : null, + group_id: r.group_id ? String(r.group_id) : null, + assignees: [], + })); + } + + // Contar pendientes sin dueño del grupo (sin límite) + static countGroupUnassigned(groupId: string): number { + const row = this.dbInstance + .prepare(` + SELECT COUNT(*) as cnt + FROM tasks t + WHERE t.group_id = ? + AND (t.completed = 0 OR t.completed_at IS NULL) + AND NOT EXISTS ( + SELECT 1 FROM task_assignments a WHERE a.task_id = t.id + ) + `) + .get(groupId) as any; + return Number(row?.cnt || 0); + } }