feat: añadir soporte de tomar y soltar tareas (claim/unassign)

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
pull/1/head
borja 2 months ago
parent 161af656ad
commit 57f5dd04e6

@ -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 <id>'
}];
}
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 <id>'
}];
}
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') { if (action !== 'nueva') {
return [{ return [{
recipient: context.sender, recipient: context.sender,

@ -281,4 +281,140 @@ export class TaskService {
.get(groupId) as any; .get(groupId) as any;
return Number(row?.cnt || 0); 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,
};
}
} }

Loading…
Cancel
Save