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