feat: SAZLT info row, 16/24h shifts, VN/NN font colors

- Added SAZLT info row (between TKB and Metro) with toggle
- Added 16h and 24h shift codes (progressive green shades)
- VN people get red font color on A/B shifts
- NN people get blue font color on A/B shifts
- Changed B (Noční) background to light blue-gray for readability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Docker Config Backup
2026-04-02 10:49:30 +02:00
parent 177975c70c
commit 785966463c
5 changed files with 125 additions and 15 deletions

View File

@@ -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<Set<string>>(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 (
<div className="min-h-screen bg-slate-900">
@@ -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}

View File

@@ -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<number, string>
d8Closures: Map<number, string>
d8Colors: Map<number, string>
sazltClosures: Map<number, string>
sazltColors: Map<number, string>
dayComments: Map<number, string>
cellComments: Map<string, string>
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<HTMLDivElement | null>
showMetro: boolean
showD8: boolean
showSazlt: boolean
hiddenValues: Set<string>
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<number, string>,
colors: Map<number, string>,
defaultBgClass: string,
@@ -698,7 +708,10 @@ export function ScheduleTable(props: ScheduleTableProps) {
</div>
{/* 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',

View File

@@ -25,8 +25,10 @@ interface ToolbarProps {
activeMonth?: number
showMetro: boolean
showD8: boolean
showSazlt: boolean
onToggleMetro: () => void
onToggleD8: () => void
onToggleSazlt: () => void
hiddenValues: Set<string>
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
</button>
<button
onClick={onToggleSazlt}
className={`px-2 py-1 rounded text-[10px] border cursor-pointer transition-colors
${showSazlt
? 'bg-orange-800/60 text-orange-200 border-orange-600'
: 'bg-slate-800 text-slate-500 border-slate-700 opacity-50'
}`}
>
SAZLT
</button>
</div>
<div className="h-5 w-px bg-slate-700" />

View File

@@ -9,8 +9,10 @@ const CELL_STYLES: Record<string, CellStyle> = {
'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<string, CellStyle> {
return { ...CELL_STYLES }
}

View File

@@ -9,6 +9,8 @@ interface ScheduleSnapshot {
metroColors: Map<number, string>
d8Closures: Map<number, string>
d8Colors: Map<number, string>
sazltClosures: Map<number, string>
sazltColors: Map<number, string>
dayComments: Map<number, string>
cellComments: Map<string, string>
}
@@ -77,6 +79,24 @@ export function useScheduleState(
return m
})
const [sazltClosures, setSazltClosuresState] = useState<Map<number, string>>(() => {
const m = new Map<number, string>()
if (initialInfoRows?.sazlt) {
for (const entry of initialInfoRows.sazlt) m.set(entry.dayIdx, entry.text)
}
return m
})
const [sazltColors, setSazltColorsState] = useState<Map<number, string>>(() => {
const m = new Map<number, string>()
if (initialInfoRows?.sazlt) {
for (const entry of initialInfoRows.sazlt) {
if (entry.color) m.set(entry.dayIdx, entry.color)
}
}
return m
})
const [dayComments, setDayCommentsState] = useState<Map<number, string>>(() => {
const m = new Map<number, string>()
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,