feat(11-03): surface reminderLeadMinutes on CalendarOccurrence + GET select (D-10)
- Add reminderLeadMinutes: number | null to CalendarOccurrence interface (D-06) - Import classifyValarms in expand.ts; derive series-level value once per VEVENT - Add reminderLeadMinutes to both non-recurring and recurring occurrence construction - Add reminderLeadMinutes to GET /api/events select for edit-mode pre-population
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ICAL from 'ical.js';
|
import ICAL from 'ical.js';
|
||||||
|
import { classifyValarms } from './vevent.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A concrete calendar event occurrence ready for UI consumption.
|
* A concrete calendar event occurrence ready for UI consumption.
|
||||||
@@ -33,6 +34,11 @@ import ICAL from 'ical.js';
|
|||||||
* Color routing (D-06, CAL-02):
|
* Color routing (D-06, CAL-02):
|
||||||
* The client routes calendarId for Schedule-X as: isShared ? 'shared' : String(ownerUserId)
|
* The client routes calendarId for Schedule-X as: isShared ? 'shared' : String(ownerUserId)
|
||||||
* The DB calendarId is also present for reference but NOT used as the Schedule-X calendarId.
|
* The DB calendarId is also present for reference but NOT used as the Schedule-X calendarId.
|
||||||
|
*
|
||||||
|
* Phase 11 Plan 03 (D-10): reminderLeadMinutes is a series-level property — all occurrences
|
||||||
|
* of a recurring master inherit the master's value. Derived from the VEVENT's VALARM via
|
||||||
|
* classifyValarms (same logic as sync.ts so the scheduler and the GET response agree).
|
||||||
|
* NULL-vs-0 is preserved (D-06): null = no reminder; 0 = same-day all-day; positive = lead.
|
||||||
*/
|
*/
|
||||||
export interface CalendarOccurrence {
|
export interface CalendarOccurrence {
|
||||||
/** `ev-<sanitized-uid>-<epochMs>` — Schedule-X-safe stable id (see makeOccurrenceId) */
|
/** `ev-<sanitized-uid>-<epochMs>` — Schedule-X-safe stable id (see makeOccurrenceId) */
|
||||||
@@ -66,6 +72,14 @@ export interface CalendarOccurrence {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
/** True when this occurrence belongs to a recurring series (has RRULE). False for single events. */
|
/** True when this occurrence belongs to a recurring series (has RRULE). False for single events. */
|
||||||
hasRrule: boolean;
|
hasRrule: boolean;
|
||||||
|
/**
|
||||||
|
* Per-event reminder lead in minutes (Phase 11, CAL-13, D-06).
|
||||||
|
* Series-level: all occurrences inherit the master's value (D-10).
|
||||||
|
* null — no reminder / custom/absolute VALARM not reducible to a single lead
|
||||||
|
* 0 — same-day all-day (fire at 9 AM on event date)
|
||||||
|
* positive — N minutes before event start (timed) or N/1440 days before (all-day)
|
||||||
|
*/
|
||||||
|
reminderLeadMinutes: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,6 +237,16 @@ export function expandOccurrences(
|
|||||||
// Capture once — used in both the non-recurring and recurring branches to populate hasRrule.
|
// Capture once — used in both the non-recurring and recurring branches to populate hasRrule.
|
||||||
const isRecurring = event.isRecurring();
|
const isRecurring = event.isRecurring();
|
||||||
|
|
||||||
|
// Phase 11 Plan 03 (D-10): series-level reminderLeadMinutes — derived once from the master
|
||||||
|
// event's VALARM via classifyValarms. All occurrences inherit this value (series-level, D-10).
|
||||||
|
// preset/offlist → specific leadMinutes; custom/none → null (D-07/NOTIF-05).
|
||||||
|
// classifyValarms wraps ICAL.parse in try/catch (T-11-07 safe); safe on parse failure → null.
|
||||||
|
const alarmClass = classifyValarms(rawVevent);
|
||||||
|
const reminderLeadMinutes: number | null =
|
||||||
|
alarmClass.kind === 'preset' || alarmClass.kind === 'offlist'
|
||||||
|
? alarmClass.leadMinutes
|
||||||
|
: null;
|
||||||
|
|
||||||
// --- 4. Non-recurring event: single occurrence check ---
|
// --- 4. Non-recurring event: single occurrence check ---
|
||||||
if (!isRecurring) {
|
if (!isRecurring) {
|
||||||
if (dtstart.compare(rangeStart) >= 0 && dtstart.compare(rangeEnd) < 0) {
|
if (dtstart.compare(rangeStart) >= 0 && dtstart.compare(rangeEnd) < 0) {
|
||||||
@@ -258,6 +282,7 @@ export function expandOccurrences(
|
|||||||
location: event.location ?? null,
|
location: event.location ?? null,
|
||||||
description: event.description ?? null,
|
description: event.description ?? null,
|
||||||
hasRrule: isRecurring, // always false in the non-recurring branch
|
hasRrule: isRecurring, // always false in the non-recurring branch
|
||||||
|
reminderLeadMinutes, // series-level (D-10)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return occurrences;
|
return occurrences;
|
||||||
@@ -305,6 +330,7 @@ export function expandOccurrences(
|
|||||||
location: event.location ?? null,
|
location: event.location ?? null,
|
||||||
description: event.description ?? null,
|
description: event.description ?? null,
|
||||||
hasRrule: isRecurring, // always true in the recurring branch
|
hasRrule: isRecurring, // always true in the recurring branch
|
||||||
|
reminderLeadMinutes, // series-level — all occurrences inherit the master's value (D-10)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
|
|||||||
userId: users.id,
|
userId: users.id,
|
||||||
userColor: users.color,
|
userColor: users.color,
|
||||||
ownerName: users.displayName,
|
ownerName: users.displayName,
|
||||||
|
// Phase 11 Plan 03: surface reminderLeadMinutes for edit-mode pre-population (D-06/D-10)
|
||||||
|
reminderLeadMinutes: calendarEvents.reminderLeadMinutes,
|
||||||
})
|
})
|
||||||
.from(calendarEvents)
|
.from(calendarEvents)
|
||||||
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
|
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
|
||||||
|
|||||||
Reference in New Issue
Block a user