2026-06-17 16:02:04 -04:00
|
|
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
|
|
|
import Header from './components/Header'
|
|
|
|
|
import FailedBanner from './components/FailedBanner'
|
|
|
|
|
import Calendar from './components/Calendar'
|
|
|
|
|
import RightPanel from './components/RightPanel'
|
|
|
|
|
import SettingsModal from './components/SettingsModal'
|
|
|
|
|
import { api, groupScheduleItems } from './api'
|
2026-06-16 15:07:59 -04:00
|
|
|
|
2026-06-17 16:02:04 -04:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Helpers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toKey(d) { return d.toISOString().slice(0, 10) }
|
|
|
|
|
|
|
|
|
|
function addDays(date, n) {
|
|
|
|
|
const d = new Date(date)
|
|
|
|
|
d.setDate(d.getDate() + n)
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getTonightWindowKeys() {
|
|
|
|
|
const now = new Date()
|
|
|
|
|
const hour = now.getHours()
|
|
|
|
|
const today = new Date(now)
|
|
|
|
|
today.setHours(0, 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
if (hour < 1) {
|
|
|
|
|
return {
|
|
|
|
|
shift3Date: toKey(addDays(today, -1)),
|
|
|
|
|
shift1Date: toKey(today),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
shift3Date: toKey(today),
|
|
|
|
|
shift1Date: toKey(addDays(today, 1)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toTonightConfigRows(scheduleData) {
|
|
|
|
|
const { shift3Date, shift1Date } = getTonightWindowKeys()
|
|
|
|
|
const tonightTests = [
|
|
|
|
|
...((scheduleData[shift3Date]?.shift3) ?? []),
|
|
|
|
|
...((scheduleData[shift1Date]?.shift1) ?? []),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const bySta = new Map()
|
|
|
|
|
|
|
|
|
|
for (const test of tonightTests) {
|
|
|
|
|
const config = test.config ?? {}
|
|
|
|
|
for (const entry of Object.values(config)) {
|
|
|
|
|
const staRaw = entry?.sta
|
|
|
|
|
const testPoint = entry?.test_point
|
|
|
|
|
if (!staRaw || !testPoint) continue
|
|
|
|
|
|
|
|
|
|
for (const staPart of String(staRaw).split(',')) {
|
|
|
|
|
const sta = staPart.trim().toUpperCase()
|
|
|
|
|
if (!sta) continue
|
|
|
|
|
|
|
|
|
|
if (!bySta.has(sta)) {
|
|
|
|
|
bySta.set(sta, { sta, points: new Set() })
|
|
|
|
|
}
|
|
|
|
|
bySta.get(sta).points.add(testPoint)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(bySta.values())
|
|
|
|
|
.map((row) => ({
|
|
|
|
|
sta: row.sta,
|
|
|
|
|
testPoints: Array.from(row.points).sort(),
|
|
|
|
|
}))
|
|
|
|
|
.sort((a, b) => a.sta.localeCompare(b.sta))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_SETTINGS = {
|
|
|
|
|
p2pCoeCsvPath: '',
|
|
|
|
|
p3pCsvPath: '',
|
|
|
|
|
refResultDir: '',
|
|
|
|
|
dutResultDir: '',
|
|
|
|
|
testExclusion: '',
|
|
|
|
|
holidays: '',
|
|
|
|
|
startDateOverride: '',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export default function App() {
|
|
|
|
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
|
|
|
const [settings, setSettings] = useState(DEFAULT_SETTINGS)
|
|
|
|
|
const [daytimeEnabled, setDaytimeEnabled] = useState(false)
|
|
|
|
|
const [topPriority, setTopPriority] = useState('')
|
|
|
|
|
const [lowestPriority, setLowestPriority] = useState('')
|
|
|
|
|
const [failedTests, setFailedTests] = useState([])
|
|
|
|
|
const [scheduleData, setScheduleData] = useState({})
|
|
|
|
|
const [completionDate, setCompletionDate] = useState(null)
|
|
|
|
|
const [weekStart, setWeekStart] = useState(() => getMondayOfWeek(new Date()))
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
const [error, setError] = useState(null)
|
|
|
|
|
const tonightConfigRows = useMemo(() => toTonightConfigRows(scheduleData), [scheduleData])
|
|
|
|
|
|
|
|
|
|
// Fetch schedule for the given weekStart (Monday)
|
|
|
|
|
const fetchSchedule = useCallback(async (start) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = await api.getScheduleWeek(toKey(start))
|
|
|
|
|
setScheduleData(groupScheduleItems(data.items))
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to fetch schedule:', e)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// On mount: load settings + holidays + schedule for today's week
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
async function init() {
|
|
|
|
|
try {
|
|
|
|
|
const [saved, holidayData] = await Promise.all([
|
|
|
|
|
api.getSettings(),
|
|
|
|
|
api.getHolidays(),
|
|
|
|
|
])
|
|
|
|
|
setSettings(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
...saved,
|
|
|
|
|
holidays: (holidayData.dates ?? []).join(', '),
|
|
|
|
|
}))
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Backend not reachable on load:', e.message)
|
|
|
|
|
}
|
|
|
|
|
await fetchSchedule(getMondayOfWeek(new Date()))
|
|
|
|
|
}
|
|
|
|
|
init()
|
|
|
|
|
}, [fetchSchedule])
|
|
|
|
|
|
|
|
|
|
// Refetch whenever the displayed week changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchSchedule(weekStart)
|
|
|
|
|
}, [weekStart, fetchSchedule])
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
async function handleSaveSettings(newSettings) {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
await api.saveSettings(newSettings)
|
|
|
|
|
|
|
|
|
|
const holidayDates = (newSettings.holidays ?? '')
|
|
|
|
|
.split(',').map(s => s.trim()).filter(Boolean)
|
|
|
|
|
await api.saveHolidays(holidayDates)
|
|
|
|
|
|
|
|
|
|
if (newSettings.p2pCoeCsvPath?.trim()) {
|
|
|
|
|
await api.loadCsv(newSettings.p2pCoeCsvPath.trim())
|
|
|
|
|
}
|
|
|
|
|
if (newSettings.p3pCsvPath?.trim()) {
|
|
|
|
|
await api.loadCsv(newSettings.p3pCsvPath.trim())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSettings(newSettings)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setError(e.message)
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleRemakeSchedule() {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const result = await api.compileSchedule({
|
|
|
|
|
start_date: settings.startDateOverride?.trim() || null,
|
|
|
|
|
daytime_testing_today: daytimeEnabled,
|
|
|
|
|
top_priority_tests: topPriority.split(',').map(s => s.trim()).filter(Boolean),
|
|
|
|
|
lowest_priority_tests: lowestPriority.split(',').map(s => s.trim()).filter(Boolean),
|
|
|
|
|
rule: settings.testExclusion ?? '',
|
|
|
|
|
})
|
|
|
|
|
setCompletionDate(result.completion_date ?? null)
|
|
|
|
|
await fetchSchedule(weekStart)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setError(e.message)
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleRerunDecision(rerunDuringDay) {
|
|
|
|
|
// TODO: POST /api/failed-tests/rerun-decision once backend endpoint exists
|
|
|
|
|
console.log('Rerun during day:', rerunDuringDay)
|
|
|
|
|
setFailedTests([])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
2026-06-16 15:07:59 -04:00
|
|
|
|
|
|
|
|
return (
|
2026-06-17 16:02:04 -04:00
|
|
|
<div className="flex flex-col min-h-screen bg-gray-950 text-gray-100">
|
|
|
|
|
<Header onOpenSettings={() => setSettingsOpen(true)} />
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="px-6 py-2 bg-orange-900/60 border-b border-orange-700 text-orange-200 text-sm flex items-center justify-between">
|
|
|
|
|
<span>{error}</span>
|
|
|
|
|
<button className="ml-3 text-orange-400 hover:text-white" onClick={() => setError(null)}>✕</button>
|
2026-06-16 15:07:59 -04:00
|
|
|
</div>
|
2026-06-17 16:02:04 -04:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<FailedBanner
|
|
|
|
|
failedTests={failedTests}
|
|
|
|
|
estimatedMinutes={0}
|
|
|
|
|
onDecision={handleRerunDecision}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<main className="flex flex-1 gap-4 p-4 overflow-hidden">
|
|
|
|
|
<div className="flex-1 min-w-0 flex flex-col">
|
|
|
|
|
<Calendar
|
|
|
|
|
scheduleData={scheduleData}
|
|
|
|
|
daytimeDateKey={daytimeEnabled ? toKey(new Date()) : null}
|
|
|
|
|
weekStart={weekStart}
|
|
|
|
|
onWeekChange={setWeekStart}
|
|
|
|
|
/>
|
2026-06-16 15:07:59 -04:00
|
|
|
</div>
|
|
|
|
|
|
2026-06-17 16:02:04 -04:00
|
|
|
<RightPanel
|
|
|
|
|
completionDate={completionDate}
|
|
|
|
|
daytimeEnabled={daytimeEnabled}
|
|
|
|
|
onDaytimeEnabledChange={setDaytimeEnabled}
|
|
|
|
|
topPriority={topPriority}
|
|
|
|
|
onTopPriorityChange={setTopPriority}
|
|
|
|
|
lowestPriority={lowestPriority}
|
|
|
|
|
onLowestPriorityChange={setLowestPriority}
|
|
|
|
|
onRemakeSchedule={handleRemakeSchedule}
|
|
|
|
|
loading={loading}
|
|
|
|
|
tonightConfigRows={tonightConfigRows}
|
|
|
|
|
/>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<SettingsModal
|
|
|
|
|
isOpen={settingsOpen}
|
|
|
|
|
onClose={() => setSettingsOpen(false)}
|
|
|
|
|
settings={settings}
|
|
|
|
|
onSave={handleSaveSettings}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-06-16 15:07:59 -04:00
|
|
|
)
|
|
|
|
|
}
|