This commit is contained in:
2026-06-17 16:02:04 -04:00
parent 6762586e2d
commit e51a6777fc
21 changed files with 1399 additions and 506 deletions
+130
View File
@@ -0,0 +1,130 @@
import DayColumn from './DayColumn'
function addDays(date, n) {
const d = new Date(date)
d.setDate(d.getDate() + n)
return d
}
function formatDateRange(monday) {
const sunday = addDays(monday, 6)
const opts = { month: 'short', day: 'numeric' }
const startStr = monday.toLocaleDateString('en-US', opts)
const endStr = sunday.toLocaleDateString('en-US', {
month: sunday.getMonth() !== monday.getMonth() ? 'short' : undefined,
day: 'numeric',
})
return `${startStr} ${endStr}`
}
function toDateKey(date) {
return date.toISOString().slice(0, 10)
}
function getMondayOfWeek(date) {
const d = new Date(date)
const day = d.getDay()
d.setDate(d.getDate() + (day === 0 ? -6 : 1 - day))
d.setHours(0, 0, 0, 0)
return d
}
// Returns the two shifts that form the next/active test window.
// Window = Shift 3 of day D → Shift 1 of day D+1
function getNextTestWindow() {
const now = new Date()
const hour = now.getHours()
const tod = new Date(now); tod.setHours(0, 0, 0, 0)
// midnight1AM: still inside shift 3 that started yesterday
if (hour < 1) {
return { shift3Date: toDateKey(addDays(tod, -1)), shift1Date: toDateKey(tod) }
}
// 1AM5PM: shift 1 finished or in daytime gap — next window starts at 5PM today
// 5PMmidnight: currently inside shift 3 of today
// Both cases: upcoming/active window is shift 3 today + shift 1 tomorrow
return { shift3Date: toDateKey(tod), shift1Date: toDateKey(addDays(tod, 1)) }
}
// scheduleData: { "YYYY-MM-DD": { shift1: [...], shift2: [...], shift3: [...] }, ... }
export default function Calendar({ scheduleData = {}, daytimeDateKey = null, weekStart, onWeekChange }) {
const today = new Date()
today.setHours(0, 0, 0, 0)
const activeWindow = getNextTestWindow()
const days = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i))
// Reorder columns: Sun Mon Tue Wed Thu Fri Sat
// weekStart is Monday, so days[0]=Mon ... days[6]=Sun → put Sunday first
const orderedDays = [ ...days.slice(0, 7)]
return (
<div className="flex flex-col gap-3 h-full">
{/* Week navigation */}
<div className="flex items-center gap-3">
<button
onClick={() => onWeekChange(addDays(weekStart, -7))}
className="p-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Previous week"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</button>
<span className="text-sm font-medium text-gray-300 min-w-36 text-center">
{formatDateRange(weekStart)}
</span>
<button
onClick={() => onWeekChange(addDays(weekStart, 7))}
className="p-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Next week"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
<button
onClick={() => onWeekChange(getMondayOfWeek(new Date()))}
className="px-2 py-1 text-xs font-semibold text-gray-300 border border-gray-600 rounded hover:bg-gray-700 hover:text-white transition-colors"
title="Jump to this week"
>
Today
</button>
</div>
{/* 7-day grid */}
<div className="flex gap-1.5 flex-1 overflow-x-auto">
{orderedDays.map((date) => {
const key = toDateKey(date)
const shifts = scheduleData[key] ?? { shift1: [], shift2: [], shift3: [] }
const isToday = date.getTime() === today.getTime()
const isWeekend = date.getDay() === 0 || date.getDay() === 6
// Which shifts on this date are part of the active test window?
const activeShifts = new Set()
if (key === activeWindow.shift3Date) activeShifts.add(3)
if (key === activeWindow.shift1Date) activeShifts.add(1)
return (
<DayColumn
key={key}
date={date}
isToday={isToday}
activeShifts={activeShifts}
shifts={shifts}
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
/>
)
})}
</div>
{/* Legend */}
<div className="flex gap-4 text-xs text-gray-400">
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-gray-500 inline-block" />Pending</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-green-700 inline-block" />Completed</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-red-700 inline-block" />Failed</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-yellow-700 inline-block" />Invalid</span>
</div>
</div>
)
}