Files
TKB_plan/web/export_excel.py
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

109 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Generate Excel export from saved schedule using template."""
import json
import sys
import openpyxl
from openpyxl.styles import PatternFill
from copy import copy
TEMPLATE = sys.argv[1]
SCHEDULE = sys.argv[2]
OUTPUT = sys.argv[3]
STATION_ROWS = {
'DE': 13, 'HR': 14, 'MA': 15, 'ST': 16, 'MSA': 17, 'MUA': 18,
'NM': 19, 'FL': 20, 'JP': 21, 'ZL': 22, 'SN': 23, 'AN': 24,
'KN': 25, 'NA': 26, 'MSB': 27, 'NR': 28, 'FRB': 29, 'KR': 30,
'KC': 31, 'VY': 32, 'IP': 33, 'MUC': 34, 'HN': 35, 'FRC': 36,
'VL': 37, 'NH': 38, 'KB': 39,
}
NO_FILL = PatternFill(fill_type=None)
with open(SCHEDULE) as f:
raw = json.load(f)
data = raw['data'] if 'data' in raw and 'stations' not in raw else raw
wb = openpyxl.load_workbook(TEMPLATE)
ws = wb.active
# Identify weekend columns: DEN row (row 11) has fill on weekends/holidays
# Capture the DATA ROW fill (row 13) for those columns — it has the correct theme=0 gray
weekend_cols = set()
for col in range(7, 281):
den_cell = ws.cell(row=11, column=col)
if den_cell.fill and den_cell.fill.fill_type == 'solid':
weekend_cols.add(col)
col_fills = {}
for col in weekend_cols:
data_cell = ws.cell(row=13, column=col)
if data_cell.fill and data_cell.fill.fill_type == 'solid':
theme = data_cell.fill.fgColor.theme if hasattr(data_cell.fill.fgColor, 'theme') and isinstance(data_cell.fill.fgColor.theme, int) else None
if theme == 0: # Gray weekend fill, not teal s/v fill
col_fills[col] = copy(data_cell.fill)
for station in data['stations']:
row = STATION_ROWS.get(station['code'])
if not row:
continue
# Step 1: Clear all values
for col in range(7, 281):
ws.cell(row=row, column=col).value = None
# Step 2: Restore original column fills (weekends etc)
for col in range(7, 281):
if col in col_fills:
ws.cell(row=row, column=col).fill = copy(col_fills[col])
else:
ws.cell(row=row, column=col).fill = NO_FILL
# Step 3: Write data values (no extra fills — only weekend column fills from step 2)
for idx_str, cell_data in station['data'].items():
idx = int(idx_str)
col = idx + 7
if col < 7 or col > 280:
continue
val = cell_data.get('v')
if val is not None:
ws.cell(row=row, column=col).value = val
# Add DEN row comments
if 'dayComments' in data:
comments = data['dayComments']
# Handle both list format [{dayIdx, text}] and dict format {"idx": "text"}
if isinstance(comments, list):
for c in comments:
idx = c.get('dayIdx', 0)
text = c.get('text', '')
if text:
col = idx + 7
if 7 <= col <= 280:
from openpyxl.comments import Comment
ws.cell(row=11, column=col).comment = Comment(text, 'Metro HMG')
elif isinstance(comments, dict):
for idx_str, text in comments.items():
if text:
col = int(idx_str) + 7
if 7 <= col <= 280:
from openpyxl.comments import Comment
ws.cell(row=11, column=col).comment = Comment(text, 'Metro HMG')
# Add cell comments (per station+day)
if 'cellComments' in data:
from openpyxl.comments import Comment as XlComment
for c in data.get('cellComments', []):
code = c.get('stationCode', '')
idx = c.get('dayIdx', 0)
text = c.get('text', '')
row = STATION_ROWS.get(code)
if row and text:
col = idx + 7
if 7 <= col <= 280:
ws.cell(row=row, column=col).comment = XlComment(text, 'Metro HMG')
wb.save(OUTPUT)
print(f'Exported to {OUTPUT}', file=sys.stderr)