131 lines
5.1 KiB
React
131 lines
5.1 KiB
React
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)
|
||
|
||
// midnight–1AM: still inside shift 3 that started yesterday
|
||
if (hour < 1) {
|
||
return { shift3Date: toDateKey(addDays(tod, -1)), shift1Date: toDateKey(tod) }
|
||
}
|
||
// 1AM–5PM: shift 1 finished or in daytime gap — next window starts at 5PM today
|
||
// 5PM–midnight: 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>
|
||
)
|
||
}
|
||
|