Files
chore/frontend/vue-app/src/common/scheduleUtils.ts
Ryan Kegel 234adbe05f
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m45s
Add TimeSelector and ScheduleModal components with tests
- Implemented TimeSelector component for selecting time with AM/PM toggle and minute/hour increment/decrement functionality.
- Created ScheduleModal component for scheduling chores with options for specific days or intervals.
- Added utility functions for scheduling logic in scheduleUtils.ts.
- Developed comprehensive tests for TimeSelector and scheduleUtils functions to ensure correct behavior.
2026-02-23 15:44:55 -05:00

124 lines
4.1 KiB
TypeScript

import type { ChoreSchedule, DayConfig } from './models'
/**
* Returns the JS day-of-week index for a given Date.
* 0 = Sunday, 6 = Saturday (matches Python / our DayConfig.day convention).
*/
function getLocalWeekday(d: Date): number {
return d.getDay()
}
/**
* 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.
*/
export function intervalHitsToday(
anchorWeekday: number,
intervalDays: number,
localDate: Date,
): boolean {
const todayWeekday = getLocalWeekday(localDate)
// 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 today = new Date(localDate)
today.setHours(0, 0, 0, 0)
const diffDays = Math.round((today.getTime() - anchor.getTime()) / 86400000)
return diffDays % intervalDays === 0
}
/**
* Returns true if the schedule applies today (localDate).
* - 'days' mode: today's weekday is in day_configs
* - 'interval' mode: intervalHitsToday
*/
export function isScheduledToday(schedule: ChoreSchedule, localDate: Date): boolean {
if (schedule.mode === 'days') {
const todayWeekday = getLocalWeekday(localDate)
return schedule.day_configs.some((dc: DayConfig) => dc.day === todayWeekday)
} else {
return intervalHitsToday(schedule.anchor_weekday, 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).
*/
export function getDueTimeToday(
schedule: ChoreSchedule,
localDate: Date,
): { hour: number; minute: number } | null {
if (schedule.mode === 'days') {
const todayWeekday = getLocalWeekday(localDate)
const dayConfig = schedule.day_configs.find((dc: DayConfig) => dc.day === todayWeekday)
if (!dayConfig) return null
return { hour: dayConfig.hour, minute: dayConfig.minute }
} else {
if (!intervalHitsToday(schedule.anchor_weekday, schedule.interval_days, localDate)) return null
return { hour: schedule.interval_hour, minute: schedule.interval_minute }
}
}
/**
* Returns true if the given due time has already passed given the current local time.
*/
export function isPastTime(dueHour: number, dueMinute: number, localNow: Date): boolean {
const nowMinutes = localNow.getHours() * 60 + localNow.getMinutes()
const dueMinutes = dueHour * 60 + dueMinute
return nowMinutes >= dueMinutes
}
/**
* Returns true if extensionDate matches today's local ISO date string.
*/
export function isExtendedToday(
extensionDate: string | null | undefined,
localDate: Date,
): boolean {
if (!extensionDate) return false
return extensionDate === toLocalISODate(localDate)
}
/**
* Formats a 24h hour/minute pair into a 12h "X:XX AM/PM" label.
*/
export function formatDueTimeLabel(hour: number, minute: number): string {
const period = hour < 12 ? 'AM' : 'PM'
const h12 = hour % 12 === 0 ? 12 : hour % 12
const mm = String(minute).padStart(2, '0')
return `${h12}:${mm} ${period}`
}
/**
* Returns the number of milliseconds from localNow until the due time fires today.
* Returns 0 if it has already passed.
*/
export function msUntilExpiry(dueHour: number, dueMinute: number, localNow: Date): number {
const nowMs =
localNow.getHours() * 3600000 +
localNow.getMinutes() * 60000 +
localNow.getSeconds() * 1000 +
localNow.getMilliseconds()
const dueMs = dueHour * 3600000 + dueMinute * 60000
return Math.max(0, dueMs - nowMs)
}
/**
* Returns the local date as an ISO date string (YYYY-MM-DD) without timezone conversion.
*/
export function toLocalISODate(d: Date): string {
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}