milestone: MVP — stable layout, all core features working
Reverted sticky header attempt back to clean two-column layout. Server now sends no-cache headers for all static files. Working features: - People grid (18 TKB + 5 IT) with year-long calendar - Shift codes (4/6/8/12/A/B/D/N/U/O) with auto-colored backgrounds - Drag-and-drop cells (including color-only cells) - Multi-cell selection (click/ctrl/shift/drag) with bulk operations - Right-click context menu: set value, color palette, comments - Info rows: TKB, Metro, D8 (toggleable) - Czech holidays highlighted with names - Cell value filter toggles in legend - PDF export (2-page A4 landscape, Czech font support) - Improvement proposals system - Auto-contrast text color on manual backgrounds - Opens at current month on load TODO: sticky header (needs different approach) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -374,11 +374,9 @@ app.get('/api/export-excel', (_req, res) => {
|
|||||||
|
|
||||||
// Serve static files from dist/
|
// Serve static files from dist/
|
||||||
app.use(express.static(join(__dirname, 'dist'), {
|
app.use(express.static(join(__dirname, 'dist'), {
|
||||||
setHeaders: (res, path) => {
|
setHeaders: (res) => {
|
||||||
if (path.endsWith('.html')) {
|
|
||||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// SPA fallback
|
// SPA fallback
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ const PersonRow = memo(function PersonRow({
|
|||||||
onCellClick,
|
onCellClick,
|
||||||
onCellDragSelectStart,
|
onCellDragSelectStart,
|
||||||
onCellDragSelectMove,
|
onCellDragSelectMove,
|
||||||
nameColW,
|
|
||||||
}: {
|
}: {
|
||||||
person: Person
|
person: Person
|
||||||
dayIndex: DayInfo[]
|
dayIndex: DayInfo[]
|
||||||
@@ -92,21 +91,11 @@ const PersonRow = memo(function PersonRow({
|
|||||||
onCellClick: (personId: string, dayIdx: number, ctrlKey: boolean, shiftKey: boolean) => void
|
onCellClick: (personId: string, dayIdx: number, ctrlKey: boolean, shiftKey: boolean) => void
|
||||||
onCellDragSelectStart: (personId: string, dayIdx: number) => void
|
onCellDragSelectStart: (personId: string, dayIdx: number) => void
|
||||||
onCellDragSelectMove: (personId: string, dayIdx: number) => void
|
onCellDragSelectMove: (personId: string, dayIdx: number) => void
|
||||||
nameColW: number
|
|
||||||
}) {
|
}) {
|
||||||
const isDragging = dragState?.personId === person.id
|
const isDragging = dragState?.personId === person.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex border-b border-slate-700" style={{ height: CELL_H }}>
|
<div className="flex border-b border-slate-700/50" style={{ height: CELL_H }}>
|
||||||
<div
|
|
||||||
className="sticky left-0 z-20 bg-slate-800 border-r border-slate-600 border-b border-b-slate-600 flex items-center px-3 flex-shrink-0"
|
|
||||||
style={{ width: nameColW, minWidth: nameColW }}
|
|
||||||
>
|
|
||||||
<span className="text-xs text-slate-200 whitespace-nowrap truncate">
|
|
||||||
{person.name}
|
|
||||||
{person.note && <span className="text-slate-500 ml-1">({person.note})</span>}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{dayIndex.map((d) => {
|
{dayIndex.map((d) => {
|
||||||
const cellData = person.data[String(d.idx)]
|
const cellData = person.data[String(d.idx)]
|
||||||
const value = cellData?.v ?? ''
|
const value = cellData?.v ?? ''
|
||||||
@@ -126,7 +115,7 @@ const PersonRow = memo(function PersonRow({
|
|||||||
|
|
||||||
const bgStyle: React.CSSProperties = {}
|
const bgStyle: React.CSSProperties = {}
|
||||||
let className = `flex items-center justify-center text-sm font-mono font-bold relative
|
let className = `flex items-center justify-center text-sm font-mono font-bold relative
|
||||||
border-r border-slate-700
|
border-r border-slate-700/20
|
||||||
${isMonthStart ? 'border-l-2 border-l-slate-500' : ''}`
|
${isMonthStart ? 'border-l-2 border-l-slate-500' : ''}`
|
||||||
|
|
||||||
if (isDragOriginal) {
|
if (isDragOriginal) {
|
||||||
@@ -517,7 +506,7 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pos = idxToPos.get(dragState.previewIdx) ?? 0
|
const pos = idxToPos.get(dragState.previewIdx) ?? 0
|
||||||
const left = 200 + pos * CELL_W // 200 = nameColW
|
const left = pos * CELL_W
|
||||||
|
|
||||||
const style = getCellStyle(dragState.value)
|
const style = getCellStyle(dragState.value)
|
||||||
|
|
||||||
@@ -603,25 +592,26 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
const totalGridW = dayIndex.length * CELL_W
|
const totalGridW = dayIndex.length * CELL_W
|
||||||
const nameColW = 200
|
const nameColW = 200
|
||||||
|
|
||||||
// Helper: render an info row (TKB/Metro/D8 closures)
|
// Helper: render info row label (left column)
|
||||||
const renderInfoRow = (
|
const renderInfoRowLabel = (label: string, borderClass: string) => (
|
||||||
|
<div className={`bg-slate-800 border-r border-slate-600 border-b ${borderClass} flex items-center px-3`} style={{ height: 28 }}>
|
||||||
|
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">{label}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper: render info row cells (right column)
|
||||||
|
const renderInfoRowCells = (
|
||||||
rowId: '__tunnel__' | '__metro__' | '__d8__',
|
rowId: '__tunnel__' | '__metro__' | '__d8__',
|
||||||
label: string,
|
|
||||||
closures: Map<number, string>,
|
closures: Map<number, string>,
|
||||||
colors: Map<number, string>,
|
colors: Map<number, string>,
|
||||||
defaultBgClass: string,
|
defaultBgClass: string,
|
||||||
borderClass: string,
|
borderClass: string,
|
||||||
editBorderColor: string,
|
editBorderColor: string,
|
||||||
|
label: string,
|
||||||
contextHandler: (dayIdx: number, x: number, y: number) => void,
|
contextHandler: (dayIdx: number, x: number, y: number) => void,
|
||||||
onStartEditFn: (dayIdx: number, currentValue: string) => void,
|
onStartEditFn: (dayIdx: number, currentValue: string) => void,
|
||||||
) => (
|
) => (
|
||||||
<div className={`flex border-b ${borderClass}`} style={{ height: 28 }}>
|
<div className={`flex border-b ${borderClass}`} style={{ height: 28 }}>
|
||||||
<div
|
|
||||||
className={`sticky left-0 z-20 bg-slate-800 border-r border-slate-600 border-b ${borderClass} flex items-center px-3 flex-shrink-0`}
|
|
||||||
style={{ width: nameColW, minWidth: nameColW }}
|
|
||||||
>
|
|
||||||
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">{label}</span>
|
|
||||||
</div>
|
|
||||||
{dayIndex.map((d) => {
|
{dayIndex.map((d) => {
|
||||||
const closureVal = closures.get(d.idx) ?? ''
|
const closureVal = closures.get(d.idx) ?? ''
|
||||||
const closureColor = colors.get(d.idx)
|
const closureColor = colors.get(d.idx)
|
||||||
@@ -638,8 +628,8 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
<div
|
<div
|
||||||
key={d.idx}
|
key={d.idx}
|
||||||
className={`flex items-center justify-center text-xs font-mono font-bold relative
|
className={`flex items-center justify-center text-xs font-mono font-bold relative
|
||||||
border-r border-slate-700
|
border-r border-slate-700/20
|
||||||
${!closureColor && closureVal ? `${defaultBgClass}` : !closureColor ? (isOff ? 'bg-slate-700' : 'bg-slate-800') : ''}
|
${!closureColor && closureVal ? `${defaultBgClass}` : !closureColor ? (isOff ? 'bg-slate-700/30' : 'bg-slate-800/30') : ''}
|
||||||
${d.day === 1 ? 'border-l-2 border-l-slate-500' : ''}
|
${d.day === 1 ? 'border-l-2 border-l-slate-500' : ''}
|
||||||
cursor-pointer hover:brightness-125
|
cursor-pointer hover:brightness-125
|
||||||
`}
|
`}
|
||||||
@@ -675,21 +665,80 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
// Helper: render section header (left column)
|
||||||
<div ref={scrollRef} className="rounded-lg border border-slate-700 bg-slate-800/50 overflow-auto" style={{ maxHeight: 'calc(100vh - 160px)' }}>
|
const renderSectionHeaderLabel = (label: string, bgClass: string, textClass: string) => (
|
||||||
<div style={{ width: nameColW + totalGridW, position: 'relative' }}>
|
<div className={`${bgClass} border-r border-slate-600 flex items-center px-3`} style={{ height: CELL_H }}>
|
||||||
|
<span className={`text-xs font-bold ${textClass} uppercase tracking-wider`}>{label}</span>
|
||||||
{/* === STICKY HEADER ROWS === */}
|
|
||||||
<div className="sticky top-0 z-30 bg-slate-800">
|
|
||||||
|
|
||||||
{/* Month row */}
|
|
||||||
<div className="flex border-b border-slate-700" style={{ height: 28 }}>
|
|
||||||
<div
|
|
||||||
className="sticky left-0 z-20 bg-slate-800 border-r border-slate-600 border-b border-b-slate-600 flex items-center px-3 flex-shrink-0"
|
|
||||||
style={{ width: nameColW, minWidth: nameColW }}
|
|
||||||
>
|
|
||||||
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">Měsíc</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper: render section header bar (right column)
|
||||||
|
const renderSectionHeaderBar = (bgClass: string) => (
|
||||||
|
<div className={`${bgClass}`} style={{ height: CELL_H, width: totalGridW }} />
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-slate-700 bg-slate-800/50 overflow-hidden">
|
||||||
|
<div className="flex">
|
||||||
|
|
||||||
|
{/* === Fixed left name column === */}
|
||||||
|
<div className="flex-shrink-0 z-20 bg-slate-800 border-r border-slate-600" style={{ width: nameColW }}>
|
||||||
|
{/* Header labels */}
|
||||||
|
<div className="bg-slate-800 border-b border-slate-700 flex items-center px-3" style={{ height: 28 }}>
|
||||||
|
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">Mesic</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-800 border-b border-slate-700 flex items-center px-3" style={{ height: 24 }}>
|
||||||
|
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">Tyden</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-800 border-b border-slate-700 flex items-center px-3" style={{ height: 28 }}>
|
||||||
|
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">Den</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-800 border-b border-slate-700 flex items-center px-3" style={{ height: 24 }}>
|
||||||
|
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">Den t.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TKB info row label */}
|
||||||
|
{renderInfoRowLabel('TKB', 'border-slate-600')}
|
||||||
|
|
||||||
|
{/* Metro info row label */}
|
||||||
|
{showMetro && renderInfoRowLabel('Metro', 'border-slate-700')}
|
||||||
|
|
||||||
|
{/* D8 info row label */}
|
||||||
|
{showD8 && renderInfoRowLabel('D8', 'border-slate-600')}
|
||||||
|
|
||||||
|
{/* TKB section header */}
|
||||||
|
{renderSectionHeaderLabel('Pohotovost TKB', 'bg-indigo-700/60', 'text-indigo-100')}
|
||||||
|
|
||||||
|
{/* TKB person names */}
|
||||||
|
{tkbPeople.map((person) => (
|
||||||
|
<div key={person.id} className="bg-slate-800 border-b border-slate-700 flex items-center px-3" style={{ height: CELL_H }}>
|
||||||
|
<span className="text-xs text-slate-200 whitespace-nowrap truncate">
|
||||||
|
{person.name}
|
||||||
|
{person.note && <span className="text-slate-500 ml-1">({person.note})</span>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* IT section header */}
|
||||||
|
{renderSectionHeaderLabel('Pohotovost IT', 'bg-teal-700/60', 'text-teal-100')}
|
||||||
|
|
||||||
|
{/* IT person names */}
|
||||||
|
{itPeople.map((person) => (
|
||||||
|
<div key={person.id} className="bg-slate-800 border-b border-slate-700 flex items-center px-3" style={{ height: CELL_H }}>
|
||||||
|
<span className="text-xs text-slate-200 whitespace-nowrap truncate">
|
||||||
|
{person.name}
|
||||||
|
{person.note && <span className="text-slate-500 ml-1">({person.note})</span>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* === Scrollable grid === */}
|
||||||
|
<div ref={scrollRef} className="overflow-x-auto flex-1">
|
||||||
|
<div style={{ width: totalGridW, position: 'relative' }}>
|
||||||
|
|
||||||
|
{/* Month header row */}
|
||||||
|
<div className="flex border-b border-slate-700" style={{ height: 28 }}>
|
||||||
{monthSpans.map(m => (
|
{monthSpans.map(m => (
|
||||||
<div
|
<div
|
||||||
key={`${m.year}-${m.month}`}
|
key={`${m.year}-${m.month}`}
|
||||||
@@ -701,18 +750,12 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Week row */}
|
{/* Week header row */}
|
||||||
<div className="flex border-b border-slate-700" style={{ height: 24 }}>
|
<div className="flex border-b border-slate-700" style={{ height: 24 }}>
|
||||||
<div
|
|
||||||
className="sticky left-0 z-20 bg-slate-800 border-r border-slate-600 border-b border-b-slate-600 flex items-center px-3 flex-shrink-0"
|
|
||||||
style={{ width: nameColW, minWidth: nameColW }}
|
|
||||||
>
|
|
||||||
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">Týden</span>
|
|
||||||
</div>
|
|
||||||
{weekSpans.map((w, i) => (
|
{weekSpans.map((w, i) => (
|
||||||
<div
|
<div
|
||||||
key={`w-${i}`}
|
key={`w-${i}`}
|
||||||
className="flex items-center justify-center text-[10px] text-slate-400 border-r border-slate-700 bg-slate-800"
|
className="flex items-center justify-center text-[10px] text-slate-400 border-r border-slate-700/50 bg-slate-800/80"
|
||||||
style={{ width: w.count * CELL_W }}
|
style={{ width: w.count * CELL_W }}
|
||||||
>
|
>
|
||||||
{w.week}
|
{w.week}
|
||||||
@@ -720,14 +763,8 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Day number row */}
|
{/* Day number header row */}
|
||||||
<div className="flex border-b border-slate-700" style={{ height: 28 }}>
|
<div className="flex border-b border-slate-700" style={{ height: 28 }}>
|
||||||
<div
|
|
||||||
className="sticky left-0 z-20 bg-slate-800 border-r border-slate-600 border-b border-b-slate-600 flex items-center px-3 flex-shrink-0"
|
|
||||||
style={{ width: nameColW, minWidth: nameColW }}
|
|
||||||
>
|
|
||||||
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">Den</span>
|
|
||||||
</div>
|
|
||||||
{dayIndex.map((d) => {
|
{dayIndex.map((d) => {
|
||||||
const comment = dayComments.get(d.idx)
|
const comment = dayComments.get(d.idx)
|
||||||
const holidayName = holidays.get(`${d.year}-${d.month}-${d.day}`)
|
const holidayName = holidays.get(`${d.year}-${d.month}-${d.day}`)
|
||||||
@@ -737,7 +774,7 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
<div
|
<div
|
||||||
key={d.idx}
|
key={d.idx}
|
||||||
className={`flex items-center justify-center text-[10px] font-mono relative
|
className={`flex items-center justify-center text-[10px] font-mono relative
|
||||||
${isOff ? 'text-red-400/70 bg-slate-700' : 'text-slate-400 bg-slate-800'}
|
${isOff ? 'text-red-400/70 bg-slate-700/30' : 'text-slate-400 bg-slate-800/50'}
|
||||||
${d.day === 1 ? 'border-l-2 border-l-slate-500' : ''}
|
${d.day === 1 ? 'border-l-2 border-l-slate-500' : ''}
|
||||||
`}
|
`}
|
||||||
style={{ width: CELL_W }}
|
style={{ width: CELL_W }}
|
||||||
@@ -752,14 +789,8 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Day name row */}
|
{/* Day name header row */}
|
||||||
<div className="flex border-b border-slate-700" style={{ height: 24 }}>
|
<div className="flex border-b border-slate-700" style={{ height: 24 }}>
|
||||||
<div
|
|
||||||
className="sticky left-0 z-20 bg-slate-800 border-r border-slate-600 border-b border-b-slate-600 flex items-center px-3 flex-shrink-0"
|
|
||||||
style={{ width: nameColW, minWidth: nameColW }}
|
|
||||||
>
|
|
||||||
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold">Den t.</span>
|
|
||||||
</div>
|
|
||||||
{dayIndex.map((d, i) => {
|
{dayIndex.map((d, i) => {
|
||||||
const isHoliday = holidays.has(`${d.year}-${d.month}-${d.day}`)
|
const isHoliday = holidays.has(`${d.year}-${d.month}-${d.day}`)
|
||||||
const isOff = d.weekend || isHoliday
|
const isOff = d.weekend || isHoliday
|
||||||
@@ -767,7 +798,7 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
<div
|
<div
|
||||||
key={d.idx}
|
key={d.idx}
|
||||||
className={`flex items-center justify-center text-[9px] font-mono
|
className={`flex items-center justify-center text-[9px] font-mono
|
||||||
${isOff ? 'text-red-400/70 bg-slate-700' : 'text-slate-500 bg-slate-800'}
|
${isOff ? 'text-red-400/70 bg-slate-700/30' : 'text-slate-500 bg-slate-800/50'}
|
||||||
${d.day === 1 ? 'border-l-2 border-l-slate-500' : ''}
|
${d.day === 1 ? 'border-l-2 border-l-slate-500' : ''}
|
||||||
`}
|
`}
|
||||||
style={{ width: CELL_W }}
|
style={{ width: CELL_W }}
|
||||||
@@ -778,39 +809,26 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TKB closures row */}
|
{/* TKB info row */}
|
||||||
{renderInfoRow('__tunnel__', 'TKB', tunnelClosures, tunnelColors,
|
{renderInfoRowCells('__tunnel__', tunnelClosures, tunnelColors,
|
||||||
'bg-orange-700/40 text-orange-200', 'border-slate-600', 'border-orange-400',
|
'bg-orange-700/40 text-orange-200', 'border-slate-600', 'border-orange-400', 'TKB',
|
||||||
onTunnelContextMenu, onStartEditTunnel)}
|
onTunnelContextMenu, onStartEditTunnel)}
|
||||||
|
|
||||||
{/* Metro row */}
|
{/* Metro info row */}
|
||||||
{showMetro && renderInfoRow('__metro__', 'Metro', metroClosures, metroColors,
|
{showMetro && renderInfoRowCells('__metro__', metroClosures, metroColors,
|
||||||
'bg-blue-700/40 text-blue-200', 'border-slate-700', 'border-blue-400',
|
'bg-blue-700/40 text-blue-200', 'border-slate-700', 'border-blue-400', 'Metro',
|
||||||
(dayIdx, x, y) => onInfoRowContextMenu(dayIdx, 'metro', x, y), onStartEditMetro)}
|
(dayIdx, x, y) => onInfoRowContextMenu(dayIdx, 'metro', x, y), onStartEditMetro)}
|
||||||
|
|
||||||
{/* D8 row */}
|
{/* D8 info row */}
|
||||||
{showD8 && renderInfoRow('__d8__', 'D8', d8Closures, d8Colors,
|
{showD8 && renderInfoRowCells('__d8__', d8Closures, d8Colors,
|
||||||
'bg-green-700/40 text-green-200', 'border-slate-600', 'border-green-400',
|
'bg-green-700/40 text-green-200', 'border-slate-600', 'border-green-400', 'D8',
|
||||||
(dayIdx, x, y) => onInfoRowContextMenu(dayIdx, 'd8', x, y), onStartEditD8)}
|
(dayIdx, x, y) => onInfoRowContextMenu(dayIdx, 'd8', x, y), onStartEditD8)}
|
||||||
|
|
||||||
</div>{/* end sticky header rows */}
|
|
||||||
|
|
||||||
{/* Data rows container (with overlay) */}
|
{/* Data rows container (with overlay) */}
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
|
|
||||||
{/* TKB section header */}
|
{/* TKB section header bar */}
|
||||||
<div className="flex" style={{ height: CELL_H }}>
|
{renderSectionHeaderBar('bg-indigo-700/60')}
|
||||||
<div
|
|
||||||
className="sticky left-0 z-20 bg-indigo-700 border-r border-slate-600 flex items-center px-3 flex-shrink-0"
|
|
||||||
style={{ width: nameColW, minWidth: nameColW }}
|
|
||||||
>
|
|
||||||
<span className="text-xs font-bold text-indigo-100 uppercase tracking-wider">Pohotovost TKB</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="bg-indigo-700 flex items-center justify-center text-xs font-bold uppercase tracking-wider text-white"
|
|
||||||
style={{ width: totalGridW }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* TKB person rows */}
|
{/* TKB person rows */}
|
||||||
{tkbPeople.map((person) => (
|
{tkbPeople.map((person) => (
|
||||||
@@ -834,23 +852,11 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
onCellClick={onCellClick}
|
onCellClick={onCellClick}
|
||||||
onCellDragSelectStart={onCellDragSelectStart}
|
onCellDragSelectStart={onCellDragSelectStart}
|
||||||
onCellDragSelectMove={onCellDragSelectMove}
|
onCellDragSelectMove={onCellDragSelectMove}
|
||||||
nameColW={nameColW}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* IT section header */}
|
{/* IT section header bar */}
|
||||||
<div className="flex" style={{ height: CELL_H }}>
|
{renderSectionHeaderBar('bg-teal-700/60')}
|
||||||
<div
|
|
||||||
className="sticky left-0 z-20 bg-teal-700 border-r border-slate-600 flex items-center px-3 flex-shrink-0"
|
|
||||||
style={{ width: nameColW, minWidth: nameColW }}
|
|
||||||
>
|
|
||||||
<span className="text-xs font-bold text-teal-100 uppercase tracking-wider">Pohotovost IT</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="bg-teal-700 flex items-center justify-center text-xs font-bold uppercase tracking-wider text-white"
|
|
||||||
style={{ width: totalGridW }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* IT person rows */}
|
{/* IT person rows */}
|
||||||
{itPeople.map((person) => (
|
{itPeople.map((person) => (
|
||||||
@@ -874,20 +880,18 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
onCellClick={onCellClick}
|
onCellClick={onCellClick}
|
||||||
onCellDragSelectStart={onCellDragSelectStart}
|
onCellDragSelectStart={onCellDragSelectStart}
|
||||||
onCellDragSelectMove={onCellDragSelectMove}
|
onCellDragSelectMove={onCellDragSelectMove}
|
||||||
nameColW={nameColW}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Compare ghost blocks */}
|
{/* Compare ghost blocks */}
|
||||||
{ghostBlocks && ghostBlocks.map((gb, i) => {
|
{ghostBlocks && ghostBlocks.map((gb, i) => {
|
||||||
const left = nameColW + gb.startPos * CELL_W
|
const left = gb.startPos * CELL_W
|
||||||
// Account for section headers in top position
|
|
||||||
const tkbCount = tkbPeople.length
|
const tkbCount = tkbPeople.length
|
||||||
let top: number
|
let top: number
|
||||||
if (gb.personIdx < tkbCount) {
|
if (gb.personIdx < tkbCount) {
|
||||||
top = (1 + gb.personIdx) * CELL_H // +1 for TKB section header
|
top = (1 + gb.personIdx) * CELL_H
|
||||||
} else {
|
} else {
|
||||||
top = (1 + tkbCount + 1 + (gb.personIdx - tkbCount)) * CELL_H // +1 TKB header +1 IT header
|
top = (1 + tkbCount + 1 + (gb.personIdx - tkbCount)) * CELL_H
|
||||||
}
|
}
|
||||||
const width = gb.length * CELL_W
|
const width = gb.length * CELL_W
|
||||||
const cellStyle = getCellStyle(gb.value)
|
const cellStyle = getCellStyle(gb.value)
|
||||||
@@ -917,5 +921,8 @@ export function ScheduleTable(props: ScheduleTableProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user