clean up calendar display, added separated device runtime overrides
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import DayColumn from './DayColumn'
|
||||
import { getDeviceAccentClass } from './TestCard'
|
||||
|
||||
@@ -58,6 +59,11 @@ export default function Calendar({
|
||||
windowLookup = new Map(),
|
||||
onWindowSelect,
|
||||
}) {
|
||||
const SLOT_CHROME_PX = 10
|
||||
const TEST_CARD_ROW_HEIGHT_PX = 22
|
||||
const TEST_CARD_ROW_GAP_PX = 2
|
||||
const DAY_HEADER_HEIGHT_PX = 48
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
@@ -68,6 +74,36 @@ export default function Calendar({
|
||||
// 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)]
|
||||
const shiftMinRows = useMemo(() => {
|
||||
const minima = { shift2: 1, shift3: 1, nextDayShift1: 1 }
|
||||
|
||||
for (const date of orderedDays) {
|
||||
const key = toDateKey(date)
|
||||
const shifts = scheduleData[key] ?? { shift1: [], shift2: [], shift3: [] }
|
||||
const nextKey = toDateKey(addDays(date, 1))
|
||||
const nextDayShift1 = scheduleData[nextKey]?.shift1 ?? []
|
||||
|
||||
minima.shift2 = Math.max(minima.shift2, shifts.shift2.length || 1)
|
||||
minima.shift3 = Math.max(minima.shift3, shifts.shift3.length || 1)
|
||||
minima.nextDayShift1 = Math.max(minima.nextDayShift1, nextDayShift1.length || 1)
|
||||
}
|
||||
|
||||
return minima
|
||||
}, [orderedDays, scheduleData])
|
||||
const slotHeights = useMemo(() => {
|
||||
const calcSlotHeight = (rows) => {
|
||||
const safeRows = Math.max(1, rows || 1)
|
||||
const contentHeight = (safeRows * TEST_CARD_ROW_HEIGHT_PX) + ((safeRows - 1) * TEST_CARD_ROW_GAP_PX)
|
||||
return contentHeight + SLOT_CHROME_PX
|
||||
}
|
||||
|
||||
return {
|
||||
shift2: calcSlotHeight(shiftMinRows.shift2),
|
||||
shift3: calcSlotHeight(shiftMinRows.shift3),
|
||||
nextDayShift1: calcSlotHeight(shiftMinRows.nextDayShift1),
|
||||
}
|
||||
}, [shiftMinRows])
|
||||
const totalShiftHeightPx = slotHeights.shift2 + slotHeights.shift3 + slotHeights.nextDayShift1
|
||||
const deviceLegendItems = Array.from(
|
||||
new Set(
|
||||
Object.values(scheduleData)
|
||||
@@ -135,37 +171,50 @@ export default function Calendar({
|
||||
</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
|
||||
// Get next day's shift1 for display
|
||||
const nextDate = addDays(date, 1)
|
||||
const nextKey = toDateKey(nextDate)
|
||||
const nextDayShift1 = scheduleData[nextKey]?.shift1 ?? []
|
||||
// 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)
|
||||
// Check if next day's shift1 is active
|
||||
const nextDayShift1Active = nextKey === activeWindow.shift1Date
|
||||
return (
|
||||
<DayColumn
|
||||
key={key}
|
||||
date={date}
|
||||
isToday={isToday}
|
||||
activeShifts={activeShifts}
|
||||
shifts={shifts}
|
||||
nextDayShift1={nextDayShift1}
|
||||
nextDayShift1Active={nextDayShift1Active}
|
||||
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
|
||||
windowLookup={windowLookup}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<div className="flex gap-2 flex-1 min-h-0 overflow-hidden">
|
||||
<div className="w-10 shrink-0 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
||||
<div style={{ height: `${DAY_HEADER_HEIGHT_PX}px` }} />
|
||||
<div className="relative" style={{ height: `${totalShiftHeightPx}px` }}>
|
||||
<span className="absolute left-0 top-0">9AM</span>
|
||||
<span className="absolute left-0" style={{ top: `${slotHeights.shift2}px` }}>5PM</span>
|
||||
<span className="absolute left-0" style={{ top: `${slotHeights.shift2 + slotHeights.shift3}px` }}>12AM</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
// Get next day's shift1 for display
|
||||
const nextDate = addDays(date, 1)
|
||||
const nextKey = toDateKey(nextDate)
|
||||
const nextDayShift1 = scheduleData[nextKey]?.shift1 ?? []
|
||||
// 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)
|
||||
// Check if next day's shift1 is active
|
||||
const nextDayShift1Active = nextKey === activeWindow.shift1Date
|
||||
return (
|
||||
<DayColumn
|
||||
key={key}
|
||||
date={date}
|
||||
isToday={isToday}
|
||||
activeShifts={activeShifts}
|
||||
shifts={shifts}
|
||||
nextDayShift1={nextDayShift1}
|
||||
nextDayShift1Active={nextDayShift1Active}
|
||||
shiftMinRows={shiftMinRows}
|
||||
slotHeights={slotHeights}
|
||||
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
|
||||
windowLookup={windowLookup}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
|
||||
@@ -10,6 +10,8 @@ export default function DayColumn({
|
||||
nextDayShift1 = [],
|
||||
nextDayShift1Active = false,
|
||||
showShift2 = false,
|
||||
shiftMinRows = { shift2: 1, shift3: 1, nextDayShift1: 1 },
|
||||
slotHeights = { shift2: 0, shift3: 0, nextDayShift1: 0 },
|
||||
windowLookup = new Map(),
|
||||
onWindowSelect,
|
||||
}) {
|
||||
@@ -27,7 +29,7 @@ export default function DayColumn({
|
||||
<div className="flex flex-col min-w-0 flex-1 rounded-lg border border-gray-700 bg-gray-800/40">
|
||||
{/* Day header — subtle today ring, no full-column highlight */}
|
||||
<div
|
||||
className={`text-center py-1.5 rounded-t-lg ${
|
||||
className={`h-12 flex flex-col justify-center text-center py-1.5 rounded-t-lg ${
|
||||
isToday ? 'bg-gray-600 text-white' : 'bg-gray-700/60 text-gray-300'
|
||||
}`}
|
||||
>
|
||||
@@ -40,29 +42,32 @@ export default function DayColumn({
|
||||
</div>
|
||||
|
||||
{/* Shifts */}
|
||||
<div className="flex flex-col flex-1 px-0.5 py-1">
|
||||
<div className="flex flex-col flex-1 px-0.5">
|
||||
<ShiftSlot
|
||||
label="9AM–5PM"
|
||||
tests={shifts.shift2}
|
||||
visible={true}
|
||||
active={activeShifts.has(2)}
|
||||
windowDetails={getWindowForShift(2)}
|
||||
minContentRows={shiftMinRows.shift2}
|
||||
slotHeightPx={slotHeights.shift2}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
<ShiftSlot
|
||||
label="5PM–12AM"
|
||||
tests={shifts.shift3}
|
||||
visible={true}
|
||||
active={activeShifts.has(3)}
|
||||
windowDetails={getWindowForShift(3)}
|
||||
minContentRows={shiftMinRows.shift3}
|
||||
slotHeightPx={slotHeights.shift3}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
<ShiftSlot
|
||||
label="12AM–9AM (next day)"
|
||||
tests={nextDayShift1}
|
||||
visible={true}
|
||||
active={nextDayShift1Active}
|
||||
windowDetails={null}
|
||||
minContentRows={shiftMinRows.nextDayShift1}
|
||||
slotHeightPx={slotHeights.nextDayShift1}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -152,40 +152,84 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
|
||||
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
|
||||
Runtime Overrides
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<Field label="P2P Minutes">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
className={INPUT_CLS}
|
||||
placeholder="80"
|
||||
value={form.p2pRuntimeMinutes ?? ''}
|
||||
onChange={(e) => set('p2pRuntimeMinutes', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="COE Minutes">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
className={INPUT_CLS}
|
||||
placeholder="115"
|
||||
value={form.coeRuntimeMinutes ?? ''}
|
||||
onChange={(e) => set('coeRuntimeMinutes', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="P3P Minutes">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
className={INPUT_CLS}
|
||||
placeholder="105"
|
||||
value={form.p3pRuntimeMinutes ?? ''}
|
||||
onChange={(e) => set('p3pRuntimeMinutes', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-gray-300 mb-2">DUT</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<Field label="P2P Minutes">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
className={INPUT_CLS}
|
||||
placeholder="80"
|
||||
value={form.dutP2pRuntimeMinutes ?? ''}
|
||||
onChange={(e) => set('dutP2pRuntimeMinutes', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="COE Minutes">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
className={INPUT_CLS}
|
||||
placeholder="115"
|
||||
value={form.dutCoeRuntimeMinutes ?? ''}
|
||||
onChange={(e) => set('dutCoeRuntimeMinutes', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="P3P Minutes">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
className={INPUT_CLS}
|
||||
placeholder="105"
|
||||
value={form.dutP3pRuntimeMinutes ?? ''}
|
||||
onChange={(e) => set('dutP3pRuntimeMinutes', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-gray-300 mb-2">REF</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<Field label="P2P Minutes">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
className={INPUT_CLS}
|
||||
placeholder="80"
|
||||
value={form.refP2pRuntimeMinutes ?? ''}
|
||||
onChange={(e) => set('refP2pRuntimeMinutes', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="COE Minutes">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
className={INPUT_CLS}
|
||||
placeholder="115"
|
||||
value={form.refCoeRuntimeMinutes ?? ''}
|
||||
onChange={(e) => set('refCoeRuntimeMinutes', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="P3P Minutes">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
className={INPUT_CLS}
|
||||
placeholder="105"
|
||||
value={form.refP3pRuntimeMinutes ?? ''}
|
||||
onChange={(e) => set('refP3pRuntimeMinutes', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import TestCard from './TestCard'
|
||||
|
||||
const TEST_CARD_ROW_HEIGHT_PX = 22
|
||||
const TEST_CARD_ROW_GAP_PX = 2
|
||||
|
||||
export default function ShiftSlot({
|
||||
label,
|
||||
tests = [],
|
||||
visible = true,
|
||||
active = false,
|
||||
windowDetails = null,
|
||||
minContentRows = 1,
|
||||
slotHeightPx = null,
|
||||
onWindowSelect,
|
||||
}) {
|
||||
if (!visible) return null
|
||||
|
||||
const clickable = Boolean(windowDetails && onWindowSelect)
|
||||
const targetRows = Math.max(1, minContentRows)
|
||||
const minContentHeightPx = (targetRows * TEST_CARD_ROW_HEIGHT_PX) + ((targetRows - 1) * TEST_CARD_ROW_GAP_PX)
|
||||
const outerMinHeightPx = Number.isFinite(slotHeightPx) && slotHeightPx > 0
|
||||
? slotHeightPx
|
||||
: minContentHeightPx + 10
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -23,26 +32,16 @@ export default function ShiftSlot({
|
||||
onWindowSelect(windowDetails)
|
||||
}
|
||||
} : undefined}
|
||||
style={{ minHeight: `${outerMinHeightPx}px` }}
|
||||
className={`border-t pt-1 pb-1.5 transition-colors ${
|
||||
active ? 'border-blue-500/50 bg-blue-950/20 rounded' : 'border-gray-700/60'
|
||||
} ${clickable ? 'cursor-pointer hover:bg-gray-700/25 focus:outline-none focus:ring-1 focus:ring-cyan-400/70' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 px-1 mb-1">
|
||||
<p className={`text-[10px] font-semibold uppercase tracking-wider ${
|
||||
active ? 'text-blue-400' : 'text-gray-500'
|
||||
}`}>
|
||||
{label}
|
||||
</p>
|
||||
{clickable && (
|
||||
<span className="text-[9px] font-semibold uppercase tracking-[0.2em] text-cyan-400/80">
|
||||
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 px-1 min-h-4">
|
||||
{tests.length === 0 ? (
|
||||
<span className="text-[10px] text-gray-600 italic">—</span>
|
||||
) : (
|
||||
<div
|
||||
className="flex flex-col gap-0.5 px-1 min-h-4"
|
||||
style={{ minHeight: `${minContentHeightPx}px` }}
|
||||
>
|
||||
{tests.length === 0 ? null : (
|
||||
tests.map((test) => <TestCard key={`${test.test_id}-${test.device}`} test={test} />)
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user