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>
This commit is contained in:
108
web/export_excel.py
Normal file
108
web/export_excel.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user