feat: añadir handlers completar/tomar/soltar y enrutar comandos

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
main
brobert 4 days ago
parent 5c6cac2b12
commit ada071d220

@ -0,0 +1,133 @@
import { TaskService } from '../../../tasks/service';
import { GroupSyncService } from '../../group-sync';
import { ICONS } from '../../../utils/icons';
import { codeId, formatDDMM } from '../../../utils/formatting';
import { parseMultipleIds, resolveTaskIdFromInput, enforceMembership } from '../shared';
type Ctx = {
sender: string;
groupId: string;
message: string;
};
type Msg = {
recipient: string;
message: string;
mentions?: string[];
};
export async function handleCompletar(context: Ctx): Promise<Msg[]> {
const tokens = (context.message || '').trim().split(/\s+/);
const { ids, truncated } = parseMultipleIds(tokens.slice(2), 10);
// Sin IDs: ayuda de uso
if (ids.length === 0) {
return [{
recipient: context.sender,
message: ' Uso: `/t x 26` o múltiples: `/t x 14 19 24` o `/t x 14,19,24` (máx. 10)'
}];
}
// Caso de 1 ID: mantener comportamiento actual
if (ids.length === 1) {
const idInput = ids[0];
const resolvedId = 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,
message: `⚠️ Tarea ${codeId(resolvedId)} no encontrada.`
}];
}
if (!enforceMembership(context.sender, task)) {
return [{
recipient: context.sender,
message: 'No puedes completar esta tarea porque no eres de este grupo.'
}];
}
const res = TaskService.completeTask(resolvedId, context.sender);
const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
if (res.status === 'not_found') {
return [{
recipient: context.sender,
message: `⚠️ Tarea ${codeId(resolvedId)} no encontrada.`
}];
}
if (res.status === 'already') {
return [{
recipient: context.sender,
message: ` ${codeId(resolvedId, res.task?.display_code)} _Ya estaba completada_ — ${res.task?.description || '(sin descripción)'}${due}`
}];
}
return [{
recipient: context.sender,
message: `${ICONS.complete} ${codeId(resolvedId, res.task?.display_code)} _completada_ — ${res.task?.description || '(sin descripción)'}${due}`
}];
}
// Modo múltiple
let cntUpdated = 0, cntAlready = 0, cntNotFound = 0, cntBlocked = 0;
const lines: string[] = [];
if (truncated) {
lines.push('⚠️ Se procesarán solo los primeros 10 IDs.');
}
for (const idInput of ids) {
const resolvedId = resolveTaskIdFromInput(idInput);
if (!resolvedId) {
lines.push(`⚠️ ${codeId(idInput)} no encontrada.`);
cntNotFound++;
continue;
}
const task = TaskService.getTaskById(resolvedId);
if (task && !enforceMembership(context.sender, task)) {
lines.push(`🚫 ${codeId(resolvedId)} — no permitido (no eres miembro activo).`);
cntBlocked++;
continue;
}
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(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(resolvedId, res.task?.display_code)} completada — ${res.task?.description || '(sin descripción)'}${due}`);
cntUpdated++;
} else if (res.status === 'not_found') {
lines.push(`⚠️ ${codeId(resolvedId)} no encontrada.`);
cntNotFound++;
}
}
// Resumen final
const summary: string[] = [];
if (cntUpdated) summary.push(`completadas ${cntUpdated}`);
if (cntAlready) summary.push(`ya estaban ${cntAlready}`);
if (cntNotFound) summary.push(`no encontradas ${cntNotFound}`);
if (cntBlocked) summary.push(`bloqueadas ${cntBlocked}`);
if (summary.length) {
lines.push('');
lines.push(`Resumen: ${summary.join(', ')}.`);
}
return [{
recipient: context.sender,
message: lines.join('\n')
}];
}

@ -0,0 +1,105 @@
import { TaskService } from '../../../tasks/service';
import { GroupSyncService } from '../../group-sync';
import { ICONS } from '../../../utils/icons';
import { codeId, formatDDMM, italic } from '../../../utils/formatting';
import { resolveTaskIdFromInput, enforceMembership } from '../shared';
type Ctx = {
sender: string;
groupId: string;
message: string;
};
type Msg = {
recipient: string;
message: string;
mentions?: string[];
};
export async function handleSoltar(context: Ctx): Promise<Msg[]> {
const tokens = (context.message || '').trim().split(/\s+/);
const idToken = tokens[2];
const idInput = idToken ? parseInt(idToken, 10) : NaN;
if (!idInput || Number.isNaN(idInput)) {
return [{
recipient: context.sender,
message: ' Uso: `/t soltar 26`'
}];
}
const resolvedId = 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,
message: `⚠️ Tarea ${codeId(resolvedId)} no encontrada.`
}];
}
if (!enforceMembership(context.sender, task)) {
return [{
recipient: context.sender,
message: '⚠️ No puedes soltar esta tarea porque no eres de este grupo.'
}];
}
const res = TaskService.unassignTask(resolvedId, context.sender);
const due = res.task?.due_date ? `${ICONS.date} ${formatDDMM(res.task?.due_date)}` : '';
if (res.status === 'forbidden_personal') {
return [{
recipient: context.sender,
message: '⚠️ No puedes soltar una tarea personal. Márcala como completada para eliminarla'
}];
}
if (res.status === 'not_found') {
return [{
recipient: context.sender,
message: `⚠️ Tarea ${codeId(resolvedId)} no encontrada.`
}];
}
if (res.status === 'completed') {
return [{
recipient: context.sender,
message: ` ${codeId(resolvedId, res.task?.display_code)} ya estaba completada — ${res.task?.description || '(sin descripción)'}${due}`
}];
}
if (res.status === 'not_assigned') {
return [{
recipient: context.sender,
message: ` ${codeId(resolvedId, res.task?.display_code)} no la tenías asignada — ${res.task?.description || '(sin descripción)'}${due}`
}];
}
if (res.now_unassigned) {
const lines = [
`${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.')
].filter(Boolean);
return [{
recipient: context.sender,
message: lines.join('\n')
}];
}
const lines = [
`${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);
return [{
recipient: context.sender,
message: lines.join('\n')
}];
}

@ -0,0 +1,149 @@
import { TaskService } from '../../../tasks/service';
import { GroupSyncService } from '../../group-sync';
import { ICONS } from '../../../utils/icons';
import { codeId, formatDDMM, italic } from '../../../utils/formatting';
import { parseMultipleIds, resolveTaskIdFromInput, enforceMembership } from '../shared';
type Ctx = {
sender: string;
groupId: string;
message: string;
};
type Msg = {
recipient: string;
message: string;
mentions?: string[];
};
export async function handleTomar(context: Ctx): Promise<Msg[]> {
const tokens = (context.message || '').trim().split(/\s+/);
const { ids, truncated } = parseMultipleIds(tokens.slice(2), 10);
// Sin IDs: ayuda de uso
if (ids.length === 0) {
return [{
recipient: context.sender,
message: ' Uso: `/t tomar 26` o múltiples: `/t tomar 12 19 50` o `/t tomar 12,19,50` (máx. 10)'
}];
}
// Caso de 1 ID: mantener comportamiento actual
if (ids.length === 1) {
const idInput = ids[0];
const resolvedId = 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,
message: `⚠️ Tarea ${codeId(resolvedId)} no encontrada.`
}];
}
if (!enforceMembership(context.sender, task)) {
return [{
recipient: context.sender,
message: 'No puedes tomar esta tarea porque no eres de este grupo.'
}];
}
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') {
return [{
recipient: context.sender,
message: `⚠️ Tarea ${codeId(resolvedId)} no encontrada.`
}];
}
if (res.status === 'completed') {
return [{
recipient: context.sender,
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(resolvedId, res.task?.display_code)} ya la tenías — ${res.task?.description || '(sin descripción)'}${due}`
}];
}
const lines = [
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);
return [{
recipient: context.sender,
message: lines.join('\n')
}];
}
// Modo múltiple
let cntClaimed = 0, cntAlready = 0, cntCompleted = 0, cntNotFound = 0, cntBlocked = 0;
const lines: string[] = [];
if (truncated) {
lines.push('⚠️ Se procesarán solo los primeros 10 IDs.');
}
for (const idInput of ids) {
const resolvedId = resolveTaskIdFromInput(idInput);
if (!resolvedId) {
lines.push(`⚠️ ${codeId(idInput)} no encontrada.`);
cntNotFound++;
continue;
}
const task = TaskService.getTaskById(resolvedId);
if (task && !enforceMembership(context.sender, task)) {
lines.push(`🚫 ${codeId(resolvedId)} — no permitido (no eres miembro activo).`);
cntBlocked++;
continue;
}
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(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(resolvedId, res.task?.display_code)} tomada — ${res.task?.description || '(sin descripción)'}${due}`);
cntClaimed++;
} else if (res.status === 'completed') {
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(resolvedId)} no encontrada.`);
cntNotFound++;
}
}
// Resumen final
const summary: string[] = [];
if (cntClaimed) summary.push(`tomadas ${cntClaimed}`);
if (cntAlready) summary.push(`ya las tenías ${cntAlready}`);
if (cntCompleted) summary.push(`ya completadas ${cntCompleted}`);
if (cntNotFound) summary.push(`no encontradas ${cntNotFound}`);
if (cntBlocked) summary.push(`bloqueadas ${cntBlocked}`);
if (summary.length) {
lines.push('');
lines.push(`Resumen: ${summary.join(', ')}.`);
}
return [{
recipient: context.sender,
message: lines.join('\n')
}];
}

@ -8,6 +8,9 @@ import { ACTION_ALIASES } from './shared';
import { handleConfigurar } from './handlers/configurar';
import { handleWeb } from './handlers/web';
import { handleVer } from './handlers/ver';
import { handleCompletar } from './handlers/completar';
import { handleTomar } from './handlers/tomar';
import { handleSoltar } from './handlers/soltar';
import { ResponseQueue } from '../response-queue';
import { isGroupId } from '../../utils/whatsapp';
import { Metrics } from '../metrics';
@ -62,6 +65,21 @@ export async function route(context: RouteContext, deps?: { db: Database }): Pro
return await handleVer(context as any);
}
if (action === 'completar') {
try { ResponseQueue.setOnboardingAggregatesMetrics(); } catch {}
return await handleCompletar(context as any);
}
if (action === 'tomar') {
try { ResponseQueue.setOnboardingAggregatesMetrics(); } catch {}
return await handleTomar(context as any);
}
if (action === 'soltar') {
try { ResponseQueue.setOnboardingAggregatesMetrics(); } catch {}
return await handleSoltar(context as any);
}
if (action === 'configurar') {
try { ResponseQueue.setOnboardingAggregatesMetrics(); } catch {}
return handleConfigurar(context as any, { db: database });

Loading…
Cancel
Save