# 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 // 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 = { '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 { 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 dayComments: Map cellComments: Map } export function useScheduleState( initialPeople: Person[], dayIndex: DayInfo[], initialTunnelClosures?: TunnelClosure[], initialDayComments?: DayComment[], initialCellComments?: CellComment[], ) { const [people, setPeople] = useState(() => JSON.parse(JSON.stringify(initialPeople))) const [tunnelClosures, setTunnelClosures] = useState>(() => { const m = new Map() if (initialTunnelClosures) { for (const tc of initialTunnelClosures) m.set(tc.dayIdx, tc.text) } return m }) const [dayComments, setDayComments] = useState>(() => { const m = new Map() if (initialDayComments) { for (const dc of initialDayComments) m.set(dc.dayIdx, dc.text) } return m }) const [cellComments, setCellComments] = useState>(() => { const m = new Map() if (initialCellComments) { for (const cc of initialCellComments) m.set(`${cc.personId}-${cc.dayIdx}`, cc.text) } return m }) const historyRef = useRef([]) 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 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(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 TKB Plán služeb ``` **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 { 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