import json from datetime import date from io import BytesIO from openpyxl import Workbook from reportlab.lib.pagesizes import A4, landscape from reportlab.pdfgen import canvas from app.services.calculations import minutes_to_hhmm from app.services.targets import monday_of def create_excel_export(rows: list[dict], week_summaries: list[dict], totals: dict, title: str) -> bytes: workbook = Workbook() sheet = workbook.active sheet.title = "Tage" headers = [ "Datum", "Wochentag", "KW", "Start", "Ende", "Pause (min)", "Brutto", "Netto", "Stundenausgleich", "Sonderstatus", "Wochen-Soll", "Wochen-Delta", "Notiz", ] sheet.append(headers) for row in rows: sheet.append( [ row["date"].isoformat(), row["weekday_name"], row["iso_week"], row["start_time"] or "", row["end_time"] or "", row["break_minutes"], minutes_to_hhmm(row["gross_minutes"]), minutes_to_hhmm(row["net_minutes"]), minutes_to_hhmm(row["overtime_adjustment_minutes"]), row["special_status_label"] or "", minutes_to_hhmm(row["weekly_target_minutes"]), minutes_to_hhmm(row["weekly_delta_minutes"]), row["notes"] or "", ] ) for col in ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"]: sheet.column_dimensions[col].width = 16 summary = workbook.create_sheet("Wochen") summary_headers = ["KW-Start", "KW-Ende", "Ist", "Soll", "Delta"] summary.append(summary_headers) for item in week_summaries: summary.append( [ item["week_start"].isoformat(), item["week_end"].isoformat(), minutes_to_hhmm(item["ist_minutes"]), minutes_to_hhmm(item["soll_minutes"]), minutes_to_hhmm(item["delta_minutes"]), ] ) summary.append([]) summary.append(["Gesamt", "", minutes_to_hhmm(totals["ist_minutes"]), "", minutes_to_hhmm(totals["delta_minutes"])]) meta = workbook.create_sheet("Meta") meta.append([title]) meta.append([f"Zeitraum: {totals['from_date'].isoformat()} bis {totals['to_date'].isoformat()}"]) output = BytesIO() workbook.save(output) return output.getvalue() def create_pdf_export(rows: list[dict], week_summaries: list[dict], totals: dict, title: str) -> bytes: output = BytesIO() pdf = canvas.Canvas(output, pagesize=landscape(A4)) width, height = landscape(A4) y = height - 35 pdf.setFont("Helvetica-Bold", 13) pdf.drawString(24, y, title) y -= 18 pdf.setFont("Helvetica", 10) pdf.drawString(24, y, f"Zeitraum: {totals['from_date'].isoformat()} bis {totals['to_date'].isoformat()}") y -= 24 pdf.setFont("Helvetica-Bold", 8) pdf.drawString(24, y, "Datum") pdf.drawString(88, y, "Tag") pdf.drawString(124, y, "KW") pdf.drawString(154, y, "Start") pdf.drawString(198, y, "Ende") pdf.drawString(242, y, "Pause") pdf.drawString(286, y, "Brutto") pdf.drawString(338, y, "Netto") pdf.drawString(390, y, "Ausgl.") pdf.drawString(436, y, "Status") pdf.drawString(490, y, "Soll") pdf.drawString(542, y, "W-Delta") pdf.drawString(610, y, "Notiz") y -= 12 pdf.setFont("Helvetica", 8) for row in rows: if y < 40: pdf.showPage() y = height - 30 pdf.setFont("Helvetica", 8) note = (row["notes"] or "").strip() if len(note) > 18: note = f"{note[:15]}..." pdf.drawString(24, y, row["date"].isoformat()) pdf.drawString(88, y, row["weekday_short"]) pdf.drawString(124, y, str(row["iso_week"])) pdf.drawString(154, y, row["start_time"] or "-") pdf.drawString(198, y, row["end_time"] or "-") pdf.drawString(242, y, str(row["break_minutes"])) pdf.drawString(286, y, minutes_to_hhmm(row["gross_minutes"])) pdf.drawString(338, y, minutes_to_hhmm(row["net_minutes"])) pdf.drawString(390, y, minutes_to_hhmm(row["overtime_adjustment_minutes"])) pdf.drawString(436, y, row["special_status_label"] or "-") pdf.drawString(490, y, minutes_to_hhmm(row["weekly_target_minutes"])) pdf.drawString(542, y, minutes_to_hhmm(row["weekly_delta_minutes"])) pdf.drawString(610, y, note) y -= 11 y -= 12 pdf.setFont("Helvetica-Bold", 10) pdf.drawString(24, y, "Wochenzusammenfassung") y -= 14 pdf.setFont("Helvetica", 9) for item in week_summaries: if y < 40: pdf.showPage() y = height - 30 pdf.setFont("Helvetica", 9) line = ( f"{item['week_start'].isoformat()} - {item['week_end'].isoformat()} | " f"Ist {minutes_to_hhmm(item['ist_minutes'])} | " f"Soll {minutes_to_hhmm(item['soll_minutes'])} | " f"Delta {minutes_to_hhmm(item['delta_minutes'])}" ) pdf.drawString(24, y, line) y -= 12 y -= 10 pdf.setFont("Helvetica-Bold", 10) pdf.drawString( 24, y, f"Gesamt Ist: {minutes_to_hhmm(totals['ist_minutes'])} | Gesamt Delta: {minutes_to_hhmm(totals['delta_minutes'])}", ) pdf.save() return output.getvalue() def create_backup_export(payload: dict) -> bytes: return json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8") def build_export_rows( days: list[date], entries_by_date: dict, week_target_map: dict[date, int], week_ist_map: dict[date, int], week_delta_map: dict[date, int], special_status_map: dict[date, str] | None = None, overtime_adjustment_map: dict[date, int] | None = None, ) -> list[dict]: weekday_names = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] weekday_short = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] special_status_map = special_status_map or {} overtime_adjustment_map = overtime_adjustment_map or {} special_status_labels = { "holiday": "Feiertag", "sick": "Krankheit", } rows: list[dict] = [] for day in days: entry = entries_by_date.get(day) week_start = monday_of(day) weekly_target = week_target_map[week_start] weekly_delta = week_delta_map[week_start] if entry: gross = entry.end_minutes - entry.start_minutes net = gross - entry.break_minutes start_time = f"{entry.start_minutes // 60:02d}:{entry.start_minutes % 60:02d}" end_time = f"{entry.end_minutes // 60:02d}:{entry.end_minutes % 60:02d}" break_minutes = entry.break_minutes notes = entry.notes else: gross = 0 net = 0 start_time = None end_time = None break_minutes = 0 notes = None rows.append( { "date": day, "weekday_name": weekday_names[day.weekday()], "weekday_short": weekday_short[day.weekday()], "iso_week": day.isocalendar()[1], "start_time": start_time, "end_time": end_time, "break_minutes": break_minutes, "gross_minutes": gross, "net_minutes": net, "overtime_adjustment_minutes": overtime_adjustment_map.get(day, 0), "special_status_label": special_status_labels.get(special_status_map.get(day, "")), "weekly_target_minutes": weekly_target, "weekly_delta_minutes": weekly_delta, "notes": notes, } ) return rows