From 57f5dd04e6b01fa99e33686d248281ed42bd66b7 Mon Sep 17 00:00:00 2001 From: borja Date: Sat, 6 Sep 2025 22:51:55 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1adir=20soporte=20de=20tomar=20y?= =?UTF-8?q?=20soltar=20tareas=20(claim/unassign)?= 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 | 85 +++++++++++++++++++++++++ src/tasks/service.ts | 136 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) diff --git a/src/services/command.ts b/src/services/command.ts index 28b2217..9f77062 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -407,6 +407,91 @@ export class CommandService { }]; } + // Tomar tarea + if (action === 'tomar') { + const idToken = tokens[2]; + const id = idToken ? parseInt(idToken, 10) : NaN; + if (!id || Number.isNaN(id)) { + return [{ + recipient: context.sender, + message: 'Uso: /t tomar ' + }]; + } + + const res = TaskService.claimTask(id, context.sender); + const due = res.task?.due_date ? ` — 📅 ${formatDDMM(res.task?.due_date)}` : ''; + + if (res.status === 'not_found') { + return [{ + recipient: context.sender, + message: `⚠️ Tarea ${id} no encontrada.` + }]; + } + if (res.status === 'completed') { + return [{ + recipient: context.sender, + message: `ℹ️ ${id} ya estaba completada — “*${res.task?.description || '(sin descripción)'}*”${due}` + }]; + } + if (res.status === 'already') { + return [{ + recipient: context.sender, + message: `ℹ️ ${id} ya la tenías — “*${res.task?.description || '(sin descripción)'}*”${due}` + }]; + } + + return [{ + recipient: context.sender, + message: `👤 Has tomado ${id} — “*${res.task?.description || '(sin descripción)'}*”${due}` + }]; + } + + // Soltar tarea + if (action === 'soltar') { + const idToken = tokens[2]; + const id = idToken ? parseInt(idToken, 10) : NaN; + if (!id || Number.isNaN(id)) { + return [{ + recipient: context.sender, + message: 'Uso: /t soltar ' + }]; + } + + const res = TaskService.unassignTask(id, context.sender); + const due = res.task?.due_date ? ` — 📅 ${formatDDMM(res.task?.due_date)}` : ''; + + if (res.status === 'not_found') { + return [{ + recipient: context.sender, + message: `⚠️ Tarea ${id} no encontrada.` + }]; + } + if (res.status === 'completed') { + return [{ + recipient: context.sender, + message: `ℹ️ ${id} ya estaba completada — “*${res.task?.description || '(sin descripción)'}*”${due}` + }]; + } + if (res.status === 'not_assigned') { + return [{ + recipient: context.sender, + message: `ℹ️ ${id} no la tenías asignada — “*${res.task?.description || '(sin descripción)'}*”${due}` + }]; + } + + if (res.now_unassigned) { + return [{ + recipient: context.sender, + message: `👥 ${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}` + }]; + } + if (action !== 'nueva') { return [{ recipient: context.sender, diff --git a/src/tasks/service.ts b/src/tasks/service.ts index f9f6b0f..49fbe52 100644 --- a/src/tasks/service.ts +++ b/src/tasks/service.ts @@ -281,4 +281,140 @@ export class TaskService { .get(groupId) as any; return Number(row?.cnt || 0); } + + // Tomar tarea (claim): idempotente + static claimTask(taskId: number, userId: string): { + status: 'claimed' | 'already' | 'not_found' | 'completed'; + task?: { id: number; description: string; due_date: string | null }; + } { + const ensuredUser = ensureUserExists(userId, this.dbInstance); + if (!ensuredUser) { + throw new Error('No se pudo asegurar el usuario'); + } + + const existing = this.dbInstance + .prepare(` + SELECT id, description, due_date, completed, completed_at + FROM tasks + WHERE id = ? + `) + .get(taskId) as any; + + if (!existing) { + return { status: 'not_found' }; + } + + if (existing.completed || existing.completed_at) { + return { + status: 'completed', + task: { + id: Number(existing.id), + description: String(existing.description || ''), + due_date: existing.due_date ? String(existing.due_date) : null, + }, + }; + } + + const already = this.dbInstance + .prepare(`SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?`) + .get(taskId, ensuredUser); + + if (already) { + return { + status: 'already', + task: { + id: Number(existing.id), + description: String(existing.description || ''), + due_date: existing.due_date ? String(existing.due_date) : null, + }, + }; + } + + const insertAssignment = this.dbInstance.prepare(` + INSERT OR IGNORE INTO task_assignments (task_id, user_id, assigned_by) + VALUES (?, ?, ?) + `); + + this.dbInstance.transaction(() => { + insertAssignment.run(taskId, ensuredUser, ensuredUser); + })(); + + return { + status: 'claimed', + task: { + id: Number(existing.id), + description: String(existing.description || ''), + due_date: existing.due_date ? String(existing.due_date) : null, + }, + }; + } + + // Soltar tarea (unassign): idempotente + static unassignTask(taskId: number, userId: string): { + status: 'unassigned' | 'not_assigned' | 'not_found' | 'completed'; + task?: { id: number; description: string; due_date: string | null }; + now_unassigned?: boolean; // true si tras soltar no quedan asignados + } { + const ensuredUser = ensureUserExists(userId, this.dbInstance); + if (!ensuredUser) { + throw new Error('No se pudo asegurar el usuario'); + } + + const existing = this.dbInstance + .prepare(` + SELECT id, description, due_date, completed, completed_at + FROM tasks + WHERE id = ? + `) + .get(taskId) as any; + + if (!existing) { + return { status: 'not_found' }; + } + + if (existing.completed || existing.completed_at) { + return { + status: 'completed', + task: { + id: Number(existing.id), + description: String(existing.description || ''), + due_date: existing.due_date ? String(existing.due_date) : null, + }, + }; + } + + const deleteStmt = this.dbInstance.prepare(` + DELETE FROM task_assignments + WHERE task_id = ? AND user_id = ? + `); + + const result = deleteStmt.run(taskId, ensuredUser) as any; + + const cntRow = this.dbInstance + .prepare(`SELECT COUNT(*) as cnt FROM task_assignments WHERE task_id = ?`) + .get(taskId) as any; + const remaining = Number(cntRow?.cnt || 0); + + if (result.changes && result.changes > 0) { + return { + status: 'unassigned', + task: { + id: Number(existing.id), + description: String(existing.description || ''), + due_date: existing.due_date ? String(existing.due_date) : null, + }, + now_unassigned: remaining === 0, + }; + } + + return { + status: 'not_assigned', + task: { + id: Number(existing.id), + description: String(existing.description || ''), + due_date: existing.due_date ? String(existing.due_date) : null, + }, + now_unassigned: remaining === 0, + }; + } }