frontend changes: change shift order display, added config map
This commit is contained in:
@@ -141,10 +141,16 @@ export default function Calendar({
|
||||
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}
|
||||
@@ -152,6 +158,8 @@ export default function Calendar({
|
||||
isToday={isToday}
|
||||
activeShifts={activeShifts}
|
||||
shifts={shifts}
|
||||
nextDayShift1={nextDayShift1}
|
||||
nextDayShift1Active={nextDayShift1Active}
|
||||
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
|
||||
windowLookup={windowLookup}
|
||||
onWindowSelect={onWindowSelect}
|
||||
|
||||
@@ -7,6 +7,8 @@ export default function DayColumn({
|
||||
isToday,
|
||||
activeShifts = new Set(),
|
||||
shifts = {},
|
||||
nextDayShift1 = [],
|
||||
nextDayShift1Active = false,
|
||||
showShift2 = false,
|
||||
windowLookup = new Map(),
|
||||
onWindowSelect,
|
||||
@@ -39,14 +41,6 @@ export default function DayColumn({
|
||||
|
||||
{/* Shifts */}
|
||||
<div className="flex flex-col flex-1 px-0.5 py-1">
|
||||
<ShiftSlot
|
||||
label="12AM–9AM"
|
||||
tests={shifts.shift1}
|
||||
visible={true}
|
||||
active={activeShifts.has(1)}
|
||||
windowDetails={getWindowForShift(1)}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
<ShiftSlot
|
||||
label="9AM–5PM"
|
||||
tests={shifts.shift2}
|
||||
@@ -63,6 +57,14 @@ export default function DayColumn({
|
||||
windowDetails={getWindowForShift(3)}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
<ShiftSlot
|
||||
label="12AM–9AM (next day)"
|
||||
tests={nextDayShift1}
|
||||
visible={true}
|
||||
active={nextDayShift1Active}
|
||||
windowDetails={null}
|
||||
onWindowSelect={onWindowSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import cgw453Image from '../assets/CGW453.PNG'
|
||||
|
||||
function formatCompletionDate(value) {
|
||||
if (!value) return null
|
||||
|
||||
@@ -28,6 +30,11 @@ export default function RightPanel({
|
||||
|
||||
return (
|
||||
<aside className="flex flex-col gap-4 w-60 shrink-0 pt-8">
|
||||
{/* Device Image */}
|
||||
<div className="flex justify-center mb-2">
|
||||
<img src={cgw453Image} alt="CGW453 Device" className="h-32 object-contain" />
|
||||
</div>
|
||||
|
||||
{/* Estimated Completion */}
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-500 mb-1">
|
||||
@@ -72,35 +79,6 @@ export default function RightPanel({
|
||||
>
|
||||
{loading ? 'Working…' : 'Remake Schedule'}
|
||||
</button>
|
||||
|
||||
{/* Config for tonight */}
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-500 mb-1.5">
|
||||
Config for Today
|
||||
</p>
|
||||
{tonightConfigRows.length === 0 ? (
|
||||
<p className="text-xs text-gray-500">No STA placement mapping available for tonight.</p>
|
||||
) : (
|
||||
<div className="max-h-52 overflow-y-auto rounded border border-gray-700">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-900 sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left text-gray-400 font-semibold px-2 py-1.5 border-b border-gray-700">STA</th>
|
||||
<th className="text-left text-gray-400 font-semibold px-2 py-1.5 border-b border-gray-700">Testpoint</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tonightConfigRows.map((row) => (
|
||||
<tr key={row.sta} className="odd:bg-gray-950 even:bg-gray-900/70">
|
||||
<td className="text-gray-200 font-semibold px-2 py-1.5 border-b border-gray-800">{row.sta}</td>
|
||||
<td className="text-gray-300 px-2 py-1.5 border-b border-gray-800">{row.testPoints.join(', ')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { getDeviceAccentClass } from './TestCard'
|
||||
|
||||
const CONFIG_MAP_IMAGE_MODULES = import.meta.glob('../assets/TC*_map/*.png', {
|
||||
eager: true,
|
||||
import: 'default',
|
||||
})
|
||||
|
||||
const STATUS_STYLES = {
|
||||
pending: 'bg-gray-700 text-gray-200 border-gray-600',
|
||||
@@ -90,6 +96,16 @@ function formatConfigEntries(config) {
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function getConfigMapImages(configKey) {
|
||||
if (!configKey) return []
|
||||
|
||||
const folderToken = `/${configKey.toUpperCase()}_map/`
|
||||
return Object.entries(CONFIG_MAP_IMAGE_MODULES)
|
||||
.filter(([path]) => path.includes(folderToken))
|
||||
.sort(([pathA], [pathB]) => pathA.localeCompare(pathB, undefined, { numeric: true }))
|
||||
.map(([, assetUrl]) => assetUrl)
|
||||
}
|
||||
|
||||
function TestRow({ test }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const statusStyle = STATUS_STYLES[test.status] ?? STATUS_STYLES.pending
|
||||
@@ -103,15 +119,18 @@ function TestRow({ test }) {
|
||||
className="w-full px-4 py-3 text-left hover:bg-gray-800/80 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{test.test_id}</p>
|
||||
<p className="mt-1 text-xs text-gray-400">{test.device ?? '—'} · {formatShiftLabel(test)}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${getDeviceAccentClass(test.device)}`} aria-hidden="true" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{test.test_id}</p>
|
||||
<p className="mt-1 text-xs text-gray-400">{test.device ?? '—'} · {formatShiftLabel(test)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className={`px-2 py-1 rounded-full border text-[11px] font-semibold uppercase tracking-wide ${statusStyle}`}>
|
||||
{test.status ?? 'pending'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">{formatHours(test.estimated_minutes)}</span>
|
||||
<span className="text-xs text-gray-400">{Number.isFinite(test.estimated_minutes) && test.estimated_minutes > 0 ? `${test.estimated_minutes} min` : '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -127,14 +146,6 @@ function TestRow({ test }) {
|
||||
<p className="text-gray-500 uppercase tracking-wide text-[11px] mb-1">Rotation</p>
|
||||
<p>{test.rotation ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 uppercase tracking-wide text-[11px] mb-1">Priority</p>
|
||||
<p>{test.priority ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 uppercase tracking-wide text-[11px] mb-1">Sequence</p>
|
||||
<p>{test.sequence_in_shift ?? '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
@@ -171,7 +182,15 @@ function TestRow({ test }) {
|
||||
}
|
||||
|
||||
export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose }) {
|
||||
const [showConfigMap, setShowConfigMap] = useState(false)
|
||||
const windowTestConfig = useMemo(() => getWindowTestConfig(windowDetails), [windowDetails])
|
||||
const windowConfigMapKey = useMemo(
|
||||
() => (windowTestConfig.length > 0 ? String(windowTestConfig[0]).toUpperCase() : null),
|
||||
[windowTestConfig],
|
||||
)
|
||||
const configMapImages = useMemo(() => getConfigMapImages(windowConfigMapKey), [windowConfigMapKey])
|
||||
const hasConfigMapImages = configMapImages.length > 0
|
||||
const showMapPane = showConfigMap && hasConfigMapImages
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return undefined
|
||||
@@ -201,11 +220,13 @@ export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose
|
||||
/>
|
||||
|
||||
<aside
|
||||
className={`absolute inset-y-0 right-0 w-full max-w-[30rem] border-l border-gray-700 bg-gray-950/96 shadow-2xl transition-transform duration-300 ${
|
||||
className={`absolute inset-y-0 right-0 border-l border-gray-700 bg-gray-950/96 shadow-2xl transition-[width,transform] duration-300 ease-in-out ${
|
||||
showMapPane ? 'w-full' : 'w-[30rem]'
|
||||
} ${
|
||||
isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex h-full min-w-0 flex-col">
|
||||
<div className="flex items-start justify-between gap-4 border-b border-gray-800 px-5 py-5">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-cyan-400">Test Window</p>
|
||||
@@ -226,63 +247,106 @@ export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose
|
||||
</div>
|
||||
|
||||
{windowDetails && (
|
||||
<div className="flex-1 overflow-y-auto px-5 py-5">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-2xl border border-gray-800 bg-gray-900/80 px-4 py-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-gray-500">Available Run Time</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-white">{formatHours(windowDetails.available_runtime_minutes)}</p>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<div className="flex min-h-full min-w-0">
|
||||
<div className={`${showMapPane ? 'w-[30rem] min-w-[30rem] border-r border-gray-800' : 'w-full'} px-5 py-5`}>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-2xl px-4 py-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-gray-500">Available Run Time</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-white">{formatHours(windowDetails.available_runtime_minutes)}</p>
|
||||
</div>
|
||||
<div className="rounded-2xl px-4 py-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-gray-500">Estimated Run Time</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-white">{formatHours(windowDetails.estimated_runtime_minutes)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-gray-800 bg-gray-900/80 px-4 py-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-gray-500">Estimated Run Time</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-white">{formatHours(windowDetails.estimated_runtime_minutes)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 rounded-2xl border border-gray-800 bg-gray-900/80 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-semibold text-white">Config</p>
|
||||
<span className="rounded-full border border-cyan-700/60 bg-cyan-900/30 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-cyan-200">
|
||||
{windowTestConfig.length > 0 ? windowTestConfig.join(', ') : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
{windowDetails.configRows?.length ? (
|
||||
<div className="mt-3 overflow-hidden rounded-xl border border-gray-800">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-950/90">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-semibold text-gray-400 border-b border-gray-800">STA</th>
|
||||
<th className="px-3 py-2 text-left font-semibold text-gray-400 border-b border-gray-800">Testpoint</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{windowDetails.configRows.map((row, index) => (
|
||||
<tr key={`${row.sta}-${index}`} className="odd:bg-gray-950 even:bg-gray-900/60">
|
||||
<td className="px-3 py-2 border-b border-gray-800 font-semibold text-gray-200">{row.sta}</td>
|
||||
<td className="px-3 py-2 border-b border-gray-800 text-gray-300">{row.testPoints.join(', ')}</td>
|
||||
<div className="mt-5 rounded-2xl border border-gray-800 bg-gray-900/80 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-semibold text-white">Config</p>
|
||||
<span className="rounded-full border border-cyan-700/60 bg-cyan-900/30 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-cyan-200">
|
||||
{windowTestConfig.length > 0 ? windowTestConfig.join(', ') : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
{windowDetails.configRows?.length ? (
|
||||
<div className="mt-3 overflow-hidden rounded-xl border border-gray-800">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-950/90">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-semibold text-gray-400 border-b border-gray-800">STA</th>
|
||||
<th className="px-3 py-2 text-left font-semibold text-gray-400 border-b border-gray-800">Testpoint</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 text-xs text-gray-500">No STA placement mapping available for this window.</p>
|
||||
)}
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{windowDetails.configRows.map((row, index) => (
|
||||
<tr key={`${row.sta}-${index}`} className="odd:bg-gray-950 even:bg-gray-900/60">
|
||||
<td className="px-3 py-2 border-b border-gray-800 font-semibold text-gray-200">{row.sta}</td>
|
||||
<td className="px-3 py-2 border-b border-gray-800 text-gray-300">{row.testPoints.join(', ')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 text-xs text-gray-500">No STA placement mapping available for this window.</p>
|
||||
)}
|
||||
|
||||
<div className="mt-5">
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<p className="text-sm font-semibold text-white">Tests Scheduled In This Window</p>
|
||||
<span className="text-xs text-gray-500">{windowDetails.tests?.length ?? 0} tests</span>
|
||||
</div>
|
||||
{windowDetails.tests?.length ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
{windowDetails.tests.map((test) => (
|
||||
<TestRow key={`${test.test_id}-${test.device}-${test.scheduled_date}-${test.shift_index}`} test={test} />
|
||||
))}
|
||||
<div className="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfigMap((value) => !value)}
|
||||
disabled={!hasConfigMapImages}
|
||||
className="inline-flex items-center gap-2 rounded-md border border-gray-700 bg-gray-900 px-3 py-1.5 text-xs font-semibold text-gray-200 hover:bg-gray-800 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<span>{showMapPane ? 'Hide' : 'Show'} Config Map</span>
|
||||
<span className="text-gray-400">{windowConfigMapKey ?? 'N/A'}</span>
|
||||
</button>
|
||||
|
||||
{!hasConfigMapImages && (
|
||||
<p className="mt-2 text-xs text-gray-500">No config map images found for this window config.</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-gray-800 bg-gray-900/50 px-4 py-6 text-sm text-gray-500">
|
||||
No tests are currently scheduled in this window.
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<p className="text-sm font-semibold text-white">Tests Scheduled In This Window</p>
|
||||
<span className="text-xs text-gray-500">{windowDetails.tests?.length ?? 0} tests</span>
|
||||
</div>
|
||||
{windowDetails.tests?.length ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
{windowDetails.tests.map((test) => (
|
||||
<TestRow key={`${test.test_id}-${test.device}-${test.scheduled_date}-${test.shift_index}`} test={test} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-gray-800 bg-gray-900/50 px-4 py-6 text-sm text-gray-500">
|
||||
No tests are currently scheduled in this window.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showMapPane && (
|
||||
<div className="min-w-0 flex-1 px-6 py-5">
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-semibold text-white">Config Map</p>
|
||||
<span className="rounded-full border border-cyan-700/60 bg-cyan-900/30 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-cyan-200">
|
||||
{windowConfigMapKey}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
{configMapImages.map((imageUrl, index) => (
|
||||
<div key={`${windowConfigMapKey}-map-${index + 1}`} className="rounded-xl p-2">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={`${windowConfigMapKey} config map ${index + 1}`}
|
||||
className="w-full max-h-[78vh] rounded-lg object-contain"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user