From ee4297e1ebdbc712272bb797c416fa3e12c2b83c Mon Sep 17 00:00:00 2001 From: Michael M Date: Tue, 10 Mar 2026 11:55:30 -0400 Subject: [PATCH] init --- README.md | 233 ++++++++++++++++++++++++++++++++- config.json | 10 ++ generate_week_schedule_xlsx.py | 205 +++++++++++++++++++++++++++++ 3 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 config.json create mode 100644 generate_week_schedule_xlsx.py diff --git a/README.md b/README.md index 96f06f8..b4a078f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,233 @@ -# CrewChronicle +# Weekly Crew Schedule Generator +A small Python utility that generates a **weekly crew scheduling workbook** (`.xlsx`) for **Monday–Friday**. +Each day gets its own sheet with **time slots** and **one column per crew lead / supervisor**. + +The workbook is designed to be: + +* **Easy to edit** +* **Clean when printed** +* **Simple to export to PDF** + +This is useful for scheduling **customer visits, job assignments, or crew work plans**. + +--- + +# Features + +* Generates **one sheet per weekday** + + * Monday + * Tuesday + * Wednesday + * Thursday + * Friday + +* Configurable via `config.json` + + * Start time + * End time + * Slot size (minutes) + * Crew / supervisor names + +* Default schedule: + + * **08:00 → 17:00** + * **60 minute slots** + +* Clean printable layout + +* Works with: + + * **LibreOffice Calc** + * **Microsoft Excel** + * **OnlyOffice** + +* Easily **exportable to PDF** + +--- + +# Example Layout + +``` +----------------------------------------------------------- +| Time Slot | Crew 1 | Crew 2 | Crew 3 | Crew 4 | Crew 5 | +----------------------------------------------------------- +| 08:00-09:00 | | | | | | +| 09:00-10:00 | | | | | | +| 10:00-11:00 | | | | | | +| ... | +----------------------------------------------------------- +``` + +Each **row** represents a time slot. +Each **column** represents a crew lead or supervisor. + +You can write things like: + +``` +Customer Name +Address +Notes +``` + +inside each cell. + +--- + +# Requirements + +Python **3.8+** + +Python package: + +``` +openpyxl +``` + +Install it with: + +``` +pip install openpyxl +``` + +--- + +# Project Structure + +``` +crew_schedule/ +│ +├─ generate_week_schedule_xlsx.py +├─ config.json +├─ README.md +``` + +--- + +# Configuration + +Edit `config.json` to define crews and schedule hours. + +Example: + +```json +{ + "start_time": "08:00", + "end_time": "17:00", + "slot_minutes": 60, + "crews": [ + "Crew 1", + "Crew 2", + "Crew 3" + ] +} +``` + +## Example With Named Supervisors + +```json +{ + "start_time": "07:00", + "end_time": "16:00", + "slot_minutes": 60, + "crews": [ + "Mike", + "Sara", + "John", + "Chris", + "Dana" + ] +} +``` + +--- + +# Usage + +Generate the schedule for the **current week**: + +``` +python generate_week_schedule_xlsx.py +``` + +Generate a specific week: + +``` +python generate_week_schedule_xlsx.py 2026-03-09 +``` + +The script automatically aligns the provided date to **Monday**. + +--- + +# Output + +Example output file: + +``` +weekly_schedule_2026-03-09.xlsx +``` + +Inside the workbook: + +``` +Monday +Tuesday +Wednesday +Thursday +Friday +``` + +Each sheet contains: + +* Time slots +* Crew columns +* Large writable cells + +--- + +# Exporting to PDF + +### LibreOffice + +1. Open the `.xlsx` file +2. Fill in the schedule +3. Click: + +``` +File → Export As → Export as PDF +``` + +### Microsoft Excel + +``` +File → Export → Create PDF/XPS +``` + +--- + +# Customization Ideas + +Possible improvements: + +* Color coding for crews +* Customer / Supervisor / Notes sub-lines +* Automatic job numbering +* Multiple weeks generated at once +* Direct PDF generation +* ICS calendar export + +--- + +# License + +MIT License + +Use freely, modify freely. + +--- + +# Author + +Internal scheduling utility designed for **crew-based weekly planning**. diff --git a/config.json b/config.json new file mode 100644 index 0000000..743df6b --- /dev/null +++ b/config.json @@ -0,0 +1,10 @@ +{ + "start_time": "08:00", + "end_time": "17:00", + "slot_minutes": 60, + "crews": [ + "James S", + "Noah B", + "Stephen" + ] +} diff --git a/generate_week_schedule_xlsx.py b/generate_week_schedule_xlsx.py new file mode 100644 index 0000000..e4f3067 --- /dev/null +++ b/generate_week_schedule_xlsx.py @@ -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())