Files
TKB_plan/docs/plans/2026-04-01-shift-scheduler-rewrite.md
Docker Config Backup b4158d687f feat: TKB shift scheduler — personnel shift planning web app
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>
2026-04-02 09:48:38 +02:00

839 lines
24 KiB
Markdown

# 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):**
1. Pauzer Libor
2. Vörös Pavel
3. Janouš Petr
4. Franek Lukáš
5. Svoboda Daniel
6. Dvořák Václav
7. Vondrák Pavel
8. Čeleda Olda
9. Hanzlík Marek
10. Kohl David
11. Dittrich Vladimír
12. Toman Milan
13. Glaser Ondřej
14. Herbst David
15. Ryba Ondřej
16. Zábranský Petr
17. Žemlička Miroslav
18. Teslík Hynek
**IT Group (rows 31-39):**
1. Vörös Pavel (same person, IT duties)
2. Janouš Petr
3. Glaser Ondřej
4. Robert Štefan
5. 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:
```typescript
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**
```bash
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**
```typescript
// 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**
```bash
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`:
```javascript
// 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**
```bash
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:
- `people` array with per-cell data
- `tunnelClosures` map
- `dayComments` and `cellComments` maps
- `setCell(personId, dayIdx, value)` — set or clear a cell
- `moveCell(personId, fromIdx, toIdx)` — move a cell value (for drag)
- `setTunnelClosure(dayIdx, text)` — set tunnel closure text
- Undo history
- Import/export for save/load
```typescript
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**
```bash
rm web/src/blockParser.ts web/src/constraints.ts
```
**Step 3: Commit**
```bash
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 to `web/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.
```typescript
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**
```bash
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:
1. Header rendering (months, weeks, days, day names, tunnel closures)
2. Person row rendering with cell color coding
3. Group separators between TKB and IT
4. Inline cell editing
5. Drag-and-drop support
6. Comment indicators
7. Weekend dimming
**Step 2: Commit**
```bash
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**
```bash
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**
```bash
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 `Station` references with `Person`
- Replace `useScheduleState` call parameters (people instead of stations, tunnelClosures)
- Replace `useDragBlock` with `useDragCell`
- Remove `blocks`, `violations`, `moveBlock`, `moveSV`, `activeFilters`, `compressedView`
- Update `normalizeDayIndex` to cover full year (Jan 1 - Dec 31 2026)
- Update header text: "TKB Plán služeb" instead of "HMG Profylaxe"
- Remove `ALL_PHASES` constant
- Pass `tunnelClosures` and `setTunnelClosure` to ScheduleTable
- Update `ScheduleApp` props and wiring
**Step 2: Update Login.tsx credentials**
- Change username to `tkb`, password to `sluzby` (or keep configurable)
- Update title text
**Step 3: Update index.html title**
```html
<title>TKB Plán služeb</title>
```
**Step 4: Commit**
```bash
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_ROWS` constant
- Update validation: check for `people` instead of `stations` in 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**
```bash
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:
```typescript
// Excel import/export — to be implemented for TKB shift format
export function importFromExcel(_file: File): Promise<null> {
return Promise.resolve(null)
}
```
**Step 2: Commit**
```bash
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**
```bash
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**
```bash
npm run dev
# In parallel: node server.js
```
Test:
1. Login works
2. Can create new file
3. Grid shows TKB people, then separator, then IT people
4. Can click cell and type "8" → green background appears
5. Can type "D" → yellow background
6. Can type "N" → red background
7. Can drag a filled cell to another day
8. Right-click shows context menu with comment options
9. Tunnel closure row is editable
10. Can scroll through all 12 months
11. Save and reload preserves data
12. Month navigation buttons work
**Step 4: Commit**
```bash
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