Files
box-box/frontend/src/lib/schedule.ts

169 lines
4.9 KiB
TypeScript
Raw Normal View History

2026-05-25 10:29:31 -04:00
import type { Meeting, Session } from '../types'
const DEFAULT_SESSION_DURATION_MS = 3 * 60 * 60 * 1000
export function parseScheduleTime(value: string): Date | null {
if (!value) return null
const parsed = Date.parse(value)
if (!Number.isNaN(parsed)) return new Date(parsed)
const dateOnly = Date.parse(value.slice(0, 10))
return Number.isNaN(dateOnly) ? null : new Date(dateOnly)
}
export function meetingStartTime(meeting: Meeting): Date | null {
return parseScheduleTime(meeting.date_start)
}
export function meetingEndTime(meeting: Meeting): Date | null {
const end = parseScheduleTime(meeting.date_end)
if (end) return end
const start = meetingStartTime(meeting)
return start ? new Date(start.getTime() + 72 * 60 * 60 * 1000) : null
}
export function sessionStartTime(session: Session): Date | null {
return parseScheduleTime(session.date_start)
}
export function sessionEndTime(session: Session): Date | null {
const end = parseScheduleTime(session.date_end)
if (end) return end
const start = sessionStartTime(session)
return start ? new Date(start.getTime() + DEFAULT_SESSION_DURATION_MS) : null
}
export function sortSessionsByStart(sessions: Session[]): Session[] {
return [...sessions].sort((a, b) => {
const left = sessionStartTime(a)?.getTime() ?? 0
const right = sessionStartTime(b)?.getTime() ?? 0
if (left !== right) return left - right
return a.date_start.localeCompare(b.date_start)
})
}
export function currentMeeting(meetings: Meeting[], now: Date): Meeting | null {
let selected: Meeting | null = null
let latest = 0
for (const meeting of meetings) {
const start = meetingStartTime(meeting)
if (!start || start > now) continue
const end = meetingEndTime(meeting)
if (!end || now > new Date(end.getTime() + 24 * 60 * 60 * 1000)) continue
const startMs = start.getTime()
if (!selected || startMs > latest) {
selected = meeting
latest = startMs
}
}
return selected
}
export function nextUpcomingMeeting(meetings: Meeting[], now: Date): Meeting | null {
for (const meeting of meetings) {
const start = meetingStartTime(meeting)
if (start && start > now) return meeting
}
return null
}
export function mostRecentPastMeeting(meetings: Meeting[], now: Date): Meeting | null {
let selected: Meeting | null = null
let latest = 0
for (const meeting of meetings) {
const start = meetingStartTime(meeting)
if (!start || start > now) continue
const startMs = start.getTime()
if (!selected || startMs > latest) {
selected = meeting
latest = startMs
}
}
return selected
}
export function pickFocusMeeting(meetings: Meeting[], now: Date): Meeting | null {
return (
currentMeeting(meetings, now) ??
nextUpcomingMeeting(meetings, now) ??
mostRecentPastMeeting(meetings, now) ??
meetings[0] ??
null
)
}
export function meetingHasStarted(meeting: Meeting, now: Date): boolean {
const start = meetingStartTime(meeting)
return start != null && now >= start
}
export function currentAndNextSession(
sessions: Session[],
now: Date,
): { current: Session | null; next: Session | null } {
const sorted = sortSessionsByStart(sessions)
for (const session of sorted) {
const start = sessionStartTime(session)
const end = sessionEndTime(session)
if (!start || !end) continue
if (now >= start && now < end) {
return { current: session, next: null }
}
if (now < start) {
return { current: null, next: session }
}
}
return { current: null, next: null }
}
export function formatCountdown(target: Date, now: Date): string {
const diffMs = Math.max(0, target.getTime() - now.getTime())
const totalSeconds = Math.floor(diffMs / 1000)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const mins = Math.floor((totalSeconds % 3600) / 60)
const secs = totalSeconds % 60
return `${days}d ${String(hours).padStart(2, '0')}h ${String(mins).padStart(2, '0')}m ${String(secs).padStart(2, '0')}s`
}
export function formatSessionScheduleTime(value: string): string {
const date = parseScheduleTime(value)
if (!date) return '—'
return date.toLocaleString('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
}
export type FocusMeetingKind = 'current' | 'next' | 'recent' | 'fallback'
export function focusMeetingKind(meeting: Meeting, now: Date): FocusMeetingKind {
if (currentMeeting([meeting], now)) return 'current'
if (nextUpcomingMeeting([meeting], now)) return 'next'
if (mostRecentPastMeeting([meeting], now)) return 'recent'
return 'fallback'
}
export function focusMeetingLabel(kind: FocusMeetingKind): string {
switch (kind) {
case 'current':
return 'Current Weekend'
case 'next':
return 'Next Weekend'
case 'recent':
return 'Recent Local Weekend'
default:
return 'Weekend'
}
}