/** * Utilidades compartidas para handlers de comandos (Etapa 1). * Aún no se usan desde CommandService; servirán en etapas siguientes. */ import { TaskService } from '../../tasks/service'; import { GroupSyncService } from '../group-sync'; import { ICONS } from '../../utils/icons'; import { codeId, formatDDMM } from '../../utils/formatting'; export const ACTION_ALIASES: Record = { 'n': 'nueva', 'nueva': 'nueva', 'crear': 'nueva', '+': 'nueva', 'ver': 'ver', 'mostrar': 'ver', 'listar': 'ver', 'ls': 'ver', 'mias': 'ver', 'mías': 'ver', 'todas': 'ver', 'todos': 'ver', 'x': 'completar', 'hecho': 'completar', 'completar': 'completar', 'done': 'completar', 'tomar': 'tomar', 'claim': 'tomar', 'asumir': 'tomar', 'asumo': 'tomar', 'soltar': 'soltar', 'unassign': 'soltar', 'dejar': 'soltar', 'liberar': 'soltar', 'renunciar': 'soltar', 'ayuda': 'ayuda', 'help': 'ayuda', 'info': 'ayuda', '?': 'ayuda', 'config': 'configurar', 'configurar': 'configurar', 'web': 'web' }; export const SCOPE_ALIASES: Record = { 'todo': 'todos', 'todos': 'todos', 'todas': 'todos', 'mis': 'mis', 'mias': 'mis', 'mías': 'mis', 'yo': 'mis' }; export const CTA_HELP = 'ℹ️ Tus tareas: `t mias` · Todas: `t todas` · Info: `t info` · Web: `t web`'; /** * Formatea un Date a YYYY-MM-DD respetando TZ (por defecto Europe/Madrid). */ function ymdInTZ(d: Date, tz?: string): string { const TZ = (tz && tz.trim()) || (process.env.TZ && process.env.TZ.trim()) || 'Europe/Madrid'; const parts = new Intl.DateTimeFormat('en-GB', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(d); const get = (t: string) => parts.find(p => p.type === t)?.value || ''; return `${get('year')}-${get('month')}-${get('day')}`; } export function todayYMD(tz?: string): string { return ymdInTZ(new Date(), tz); } /** * Parsea múltiples IDs desde tokens, deduplica, y aplica límite. */ export function parseMultipleIds(tokens: string[], max: number = 10): { ids: number[]; truncated: boolean } { const raw = (tokens || []).join(' ').trim(); const all = raw ? raw .split(/[,\s]+/) .map(t => t.trim()) .filter(Boolean) .map(t => parseInt(t, 10)) .filter(n => Number.isFinite(n) && n > 0) : []; const dedup: number[] = []; const seen = new Set(); for (const n of all) { if (!seen.has(n)) { seen.add(n); dedup.push(n); } } const truncated = dedup.length > max; const ids = dedup.slice(0, max); return { ids, truncated }; } /** * Resuelve un ID de entrada (display_code) a task.id si está activa. */ export function resolveTaskIdFromInput(n: number): number | null { const byCode = TaskService.getActiveTaskByDisplayCode(n); return byCode ? byCode.id : null; } /** * Aplica la política de membresía para acciones sobre una tarea. * Devuelve true si el usuario está permitido según flags/env. */ export function enforceMembership(sender: string, task: { group_id?: string | null }, enforceFlag?: boolean): boolean { const enforce = typeof enforceFlag === 'boolean' ? enforceFlag : String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true'; const gid = task?.group_id || null; if (!gid) return true; // tareas personales no requieren membresía if (!enforce) return true; if (!GroupSyncService.isSnapshotFresh(gid)) return true; return GroupSyncService.isUserActiveInGroup(sender, gid); } /** Formatea el sufijo de fecha de vencimiento para una respuesta de tarea. */ export function formatDue(task: { due_date?: string | null } | null | undefined): string { return task?.due_date ? ` — ${ICONS.date} ${formatDDMM(task.due_date)}` : ''; } /** Construye el texto de resumen para procesamiento por lotes. */ export function buildSummary(counts: Record, labels: Record): string { const parts: string[] = []; for (const key of Object.keys(counts)) { if (counts[key]) parts.push(`${labels[key]} ${counts[key]}`); } return parts.length ? `Resumen: ${parts.join(', ')}.` : ''; } /** Construye el fragmento " — ⚠️ 📅 DD/MM" o vacío para una tarea según si está vencida. */ export function formatDatePart(due_date: string | null | undefined, todayYMD: string): string { if (!due_date) return ''; const overdue = due_date < todayYMD; return ` — ${overdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(due_date)}`; } /** Renderiza una línea de tarea con su código, descripción, fecha y dueño. */ export function formatTaskLine( t: { id: number; description?: string | null; due_date?: string | null; display_code?: number | null }, owner: string, todayYMD: string ): string { const dc = (t as any).display_code as number | undefined; const datePart = formatDatePart(t.due_date, todayYMD); return `- ${codeId(t.id, dc)} ${t.description || '(sin descripción)'}${datePart} — ${owner}`; } /** Outcome of a single action in a multi-ID batch. */ export interface BatchOutcome { status: string; line: string; } /** * Generic multi-ID batch handler. * * Iterates over IDs, calls `action` for each, collects outcomes, * counts statuses, builds a summary and returns a single Msg. */ export function handleBatch( sender: string, ids: number[], truncated: boolean, action: (idInput: number, sender: string) => BatchOutcome, statusLabels: Record, usageMessage: string, ): { recipient: string; message: string }[] { if (ids.length === 0) { return [{ recipient: sender, message: usageMessage }]; } const lines: string[] = []; if (truncated) lines.push('⚠️ Se procesarán solo los primeros 10 IDs.'); const counts: Record = {}; for (const idInput of ids) { const outcome = action(idInput, sender); lines.push(outcome.line); counts[outcome.status] = (counts[outcome.status] || 0) + 1; } const summary = buildSummary(counts, statusLabels); if (summary) { lines.push(''); lines.push(summary); } return [{ recipient: sender, message: lines.join('\n') }]; } /** Resuelve un ID de entrada, carga la tarea y aplica membresía. Retorna error o {resolvedId, task}. */ export function resolveAndValidate( idInput: number, sender: string ): { resolvedId: number; task: any } | { error: string } { const resolvedId = resolveTaskIdFromInput(idInput); if (!resolvedId) return { error: `⚠️ Tarea ${codeId(idInput)} no encontrada.` }; const task = TaskService.getTaskById(resolvedId); if (!task) return { error: `⚠️ Tarea ${codeId(resolvedId)} no encontrada.` }; if (!enforceMembership(sender, task)) return { error: `🚫 ${codeId(resolvedId)} — no permitido (no eres miembro activo).` }; return { resolvedId, task }; }