feat: Refactor ScheduleModal to support interval scheduling with date input and deadline toggle
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m30s
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m30s
- Updated ChoreSchedule model to include anchor_date and interval_has_deadline. - Refactored interval scheduling logic in scheduleUtils to use anchor_date. - Introduced DateInputField component for selecting anchor dates in ScheduleModal. - Enhanced ScheduleModal to include a stepper for interval days and a toggle for deadline. - Updated tests for ScheduleModal and scheduleUtils to reflect new interval scheduling logic. - Added DateInputField tests to ensure proper functionality and prop handling.
This commit is contained in:
@@ -24,8 +24,9 @@ export interface ChoreSchedule {
|
||||
default_hour?: number // master deadline hour; present when mode='days'
|
||||
default_minute?: number // master deadline minute; present when mode='days'
|
||||
// mode='interval'
|
||||
interval_days: number // 2–7
|
||||
anchor_weekday: number // 0=Sun–6=Sat
|
||||
interval_days: number // 1–7
|
||||
anchor_date: string // ISO date string e.g. "2026-02-25"; "" means use today
|
||||
interval_has_deadline: boolean // false = "Anytime" (no deadline)
|
||||
interval_hour: number
|
||||
interval_minute: number
|
||||
created_at: number
|
||||
|
||||
@@ -11,27 +11,32 @@ function getLocalWeekday(d: Date): number {
|
||||
/**
|
||||
* Returns true if the interval schedule hits on the given localDate.
|
||||
*
|
||||
* Anchor: the most recent occurrence of `anchorWeekday` on or before today (this week).
|
||||
* Pattern: every `intervalDays` days starting from that anchor.
|
||||
* Anchor: the ISO date string from `anchor_date` (e.g. "2026-02-26").
|
||||
* An empty string means "use localDate as anchor" (backward compat) which
|
||||
* hits on today (diffDays=0) and every intervalDays days after.
|
||||
*
|
||||
* Dates before the anchor always return false (scheduling hasn't started yet).
|
||||
*/
|
||||
export function intervalHitsToday(
|
||||
anchorWeekday: number,
|
||||
anchorDate: string,
|
||||
intervalDays: number,
|
||||
localDate: Date,
|
||||
): boolean {
|
||||
const todayWeekday = getLocalWeekday(localDate)
|
||||
// Parse anchor: use localDate itself when anchor is empty (backward compat)
|
||||
let anchor: Date
|
||||
if (anchorDate) {
|
||||
const [y, m, d] = anchorDate.split('-').map(Number)
|
||||
anchor = new Date(y, m - 1, d, 0, 0, 0, 0)
|
||||
} else {
|
||||
anchor = new Date(localDate)
|
||||
anchor.setHours(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
// Find the most recent anchorWeekday on or before today within the current week.
|
||||
// We calculate the anchor as: (today - daysSinceAnchor)
|
||||
const daysSinceAnchor = (todayWeekday - anchorWeekday + 7) % 7
|
||||
const anchor = new Date(localDate)
|
||||
anchor.setDate(anchor.getDate() - daysSinceAnchor)
|
||||
anchor.setHours(0, 0, 0, 0)
|
||||
const target = new Date(localDate)
|
||||
target.setHours(0, 0, 0, 0)
|
||||
|
||||
const today = new Date(localDate)
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
const diffDays = Math.round((today.getTime() - anchor.getTime()) / 86400000)
|
||||
const diffDays = Math.round((target.getTime() - anchor.getTime()) / 86_400_000)
|
||||
if (diffDays < 0) return false // before anchor — not started yet
|
||||
return diffDays % intervalDays === 0
|
||||
}
|
||||
|
||||
@@ -45,13 +50,16 @@ export function isScheduledToday(schedule: ChoreSchedule, localDate: Date): bool
|
||||
const todayWeekday = getLocalWeekday(localDate)
|
||||
return schedule.day_configs.some((dc: DayConfig) => dc.day === todayWeekday)
|
||||
} else {
|
||||
return intervalHitsToday(schedule.anchor_weekday, schedule.interval_days, localDate)
|
||||
return intervalHitsToday(schedule.anchor_date ?? '', schedule.interval_days, localDate)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the due time {hour, minute} for today, or null if no due time is configured
|
||||
* for today (e.g. the day is not scheduled).
|
||||
* Returns the due time {hour, minute} for today, or null if:
|
||||
* - the day is not scheduled, OR
|
||||
* - the schedule has no deadline (interval_has_deadline === false → "Anytime")
|
||||
*
|
||||
* Callers treat null as "active all day with no expiry".
|
||||
*/
|
||||
export function getDueTimeToday(
|
||||
schedule: ChoreSchedule,
|
||||
@@ -63,7 +71,10 @@ export function getDueTimeToday(
|
||||
if (!dayConfig) return null
|
||||
return { hour: dayConfig.hour, minute: dayConfig.minute }
|
||||
} else {
|
||||
if (!intervalHitsToday(schedule.anchor_weekday, schedule.interval_days, localDate)) return null
|
||||
if (!intervalHitsToday(schedule.anchor_date ?? '', schedule.interval_days, localDate))
|
||||
return null
|
||||
// interval_has_deadline === false means "Anytime" — no expiry time
|
||||
if (schedule.interval_has_deadline === false) return null
|
||||
return { hour: schedule.interval_hour, minute: schedule.interval_minute }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user