diff --git a/web/src/App.tsx b/web/src/App.tsx index 933df49..44ff6ab 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -231,10 +231,11 @@ interface ScheduleAppProps { function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileNameChange, onClearCompare }: ScheduleAppProps) { const { people, tunnelClosures, tunnelColors, - metroClosures, metroColors, d8Closures, d8Colors, + metroClosures, metroColors, d8Closures, d8Colors, sazltClosures, sazltColors, dayComments, cellComments, setCell, setCellColor, moveCell, setTunnelClosure, setTunnelClosureColor, setMetroClosure, setMetroClosureColor, setD8Closure, setD8ClosureColor, + setSazltClosure, setSazltClosureColor, undo, canUndo, addDayComment, removeDayComment, addCellComment, removeCellComment, @@ -407,6 +408,7 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName const [showProposals, setShowProposals] = useState(false) const [showMetro, setShowMetro] = useState(true) const [showD8, setShowD8] = useState(true) + const [showSazlt, setShowSazlt] = useState(true) const [hiddenValues, setHiddenValues] = useState>(new Set()) const handleExportPdf = useCallback(async (month: number) => { @@ -428,17 +430,20 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName const contextInfoRowClosure = contextMenu ? contextInfoRowId === 'metro' ? metroClosures.get(contextMenu.dayIdx) : contextInfoRowId === 'd8' ? d8Closures.get(contextMenu.dayIdx) + : contextInfoRowId === 'sazlt' ? sazltClosures.get(contextMenu.dayIdx) : undefined : undefined const contextInfoRowColor = contextMenu ? contextInfoRowId === 'metro' ? metroColors.get(contextMenu.dayIdx) : contextInfoRowId === 'd8' ? d8Colors.get(contextMenu.dayIdx) + : contextInfoRowId === 'sazlt' ? sazltColors.get(contextMenu.dayIdx) : undefined : undefined const handleInfoRowSetColor = useCallback((dayIdx: number, color: string | null) => { if (contextInfoRowId === 'metro') setMetroClosureColor(dayIdx, color) else if (contextInfoRowId === 'd8') setD8ClosureColor(dayIdx, color) - }, [contextInfoRowId, setMetroClosureColor, setD8ClosureColor]) + else if (contextInfoRowId === 'sazlt') setSazltClosureColor(dayIdx, color) + }, [contextInfoRowId, setMetroClosureColor, setD8ClosureColor, setSazltClosureColor]) return (
@@ -477,6 +482,8 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName showD8={showD8} onToggleMetro={() => setShowMetro(v => !v)} onToggleD8={() => setShowD8(v => !v)} + showSazlt={showSazlt} + onToggleSazlt={() => setShowSazlt(v => !v)} hiddenValues={hiddenValues} onToggleValue={(code: string) => setHiddenValues(prev => { const next = new Set(prev) @@ -497,6 +504,8 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName metroColors={metroColors} d8Closures={d8Closures} d8Colors={d8Colors} + sazltClosures={sazltClosures} + sazltColors={sazltColors} dayComments={dayComments} cellComments={cellComments} dragState={dragState} @@ -505,8 +514,10 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName onSetTunnelClosure={setTunnelClosure} onSetMetroClosure={setMetroClosure} onSetD8Closure={setD8Closure} + onSetSazltClosure={setSazltClosure} showMetro={showMetro} showD8={showD8} + showSazlt={showSazlt} hiddenValues={hiddenValues} scrollRef={scrollRef} onContextMenu={handleContextMenu} diff --git a/web/src/ScheduleTable.tsx b/web/src/ScheduleTable.tsx index edc6264..5a9372b 100644 --- a/web/src/ScheduleTable.tsx +++ b/web/src/ScheduleTable.tsx @@ -1,6 +1,6 @@ import { useMemo, useState, useCallback, useRef, memo, useEffect } from 'react' import type { DayInfo, Person, DragState, ScheduleData } from './types' -import { getCellStyle, getContrastColor } from './cellColors' +import { getCellStyle, getCellStyleForPerson, getContrastColor } from './cellColors' import { getHolidayMap } from './holidays' const CELL_W = 32 @@ -27,6 +27,8 @@ interface ScheduleTableProps { metroColors: Map d8Closures: Map d8Colors: Map + sazltClosures: Map + sazltColors: Map dayComments: Map cellComments: Map dragState: DragState | null @@ -35,9 +37,11 @@ interface ScheduleTableProps { onSetTunnelClosure: (dayIdx: number, text: string | null) => void onSetMetroClosure: (dayIdx: number, text: string | null) => void onSetD8Closure: (dayIdx: number, text: string | null) => void + onSetSazltClosure: (dayIdx: number, text: string | null) => void scrollRef: React.RefObject showMetro: boolean showD8: boolean + showSazlt: boolean hiddenValues: Set onContextMenu: (dayIdx: number, personId: string | null, x: number, y: number, selectedCells: SelectedCell[]) => void onTunnelContextMenu: (dayIdx: number, x: number, y: number) => void @@ -46,7 +50,7 @@ interface ScheduleTableProps { } interface EditingCell { - personId: string | '__tunnel__' | '__metro__' | '__d8__' + personId: string | '__tunnel__' | '__metro__' | '__d8__' | '__sazlt__' dayIdx: number value: string } @@ -101,7 +105,7 @@ const PersonRow = memo(function PersonRow({ const value = cellData?.v ?? '' const isColorOnly = !value && !!cellData?.color const isValueHidden = !!(value && hiddenValues.has(value)) || (isColorOnly && hiddenValues.has('__color_only__')) - const style = getCellStyle(value || undefined) + const style = getCellStyleForPerson(value || undefined, person.note) const isHoliday = holidays.has(`${d.year}-${d.month}-${d.day}`) const isWeekend = d.weekend || isHoliday const commentKey = `${person.id}-${d.idx}` @@ -232,11 +236,11 @@ const PersonRow = memo(function PersonRow({ export function ScheduleTable(props: ScheduleTableProps) { const { dayIndex, people, tunnelClosures, tunnelColors, - metroClosures, metroColors, d8Closures, d8Colors, + metroClosures, metroColors, d8Closures, d8Colors, sazltClosures, sazltColors, dayComments, cellComments, dragState, onCellPointerDown, onSetCell, onSetTunnelClosure, - onSetMetroClosure, onSetD8Closure, - showMetro, showD8, hiddenValues, + onSetMetroClosure, onSetD8Closure, onSetSazltClosure, + showMetro, showD8, showSazlt, hiddenValues, scrollRef, onContextMenu, onTunnelContextMenu, onInfoRowContextMenu, compareData, } = props @@ -324,6 +328,10 @@ export function ScheduleTable(props: ScheduleTableProps) { setEditingCell({ personId: '__d8__', dayIdx, value: currentValue }) }, []) + const onStartEditSazlt = useCallback((dayIdx: number, currentValue: string) => { + setEditingCell({ personId: '__sazlt__', dayIdx, value: currentValue }) + }, []) + const onEditChange = useCallback((value: string) => { setEditingCell(prev => prev ? { ...prev, value } : null) }, []) @@ -337,11 +345,13 @@ export function ScheduleTable(props: ScheduleTableProps) { onSetMetroClosure(editingCell.dayIdx, trimmed || null) } else if (editingCell.personId === '__d8__') { onSetD8Closure(editingCell.dayIdx, trimmed || null) + } else if (editingCell.personId === '__sazlt__') { + onSetSazltClosure(editingCell.dayIdx, trimmed || null) } else { onSetCell(editingCell.personId, editingCell.dayIdx, trimmed || null) } setEditingCell(null) - }, [editingCell, onSetCell, onSetTunnelClosure, onSetMetroClosure, onSetD8Closure]) + }, [editingCell, onSetCell, onSetTunnelClosure, onSetMetroClosure, onSetD8Closure, onSetSazltClosure]) const onEditCancel = useCallback(() => { setEditingCell(null) @@ -601,7 +611,7 @@ export function ScheduleTable(props: ScheduleTableProps) { // Helper: render info row cells (right column) const renderInfoRowCells = ( - rowId: '__tunnel__' | '__metro__' | '__d8__', + rowId: '__tunnel__' | '__metro__' | '__d8__' | '__sazlt__', closures: Map, colors: Map, defaultBgClass: string, @@ -698,7 +708,10 @@ export function ScheduleTable(props: ScheduleTableProps) {
{/* TKB info row label */} - {renderInfoRowLabel('TKB', 'border-slate-600')} + {renderInfoRowLabel('TKB', 'border-slate-700')} + + {/* SAZLT info row label */} + {showSazlt && renderInfoRowLabel('SAZLT', 'border-slate-700')} {/* Metro info row label */} {showMetro && renderInfoRowLabel('Metro', 'border-slate-700')} @@ -811,9 +824,14 @@ export function ScheduleTable(props: ScheduleTableProps) { {/* TKB info row */} {renderInfoRowCells('__tunnel__', tunnelClosures, tunnelColors, - 'bg-orange-700/40 text-orange-200', 'border-slate-600', 'border-orange-400', 'TKB', + 'bg-orange-700/40 text-orange-200', 'border-slate-700', 'border-orange-400', 'TKB', onTunnelContextMenu, onStartEditTunnel)} + {/* SAZLT info row */} + {showSazlt && renderInfoRowCells('__sazlt__', sazltClosures, sazltColors, + 'bg-amber-700/40 text-amber-200', 'border-slate-700', 'border-amber-400', 'SAZLT', + (dayIdx, x, y) => onInfoRowContextMenu(dayIdx, 'sazlt', x, y), onStartEditSazlt)} + {/* Metro info row */} {showMetro && renderInfoRowCells('__metro__', metroClosures, metroColors, 'bg-blue-700/40 text-blue-200', 'border-slate-700', 'border-blue-400', 'Metro', diff --git a/web/src/Toolbar.tsx b/web/src/Toolbar.tsx index d0cdcdd..d4e8b20 100644 --- a/web/src/Toolbar.tsx +++ b/web/src/Toolbar.tsx @@ -25,8 +25,10 @@ interface ToolbarProps { activeMonth?: number showMetro: boolean showD8: boolean + showSazlt: boolean onToggleMetro: () => void onToggleD8: () => void + onToggleSazlt: () => void hiddenValues: Set onToggleValue: (code: string) => void diffFileName?: string | null @@ -45,8 +47,10 @@ export function Toolbar({ activeMonth, showMetro, showD8, + showSazlt, onToggleMetro, onToggleD8, + onToggleSazlt, hiddenValues, onToggleValue, diffFileName, @@ -177,6 +181,16 @@ export function Toolbar({ > D8 +
diff --git a/web/src/cellColors.ts b/web/src/cellColors.ts index 677830e..f81b740 100644 --- a/web/src/cellColors.ts +++ b/web/src/cellColors.ts @@ -9,8 +9,10 @@ const CELL_STYLES: Record = { '6': { bg: '#E8F5E9', text: '#333', label: '6h směna' }, '8': { bg: '#CCFFCC', text: '#333', label: '8h směna' }, '12': { bg: '#A3D977', text: '#333', label: '12h směna' }, + '16': { bg: '#66BB6A', text: '#fff', label: '16h směna' }, + '24': { bg: '#2E7D32', text: '#fff', label: '24h směna' }, 'A': { bg: '#FFE082', text: '#333', label: 'Ranní (denní směna)' }, - 'B': { bg: '#5C6BC0', text: '#fff', label: 'Noční směna' }, + 'B': { bg: '#B0BEC5', text: '#333', label: 'Noční směna' }, 'D': { bg: '#FFFF00', text: '#333', label: 'Dovolená' }, 'D/2': { bg: '#FFF9C4', text: '#333', label: 'Půl den dovolená' }, 'N': { bg: '#FF4444', text: '#fff', label: 'Nemocenská' }, @@ -25,6 +27,19 @@ export function getCellStyle(value: string | undefined): CellStyle | null { return CELL_STYLES[v] ?? null } +// Person-aware style: VN/NN people get distinct font colors for A/B shifts +export function getCellStyleForPerson(value: string | undefined, personNote?: string): CellStyle | null { + if (!value) return null + const v = value.trim() + const base = CELL_STYLES[v] + if (!base) return null + if (v === 'A' || v === 'B') { + if (personNote === 'VN') return { ...base, text: '#D32F2F' } // red font + if (personNote === 'NN') return { ...base, text: '#1565C0' } // blue font + } + return base +} + export function getAllCellStyles(): Record { return { ...CELL_STYLES } } diff --git a/web/src/useScheduleState.ts b/web/src/useScheduleState.ts index ef7e006..a31276d 100644 --- a/web/src/useScheduleState.ts +++ b/web/src/useScheduleState.ts @@ -9,6 +9,8 @@ interface ScheduleSnapshot { metroColors: Map d8Closures: Map d8Colors: Map + sazltClosures: Map + sazltColors: Map dayComments: Map cellComments: Map } @@ -77,6 +79,24 @@ export function useScheduleState( return m }) + const [sazltClosures, setSazltClosuresState] = useState>(() => { + const m = new Map() + if (initialInfoRows?.sazlt) { + for (const entry of initialInfoRows.sazlt) m.set(entry.dayIdx, entry.text) + } + return m + }) + + const [sazltColors, setSazltColorsState] = useState>(() => { + const m = new Map() + if (initialInfoRows?.sazlt) { + for (const entry of initialInfoRows.sazlt) { + if (entry.color) m.set(entry.dayIdx, entry.color) + } + } + return m + }) + const [dayComments, setDayCommentsState] = useState>(() => { const m = new Map() if (initialDayComments) { @@ -104,11 +124,13 @@ export function useScheduleState( metroColors: new Map(metroColors), d8Closures: new Map(d8Closures), d8Colors: new Map(d8Colors), + sazltClosures: new Map(sazltClosures), + sazltColors: new Map(sazltColors), dayComments: new Map(dayComments), cellComments: new Map(cellComments), }) if (historyRef.current.length > 50) historyRef.current.shift() - }, [people, tunnelClosures, tunnelColors, metroClosures, metroColors, d8Closures, d8Colors, dayComments, cellComments]) + }, [people, tunnelClosures, tunnelColors, metroClosures, metroColors, d8Closures, d8Colors, sazltClosures, sazltColors, dayComments, cellComments]) const setCell = useCallback((personId: string, dayIdx: number, value: string | null) => { pushHistory() @@ -200,6 +222,26 @@ export function useScheduleState( }) }, [pushHistory]) + const setSazltClosure = useCallback((dayIdx: number, text: string | null) => { + pushHistory() + setSazltClosuresState(prev => { + const next = new Map(prev) + if (text) next.set(dayIdx, text) + else next.delete(dayIdx) + return next + }) + }, [pushHistory]) + + const setSazltClosureColor = useCallback((dayIdx: number, color: string | null) => { + pushHistory() + setSazltColorsState(prev => { + const next = new Map(prev) + if (color) next.set(dayIdx, color) + else next.delete(dayIdx) + return next + }) + }, [pushHistory]) + const moveCell = useCallback((personId: string, fromIdx: number, toIdx: number) => { if (fromIdx === toIdx) return pushHistory() @@ -263,6 +305,8 @@ export function useScheduleState( setMetroColorsState(snapshot.metroColors) setD8ClosuresState(snapshot.d8Closures) setD8ColorsState(snapshot.d8Colors) + setSazltClosuresState(snapshot.sazltClosures) + setSazltColorsState(snapshot.sazltColors) setDayCommentsState(snapshot.dayComments) setCellCommentsState(snapshot.cellComments) }, []) @@ -285,6 +329,10 @@ export function useScheduleState( dayIdx, text, ...(d8Colors.get(dayIdx) ? { color: d8Colors.get(dayIdx) } : {}), })), + sazlt: Array.from(sazltClosures.entries()).map(([dayIdx, text]) => ({ + dayIdx, text, + ...(sazltColors.get(dayIdx) ? { color: sazltColors.get(dayIdx) } : {}), + })), }, dayComments: Array.from(dayComments.entries()).map(([dayIdx, text]) => ({ dayIdx, text })), cellComments: Array.from(cellComments.entries()).map(([key, text]) => { @@ -293,7 +341,7 @@ export function useScheduleState( const dayIdx = parseInt(key.substring(dashIdx + 1)) return { personId, dayIdx, text } }), - }), [dayIndex, people, tunnelClosures, tunnelColors, metroClosures, metroColors, d8Closures, d8Colors, dayComments, cellComments]) + }), [dayIndex, people, tunnelClosures, tunnelColors, metroClosures, metroColors, d8Closures, d8Colors, sazltClosures, sazltColors, dayComments, cellComments]) return { people, @@ -303,6 +351,8 @@ export function useScheduleState( metroColors, d8Closures, d8Colors, + sazltClosures, + sazltColors, dayComments, cellComments, setCell, @@ -314,6 +364,8 @@ export function useScheduleState( setMetroClosureColor, setD8Closure, setD8ClosureColor, + setSazltClosure, + setSazltClosureColor, addDayComment, removeDayComment, addCellComment,