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:
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user