diff --git a/apps/bot/src/telegram/menus.ts b/apps/bot/src/telegram/menus.ts index 535ad62..e5c8779 100644 --- a/apps/bot/src/telegram/menus.ts +++ b/apps/bot/src/telegram/menus.ts @@ -1,6 +1,8 @@ import { InlineKeyboard } from "grammy"; import { db } from "../db.js"; import { sessionManager } from "../whatsapp/session-manager.js"; +import { listRemindersForOperator, getReminderWithDetails } from "../reminders/crud.js"; +import { DateTime } from "luxon"; // BotFather-style navigation: every leaf has a way home, every branch shows // you where you are. All callbacks edit the same message. @@ -24,8 +26,9 @@ export type MenuView = { export function mainMenu(): MenuView { const keyboard = new InlineKeyboard() .text("๐Ÿ“’ Accounts", "m:accounts") - .text("๐Ÿ“ก Pair New", "m:pair") + .text("๐Ÿ“… Reminders", "m:reminders") .row() + .text("๐Ÿ“ก Pair New", "m:pair") .text("โ“ Help", "m:help"); return { text: @@ -246,3 +249,138 @@ export function unpairDoneMenu(label: string): MenuView { keyboard, }; } + +export async function remindersMenu(operatorId: string, operatorTimezone: string): Promise { + const list = await listRemindersForOperator(operatorId, 30); + const keyboard = new InlineKeyboard(); + for (const r of list) { + const when = r.scheduledAt + ? DateTime.fromJSDate(r.scheduledAt).setZone(operatorTimezone).toFormat("dd MMM HH:mm") + : "โ€”"; + const label = `${r.status === "active" ? "๐ŸŸข" : r.status === "ended" ? "โšช" : "โธ"} ${r.name} ยท ${when}`; + keyboard.text(label.slice(0, 60), `rm:${r.id}`).row(); + } + keyboard.text("โž• New Reminder", "rm:new").row().text("โฌ… Main Menu", "m:main"); + if (list.length === 0) { + return { + text: "๐Ÿ“… *Reminders*\n\nYou haven't created any reminders yet.", + keyboard, + }; + } + return { + text: `๐Ÿ“… *Reminders* (${list.length})\n\nTap one to view, or *โž• New* to schedule a fresh one.`, + keyboard, + }; +} + +export async function reminderDetailMenu( + reminderId: string, + operatorTimezone: string, +): Promise { + const rem = await getReminderWithDetails(reminderId); + if (!rem) return null; + const when = rem.scheduledAt + ? DateTime.fromJSDate(rem.scheduledAt).setZone(operatorTimezone).toFormat("yyyy-MM-dd HH:mm") + : "โ€”"; + const messagePreview = rem.messages + .map((m) => (m.kind === "text" ? m.textContent : `[${m.kind}]`)) + .filter(Boolean) + .join("\n"); + + const keyboard = new InlineKeyboard() + .text("๐Ÿ—‘ Delete", `rm_del:${reminderId}`) + .row() + .text("โฌ… Reminders", "m:reminders") + .text("โฌ… Main Menu", "m:main"); + + return { + text: + `๐Ÿ“… *${rem.name}*\n\n` + + `When: \`${when}\` (${rem.timezone})\n` + + `Status: \`${rem.status}\`\n` + + `Targets: ${rem.targets.length}\n\n` + + `*Body:*\n${messagePreview || "(empty)"}`, + keyboard, + }; +} + +export function reminderPickAccountMenu( + accounts: { id: string; label: string; phoneNumber: string | null }[], +): MenuView { + const keyboard = new InlineKeyboard(); + for (const a of accounts) { + const phone = a.phoneNumber ? ` (+${a.phoneNumber})` : ""; + keyboard.text(`๐Ÿ“’ ${a.label}${phone}`, `rm_acc:${a.id}`).row(); + } + keyboard.text("โฌ… Cancel", "m:reminders"); + return { + text: "โž• *New Reminder โ€” Step 1 / 4*\n\nWhich WhatsApp account should send it?", + keyboard, + }; +} + +export function reminderPickGroupMenu( + groups: { id: string; name: string }[], +): MenuView { + const keyboard = new InlineKeyboard(); + for (const g of groups.slice(0, 30)) { + const name = g.name.length > 32 ? `${g.name.slice(0, 31)}โ€ฆ` : g.name; + keyboard.text(`๐Ÿ‘ฅ ${name}`, `rm_grp:${g.id}`).row(); + } + keyboard.text("โฌ… Cancel", "m:reminders"); + return { + text: "โž• *New Reminder โ€” Step 2 / 4*\n\nWhich group?", + keyboard, + }; +} + +export function reminderComposeMenu(): MenuView { + const keyboard = new InlineKeyboard().text("โฌ… Cancel", "m:reminders"); + return { + text: + "โž• *New Reminder โ€” Step 3 / 4*\n\n" + + "Send the message body now โ€” text, photo, video, or document.\n\n" + + "Reply to *this* message with what you want sent. " + + "If you send media with a caption, the caption is included.", + keyboard, + }; +} + +export function reminderTimeMenu(): MenuView { + const keyboard = new InlineKeyboard() + .text("๐Ÿ• In 1 hour", "rm_t:in_1h") + .text("๐Ÿ•’ In 3 hours", "rm_t:in_3h") + .row() + .text("๐ŸŒ… Tomorrow 9 AM", "rm_t:tomorrow_9am") + .text("๐Ÿ“… Next Mon 9 AM", "rm_t:next_mon_9am") + .row() + .text("โŒจ๏ธ Custom date/time", "rm_t:custom") + .row() + .text("โฌ… Cancel", "m:reminders"); + return { + text: + "โž• *New Reminder โ€” Step 4 / 4*\n\n" + + "When should it fire? Pick a quick option or type a date/time.", + keyboard, + }; +} + +export function reminderConfirmMenu(summary: { + accountLabel: string; + groupName: string; + body: string; + whenLocal: string; +}): MenuView { + const keyboard = new InlineKeyboard() + .text("โœ… Schedule", "rm_save") + .text("โฌ… Cancel", "m:reminders"); + return { + text: + "*Review*\n\n" + + `Account: ${summary.accountLabel}\n` + + `Group: ${summary.groupName}\n` + + `When: ${summary.whenLocal}\n\n` + + `*Body:*\n${summary.body}`, + keyboard, + }; +}