Phase 11: Per-Event Reminders (CAL-13/14, NOTIF-04/05/06) #19
@@ -79,11 +79,23 @@ function yyyyMmDd(d: Date): string {
|
||||
/**
|
||||
* Humanize a lead time in minutes to a human-readable string.
|
||||
*
|
||||
* CR-02 (Phase 11 Plan 05): added isAllDay parameter. All-day events use day-granularity
|
||||
* wording ("Today", "Tomorrow", "In N days/1 week") because the lead is day-based
|
||||
* and "Starts in 0 min" for a same-day all-day event (lead=0) is factually wrong.
|
||||
* Timed events keep the existing "Starts in N min/hours/days" wording (default isAllDay=false).
|
||||
*
|
||||
* D-09: "Starts in 30 min" / "Starts in 1 hour" / "Starts in 1 day" etc.
|
||||
* Branch order is important — check < 120 before the hours calculation to
|
||||
* prevent Math.round(90/60)=2 erroneously giving "2 hours" for a 90-min lead.
|
||||
*/
|
||||
export function humanizeLeadMinutes(leadMinutes: number): string {
|
||||
export function humanizeLeadMinutes(leadMinutes: number, isAllDay = false): string {
|
||||
if (isAllDay) {
|
||||
// Day-granularity wording for all-day event reminders
|
||||
if (leadMinutes === 0) return 'Today';
|
||||
if (leadMinutes <= 1440) return 'Tomorrow';
|
||||
if (leadMinutes === 10080) return 'In 1 week';
|
||||
return `In ${Math.round(leadMinutes / 1440)} days`;
|
||||
}
|
||||
if (leadMinutes < 60) return `Starts in ${leadMinutes} min`;
|
||||
if (leadMinutes < 120) return 'Starts in 1 hour';
|
||||
if (leadMinutes < 1440) return `Starts in ${Math.round(leadMinutes / 60)} hours`;
|
||||
@@ -186,6 +198,7 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
dtstartMs: number; // key component — used for compound dedup key
|
||||
pruneMs: number; // when to prune: dtstartUtc for timed; end-of-event-date for all-day
|
||||
reminderLeadMinutes: number;
|
||||
isAllDay: boolean; // CR-02 (Plan 05): needed for allDay-aware humanizeLeadMinutes
|
||||
dateStr: string;
|
||||
subs: SubRow[];
|
||||
}
|
||||
@@ -214,6 +227,7 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
dtstartMs,
|
||||
pruneMs: dtstartMs, // timed: prune when event has started (dtstartUtc <= now)
|
||||
reminderLeadMinutes: lead,
|
||||
isAllDay: false, // CR-02: timed events use the existing "Starts in N..." wording
|
||||
dateStr: yyyyMmDd(dtstartUtc),
|
||||
subs: [],
|
||||
});
|
||||
@@ -262,6 +276,7 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
dtstartMs,
|
||||
pruneMs,
|
||||
reminderLeadMinutes: lead,
|
||||
isAllDay: true, // CR-02: all-day events use day-granularity wording
|
||||
dateStr: dtstartDate,
|
||||
subs: [],
|
||||
});
|
||||
@@ -289,7 +304,8 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
// Null-safe title fallback (D-02 / NOTIF-01): use uid if title is NULL.
|
||||
title: event.title ?? event.uid,
|
||||
// D-09: humanized body driven by the configured lead (DB ground truth), not live delta.
|
||||
body: humanizeLeadMinutes(event.reminderLeadMinutes),
|
||||
// CR-02 (Plan 05): pass isAllDay so all-day events get day-granularity wording.
|
||||
body: humanizeLeadMinutes(event.reminderLeadMinutes, event.isAllDay),
|
||||
tag: `reminder-${event.uid}`,
|
||||
navigate: `/calendar?date=${event.dateStr}&event=${event.uid}`,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user