206 lines
6.2 KiB
Python
206 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from datetime import date, datetime, time, timedelta
|
|
from pathlib import Path
|
|
|
|
from openpyxl import Workbook
|
|
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
|
from openpyxl.utils import get_column_letter
|
|
|
|
|
|
CONFIG_FILE = Path("config.json")
|
|
|
|
|
|
def parse_date(value: str) -> date:
|
|
return datetime.strptime(value, "%Y-%m-%d").date()
|
|
|
|
|
|
def parse_time(value: str) -> time:
|
|
return datetime.strptime(value, "%H:%M").time()
|
|
|
|
|
|
def get_monday(any_day: date) -> date:
|
|
return any_day - timedelta(days=any_day.weekday())
|
|
|
|
|
|
def load_config(path: Path) -> dict:
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"Missing config file: {path}")
|
|
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
data = json.load(handle)
|
|
|
|
required_keys = ["start_time", "end_time", "slot_minutes", "crews"]
|
|
for key in required_keys:
|
|
if key not in data:
|
|
raise ValueError(f"config.json is missing '{key}'")
|
|
|
|
crews = data["crews"]
|
|
if not isinstance(crews, list) or not crews:
|
|
raise ValueError("'crews' must be a non-empty array")
|
|
|
|
clean_crews: list[str] = []
|
|
for crew in crews:
|
|
if not isinstance(crew, str):
|
|
raise ValueError("Each crew name must be a string")
|
|
|
|
crew_name = crew.strip()
|
|
if not crew_name:
|
|
raise ValueError("Crew names cannot be empty")
|
|
|
|
clean_crews.append(crew_name)
|
|
|
|
slot_minutes = int(data["slot_minutes"])
|
|
if slot_minutes <= 0:
|
|
raise ValueError("'slot_minutes' must be greater than zero")
|
|
|
|
return {
|
|
"start_time": parse_time(data["start_time"]),
|
|
"end_time": parse_time(data["end_time"]),
|
|
"slot_minutes": slot_minutes,
|
|
"crews": clean_crews,
|
|
}
|
|
|
|
|
|
def generate_time_slots(start: time, end: time, slot_minutes: int) -> list[str]:
|
|
start_dt = datetime.combine(date.today(), start)
|
|
end_dt = datetime.combine(date.today(), end)
|
|
|
|
if end_dt <= start_dt:
|
|
raise ValueError("'end_time' must be later than 'start_time'")
|
|
|
|
slots: list[str] = []
|
|
current = start_dt
|
|
|
|
while current < end_dt:
|
|
next_slot = current + timedelta(minutes=slot_minutes)
|
|
slots.append(
|
|
f"{current.strftime('%I:%M %p')} - {next_slot.strftime('%I:%M %p')}"
|
|
)
|
|
current = next_slot
|
|
|
|
return slots
|
|
|
|
|
|
def apply_page_setup(worksheet) -> None:
|
|
worksheet.page_setup.orientation = "landscape"
|
|
worksheet.page_setup.fitToWidth = 1
|
|
worksheet.page_setup.fitToHeight = 0
|
|
worksheet.print_options.horizontalCentered = True
|
|
worksheet.sheet_view.showGridLines = False
|
|
worksheet.freeze_panes = "B4"
|
|
|
|
|
|
def style_sheet(worksheet, schedule_date: date, crews: list[str], time_slots: list[str]) -> None:
|
|
thin_side = Side(style="thin", color="000000")
|
|
border = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side)
|
|
|
|
title_fill = PatternFill(fill_type="solid", fgColor="D9EAF7")
|
|
header_fill = PatternFill(fill_type="solid", fgColor="EDEDED")
|
|
|
|
title_font = Font(size=18, bold=True)
|
|
header_font = Font(size=11, bold=True)
|
|
cell_font = Font(size=10)
|
|
|
|
center = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
|
top_wrap = Alignment(horizontal="left", vertical="top", wrap_text=True)
|
|
|
|
sheet_title = schedule_date.strftime("%A").upper()
|
|
sheet_date = schedule_date.strftime("%B %d, %Y")
|
|
|
|
last_col = 1 + len(crews)
|
|
last_col_letter = get_column_letter(last_col)
|
|
|
|
worksheet.merge_cells(f"A1:{last_col_letter}1")
|
|
worksheet["A1"] = f"{sheet_title} - {sheet_date}"
|
|
worksheet["A1"].font = title_font
|
|
worksheet["A1"].alignment = center
|
|
worksheet["A1"].fill = title_fill
|
|
worksheet["A1"].border = border
|
|
worksheet.row_dimensions[1].height = 28
|
|
|
|
worksheet["A3"] = "Time Slot"
|
|
worksheet["A3"].font = header_font
|
|
worksheet["A3"].alignment = center
|
|
worksheet["A3"].fill = header_fill
|
|
worksheet["A3"].border = border
|
|
|
|
for crew_index, crew_name in enumerate(crews, start=2):
|
|
cell = worksheet.cell(row=3, column=crew_index)
|
|
cell.value = crew_name
|
|
cell.font = header_font
|
|
cell.alignment = center
|
|
cell.fill = header_fill
|
|
cell.border = border
|
|
|
|
worksheet.column_dimensions["A"].width = 18
|
|
|
|
for crew_index in range(2, last_col + 1):
|
|
worksheet.column_dimensions[get_column_letter(crew_index)].width = 28
|
|
|
|
row_index = 4
|
|
for slot in time_slots:
|
|
time_cell = worksheet.cell(row=row_index, column=1)
|
|
time_cell.value = slot
|
|
time_cell.font = header_font
|
|
time_cell.alignment = center
|
|
time_cell.border = border
|
|
|
|
for crew_index in range(2, last_col + 1):
|
|
cell = worksheet.cell(row=row_index, column=crew_index)
|
|
cell.value = ""
|
|
cell.font = cell_font
|
|
cell.alignment = top_wrap
|
|
cell.border = border
|
|
|
|
worksheet.row_dimensions[row_index].height = 70
|
|
row_index += 1
|
|
|
|
apply_page_setup(worksheet)
|
|
|
|
|
|
def build_workbook(monday: date, crews: list[str], time_slots: list[str]) -> Workbook:
|
|
workbook = Workbook()
|
|
default_sheet = workbook.active
|
|
workbook.remove(default_sheet)
|
|
|
|
for day_offset in range(5):
|
|
schedule_date = monday + timedelta(days=day_offset)
|
|
sheet_name = schedule_date.strftime("%A")
|
|
worksheet = workbook.create_sheet(title=sheet_name)
|
|
style_sheet(worksheet, schedule_date, crews, time_slots)
|
|
|
|
return workbook
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
input_date = parse_date(sys.argv[1]) if len(sys.argv) > 1 else date.today()
|
|
monday = get_monday(input_date)
|
|
|
|
config = load_config(CONFIG_FILE)
|
|
time_slots = generate_time_slots(
|
|
config["start_time"],
|
|
config["end_time"],
|
|
config["slot_minutes"],
|
|
)
|
|
|
|
workbook = build_workbook(monday, config["crews"], time_slots)
|
|
|
|
output_path = Path(f"weekly_schedule_{monday.isoformat()}.xlsx")
|
|
workbook.save(output_path)
|
|
|
|
print(f"Created: {output_path.resolve()}")
|
|
return 0
|
|
|
|
except Exception as exc:
|
|
print(f"Error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|