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

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):

  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:

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:

  • 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
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 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.

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:

  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

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 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

<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_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

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:

  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

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