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 {} 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) { if (low >= todayYMD) {
dateCandidates.push({ index: i, ymd: low }); dateCandidates.push({ index: i, ymd: low });
dateTokenIndexes.add(i); dateTokenIndexes.add(i);
} const task = TaskService.getTaskById(resolvedId)!;
continue; continue;
} }
@ -107,6 +107,14 @@ export class CommandService {
return { action, description, dueDate }; 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( private static async processTareaCommand(
context: CommandContext context: CommandContext
): Promise<CommandResponse[]> { ): Promise<CommandResponse[]> {
@ -222,7 +230,7 @@ export class CommandService {
recipient: context.sender, recipient: context.sender,
message: '_Este comando se usa en grupos. Prueba:_ `/t ver mis`' message: '_Este comando se usa en grupos. Prueba:_ `/t ver mis`'
}]; }];
} const task = TaskService.getTaskById(resolvedId)!;
if (!GroupSyncService.isGroupActive(context.groupId)) { if (!GroupSyncService.isGroupActive(context.groupId)) {
return [{ return [{
recipient: context.sender, recipient: context.sender,
@ -242,7 +250,7 @@ export class CommandService {
const rendered = items.map((t) => { const rendered = items.map((t) => {
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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); const total = TaskService.countGroupUnassigned(context.groupId);
@ -291,7 +299,7 @@ export class CommandService {
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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(...rendered);
sections.push(''); sections.push('');
@ -323,7 +331,7 @@ export class CommandService {
const renderedUnassigned = unassigned.map((t) => { const renderedUnassigned = unassigned.map((t) => {
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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); sections.push(...renderedUnassigned);
@ -352,7 +360,7 @@ export class CommandService {
const renderedUnassigned = unassigned.map((t) => { const renderedUnassigned = unassigned.map((t) => {
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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); sections.push(...renderedUnassigned);
@ -418,7 +426,7 @@ export class CommandService {
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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); const total = TaskService.countGroupPending(context.groupId);
@ -469,7 +477,7 @@ export class CommandService {
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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(...rendered);
sections.push(''); sections.push('');
@ -516,9 +524,16 @@ export class CommandService {
// Caso de 1 ID: mantener comportamiento actual // Caso de 1 ID: mantener comportamiento actual
if (ids.length === 1) { 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) { if (!task) {
return [{ return [{
recipient: context.sender, 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; const who = (await ContactsService.getDisplayName(context.sender)) || context.sender;
if (res.status === 'not_found') { if (res.status === 'not_found') {
return [{ return [{
@ -545,14 +560,14 @@ export class CommandService {
const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''; const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
return [{ return [{
recipient: context.sender, 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)}` : ''; const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
return [{ return [{
recipient: context.sender, 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.'); lines.push('⚠️ Se procesarán solo los primeros 10 IDs.');
} }
for (const id of ids) { for (const idInput of ids) {
const task = TaskService.getTaskById(id); const resolvedId = this.resolveTaskIdFromInput(idInput);
if (!task) { if (!resolvedId) {
lines.push(`⚠️ ${codeId(id)} no encontrada.`); lines.push(`⚠️ ${codeId(idInput)} no encontrada.`);
cntNotFound++; cntNotFound++;
continue; continue;
} }
@ -579,16 +594,16 @@ export class CommandService {
continue; 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)}` : ''; const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
if (res.status === 'already') { 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++; cntAlready++;
} else if (res.status === 'updated') { } 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++; cntUpdated++;
} else if (res.status === 'not_found') { } else if (res.status === 'not_found') {
lines.push(`⚠️ ${codeId(id)} no encontrada.`); lines.push(`⚠️ ${codeId(resolvedId)} no encontrada.`);
cntNotFound++; cntNotFound++;
} }
} }
@ -636,9 +651,17 @@ export class CommandService {
// Caso de 1 ID: mantener comportamiento actual // Caso de 1 ID: mantener comportamiento actual
if (ids.length === 1) { 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) { if (!task) {
return [{ return [{
recipient: context.sender, 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)}` : ''; const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
if (res.status === 'not_found') { if (res.status === 'not_found') {
@ -665,18 +688,18 @@ export class CommandService {
if (res.status === 'completed') { if (res.status === 'completed') {
return [{ return [{
recipient: context.sender, 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') { if (res.status === 'already') {
return [{ return [{
recipient: context.sender, 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 = [ 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?.description || '(sin descripción)'}`,
res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '' res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''
].filter(Boolean); ].filter(Boolean);
@ -695,10 +718,10 @@ export class CommandService {
lines.push('⚠️ Se procesarán solo los primeros 10 IDs.'); lines.push('⚠️ Se procesarán solo los primeros 10 IDs.');
} }
for (const id of ids) { for (const idInput of ids) {
const task = TaskService.getTaskById(id); const resolvedId = this.resolveTaskIdFromInput(idInput);
if (!task) { if (!resolvedId) {
lines.push(`⚠️ ${codeId(id)} no encontrada.`); lines.push(`⚠️ ${codeId(idInput)} no encontrada.`);
cntNotFound++; cntNotFound++;
continue; continue;
} }
@ -709,19 +732,19 @@ export class CommandService {
continue; 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)}` : ''; const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
if (res.status === 'already') { 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++; cntAlready++;
} else if (res.status === 'claimed') { } 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++; cntClaimed++;
} else if (res.status === 'completed') { } 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++; cntCompleted++;
} else if (res.status === 'not_found') { } else if (res.status === 'not_found') {
lines.push(`⚠️ ${codeId(id)} no encontrada.`); lines.push(`⚠️ ${codeId(resolvedId)} no encontrada.`);
cntNotFound++; cntNotFound++;
} }
} }
@ -747,15 +770,23 @@ export class CommandService {
// Soltar tarea (con validación opcional de membresía) // Soltar tarea (con validación opcional de membresía)
if (action === 'soltar') { if (action === 'soltar') {
const idToken = tokens[2]; const idToken = tokens[2];
const id = idToken ? parseInt(idToken, 10) : NaN; const idInput = idToken ? parseInt(idToken, 10) : NaN;
if (!id || Number.isNaN(id)) { if (!idInput || Number.isNaN(idInput)) {
return [{ return [{
recipient: context.sender, recipient: context.sender,
message: ' Uso: `/t soltar 26`' 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) { if (!task) {
return [{ return [{
recipient: context.sender, 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)}` : ''; const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
if (res.status === 'not_found') { if (res.status === 'not_found') {
@ -794,7 +825,7 @@ export class CommandService {
if (res.now_unassigned) { if (res.now_unassigned) {
const lines = [ const lines = [
`${ICONS.unassigned} ${codeId(id)}`, `${ICONS.unassigned} ${codeId(resolvedId, res.task?.display_code)}`,
`${res.task?.description || '(sin descripción)'}`, `${res.task?.description || '(sin descripción)'}`,
res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '', res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '',
italic('queda sin responsable.') italic('queda sin responsable.')
@ -806,7 +837,7 @@ export class CommandService {
} }
const lines = [ const lines = [
`${ICONS.unassign} ${codeId(id)}`, `${ICONS.unassign} ${codeId(resolvedId, res.task?.display_code)}`,
`${res.task?.description || '(sin descripción)'}`, `${res.task?.description || '(sin descripción)'}`,
res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '' res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : ''
].filter(Boolean); ].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`); const mentionsForSending = assignmentUserIds.map(uid => `${uid}@s.whatsapp.net`);
// Resolver nombres útiles // Resolver nombres útiles
@ -986,7 +1020,7 @@ export class CommandService {
? `${ICONS.unassigned} sin responsable${groupName ? ` (${groupName})` : ''}` ? `${ICONS.unassigned} sin responsable${groupName ? ` (${groupName})` : ''}`
: `${assignmentUserIds.length > 1 ? '👥' : '👤'} ${assignedDisplayNames.join(', ')}`; : `${assignmentUserIds.length > 1 ? '👥' : '👤'} ${assignedDisplayNames.join(', ')}`;
const ackLines = [ 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, dueFmt ? `${ICONS.date} ${dueFmt}` : null,
ownerPart ownerPart
].filter(Boolean); ].filter(Boolean);
@ -1002,7 +1036,7 @@ export class CommandService {
responses.push({ responses.push({
recipient: uid, recipient: uid,
message: [ message: [
`${ICONS.assignNotice} ${codeId(taskId)}`, `${ICONS.assignNotice} ${codeId(taskId, createdTask?.display_code)}`,
`${description || '(sin descripción)'}`, `${description || '(sin descripción)'}`,
formatDDMM(dueDate) ? `${ICONS.date} ${formatDDMM(dueDate)}` : null, formatDDMM(dueDate) ? `${ICONS.date} ${formatDDMM(dueDate)}` : null,
groupName ? `Grupo: ${groupName}` : null, groupName ? `Grupo: ${groupName}` : null,

@ -147,7 +147,7 @@ export class RemindersService {
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`; : `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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(...rendered);
} }
@ -170,7 +170,7 @@ export class RemindersService {
const renderedUnassigned = unassigned.map((t) => { const renderedUnassigned = unassigned.map((t) => {
const isOverdue = t.due_date ? t.due_date < todayYMD : false; const isOverdue = t.due_date ? t.due_date < todayYMD : false;
const datePart = t.due_date ? `${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : ''; 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); sections.push(...renderedUnassigned);

@ -17,10 +17,39 @@ export class TaskService {
static dbInstance: Database = db; static dbInstance: Database = db;
static createTask(task: CreateTaskInput, assignments: CreateAssignmentInput[] = []): number { static createTask(task: CreateTaskInput, assignments: CreateAssignmentInput[] = []): number {
const MAX_DISPLAY_CODE = 9999;
const runTx = this.dbInstance.transaction(() => { 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(` const insertTask = this.dbInstance.prepare(`
INSERT INTO tasks (description, due_date, group_id, created_by) INSERT INTO tasks (description, due_date, group_id, created_by, display_code)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`); `);
const ensuredCreator = ensureUserExists(task.created_by, this.dbInstance); 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( const runResult = insertTask.run(
task.description, task.description,
task.due_date ?? null, task.due_date ?? null,
groupIdToInsert, groupIdToInsert,
ensuredCreator ensuredCreator,
displayCode
); );
const taskId = Number((runResult as any).lastInsertRowid); const taskId = Number((runResult as any).lastInsertRowid);
@ -78,11 +111,12 @@ export class TaskService {
description: string; description: string;
due_date: string | null; due_date: string | null;
group_id: string | null; group_id: string | null;
display_code: number | null;
assignees: string[]; assignees: string[];
}> { }> {
const rows = this.dbInstance const rows = this.dbInstance
.prepare(` .prepare(`
SELECT id, description, due_date, group_id SELECT id, description, due_date, group_id, display_code
FROM tasks FROM tasks
WHERE group_id = ? WHERE group_id = ?
AND COALESCE(completed, 0) = 0 AND completed_at IS NULL AND COALESCE(completed, 0) = 0 AND completed_at IS NULL
@ -108,6 +142,7 @@ export class TaskService {
description: String(r.description || ''), description: String(r.description || ''),
due_date: r.due_date ? String(r.due_date) : null, due_date: r.due_date ? String(r.due_date) : null,
group_id: r.group_id ? String(r.group_id) : null, group_id: r.group_id ? String(r.group_id) : null,
display_code: r.display_code != null ? Number(r.display_code) : null,
assignees, assignees,
}; };
}); });
@ -119,11 +154,12 @@ export class TaskService {
description: string; description: string;
due_date: string | null; due_date: string | null;
group_id: string | null; group_id: string | null;
display_code: number | null;
assignees: string[]; assignees: string[];
}> { }> {
const rows = this.dbInstance const rows = this.dbInstance
.prepare(` .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 FROM tasks t
INNER JOIN task_assignments a ON a.task_id = t.id INNER JOIN task_assignments a ON a.task_id = t.id
WHERE a.user_id = ? WHERE a.user_id = ?
@ -150,6 +186,7 @@ export class TaskService {
description: String(r.description || ''), description: String(r.description || ''),
due_date: r.due_date ? String(r.due_date) : null, due_date: r.due_date ? String(r.due_date) : null,
group_id: r.group_id ? String(r.group_id) : null, group_id: r.group_id ? String(r.group_id) : null,
display_code: r.display_code != null ? Number(r.display_code) : null,
assignees, assignees,
}; };
}); });
@ -185,13 +222,13 @@ export class TaskService {
// Completar tarea: registra quién completó e idempotente // Completar tarea: registra quién completó e idempotente
static completeTask(taskId: number, completedBy: string): { static completeTask(taskId: number, completedBy: string): {
status: 'updated' | 'already' | 'not_found'; 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 ensured = ensureUserExists(completedBy, this.dbInstance);
const existing = this.dbInstance const existing = this.dbInstance
.prepare(` .prepare(`
SELECT id, description, due_date, completed, completed_at SELECT id, description, due_date, completed, completed_at, display_code
FROM tasks FROM tasks
WHERE id = ? WHERE id = ?
`) `)
@ -208,6 +245,7 @@ export class TaskService {
id: Number(existing.id), id: Number(existing.id),
description: String(existing.description || ''), description: String(existing.description || ''),
due_date: existing.due_date ? String(existing.due_date) : null, 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), id: Number(existing.id),
description: String(existing.description || ''), description: String(existing.description || ''),
due_date: existing.due_date ? String(existing.due_date) : null, 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; description: string;
due_date: string | null; due_date: string | null;
group_id: string | null; group_id: string | null;
display_code: number | null;
assignees: string[]; assignees: string[];
}> { }> {
const rows = this.dbInstance const rows = this.dbInstance
.prepare(` .prepare(`
SELECT id, description, due_date, group_id SELECT id, description, due_date, group_id, display_code
FROM tasks FROM tasks
WHERE group_id = ? WHERE group_id = ?
AND COALESCE(completed, 0) = 0 AND completed_at IS NULL AND COALESCE(completed, 0) = 0 AND completed_at IS NULL
@ -262,6 +302,7 @@ export class TaskService {
description: String(r.description || ''), description: String(r.description || ''),
due_date: r.due_date ? String(r.due_date) : null, due_date: r.due_date ? String(r.due_date) : null,
group_id: r.group_id ? String(r.group_id) : null, group_id: r.group_id ? String(r.group_id) : null,
display_code: r.display_code != null ? Number(r.display_code) : null,
assignees: [], assignees: [],
})); }));
} }
@ -285,7 +326,7 @@ export class TaskService {
// Tomar tarea (claim): idempotente // Tomar tarea (claim): idempotente
static claimTask(taskId: number, userId: string): { static claimTask(taskId: number, userId: string): {
status: 'claimed' | 'already' | 'not_found' | 'completed'; 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); const ensuredUser = ensureUserExists(userId, this.dbInstance);
if (!ensuredUser) { if (!ensuredUser) {
@ -294,7 +335,7 @@ export class TaskService {
const existing = this.dbInstance const existing = this.dbInstance
.prepare(` .prepare(`
SELECT id, description, due_date, completed, completed_at SELECT id, description, due_date, completed, completed_at, display_code
FROM tasks FROM tasks
WHERE id = ? WHERE id = ?
`) `)
@ -311,6 +352,7 @@ export class TaskService {
id: Number(existing.id), id: Number(existing.id),
description: String(existing.description || ''), description: String(existing.description || ''),
due_date: existing.due_date ? String(existing.due_date) : null, 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), id: Number(existing.id),
description: String(existing.description || ''), description: String(existing.description || ''),
due_date: existing.due_date ? String(existing.due_date) : null, 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 // Soltar tarea (unassign): idempotente
static unassignTask(taskId: number, userId: string): { static unassignTask(taskId: number, userId: string): {
status: 'unassigned' | 'not_assigned' | 'not_found' | 'completed'; 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 now_unassigned?: boolean; // true si tras soltar no quedan asignados
} { } {
const ensuredUser = ensureUserExists(userId, this.dbInstance); const ensuredUser = ensureUserExists(userId, this.dbInstance);
@ -362,7 +405,7 @@ export class TaskService {
const existing = this.dbInstance const existing = this.dbInstance
.prepare(` .prepare(`
SELECT id, description, due_date, completed, completed_at SELECT id, description, due_date, completed, completed_at, display_code
FROM tasks FROM tasks
WHERE id = ? WHERE id = ?
`) `)
@ -402,6 +445,7 @@ export class TaskService {
id: Number(existing.id), id: Number(existing.id),
description: String(existing.description || ''), description: String(existing.description || ''),
due_date: existing.due_date ? String(existing.due_date) : null, 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, now_unassigned: remaining === 0,
}; };
@ -413,6 +457,7 @@ export class TaskService {
id: Number(existing.id), id: Number(existing.id),
description: String(existing.description || ''), description: String(existing.description || ''),
due_date: existing.due_date ? String(existing.due_date) : null, 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, now_unassigned: remaining === 0,
}; };
@ -426,6 +471,7 @@ export class TaskService {
description: string; description: string;
due_date: string | null; due_date: string | null;
group_id: string | null; group_id: string | null;
display_code: number | null;
completed: number; completed: number;
completed_at: string | null; completed_at: string | null;
} | null { } | null {
@ -435,6 +481,7 @@ export class TaskService {
description, description,
due_date, due_date,
group_id, group_id,
display_code,
COALESCE(completed, 0) as completed, COALESCE(completed, 0) as completed,
completed_at completed_at
FROM tasks 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. // Lista tareas sin responsable para múltiples grupos.
// Implementación simple: reutiliza el método existente por grupo. // Implementación simple: reutiliza el método existente por grupo.
static listUnassignedByGroups(groupIds: string[], limitPerGroup: number = 10): Map<string, Array<{ 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; return '0'.repeat(width - s.length) + s;
} }
export function codeId(id: number): string { export function codeId(id: number, displayCode?: number | null): string {
return '`' + padTaskId(id) + '`'; const n = typeof displayCode === 'number' && Number.isFinite(displayCode) && displayCode > 0 ? displayCode : id;
return '`' + padTaskId(n) + '`';
} }
export function formatDDMM(ymd?: string | null): string | null { export function formatDDMM(ymd?: string | null): string | null {

Loading…
Cancel
Save