cm_whatsapp_bot_v1/apps/web/src/lib/reminder-update.ts
yiekheng be3f28a1e6 refactor(web,bot,db): rename reminder status 'ended' → 'inactive'
The 'ended' label read like a terminal failure state ("the reminder
gave up") when in practice it just means "this reminder isn't going
to fire on its own — restart it if you want it back". 'inactive' is
the more accurate read.

* SQL migration 0009 backfills existing rows.
* Bot fire-reminder writes 'inactive' on one-off completion / no
  further occurrences.
* Web actions, queries, filters, and reminder lifecycle gates updated.
* Dashboard counter card label "Active / Paused / Ended / Total"
  becomes "Active / Paused / Inactive / Total".
* Reminders list filter tab "Ended" becomes "Inactive".
* Status pill style key renamed to match.
* Tests updated alongside the runtime changes.

Also: the "Pause sending by" deadline opt-in now renders as a
visible card-shaped row with hover state + Set/Off label on the
right, so the toggle is discoverable instead of a tiny native
checkbox tucked next to the label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:32:53 +08:00

46 lines
1.9 KiB
TypeScript

import { DateTime } from "luxon";
/**
* Resolve and validate the scheduledAt for `updateReminderAction`.
*
* Why this exists: the action is invoked from four edit forms
* (account / message / groups / when). Three of those don't change
* the time — they pass the original `scheduledAt` straight through.
* For a paused/ended one-off whose original time is now in the past
* (the operator paused it after a fire and is editing the message),
* a strict "must be future" check rejects the edit even though the
* operator only wanted to fix a typo. So:
*
* - Reject malformed ISO outright.
* - Allow past timestamps when:
* * the reminder's existing status is `paused` or `ended` —
* it won't fire in those states regardless, AND/OR
* * the submitted timestamp matches the existing one (within
* a second of rounding), i.e. the form is passing it through
* unchanged.
* - Otherwise reject past timestamps so an active reminder can't
* be scheduled into a moment that's already gone.
*
* Pure function — takes `now` as input so tests can pin the clock.
*/
export function validateUpdateScheduledAt(args: {
iso: string;
timezone: string;
existingStatus: string;
existingScheduledAt: Date | null;
now: Date;
}): { ok: true; scheduledAt: Date } | { ok: false; error: string } {
const dt = DateTime.fromISO(args.iso, { zone: args.timezone }).toJSDate();
if (Number.isNaN(dt.getTime())) {
return { ok: false, error: "Invalid date" };
}
const isPaused = args.existingStatus === "paused" || args.existingStatus === "inactive";
const sameAsExisting =
args.existingScheduledAt !== null &&
Math.abs(args.existingScheduledAt.getTime() - dt.getTime()) < 1000;
if (dt.getTime() <= args.now.getTime() && !isPaused && !sameAsExisting) {
return { ok: false, error: "Time is in the past" };
}
return { ok: true, scheduledAt: dt };
}