From f50a1fc0a7753731012c4ae161ea10e819d5b43a Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 10 May 2026 14:52:05 +0800 Subject: [PATCH] feat(web): create/update actions accept delivery window hours createReminderAction and updateReminderAction now read deliveryWindowStartHour / deliveryWindowEndHour off the input and persist them on the reminders row. Both fields are optional in the input shape (default 6/18) so existing callers don't break, and a refine validates start < end when provided. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/src/actions/reminders.ts | 37 ++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/apps/web/src/actions/reminders.ts b/apps/web/src/actions/reminders.ts index d7e984d..ca3530d 100644 --- a/apps/web/src/actions/reminders.ts +++ b/apps/web/src/actions/reminders.ts @@ -248,6 +248,12 @@ const createReminderSchema = z scheduledAtIso: z.string().datetime({ offset: true }), rrule: z.string().nullable().optional(), timezone: z.string().default(DEFAULT_TIMEZONE), + // Delivery window in the operator's timezone. End hour will gate + // the runtime fan-out in a later phase; start is documented but + // not yet enforced. Optional in the input shape for backward + // compatibility — the action body falls back to 6/18. + deliveryWindowStartHour: z.number().int().min(0).max(24).optional(), + deliveryWindowEndHour: z.number().int().min(0).max(24).optional(), }) .refine( (d) => @@ -258,7 +264,11 @@ const createReminderSchema = z message: "Add a message or attach a file", path: ["messages"], }, - ); + ) + .refine((d) => (d.deliveryWindowStartHour ?? 6) < (d.deliveryWindowEndHour ?? 18), { + message: "Delivery window start must be earlier than end", + path: ["deliveryWindowStartHour"], + }); /** Resolve the schema's union of new + legacy fields into a flat list. */ function resolveMessageParts(parsed: z.infer): Array<{ @@ -304,7 +314,15 @@ export async function createReminderAction( if (!parsed.success) { return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" }; } - const { accountId, groupIds, scheduledAtIso, rrule, timezone } = parsed.data; + const { + accountId, + groupIds, + scheduledAtIso, + rrule, + timezone, + } = parsed.data; + const deliveryWindowStartHour = parsed.data.deliveryWindowStartHour ?? 6; + const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 18; const parts = resolveMessageParts(parsed.data); const op = await getSeededOperator(); @@ -358,6 +376,8 @@ export async function createReminderAction( scheduledAt, rrule: rrule ?? null, timezone, + deliveryWindowStartHour, + deliveryWindowEndHour, status: "active", createdBy: op.id, }) @@ -407,7 +427,16 @@ export async function updateReminderAction( if (!parsed.success) { return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" }; } - const { reminderId, accountId, groupIds, scheduledAtIso, rrule, timezone } = parsed.data; + const { + reminderId, + accountId, + groupIds, + scheduledAtIso, + rrule, + timezone, + } = parsed.data; + const deliveryWindowStartHour = parsed.data.deliveryWindowStartHour ?? 6; + const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 18; const parts = resolveMessageParts(parsed.data); const op = await getSeededOperator(); @@ -467,6 +496,8 @@ export async function updateReminderAction( scheduledAt, rrule: rrule ?? null, timezone, + deliveryWindowStartHour, + deliveryWindowEndHour, // Preserve the lifecycle status. Editing fields shouldn't // implicitly re-activate a paused or ended reminder — the // user can use the explicit Restart action for that.