From 133d7670fbd512fb1309a2a94be88c38644ba3eb Mon Sep 17 00:00:00 2001 From: brobert Date: Sun, 21 Sep 2025 01:23:43 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1ade=20display=5Fcode=20global=20?= =?UTF-8?q?para=20tareas,=20con=20migraci=C3=B3n=20y=20render?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- src/db/migrations/index.ts | 24 ++++++++ src/services/command.ts | 122 ++++++++++++++++++++++++------------- src/services/reminders.ts | 4 +- src/tasks/service.ts | 88 ++++++++++++++++++++++---- src/utils/formatting.ts | 5 +- 5 files changed, 183 insertions(+), 60 deletions(-) diff --git a/src/db/migrations/index.ts b/src/db/migrations/index.ts index 6724232..3e19e5b 100644 --- a/src/db/migrations/index.ts +++ b/src/db/migrations/index.ts @@ -242,5 +242,29 @@ export const migrations: Migration[] = [ try { db.exec(`PRAGMA foreign_keys = ON;`); } catch {} } + }, + { + version: 8, + name: 'tasks-display-code-global', + checksum: 'v8-tasks-display-code-2025-09-21', + up: (db: Database) => { + // Añadir columna display_code si no existe + try { + const cols = db.query(`PRAGMA table_info(tasks)`).all() as any[]; + const hasDisplay = Array.isArray(cols) && cols.some((c: any) => String(c.name) === 'display_code'); + if (!hasDisplay) { + db.exec(`ALTER TABLE tasks ADD COLUMN display_code INTEGER NULL;`); + } + } catch {} + + // Índice único parcial: códigos únicos solo en tareas activas (no completadas) + try { + db.exec(` + CREATE UNIQUE INDEX IF NOT EXISTS uq_tasks_display_code_active + ON tasks (display_code) + WHERE display_code IS NOT NULL AND COALESCE(completed, 0) = 0; + `); + } catch {} + } } ]; diff --git a/src/services/command.ts b/src/services/command.ts index c7a78b0..5f4fabf 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -71,7 +71,7 @@ export class CommandService { if (low >= todayYMD) { dateCandidates.push({ index: i, ymd: low }); dateTokenIndexes.add(i); - } + const task = TaskService.getTaskById(resolvedId)!; continue; } @@ -107,6 +107,14 @@ export class CommandService { return { action, description, dueDate }; } + private static resolveTaskIdFromInput(n: number): number | null { + // Resolver primero por display_code en tareas activas; si no, por PK + const byCode = TaskService.getActiveTaskByDisplayCode(n); + if (byCode) return byCode.id; + const byId = TaskService.getTaskById(n); + return byId ? byId.id : null; + } + private static async processTareaCommand( context: CommandContext ): Promise { @@ -222,7 +230,7 @@ export class CommandService { recipient: context.sender, message: '_Este comando se usa en grupos. Prueba:_ `/t ver mis`' }]; - } + const task = TaskService.getTaskById(resolvedId)!; if (!GroupSyncService.isGroupActive(context.groupId)) { return [{ recipient: context.sender, @@ -242,7 +250,7 @@ export class CommandService { const rendered = items.map((t) => { const isOverdue = t.due_date ? t.due_date < todayYMD : false; const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; - return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart} — ${ICONS.unassigned} sin responsable`; + return `- ${codeId(t.id, t.display_code)} ${t.description || '(sin descripción)'}${datePart} — ${ICONS.unassigned} sin responsable`; }); const total = TaskService.countGroupUnassigned(context.groupId); @@ -291,7 +299,7 @@ export class CommandService { : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; const isOverdue = t.due_date ? t.due_date < todayYMD : false; const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; - return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart} — ${owner}`; + return `- ${codeId(t.id, t.display_code)} ${t.description || '(sin descripción)'}${datePart} — ${owner}`; })); sections.push(...rendered); sections.push(''); @@ -323,7 +331,7 @@ export class CommandService { const renderedUnassigned = unassigned.map((t) => { const isOverdue = t.due_date ? t.due_date < todayYMD : false; const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; - return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart} — ${ICONS.unassigned} sin responsable`; + return `- ${codeId(t.id, t.display_code)} ${t.description || '(sin descripción)'}${datePart} — ${ICONS.unassigned} sin responsable`; }); sections.push(...renderedUnassigned); @@ -352,7 +360,7 @@ export class CommandService { const renderedUnassigned = unassigned.map((t) => { const isOverdue = t.due_date ? t.due_date < todayYMD : false; const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; - return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart} — ${ICONS.unassigned} sin responsable`; + return `- ${codeId(t.id, t.display_code)} ${t.description || '(sin descripción)'}${datePart} — ${ICONS.unassigned} sin responsable`; }); sections.push(...renderedUnassigned); @@ -418,7 +426,7 @@ export class CommandService { : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; const isOverdue = t.due_date ? t.due_date < todayYMD : false; const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; - return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart} — ${owner}`; + return `- ${codeId(t.id, t.display_code)} ${t.description || '(sin descripción)'}${datePart} — ${owner}`; })); const total = TaskService.countGroupPending(context.groupId); @@ -469,7 +477,7 @@ export class CommandService { : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; const isOverdue = t.due_date ? t.due_date < todayYMD : false; const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; - return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart} — ${owner}`; + return `- ${codeId(t.id, t.display_code)} ${t.description || '(sin descripción)'}${datePart} — ${owner}`; })); sections.push(...rendered); sections.push(''); @@ -516,9 +524,16 @@ export class CommandService { // Caso de 1 ID: mantener comportamiento actual if (ids.length === 1) { - const id = ids[0]; + const idInput = ids[0]; + const resolvedId = this.resolveTaskIdFromInput(idInput); + if (!resolvedId) { + return [{ + recipient: context.sender, + message: `⚠️ Tarea ${codeId(idInput)} no encontrada.` + }]; + } - const task = TaskService.getTaskById(id); + const task = TaskService.getTaskById(resolvedId); if (!task) { return [{ recipient: context.sender, @@ -533,7 +548,7 @@ export class CommandService { }]; } - const res = TaskService.completeTask(id, context.sender); + const res = TaskService.completeTask(resolvedId, context.sender); const who = (await ContactsService.getDisplayName(context.sender)) || context.sender; if (res.status === 'not_found') { return [{ @@ -545,14 +560,14 @@ export class CommandService { 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}` + message: `ℹ️ ${codeId(resolvedId, res.task?.display_code)} 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: `${ICONS.complete} ${codeId(id)} completada — ${res.task?.description || '(sin descripción)'}${due}` + message: `${ICONS.complete} ${codeId(resolvedId, res.task?.display_code)} completada — ${res.task?.description || '(sin descripción)'}${due}` }]; } @@ -565,10 +580,10 @@ export class CommandService { lines.push('⚠️ Se procesarán solo los primeros 10 IDs.'); } - for (const id of ids) { - const task = TaskService.getTaskById(id); - if (!task) { - lines.push(`⚠️ ${codeId(id)} no encontrada.`); + for (const idInput of ids) { + const resolvedId = this.resolveTaskIdFromInput(idInput); + if (!resolvedId) { + lines.push(`⚠️ ${codeId(idInput)} no encontrada.`); cntNotFound++; continue; } @@ -579,16 +594,16 @@ export class CommandService { continue; } - const res = TaskService.completeTask(id, context.sender); + const res = TaskService.completeTask(resolvedId, context.sender); const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; if (res.status === 'already') { - lines.push(`ℹ️ ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}`); + lines.push(`ℹ️ ${codeId(resolvedId, res.task?.display_code)} 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}`); + lines.push(`${ICONS.complete} ${codeId(resolvedId, res.task?.display_code)} completada — ${res.task?.description || '(sin descripción)'}${due}`); cntUpdated++; } else if (res.status === 'not_found') { - lines.push(`⚠️ ${codeId(id)} no encontrada.`); + lines.push(`⚠️ ${codeId(resolvedId)} no encontrada.`); cntNotFound++; } } @@ -636,9 +651,17 @@ export class CommandService { // Caso de 1 ID: mantener comportamiento actual if (ids.length === 1) { - const id = ids[0]; + const idInput = ids[0]; - const task = TaskService.getTaskById(id); + const resolvedId = this.resolveTaskIdFromInput(idInput); + if (!resolvedId) { + return [{ + recipient: context.sender, + message: `⚠️ Tarea ${codeId(idInput)} no encontrada.` + }]; + } + + const task = TaskService.getTaskById(resolvedId); if (!task) { return [{ recipient: context.sender, @@ -653,7 +676,7 @@ export class CommandService { }]; } - const res = TaskService.claimTask(id, context.sender); + const res = TaskService.claimTask(resolvedId, context.sender); const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; if (res.status === 'not_found') { @@ -665,18 +688,18 @@ export class CommandService { if (res.status === 'completed') { return [{ recipient: context.sender, - message: `ℹ️ ${codeId(id)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}` + message: `ℹ️ ${codeId(resolvedId, res.task?.display_code)} 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}` + message: `ℹ️ ${codeId(resolvedId, res.task?.display_code)} ya la tenías — ${res.task?.description || '(sin descripción)'}${due}` }]; } const lines = [ - italic(`${ICONS.take} Has tomado ${codeId(id)}`), + italic(`${ICONS.take} Has tomado ${codeId(resolvedId, res.task?.display_code)}`), `${res.task?.description || '(sin descripción)'}`, res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '' ].filter(Boolean); @@ -695,10 +718,10 @@ export class CommandService { lines.push('⚠️ Se procesarán solo los primeros 10 IDs.'); } - for (const id of ids) { - const task = TaskService.getTaskById(id); - if (!task) { - lines.push(`⚠️ ${codeId(id)} no encontrada.`); + for (const idInput of ids) { + const resolvedId = this.resolveTaskIdFromInput(idInput); + if (!resolvedId) { + lines.push(`⚠️ ${codeId(idInput)} no encontrada.`); cntNotFound++; continue; } @@ -709,19 +732,19 @@ export class CommandService { continue; } - const res = TaskService.claimTask(id, context.sender); + const res = TaskService.claimTask(resolvedId, 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}`); + lines.push(`ℹ️ ${codeId(resolvedId, res.task?.display_code)} 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}`); + lines.push(`${ICONS.take} ${codeId(resolvedId, res.task?.display_code)} 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}`); + lines.push(`ℹ️ ${codeId(resolvedId, res.task?.display_code)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}`); cntCompleted++; } else if (res.status === 'not_found') { - lines.push(`⚠️ ${codeId(id)} no encontrada.`); + lines.push(`⚠️ ${codeId(resolvedId)} no encontrada.`); cntNotFound++; } } @@ -747,15 +770,23 @@ export class CommandService { // Soltar tarea (con validación opcional de membresía) if (action === 'soltar') { const idToken = tokens[2]; - const id = idToken ? parseInt(idToken, 10) : NaN; - if (!id || Number.isNaN(id)) { + const idInput = idToken ? parseInt(idToken, 10) : NaN; + if (!idInput || Number.isNaN(idInput)) { return [{ recipient: context.sender, message: 'ℹ️ Uso: `/t soltar 26`' }]; } - const task = TaskService.getTaskById(id); + const resolvedId = this.resolveTaskIdFromInput(idInput); + if (!resolvedId) { + return [{ + recipient: context.sender, + message: `⚠️ Tarea ${codeId(idInput)} no encontrada.` + }]; + } + + const task = TaskService.getTaskById(resolvedId); if (!task) { return [{ recipient: context.sender, @@ -770,7 +801,7 @@ export class CommandService { }]; } - const res = TaskService.unassignTask(id, context.sender); + const res = TaskService.unassignTask(resolvedId, context.sender); const due = res.task?.due_date ? ` — ${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; if (res.status === 'not_found') { @@ -794,7 +825,7 @@ export class CommandService { if (res.now_unassigned) { const lines = [ - `${ICONS.unassigned} ${codeId(id)}`, + `${ICONS.unassigned} ${codeId(resolvedId, res.task?.display_code)}`, `${res.task?.description || '(sin descripción)'}`, res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '', italic('queda sin responsable.') @@ -806,7 +837,7 @@ export class CommandService { } const lines = [ - `${ICONS.unassign} ${codeId(id)}`, + `${ICONS.unassign} ${codeId(resolvedId, res.task?.display_code)}`, `${res.task?.description || '(sin descripción)'}`, res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '' ].filter(Boolean); @@ -964,6 +995,9 @@ export class CommandService { })) ); + // Recuperar la tarea creada para obtener display_code asignado + const createdTask = TaskService.getTaskById(taskId); + const mentionsForSending = assignmentUserIds.map(uid => `${uid}@s.whatsapp.net`); // Resolver nombres útiles @@ -986,7 +1020,7 @@ export class CommandService { ? `${ICONS.unassigned} sin responsable${groupName ? ` (${groupName})` : ''}` : `${assignmentUserIds.length > 1 ? '👥' : '👤'} ${assignedDisplayNames.join(', ')}`; const ackLines = [ - `${ICONS.create} ${codeId(taskId)} ${description || '(sin descripción)'}`, + `${ICONS.create} ${codeId(taskId, createdTask?.display_code)} ${description || '(sin descripción)'}`, dueFmt ? `${ICONS.date} ${dueFmt}` : null, ownerPart ].filter(Boolean); @@ -1002,7 +1036,7 @@ export class CommandService { responses.push({ recipient: uid, message: [ - `${ICONS.assignNotice} ${codeId(taskId)}`, + `${ICONS.assignNotice} ${codeId(taskId, createdTask?.display_code)}`, `${description || '(sin descripción)'}`, formatDDMM(dueDate) ? `${ICONS.date} ${formatDDMM(dueDate)}` : null, groupName ? `Grupo: ${groupName}` : null, diff --git a/src/services/reminders.ts b/src/services/reminders.ts index e4217b2..d6e71ba 100644 --- a/src/services/reminders.ts +++ b/src/services/reminders.ts @@ -147,7 +147,7 @@ export class RemindersService { : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; const isOverdue = t.due_date ? t.due_date < todayYMD : false; const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; - return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart} — ${owner}`; + return `- ${codeId(t.id, t.display_code)} ${t.description || '(sin descripción)'}${datePart} — ${owner}`; })); sections.push(...rendered); } @@ -170,7 +170,7 @@ export class RemindersService { const renderedUnassigned = unassigned.map((t) => { const isOverdue = t.due_date ? t.due_date < todayYMD : false; const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; - return `- ${codeId(t.id)} ${t.description || '(sin descripción)'}${datePart} — ${ICONS.unassigned} sin responsable`; + return `- ${codeId(t.id, t.display_code)} ${t.description || '(sin descripción)'}${datePart} — ${ICONS.unassigned} sin responsable`; }); sections.push(...renderedUnassigned); diff --git a/src/tasks/service.ts b/src/tasks/service.ts index 2e295a8..3f6ee4a 100644 --- a/src/tasks/service.ts +++ b/src/tasks/service.ts @@ -17,10 +17,39 @@ export class TaskService { static dbInstance: Database = db; static createTask(task: CreateTaskInput, assignments: CreateAssignmentInput[] = []): number { + const MAX_DISPLAY_CODE = 9999; + const runTx = this.dbInstance.transaction(() => { + const pickNextDisplayCode = (): number => { + const rows = this.dbInstance + .prepare(` + SELECT display_code + FROM tasks + WHERE COALESCE(completed, 0) = 0 AND display_code IS NOT NULL + ORDER BY display_code ASC + `) + .all() as Array<{ display_code: number }>; + let expect = 1; + for (const r of rows) { + const dc = Number(r.display_code || 0); + if (dc < expect) continue; + if (dc === expect) { + expect++; + if (expect > MAX_DISPLAY_CODE) break; + continue; + } + // encontrado hueco + break; + } + if (expect > MAX_DISPLAY_CODE) { + throw new Error('No hay códigos disponibles (límite alcanzado)'); + } + return expect; + }; + const insertTask = this.dbInstance.prepare(` - INSERT INTO tasks (description, due_date, group_id, created_by) - VALUES (?, ?, ?, ?) + INSERT INTO tasks (description, due_date, group_id, created_by, display_code) + VALUES (?, ?, ?, ?, ?) `); const ensuredCreator = ensureUserExists(task.created_by, this.dbInstance); @@ -37,11 +66,15 @@ export class TaskService { } } + // Elegir display_code global reutilizable entre tareas activas + const displayCode = pickNextDisplayCode(); + const runResult = insertTask.run( task.description, task.due_date ?? null, groupIdToInsert, - ensuredCreator + ensuredCreator, + displayCode ); const taskId = Number((runResult as any).lastInsertRowid); @@ -78,11 +111,12 @@ export class TaskService { description: string; due_date: string | null; group_id: string | null; + display_code: number | null; assignees: string[]; }> { const rows = this.dbInstance .prepare(` - SELECT id, description, due_date, group_id + SELECT id, description, due_date, group_id, display_code FROM tasks WHERE group_id = ? AND COALESCE(completed, 0) = 0 AND completed_at IS NULL @@ -108,6 +142,7 @@ export class TaskService { description: String(r.description || ''), due_date: r.due_date ? String(r.due_date) : null, group_id: r.group_id ? String(r.group_id) : null, + display_code: r.display_code != null ? Number(r.display_code) : null, assignees, }; }); @@ -119,11 +154,12 @@ export class TaskService { description: string; due_date: string | null; group_id: string | null; + display_code: number | null; assignees: string[]; }> { const rows = this.dbInstance .prepare(` - SELECT t.id, t.description, t.due_date, t.group_id + SELECT t.id, t.description, t.due_date, t.group_id, t.display_code FROM tasks t INNER JOIN task_assignments a ON a.task_id = t.id WHERE a.user_id = ? @@ -150,6 +186,7 @@ export class TaskService { description: String(r.description || ''), due_date: r.due_date ? String(r.due_date) : null, group_id: r.group_id ? String(r.group_id) : null, + display_code: r.display_code != null ? Number(r.display_code) : null, assignees, }; }); @@ -185,13 +222,13 @@ export class TaskService { // Completar tarea: registra quién completó e idempotente static completeTask(taskId: number, completedBy: string): { status: 'updated' | 'already' | 'not_found'; - task?: { id: number; description: string; due_date: string | null }; + task?: { id: number; description: string; due_date: string | null; display_code: number | null }; } { const ensured = ensureUserExists(completedBy, this.dbInstance); const existing = this.dbInstance .prepare(` - SELECT id, description, due_date, completed, completed_at + SELECT id, description, due_date, completed, completed_at, display_code FROM tasks WHERE id = ? `) @@ -208,6 +245,7 @@ export class TaskService { id: Number(existing.id), description: String(existing.description || ''), due_date: existing.due_date ? String(existing.due_date) : null, + display_code: existing.display_code != null ? Number(existing.display_code) : null, }, }; } @@ -228,6 +266,7 @@ export class TaskService { id: Number(existing.id), description: String(existing.description || ''), due_date: existing.due_date ? String(existing.due_date) : null, + display_code: existing.display_code != null ? Number(existing.display_code) : null, }, }; } @@ -238,11 +277,12 @@ export class TaskService { description: string; due_date: string | null; group_id: string | null; + display_code: number | null; assignees: string[]; }> { const rows = this.dbInstance .prepare(` - SELECT id, description, due_date, group_id + SELECT id, description, due_date, group_id, display_code FROM tasks WHERE group_id = ? AND COALESCE(completed, 0) = 0 AND completed_at IS NULL @@ -262,6 +302,7 @@ export class TaskService { description: String(r.description || ''), due_date: r.due_date ? String(r.due_date) : null, group_id: r.group_id ? String(r.group_id) : null, + display_code: r.display_code != null ? Number(r.display_code) : null, assignees: [], })); } @@ -285,7 +326,7 @@ export class TaskService { // 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 }; + task?: { id: number; description: string; due_date: string | null; display_code: number | null }; } { const ensuredUser = ensureUserExists(userId, this.dbInstance); if (!ensuredUser) { @@ -294,7 +335,7 @@ export class TaskService { const existing = this.dbInstance .prepare(` - SELECT id, description, due_date, completed, completed_at + SELECT id, description, due_date, completed, completed_at, display_code FROM tasks WHERE id = ? `) @@ -311,6 +352,7 @@ export class TaskService { id: Number(existing.id), description: String(existing.description || ''), due_date: existing.due_date ? String(existing.due_date) : null, + display_code: existing.display_code != null ? Number(existing.display_code) : null, }, }; } @@ -345,6 +387,7 @@ export class TaskService { id: Number(existing.id), description: String(existing.description || ''), due_date: existing.due_date ? String(existing.due_date) : null, + display_code: existing.display_code != null ? Number(existing.display_code) : null, }, }; } @@ -352,7 +395,7 @@ export class TaskService { // 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 }; + task?: { id: number; description: string; due_date: string | null; display_code: number | null }; now_unassigned?: boolean; // true si tras soltar no quedan asignados } { const ensuredUser = ensureUserExists(userId, this.dbInstance); @@ -362,7 +405,7 @@ export class TaskService { const existing = this.dbInstance .prepare(` - SELECT id, description, due_date, completed, completed_at + SELECT id, description, due_date, completed, completed_at, display_code FROM tasks WHERE id = ? `) @@ -402,6 +445,7 @@ export class TaskService { id: Number(existing.id), description: String(existing.description || ''), due_date: existing.due_date ? String(existing.due_date) : null, + display_code: existing.display_code != null ? Number(existing.display_code) : null, }, now_unassigned: remaining === 0, }; @@ -413,6 +457,7 @@ export class TaskService { id: Number(existing.id), description: String(existing.description || ''), due_date: existing.due_date ? String(existing.due_date) : null, + display_code: existing.display_code != null ? Number(existing.display_code) : null, }, now_unassigned: remaining === 0, }; @@ -426,6 +471,7 @@ export class TaskService { description: string; due_date: string | null; group_id: string | null; + display_code: number | null; completed: number; completed_at: string | null; } | null { @@ -435,6 +481,7 @@ export class TaskService { description, due_date, group_id, + display_code, COALESCE(completed, 0) as completed, completed_at FROM tasks @@ -451,6 +498,23 @@ export class TaskService { }; } + // Buscar tarea activa por display_code global + static getActiveTaskByDisplayCode(displayCode: number): { id: number; description: string; due_date: string | null; display_code: number | null } | null { + const row = this.dbInstance.prepare(` + SELECT id, description, due_date, display_code + FROM tasks + WHERE display_code = ? AND COALESCE(completed, 0) = 0 + LIMIT 1 + `).get(displayCode) as any; + if (!row) return null; + return { + id: Number(row.id), + description: String(row.description || ''), + due_date: row.due_date ? String(row.due_date) : null, + display_code: row.display_code != null ? Number(row.display_code) : null, + }; + } + // Lista tareas sin responsable para múltiples grupos. // Implementación simple: reutiliza el método existente por grupo. static listUnassignedByGroups(groupIds: string[], limitPerGroup: number = 10): Map 0 ? displayCode : id; + return '`' + padTaskId(n) + '`'; } export function formatDDMM(ymd?: string | null): string | null {