|
|
|
@ -4,6 +4,7 @@ import { normalizeWhatsAppId, isGroupId } from '../utils/whatsapp';
|
|
|
|
import { TaskService } from '../tasks/service';
|
|
|
|
import { TaskService } from '../tasks/service';
|
|
|
|
import { GroupSyncService } from './group-sync';
|
|
|
|
import { GroupSyncService } from './group-sync';
|
|
|
|
import { ContactsService } from './contacts';
|
|
|
|
import { ContactsService } from './contacts';
|
|
|
|
|
|
|
|
import { ICONS } from '../utils/icons';
|
|
|
|
|
|
|
|
|
|
|
|
type CommandContext = {
|
|
|
|
type CommandContext = {
|
|
|
|
sender: string; // normalized user id (digits only), but accept raw too
|
|
|
|
sender: string; // normalized user id (digits only), but accept raw too
|
|
|
|
@ -151,6 +152,20 @@ export class CommandService {
|
|
|
|
return String(ymd);
|
|
|
|
return String(ymd);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TZ y "hoy" en TZ para marcar vencidas en listados
|
|
|
|
|
|
|
|
const TZ = process.env.TZ && process.env.TZ.trim() ? process.env.TZ : 'Europe/Madrid';
|
|
|
|
|
|
|
|
const ymdInTZ = (d: Date): string => {
|
|
|
|
|
|
|
|
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')}`;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const todayYMD = ymdInTZ(new Date());
|
|
|
|
|
|
|
|
|
|
|
|
if (!action || action === 'ayuda') {
|
|
|
|
if (!action || action === 'ayuda') {
|
|
|
|
const help = [
|
|
|
|
const help = [
|
|
|
|
'Guía rápida:',
|
|
|
|
'Guía rápida:',
|
|
|
|
@ -196,8 +211,9 @@ export class CommandService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rendered = items.map((t) => {
|
|
|
|
const rendered = items.map((t) => {
|
|
|
|
const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : '';
|
|
|
|
const isOverdue = t.due_date ? t.due_date < todayYMD : false;
|
|
|
|
return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — 👥 sin dueño`;
|
|
|
|
const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : '';
|
|
|
|
|
|
|
|
return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${ICONS.unassigned} sin dueño`;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const total = TaskService.countGroupUnassigned(context.groupId);
|
|
|
|
const total = TaskService.countGroupUnassigned(context.groupId);
|
|
|
|
@ -242,9 +258,10 @@ export class CommandService {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
const owner =
|
|
|
|
const owner =
|
|
|
|
(t.assignees?.length || 0) === 0
|
|
|
|
(t.assignees?.length || 0) === 0
|
|
|
|
? '👥 sin dueño'
|
|
|
|
? `${ICONS.unassigned} sin dueño`
|
|
|
|
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
|
|
|
|
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
|
|
|
|
const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : '';
|
|
|
|
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 `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`;
|
|
|
|
return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`;
|
|
|
|
}));
|
|
|
|
}));
|
|
|
|
sections.push(...rendered);
|
|
|
|
sections.push(...rendered);
|
|
|
|
@ -268,8 +285,9 @@ export class CommandService {
|
|
|
|
if (unassigned.length > 0) {
|
|
|
|
if (unassigned.length > 0) {
|
|
|
|
sections.push(`${groupName} — Sin dueño`);
|
|
|
|
sections.push(`${groupName} — Sin dueño`);
|
|
|
|
const renderedUnassigned = unassigned.map((t) => {
|
|
|
|
const renderedUnassigned = unassigned.map((t) => {
|
|
|
|
const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : '';
|
|
|
|
const isOverdue = t.due_date ? t.due_date < todayYMD : false;
|
|
|
|
return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — 👥 sin dueño`;
|
|
|
|
const datePart = t.due_date ? ` — ${isOverdue ? `${ICONS.warn} ` : ''}${ICONS.date} ${formatDDMM(t.due_date)}` : '';
|
|
|
|
|
|
|
|
return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${ICONS.unassigned} sin dueño`;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
sections.push(...renderedUnassigned);
|
|
|
|
sections.push(...renderedUnassigned);
|
|
|
|
|
|
|
|
|
|
|
|
@ -322,9 +340,10 @@ export class CommandService {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
const owner =
|
|
|
|
const owner =
|
|
|
|
(t.assignees?.length || 0) === 0
|
|
|
|
(t.assignees?.length || 0) === 0
|
|
|
|
? '👥 sin dueño'
|
|
|
|
? `${ICONS.unassigned} sin dueño`
|
|
|
|
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
|
|
|
|
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
|
|
|
|
const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : '';
|
|
|
|
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 `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`;
|
|
|
|
return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`;
|
|
|
|
}));
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
@ -372,9 +391,10 @@ export class CommandService {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
const owner =
|
|
|
|
const owner =
|
|
|
|
(t.assignees?.length || 0) === 0
|
|
|
|
(t.assignees?.length || 0) === 0
|
|
|
|
? '👥 sin dueño'
|
|
|
|
? `${ICONS.unassigned} sin dueño`
|
|
|
|
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
|
|
|
|
: `${t.assignees!.length > 1 ? '👥' : '👤'} ${names.join(', ')}`;
|
|
|
|
const datePart = t.due_date ? ` — 📅 ${formatDDMM(t.due_date)}` : '';
|
|
|
|
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 `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`;
|
|
|
|
return `- ${t.id}) “*${t.description || '(sin descripción)'}*”${datePart} — ${owner}`;
|
|
|
|
}));
|
|
|
|
}));
|
|
|
|
sections.push(...rendered);
|
|
|
|
sections.push(...rendered);
|
|
|
|
@ -421,7 +441,7 @@ export class CommandService {
|
|
|
|
const due = res.task?.due_date ? ` — 📅 ${formatDDMM(res.task?.due_date)}` : '';
|
|
|
|
const due = res.task?.due_date ? ` — 📅 ${formatDDMM(res.task?.due_date)}` : '';
|
|
|
|
return [{
|
|
|
|
return [{
|
|
|
|
recipient: context.sender,
|
|
|
|
recipient: context.sender,
|
|
|
|
message: `✔️ ${id} completada — “*${res.task?.description || '(sin descripción)'}*”${due}\nGracias, ${who}.`
|
|
|
|
message: `${ICONS.complete} ${id} completada — “*${res.task?.description || '(sin descripción)'}*”${due}\nGracias, ${who}.`
|
|
|
|
}];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -460,7 +480,7 @@ export class CommandService {
|
|
|
|
|
|
|
|
|
|
|
|
return [{
|
|
|
|
return [{
|
|
|
|
recipient: context.sender,
|
|
|
|
recipient: context.sender,
|
|
|
|
message: `👤 Has tomado ${id} — “*${res.task?.description || '(sin descripción)'}*”${due}`
|
|
|
|
message: `${ICONS.take} Has tomado ${id} — “*${res.task?.description || '(sin descripción)'}*”${due}`
|
|
|
|
}];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -500,13 +520,13 @@ export class CommandService {
|
|
|
|
if (res.now_unassigned) {
|
|
|
|
if (res.now_unassigned) {
|
|
|
|
return [{
|
|
|
|
return [{
|
|
|
|
recipient: context.sender,
|
|
|
|
recipient: context.sender,
|
|
|
|
message: `👥 ${id} queda sin dueño — “*${res.task?.description || '(sin descripción)'}*”${due}`
|
|
|
|
message: `${ICONS.unassigned} ${id} queda sin dueño — “*${res.task?.description || '(sin descripción)'}*”${due}`
|
|
|
|
}];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [{
|
|
|
|
return [{
|
|
|
|
recipient: context.sender,
|
|
|
|
recipient: context.sender,
|
|
|
|
message: `👤 Has soltado ${id} — “*${res.task?.description || '(sin descripción)'}*”${due}`
|
|
|
|
message: `${ICONS.unassign} Has soltado ${id} — “*${res.task?.description || '(sin descripción)'}*”${due}`
|
|
|
|
}];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -641,12 +661,12 @@ export class CommandService {
|
|
|
|
const responses: CommandResponse[] = [];
|
|
|
|
const responses: CommandResponse[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 1) Ack al creador con formato compacto
|
|
|
|
// 1) Ack al creador con formato compacto
|
|
|
|
const ackHeader = `✅ ${taskId} “*${description || '(sin descripción)'}*”`;
|
|
|
|
const ackHeader = `${ICONS.create} ${taskId} “*${description || '(sin descripción)'}*”`;
|
|
|
|
const ackLines: string[] = [ackHeader];
|
|
|
|
const ackLines: string[] = [ackHeader];
|
|
|
|
const dueFmt = formatDDMM(dueDate);
|
|
|
|
const dueFmt = formatDDMM(dueDate);
|
|
|
|
if (dueFmt) ackLines.push(`📅 ${dueFmt}`);
|
|
|
|
if (dueFmt) ackLines.push(`${ICONS.date} ${dueFmt}`);
|
|
|
|
if (assignmentUserIds.length === 0) {
|
|
|
|
if (assignmentUserIds.length === 0) {
|
|
|
|
ackLines.push(`👥 sin dueño${groupName ? ` (${groupName})` : ''}`);
|
|
|
|
ackLines.push(`${ICONS.unassigned} sin dueño${groupName ? ` (${groupName})` : ''}`);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
const assigneesList = assignedDisplayNames.join(', ');
|
|
|
|
const assigneesList = assignedDisplayNames.join(', ');
|
|
|
|
ackLines.push(`${assignmentUserIds.length > 1 ? '👥' : '👤'} ${assigneesList}`);
|
|
|
|
ackLines.push(`${assignmentUserIds.length > 1 ? '👥' : '👤'} ${assigneesList}`);
|
|
|
|
@ -663,7 +683,7 @@ export class CommandService {
|
|
|
|
responses.push({
|
|
|
|
responses.push({
|
|
|
|
recipient: uid,
|
|
|
|
recipient: uid,
|
|
|
|
message: [
|
|
|
|
message: [
|
|
|
|
`🔔 ${taskId}${formatDDMM(dueDate) ? ` — 📅 ${formatDDMM(dueDate)}` : ''}`,
|
|
|
|
`${ICONS.assignNotice} Tarea ${taskId}${formatDDMM(dueDate) ? ` — ${ICONS.date} ${formatDDMM(dueDate)}` : ''}`,
|
|
|
|
`“*${description || '(sin descripción)'}*”`,
|
|
|
|
`“*${description || '(sin descripción)'}*”`,
|
|
|
|
groupName ? `Grupo: ${groupName}` : null,
|
|
|
|
groupName ? `Grupo: ${groupName}` : null,
|
|
|
|
`Completar: /t x ${taskId}`
|
|
|
|
`Completar: /t x ${taskId}`
|
|
|
|
|