|
|
@@ -532,24 +532,94 @@ def applescript_calendar_missing(stderr: str) -> bool:
|
|
|
|
|
|
# ---------- Ausgabe ----------
|
|
|
|
|
|
+ANSI_GREEN = "\033[32m"
|
|
|
+ANSI_RED = "\033[31m"
|
|
|
+ANSI_ORANGE = "\033[38;5;208m"
|
|
|
+ANSI_RESET = "\033[0m"
|
|
|
+
|
|
|
+WEEKLY_OVERTIME_THRESHOLD = 2880 # 48 h in Minuten
|
|
|
+
|
|
|
+
|
|
|
+def format_duration(minutes: int) -> str:
|
|
|
+ hours, mins = divmod(int(minutes), 60)
|
|
|
+ return f"{hours}h {mins}min"
|
|
|
+
|
|
|
+
|
|
|
+def duration_color(minutes: int) -> str | None:
|
|
|
+ if minutes > 600:
|
|
|
+ return ANSI_RED
|
|
|
+ if minutes >= 480:
|
|
|
+ return ANSI_GREEN
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _overloaded_week_dates(events: list[dict]) -> set[date]:
|
|
|
+ """Dates belonging to ISO weeks whose combined minutes exceed 2880 (48 h)."""
|
|
|
+ weekly: dict[tuple[int, int], int] = defaultdict(int)
|
|
|
+ for ev in events:
|
|
|
+ iso = ev["date"].isocalendar()
|
|
|
+ weekly[(iso[0], iso[1])] += ev["total_minutes"]
|
|
|
+ overloaded = {k for k, v in weekly.items() if v > WEEKLY_OVERTIME_THRESHOLD}
|
|
|
+ return {ev["date"] for ev in events if (ev["date"].isocalendar()[0], ev["date"].isocalendar()[1]) in overloaded}
|
|
|
+
|
|
|
+
|
|
|
+def _print_legend() -> None:
|
|
|
+ print("Legende:")
|
|
|
+ print(f" Dauer: {ANSI_GREEN}■{ANSI_RESET} grün ≥ 8 h (≥ 480 min) "
|
|
|
+ f"{ANSI_RED}■{ANSI_RESET} rot > 10 h (> 600 min)")
|
|
|
+ print(f" Datum: {ANSI_ORANGE}■{ANSI_RESET} orange = Wochensumme > 48 h (> 2880 min)")
|
|
|
+ print()
|
|
|
+
|
|
|
+
|
|
|
def print_dry_run_table(events: list[dict], protocol: dict) -> None:
|
|
|
- rows = [("Zustand", "Datum", "Projekt", "Start", "Ende", "Min")]
|
|
|
+ overloaded_dates = _overloaded_week_dates(events)
|
|
|
+
|
|
|
+ header = ("Zustand", "Datum", "Projekt", "Start", "Ende", "Dauer")
|
|
|
+ data_rows: list[tuple[tuple[str, ...], int, date]] = []
|
|
|
for ev in events:
|
|
|
state = determine_state(ev, protocol).upper()
|
|
|
- rows.append((
|
|
|
- state,
|
|
|
- ev["date"].isoformat(),
|
|
|
- ev["project_name"],
|
|
|
- ev["start_time"].strftime("%H:%M"),
|
|
|
- ev["end_time"].strftime("%H:%M"),
|
|
|
- str(ev["total_minutes"]),
|
|
|
+ data_rows.append((
|
|
|
+ (
|
|
|
+ state,
|
|
|
+ ev["date"].isoformat(),
|
|
|
+ ev["project_name"],
|
|
|
+ ev["start_time"].strftime("%H:%M"),
|
|
|
+ ev["end_time"].strftime("%H:%M"),
|
|
|
+ format_duration(ev["total_minutes"]),
|
|
|
+ ),
|
|
|
+ ev["total_minutes"],
|
|
|
+ ev["date"],
|
|
|
))
|
|
|
- widths = [max(len(r[i]) for r in rows) for i in range(len(rows[0]))]
|
|
|
- line = " ".join(c.ljust(widths[i]) for i, c in enumerate(rows[0]))
|
|
|
- print(line)
|
|
|
- print("-" * len(line))
|
|
|
- for r in rows[1:]:
|
|
|
- print(" ".join(c.ljust(widths[i]) for i, c in enumerate(r)))
|
|
|
+
|
|
|
+ all_rows = [header] + [r for r, _, _ in data_rows]
|
|
|
+ widths = [max(len(r[i]) for r in all_rows) for i in range(len(header))]
|
|
|
+
|
|
|
+ _print_legend()
|
|
|
+
|
|
|
+ table_width = sum(widths) + 2 * (len(widths) - 1)
|
|
|
+ header_line = " ".join(c.ljust(widths[i]) for i, c in enumerate(header))
|
|
|
+ week_sep = "·" * table_width
|
|
|
+ print(header_line)
|
|
|
+ print("-" * table_width)
|
|
|
+ date_idx = 1
|
|
|
+ duration_idx = len(header) - 1
|
|
|
+ prev_iso_week: tuple[int, int] | None = None
|
|
|
+ for row, minutes, ev_date in data_rows:
|
|
|
+ iso_week = (ev_date.isocalendar()[0], ev_date.isocalendar()[1])
|
|
|
+ if prev_iso_week is not None and iso_week != prev_iso_week:
|
|
|
+ print(week_sep)
|
|
|
+ prev_iso_week = iso_week
|
|
|
+ cells = []
|
|
|
+ for i, c in enumerate(row):
|
|
|
+ padded = c.ljust(widths[i])
|
|
|
+ if i == duration_idx:
|
|
|
+ color = duration_color(minutes)
|
|
|
+ if color:
|
|
|
+ padded = f"{color}{padded}{ANSI_RESET}"
|
|
|
+ elif i == date_idx and ev_date in overloaded_dates:
|
|
|
+ padded = f"{ANSI_ORANGE}{padded}{ANSI_RESET}"
|
|
|
+ cells.append(padded)
|
|
|
+ print(" ".join(cells))
|
|
|
|
|
|
|
|
|
# ---------- Sync-Ausführung ----------
|