Full rewrite of METRO HMG for TKB tunnel department: - People-based grid (18 TKB + 5 IT), year-long calendar - Color-coded shift values (4/6/8/12/A/B/D/N/U/O) - Drag-and-drop cells, multi-cell selection (click/ctrl/shift/drag) - Right-click context menu with color palette - Tunnel closure + Metro + D8 info rows (toggleable) - Czech holidays highlighted with names - PDF export (2-page A4 landscape, DejaVu font for Czech chars) - Improvement proposals system - Sticky headers (vertical + horizontal scroll) - Cell value filter toggles in legend Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
24 KiB
TKB Shift Scheduler Rewrite
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Rewrite the METRO HMG station maintenance scheduler into a TKB personnel shift planner where rows are people (grouped TKB + IT), columns are calendar days (year-long), cells contain shift codes that determine background color, with drag-and-drop and comments.
Architecture: React grid with people as rows and calendar days as columns. Cell text (8, 12, D, N, U, etc.) drives background color via a simple lookup map. Two groups (TKB, IT) shown on one view with a visual separator. Tunnel closures displayed as an editable header row. Server remains Express with JSON file storage — Excel import/export deferred to later phase.
Tech Stack: React 19 + TypeScript + Vite + Tailwind CSS (frontend), Express/Node.js (backend), same as existing
Data Model Reference
Cell Values and Colors (from Excel analysis + dochazka scripts)
| Value | Meaning | Background Color |
|---|---|---|
8 |
8-hour day shift | #CCFFCC (light green) |
12 |
12-hour shift | #A3D977 (medium green) |
6 |
6-hour shift | #E8F5E9 (pale green) |
4 |
4-hour shift | #F1F8E9 (very pale green) |
D |
Dovolená (vacation) | #FFFF00 (yellow) |
D/2 |
Half-day vacation | #FFF9C4 (light yellow) |
N |
Nemocenská (sick) | #FF0000 / white text (red) |
U |
Uzavěra (closure) | #92D050 (bright green) |
O |
Otčovská (paternity) | #FFC000 (golden) |
x |
No VN experience marker | #E0E0E0 (gray) |
| empty | No shift | transparent |
People (from Excel sample — March 2026)
TKB Group (rows 8-25):
- Pauzer Libor
- Vörös Pavel
- Janouš Petr
- Franek Lukáš
- Svoboda Daniel
- Dvořák Václav
- Vondrák Pavel
- Čeleda Olda
- Hanzlík Marek
- Kohl David
- Dittrich Vladimír
- Toman Milan
- Glaser Ondřej
- Herbst David
- Ryba Ondřej
- Zábranský Petr
- Žemlička Miroslav
- Teslík Hynek
IT Group (rows 31-39):
- Vörös Pavel (same person, IT duties)
- Janouš Petr
- Glaser Ondřej
- Robert Štefan
- Franek Lukáš
Tunnel Closure Codes (row 1 of Excel)
SAT, LAT, ZAT-ZTT, ZAT-VTT, ATM, TAT-V, TAT-M — free-text per day
Task 1: Rewrite Data Types
Files:
- Modify:
web/src/types.ts(full rewrite)
Step 1: Rewrite types.ts
Replace the entire file with the new data model:
export interface DayInfo {
idx: number
day: number
month: number
year: number
week: number
weekend: boolean
}
export interface Person {
id: string // unique identifier
name: string // "Vörös Pavel"
group: 'TKB' | 'IT' // section
note?: string // e.g. "(NN)", "(VN)", "(all in one)"
data: Record<string, CellValue> // dayIdx -> cell value
}
export interface CellValue {
v?: string // cell text: "8", "12", "D", "N", "U", etc.
}
export interface TunnelClosure {
dayIdx: number
text: string // e.g. "SAT", "LAT", "ZAT - ZTT"
}
export interface CellComment {
personId: string
dayIdx: number
text: string
}
export interface DayComment {
dayIdx: number
text: string
}
export interface ScheduleData {
dayIndex: DayInfo[]
people: Person[]
tunnelClosures?: TunnelClosure[]
dayComments?: DayComment[]
cellComments?: CellComment[]
}
export interface DragState {
personId: string
originalIdx: number
previewIdx: number
value: string
}
export interface ContextMenuState {
x: number
y: number
dayIdx: number
personId: string | null
}
export interface DiffChange {
personId: string
dayIdx: number
type: 'added' | 'removed' | 'changed'
oldValue?: string
newValue?: string
}
export interface ScheduleFile {
id: string
name: string
createdAt: string
modifiedAt: string
}
export interface ScheduleFileWithData extends ScheduleFile {
data: ScheduleData
}
Step 2: Commit
git add web/src/types.ts
git commit -m "refactor: rewrite types for TKB shift scheduler (people, groups, shift codes)"
Task 2: Create Cell Color Map Utility
Files:
- Create:
web/src/cellColors.ts
Step 1: Create cellColors.ts
// Cell value -> background color + text color mapping
export interface CellStyle {
bg: string
text: string
label: string
}
const CELL_STYLES: Record<string, CellStyle> = {
'4': { bg: '#F1F8E9', text: '#333', label: '4h směna' },
'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' },
'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á' },
'U': { bg: '#92D050', text: '#333', label: 'Uzavěra' },
'O': { bg: '#FFC000', text: '#333', label: 'Otčovská' },
'x': { bg: '#E0E0E0', text: '#999', label: 'Bez zkušeností VN' },
}
export function getCellStyle(value: string | undefined): CellStyle | null {
if (!value) return null
const v = value.trim()
return CELL_STYLES[v] ?? null
}
export function getAllCellStyles(): Record<string, CellStyle> {
return { ...CELL_STYLES }
}
Step 2: Commit
git add web/src/cellColors.ts
git commit -m "feat: add cell color mapping for shift codes (D, N, U, hours)"
Task 3: Create Default Data
Files:
- Modify:
web/src/data.json(full rewrite)
Step 1: Write a script to generate data.json
Create a temporary Node script web/generate-data.js:
// Generates default data.json with year-long calendar and people list
const people = [
// TKB group
{ id: 'tkb-pauzer', name: 'Pauzer Libor', group: 'TKB', note: 'all in one' },
{ id: 'tkb-voros', name: 'Vörös Pavel', group: 'TKB', note: 'NN' },
{ id: 'tkb-janous', name: 'Janouš Petr', group: 'TKB', note: 'VN' },
{ id: 'tkb-franek', name: 'Franek Lukáš', group: 'TKB', note: 'NN' },
{ id: 'tkb-svoboda', name: 'Svoboda Daniel', group: 'TKB' },
{ id: 'tkb-dvorak', name: 'Dvořák Václav', group: 'TKB', note: 'VN' },
{ id: 'tkb-vondrak', name: 'Vondrák Pavel', group: 'TKB', note: 'NN' },
{ id: 'tkb-celeda', name: 'Čeleda Olda', group: 'TKB', note: 'NN' },
{ id: 'tkb-hanzlik', name: 'Hanzlík Marek', group: 'TKB', note: 'VN' },
{ id: 'tkb-kohl', name: 'Kohl David', group: 'TKB', note: 'VN' },
{ id: 'tkb-dittrich', name: 'Dittrich Vladimír', group: 'TKB', note: 'VN' },
{ id: 'tkb-toman', name: 'Toman Milan', group: 'TKB', note: 'VN' },
{ id: 'tkb-glaser', name: 'Glaser Ondřej', group: 'TKB', note: 'NN' },
{ id: 'tkb-herbst', name: 'Herbst David', group: 'TKB', note: 'NN' },
{ id: 'tkb-ryba', name: 'Ryba Ondřej', group: 'TKB', note: 'NN' },
{ id: 'tkb-zabransky', name: 'Zábranský Petr', group: 'TKB', note: 'NN' },
{ id: 'tkb-zemlicka', name: 'Žemlička Miroslav', group: 'TKB', note: 'NN' },
{ id: 'tkb-teslik', name: 'Teslík Hynek', group: 'TKB', note: 'NN' },
// IT group
{ id: 'it-voros', name: 'Vörös Pavel', group: 'IT' },
{ id: 'it-janous', name: 'Janouš Petr', group: 'IT' },
{ id: 'it-glaser', name: 'Glaser Ondřej', group: 'IT' },
{ id: 'it-stefan', name: 'Robert Štefan', group: 'IT' },
{ id: 'it-franek', name: 'Franek Lukáš', group: 'IT' },
]
function getISOWeek(date) {
const d = new Date(date.getTime())
d.setHours(0, 0, 0, 0)
d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7))
const week1 = new Date(d.getFullYear(), 0, 4)
return 1 + Math.round(((d.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7)
}
// Generate Jan 1 2026 - Dec 31 2026
const dayIndex = []
const start = new Date(2026, 0, 1)
const end = new Date(2026, 11, 31)
let idx = 0
const current = new Date(start)
while (current <= end) {
dayIndex.push({
idx,
day: current.getDate(),
month: current.getMonth() + 1,
year: current.getFullYear(),
week: getISOWeek(current),
weekend: current.getDay() === 0 || current.getDay() === 6,
})
idx++
current.setDate(current.getDate() + 1)
}
const data = {
dayIndex,
people: people.map(p => ({ ...p, data: {} })),
tunnelClosures: [],
dayComments: [],
cellComments: [],
}
const fs = require('fs')
fs.writeFileSync('src/data.json', JSON.stringify(data, null, 2))
console.log(`Generated ${dayIndex.length} days, ${people.length} people`)
Run: cd web && node generate-data.js
Then delete the generator script.
Step 2: Commit
git add web/src/data.json
git commit -m "feat: default data with TKB+IT people list and year-long 2026 calendar"
Task 4: Rewrite State Management
Files:
- Modify:
web/src/useScheduleState.ts(full rewrite) - Delete:
web/src/blockParser.ts - Delete:
web/src/constraints.ts
Step 1: Rewrite useScheduleState.ts
Remove all block/constraint logic. The hook manages:
peoplearray with per-cell datatunnelClosuresmapdayCommentsandcellCommentsmapssetCell(personId, dayIdx, value)— set or clear a cellmoveCell(personId, fromIdx, toIdx)— move a cell value (for drag)setTunnelClosure(dayIdx, text)— set tunnel closure text- Undo history
- Import/export for save/load
import { useState, useCallback, useRef } from 'react'
import type { Person, DayInfo, TunnelClosure, DayComment, CellComment, ScheduleData } from './types'
interface ScheduleSnapshot {
people: Person[]
tunnelClosures: Map<number, string>
dayComments: Map<number, string>
cellComments: Map<string, string>
}
export function useScheduleState(
initialPeople: Person[],
dayIndex: DayInfo[],
initialTunnelClosures?: TunnelClosure[],
initialDayComments?: DayComment[],
initialCellComments?: CellComment[],
) {
const [people, setPeople] = useState<Person[]>(() => JSON.parse(JSON.stringify(initialPeople)))
const [tunnelClosures, setTunnelClosures] = useState<Map<number, string>>(() => {
const m = new Map<number, string>()
if (initialTunnelClosures) {
for (const tc of initialTunnelClosures) m.set(tc.dayIdx, tc.text)
}
return m
})
const [dayComments, setDayComments] = useState<Map<number, string>>(() => {
const m = new Map<number, string>()
if (initialDayComments) {
for (const dc of initialDayComments) m.set(dc.dayIdx, dc.text)
}
return m
})
const [cellComments, setCellComments] = useState<Map<string, string>>(() => {
const m = new Map<string, string>()
if (initialCellComments) {
for (const cc of initialCellComments) m.set(`${cc.personId}-${cc.dayIdx}`, cc.text)
}
return m
})
const historyRef = useRef<ScheduleSnapshot[]>([])
const pushHistory = useCallback(() => {
historyRef.current.push({
people: JSON.parse(JSON.stringify(people)),
tunnelClosures: new Map(tunnelClosures),
dayComments: new Map(dayComments),
cellComments: new Map(cellComments),
})
if (historyRef.current.length > 50) historyRef.current.shift()
}, [people, tunnelClosures, dayComments, cellComments])
const setCell = useCallback((personId: string, dayIdx: number, value: string | null) => {
pushHistory()
setPeople(prev => {
const next = JSON.parse(JSON.stringify(prev)) as Person[]
const person = next.find(p => p.id === personId)
if (!person) return prev
if (value) {
person.data[String(dayIdx)] = { v: value }
} else {
delete person.data[String(dayIdx)]
}
return next
})
}, [pushHistory])
const moveCell = useCallback((personId: string, fromIdx: number, toIdx: number) => {
if (fromIdx === toIdx) return
pushHistory()
setPeople(prev => {
const next = JSON.parse(JSON.stringify(prev)) as Person[]
const person = next.find(p => p.id === personId)
if (!person) return prev
const value = person.data[String(fromIdx)]
if (!value) return prev
delete person.data[String(fromIdx)]
person.data[String(toIdx)] = value
return next
})
}, [pushHistory])
const setTunnelClosure = useCallback((dayIdx: number, text: string | null) => {
pushHistory()
setTunnelClosures(prev => {
const next = new Map(prev)
if (text) next.set(dayIdx, text)
else next.delete(dayIdx)
return next
})
}, [pushHistory])
const addDayComment = useCallback((dayIdx: number, text: string) => {
pushHistory()
setDayComments(prev => new Map(prev).set(dayIdx, text))
}, [pushHistory])
const removeDayComment = useCallback((dayIdx: number) => {
pushHistory()
setDayComments(prev => {
const next = new Map(prev)
next.delete(dayIdx)
return next
})
}, [pushHistory])
const addCellComment = useCallback((personId: string, dayIdx: number, text: string) => {
pushHistory()
setCellComments(prev => new Map(prev).set(`${personId}-${dayIdx}`, text))
}, [pushHistory])
const removeCellComment = useCallback((personId: string, dayIdx: number) => {
pushHistory()
setCellComments(prev => {
const next = new Map(prev)
next.delete(`${personId}-${dayIdx}`)
return next
})
}, [pushHistory])
const undo = useCallback(() => {
const snapshot = historyRef.current.pop()
if (!snapshot) return
setPeople(snapshot.people)
setTunnelClosures(snapshot.tunnelClosures)
setDayComments(snapshot.dayComments)
setCellComments(snapshot.cellComments)
}, [])
const canUndo = historyRef.current.length > 0
const getSchedulePayload = useCallback((): ScheduleData => ({
dayIndex,
people,
tunnelClosures: Array.from(tunnelClosures.entries()).map(([dayIdx, text]) => ({ dayIdx, text })),
dayComments: Array.from(dayComments.entries()).map(([dayIdx, text]) => ({ dayIdx, text })),
cellComments: Array.from(cellComments.entries()).map(([key, text]) => {
const [personId, dayIdxStr] = key.split('-')
return { personId, dayIdx: parseInt(dayIdxStr), text }
}),
}), [dayIndex, people, tunnelClosures, dayComments, cellComments])
return {
people,
tunnelClosures,
dayComments,
cellComments,
setCell,
moveCell,
setTunnelClosure,
addDayComment,
removeDayComment,
addCellComment,
removeCellComment,
undo,
canUndo,
getSchedulePayload,
}
}
Step 2: Delete obsolete files
rm web/src/blockParser.ts web/src/constraints.ts
Step 3: Commit
git add web/src/useScheduleState.ts
git rm web/src/blockParser.ts web/src/constraints.ts
git commit -m "refactor: rewrite state management for shift scheduler, remove blocks/constraints"
Task 5: Rewrite Drag-and-Drop
Files:
- Modify:
web/src/useDragBlock.ts→ rename toweb/src/useDragCell.ts
Step 1: Rewrite as useDragCell.ts
Simplified: drags a single cell value from one day column to another for the same person.
import { useState, useCallback, useRef, useEffect } from 'react'
import type { DragState } from './types'
interface UseDragCellOptions {
scrollRef: React.RefObject<HTMLDivElement | null>
cellWidth: number
minDayIdx: number
maxDayIdx: number
onMoveCell: (personId: string, fromIdx: number, toIdx: number) => void
}
export function useDragCell({ scrollRef, cellWidth, minDayIdx, maxDayIdx, onMoveCell }: UseDragCellOptions) {
const [dragState, setDragState] = useState<DragState | null>(null)
const dragRef = useRef<{
personId: string
value: string
originalIdx: number
startX: number
scrollLeft: number
autoScrollRaf: number | null
} | null>(null)
const onCellPointerDown = useCallback((
e: React.PointerEvent,
personId: string,
dayIdx: number,
value: string,
) => {
e.preventDefault()
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
const scrollLeft = scrollRef.current?.scrollLeft ?? 0
dragRef.current = {
personId,
value,
originalIdx: dayIdx,
startX: e.clientX,
scrollLeft,
autoScrollRaf: null,
}
setDragState({ personId, originalIdx: dayIdx, previewIdx: dayIdx, value })
}, [scrollRef])
useEffect(() => {
if (!dragState) return
const handleMove = (e: PointerEvent) => {
const ref = dragRef.current
if (!ref || !scrollRef.current) return
const scrollDelta = scrollRef.current.scrollLeft - ref.scrollLeft
const dx = e.clientX - ref.startX + scrollDelta
const dayOffset = Math.round(dx / cellWidth)
const newIdx = Math.max(minDayIdx, Math.min(maxDayIdx, ref.originalIdx + dayOffset))
setDragState(prev => prev ? { ...prev, previewIdx: newIdx } : null)
// Auto-scroll near edges
const rect = scrollRef.current.getBoundingClientRect()
const margin = 60
if (e.clientX < rect.left + margin) {
scrollRef.current.scrollLeft -= 4
} else if (e.clientX > rect.right - margin) {
scrollRef.current.scrollLeft += 4
}
}
const handleUp = () => {
const ref = dragRef.current
const state = dragState
if (ref && state && state.previewIdx !== state.originalIdx) {
onMoveCell(ref.personId, state.originalIdx, state.previewIdx)
}
dragRef.current = null
setDragState(null)
}
document.addEventListener('pointermove', handleMove)
document.addEventListener('pointerup', handleUp)
return () => {
document.removeEventListener('pointermove', handleMove)
document.removeEventListener('pointerup', handleUp)
}
}, [dragState, cellWidth, minDayIdx, maxDayIdx, scrollRef, onMoveCell])
return { dragState, onCellPointerDown }
}
Step 2: Delete old file and commit
git rm web/src/useDragBlock.ts
git add web/src/useDragCell.ts
git commit -m "refactor: replace block drag with single-cell drag for shifts"
Task 6: Rewrite ScheduleTable
Files:
- Modify:
web/src/ScheduleTable.tsx(full rewrite)
Step 1: Rewrite ScheduleTable.tsx
This is the largest component. Key changes:
- Rows = people (grouped TKB then IT with separator)
- Tunnel closure row at top (editable)
- Cell background driven by
getCellStyle() - Click to edit cell value (inline input)
- Comment indicators preserved (blue triangle)
- Day comment indicators (blue column header)
- Weekend columns dimmed
- Month/week headers preserved
The component should render:
[Fixed left column: person names] [Scrollable grid]
Header rows:
- Month spans
- Week numbers
- Day numbers
- Day names (Po, Út, St, Čt, Pá, So, Ne)
- Tunnel closures (editable)
Body rows:
- TKB section label
- Person rows (TKB group)
- IT section label (visual separator)
- Person rows (IT group)
Cell interactions:
- Click: Opens inline text input to type value (8, 12, D, N, U, etc.)
- Double-click or Enter: Confirms value, applies color
- Drag: Moves cell value to another day
- Right-click: Context menu for comments
- Escape: Cancels edit
Cell width: 32px (same as original). Person name column: ~200px.
Full implementation is large — write the complete component with:
- Header rendering (months, weeks, days, day names, tunnel closures)
- Person row rendering with cell color coding
- Group separators between TKB and IT
- Inline cell editing
- Drag-and-drop support
- Comment indicators
- Weekend dimming
Step 2: Commit
git add web/src/ScheduleTable.tsx
git commit -m "feat: rewrite schedule grid for person-based shift planning"
Task 7: Rewrite Context Menu
Files:
- Modify:
web/src/ContextMenu.tsx
Step 1: Rewrite ContextMenu
Simplified menu:
- For day header: add/edit/remove day comment
- For tunnel closure row: edit tunnel closure text
- For person cell:
- Set cell value (text input for shift code)
- Add/edit/remove cell comment
- Clear cell
Remove all obstacle and s/v logic.
Step 2: Commit
git add web/src/ContextMenu.tsx
git commit -m "refactor: simplify context menu for shift editing and comments"
Task 8: Rewrite Toolbar
Files:
- Modify:
web/src/Toolbar.tsx
Step 1: Rewrite Toolbar
Remove: phase filters, compressed view toggle, constraint violation badges.
Keep: Save, Save As, Undo, Export Excel buttons, save status indicator.
Add: Color legend showing all shift codes and their colors (from getAllCellStyles()).
Add: Month quick-jump buttons (Leden through Prosinec for year-long view).
Step 2: Commit
git add web/src/Toolbar.tsx
git commit -m "refactor: simplify toolbar — save/undo/legend/month navigation"
Task 9: Rewrite App.tsx
Files:
- Modify:
web/src/App.tsx
Step 1: Update App.tsx
Changes:
- Replace
Stationreferences withPerson - Replace
useScheduleStatecall parameters (people instead of stations, tunnelClosures) - Replace
useDragBlockwithuseDragCell - Remove
blocks,violations,moveBlock,moveSV,activeFilters,compressedView - Update
normalizeDayIndexto cover full year (Jan 1 - Dec 31 2026) - Update header text: "TKB Plán služeb" instead of "HMG Profylaxe"
- Remove
ALL_PHASESconstant - Pass
tunnelClosuresandsetTunnelClosureto ScheduleTable - Update
ScheduleAppprops and wiring
Step 2: Update Login.tsx credentials
- Change username to
tkb, password tosluzby(or keep configurable) - Update title text
Step 3: Update index.html title
<title>TKB Plán služeb</title>
Step 4: Commit
git add web/src/App.tsx web/src/Login.tsx web/index.html
git commit -m "feat: wire up TKB shift scheduler app shell"
Task 10: Update Server
Files:
- Modify:
web/server.js
Step 1: Update server.js
Changes:
- Remove
STATION_ROWSconstant - Update validation: check for
peopleinstead ofstationsin data payload (support both for backward compat during transition) - Update console log message: "TKB server running on..."
- Comment out or simplify Excel export/import (will need new Python scripts later)
- Keep all file management endpoints unchanged
Step 2: Commit
git add web/server.js
git commit -m "refactor: update server for people-based data model"
Task 11: Remove Obsolete Excel Scripts
Files:
- Delete or stub:
web/export_excel.py,web/import_excel.py - Modify:
web/src/excelIO.ts
Step 1: Stub excelIO.ts
Remove the old station-based import. Create a minimal placeholder that can be expanded later:
// Excel import/export — to be implemented for TKB shift format
export function importFromExcel(_file: File): Promise<null> {
return Promise.resolve(null)
}
Step 2: Commit
git add web/src/excelIO.ts
git commit -m "chore: stub excel import/export for future TKB format"
Task 12: Build and Test
Step 1: Install dependencies and build
cd web && npm install && npx vite build
Step 2: Fix any TypeScript errors
Iterate on compilation errors — the most likely issues:
- Import paths (useDragBlock → useDragCell)
- Property names (stationCode → personId, stations → people)
- Removed types (StationBlock, ConstraintViolation)
Step 3: Start dev server and verify
npm run dev
# In parallel: node server.js
Test:
- Login works
- Can create new file
- Grid shows TKB people, then separator, then IT people
- Can click cell and type "8" → green background appears
- Can type "D" → yellow background
- Can type "N" → red background
- Can drag a filled cell to another day
- Right-click shows context menu with comment options
- Tunnel closure row is editable
- Can scroll through all 12 months
- Save and reload preserves data
- Month navigation buttons work
Step 4: Commit
git add -A
git commit -m "feat: TKB shift scheduler MVP — people grid, color-coded shifts, drag-and-drop"
Out of Scope (Future Tasks)
- Excel import from TKB format (parse the Pohotovost Excel)
- Excel export to TKB format
- Adding/removing people from the UI
- Holiday calendar (Czech holidays highlighting)
- Overtime calculation
- Print view
- Multi-year support