feat: añade display_code global para tareas, con migración y render

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
pull/1/head
brobert 1 month ago
parent 4a8523746e
commit 133d7670fb

@ -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 {}
}
}
];

@ -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<CommandResponse[]> {
@ -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,

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

@ -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<string, Array<{

@ -8,8 +8,9 @@ export function padTaskId(id: number, width: number = 4): string {
return '0'.repeat(width - s.length) + s;
}
export function codeId(id: number): string {
return '`' + padTaskId(id) + '`';
export function codeId(id: number, displayCode?: number | null): string {
const n = typeof displayCode === 'number' && Number.isFinite(displayCode) && displayCode > 0 ? displayCode : id;
return '`' + padTaskId(n) + '`';
}
export function formatDDMM(ymd?: string | null): string | null {

Loading…
Cancel
Save