diff --git a/backend/alembic/versions/007_add_report_type.py b/backend/alembic/versions/007_add_report_type.py new file mode 100644 index 0000000..f6d5c04 --- /dev/null +++ b/backend/alembic/versions/007_add_report_type.py @@ -0,0 +1,35 @@ +"""Add report_type column to weekly_reports. + +Revision ID: 007_add_report_type +Revises: 006_anonymize_fall_ids +""" + +from alembic import op +import sqlalchemy as sa + +revision = "007_add_report_type" +down_revision = "006_anonymize_fall_ids" +branch_labels = None +depends_on = None + + +def upgrade(): + # Add report_type column with default 'gesamt' + op.add_column( + "weekly_reports", + sa.Column("report_type", sa.String(50), nullable=False, server_default="gesamt"), + ) + + # Drop old unique constraint (jahr, kw) + op.drop_constraint("uk_jahr_kw", "weekly_reports", type_="unique") + + # Add new unique constraint (jahr, kw, report_type) + op.create_unique_constraint( + "uk_jahr_kw_type", "weekly_reports", ["jahr", "kw", "report_type"] + ) + + +def downgrade(): + op.drop_constraint("uk_jahr_kw_type", "weekly_reports", type_="unique") + op.drop_column("weekly_reports", "report_type") + op.create_unique_constraint("uk_jahr_kw", "weekly_reports", ["jahr", "kw"]) diff --git a/backend/app/api/reports.py b/backend/app/api/reports.py index d45761a..50cc326 100644 --- a/backend/app/api/reports.py +++ b/backend/app/api/reports.py @@ -80,14 +80,21 @@ def weekly_report( def generate_report( jahr: int | None = Query(None), kw: int | None = Query(None), + report_type: str = Query("gesamt"), db: Session = Depends(get_db), user: User = Depends(require_admin), ): """Generate a full Berichtswesen Excel report and persist it to disk + DB. Admin only. Defaults to the current ISO year/week if not specified. - Depends on report_service, excel_export, and vorjahr_service (parallel tasks). + Accepts *report_type* to generate filtered reports (``onko_intensiv``, + ``galle_schild``, or the default ``gesamt``). """ + from app.services.report_service import REPORT_TYPES, REPORT_TYPE_LABELS + + if report_type not in REPORT_TYPES: + raise HTTPException(422, f"Unknown report_type: {report_type}") + if not jahr: from app.utils.kw_utils import date_to_jahr, date_to_kw @@ -105,9 +112,14 @@ def generate_report( from app.services.report_service import generate_full_report from app.services.vorjahr_service import get_vorjahr_summary - report_data = generate_full_report(db, jahr, kw) - vorjahr = get_vorjahr_summary(db, jahr) - xlsx_bytes = generate_berichtswesen_xlsx(report_data, jahr, vorjahr) + fallgruppen = list(REPORT_TYPES[report_type]) + report_data = generate_full_report(db, jahr, kw, report_type=report_type) + + # Vorjahr data: use cached summary for gesamt, live calculation for filtered + vorjahr = get_vorjahr_summary(db, jahr) if report_type == "gesamt" else None + xlsx_bytes = generate_berichtswesen_xlsx( + report_data, jahr, vorjahr, fallgruppen=fallgruppen, + ) # Persist Excel file to disk reports_dir = os.path.join( @@ -115,15 +127,23 @@ def generate_report( "reports", ) os.makedirs(reports_dir, exist_ok=True) - filename = f"Berichtswesen_{jahr}_KW{kw:02d}.xlsx" + type_label = REPORT_TYPE_LABELS.get(report_type, report_type) + if report_type != "gesamt": + filename = f"Berichtswesen_{type_label}_{jahr}_KW{kw:02d}.xlsx" + else: + filename = f"Berichtswesen_{jahr}_KW{kw:02d}.xlsx" filepath = os.path.join(reports_dir, filename) with open(filepath, "wb") as f: f.write(xlsx_bytes) - # Upsert report metadata (replace if same jahr/kw exists) + # Upsert report metadata (replace if same jahr/kw/report_type exists) report = ( db.query(WeeklyReport) - .filter(WeeklyReport.jahr == jahr, WeeklyReport.kw == kw) + .filter( + WeeklyReport.jahr == jahr, + WeeklyReport.kw == kw, + WeeklyReport.report_type == report_type, + ) .first() ) if report: @@ -135,6 +155,7 @@ def generate_report( report = WeeklyReport( jahr=jahr, kw=kw, + report_type=report_type, report_date=date.today(), report_data=report_data, generated_by=user.id, @@ -151,7 +172,7 @@ def generate_report( action="report_generated", entity_type="report", entity_id=report.id, - new_values={"jahr": jahr, "kw": kw, "filename": filename}, + new_values={"jahr": jahr, "kw": kw, "report_type": report_type, "filename": filename}, ) return ReportMeta.model_validate(report) diff --git a/backend/app/models/report.py b/backend/app/models/report.py index 1912291..48f412d 100644 --- a/backend/app/models/report.py +++ b/backend/app/models/report.py @@ -28,6 +28,9 @@ class WeeklyReport(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) jahr: Mapped[int] = mapped_column(SmallInteger, nullable=False) kw: Mapped[int] = mapped_column(SmallInteger, nullable=False) + report_type: Mapped[str] = mapped_column( + String(50), nullable=False, server_default="gesamt" + ) report_date: Mapped[dt.date] = mapped_column(Date, nullable=False) report_file_path: Mapped[Optional[str]] = mapped_column( String(500), nullable=True @@ -46,7 +49,7 @@ class WeeklyReport(Base): ) __table_args__ = ( - UniqueConstraint("jahr", "kw", name="uk_jahr_kw"), + UniqueConstraint("jahr", "kw", "report_type", name="uk_jahr_kw_type"), ) diff --git a/backend/app/schemas/report.py b/backend/app/schemas/report.py index e3f4f95..8ee7157 100644 --- a/backend/app/schemas/report.py +++ b/backend/app/schemas/report.py @@ -40,6 +40,7 @@ class ReportMeta(BaseModel): id: int jahr: int kw: int + report_type: str = "gesamt" report_date: date generated_at: datetime generated_by: Optional[int] = None diff --git a/backend/app/services/excel_export.py b/backend/app/services/excel_export.py index 7b72164..e686189 100644 --- a/backend/app/services/excel_export.py +++ b/backend/app/services/excel_export.py @@ -49,6 +49,7 @@ def generate_berichtswesen_xlsx( report_data: dict[str, Any], jahr: int, vorjahr_data: dict[str, Any] | None = None, + fallgruppen: list[str] | None = None, ) -> bytes: """Generate a Berichtswesen Excel file. @@ -59,19 +60,23 @@ def generate_berichtswesen_xlsx( vorjahr_data: Previous-year summary data for the year-over-year comparison block on Sheet 1. Structure same as *report_data* ``sheet1`` (i.e. containing ``totals`` and ``weeks``). + fallgruppen: Subset of Fallgruppen for filtered reports. + If *None*, all 5 Fallgruppen are used. Returns: The ``.xlsx`` file contents as *bytes*. """ + fgs = fallgruppen or FALLGRUPPEN wb = Workbook() _write_sheet1_kw_gesamt(wb, report_data.get("sheet1", {}), jahr, vorjahr_data) - _write_sheet2_fachgebiete(wb, report_data.get("sheet2", {}), jahr) - _write_sheet3_gutachten(wb, report_data.get("sheet3", {}), jahr) - # ICD codes live inside sheet5 - sheet5 = report_data.get("sheet5", {}) - icd_codes = sheet5.get("icd_codes", []) if isinstance(sheet5, dict) else [] - _write_sheet4_icd_onko(wb, icd_codes, jahr) + _write_sheet2_fachgebiete(wb, report_data.get("sheet2", {}), jahr, fgs) + _write_sheet3_gutachten(wb, report_data.get("sheet3", {}), jahr, fgs) + # ICD codes live inside sheet5 — only relevant if onko is included + if "onko" in fgs: + sheet5 = report_data.get("sheet5", {}) + icd_codes = sheet5.get("icd_codes", []) if isinstance(sheet5, dict) else [] + _write_sheet4_icd_onko(wb, icd_codes, jahr) # Remove the default empty sheet created by Workbook() if "Sheet" in wb.sheetnames: @@ -251,16 +256,18 @@ def _write_sheet2_fachgebiete( wb: Workbook, data: dict[str, Any], jahr: int, + fallgruppen: list[str] | None = None, ) -> None: """Write the 'Auswertung nach Fachgebieten' sheet. Layout: Row 1: A1 = "Uebersicht nach Fallgruppen" - Row 3: Merged group headers (B3:D3, E3:G3, H3:J3, K3:M3, N3:P3) - Row 4: Sub-headers: KW | Anzahl | Gutachten | Keine RM/Ablehnung (x5) + Row 3: Merged group headers (dynamic based on fallgruppen) + Row 4: Sub-headers: KW | Anzahl | Gutachten | Keine RM/Ablehnung (per fg) Row 5-56: KW 1-52 data Row 57: Summe row """ + fgs = fallgruppen or FALLGRUPPEN ws = wb.create_sheet(title="Auswertung nach Fachgebieten") weeks = _weeks_lookup(data.get("weekly", [])) @@ -270,8 +277,9 @@ def _write_sheet2_fachgebiete( ws["A1"].font = TITLE_FONT # --- Group headers (row 3) with merged cells --- - group_start_cols = [2, 5, 8, 11, 14] # B, E, H, K, N - for fg_key, start_col in zip(FALLGRUPPEN, group_start_cols): + group_start_cols = [2 + i * 3 for i in range(len(fgs))] + max_col = group_start_cols[-1] + 2 if group_start_cols else 1 + for fg_key, start_col in zip(fgs, group_start_cols): label = FALLGRUPPEN_LABELS[fg_key] cell = ws.cell(row=3, column=start_col, value=label) cell.fill = HEADER_FILL @@ -293,17 +301,17 @@ def _write_sheet2_fachgebiete( ws.cell(row=4, column=start_col, value="Anzahl") ws.cell(row=4, column=start_col + 1, value="Gutachten") ws.cell(row=4, column=start_col + 2, value="Keine R\u00fcckmeldung/Ablehnung") - _apply_header_style(ws, 4, 1, 16) + _apply_header_style(ws, 4, 1, max_col) # --- Weekly data (rows 5-56) --- - sums = {fg: {"anzahl": 0, "gutachten": 0, "keine_rm": 0} for fg in FALLGRUPPEN} + sums = {fg: {"anzahl": 0, "gutachten": 0, "keine_rm": 0} for fg in fgs} for kw in range(1, MAX_KW + 1): row = 4 + kw # row 5 = KW 1 w = weeks.get(kw, {}) ws.cell(row=row, column=1, value=kw) - for fg_key, start_col in zip(FALLGRUPPEN, group_start_cols): + for fg_key, start_col in zip(fgs, group_start_cols): fg_data = w.get(fg_key, {}) anz = _safe(fg_data.get("anzahl")) ga = _safe(fg_data.get("gutachten")) @@ -319,11 +327,11 @@ def _write_sheet2_fachgebiete( # --- Summe row (row 57) --- summe_row = 4 + MAX_KW + 1 # 57 - for fg_key, start_col in zip(FALLGRUPPEN, group_start_cols): + for fg_key, start_col in zip(fgs, group_start_cols): ws.cell(row=summe_row, column=start_col, value=sums[fg_key]["anzahl"]) ws.cell(row=summe_row, column=start_col + 1, value=sums[fg_key]["gutachten"]) ws.cell(row=summe_row, column=start_col + 2, value=sums[fg_key]["keine_rm"]) - _apply_header_style(ws, summe_row, 1, 16) + _apply_header_style(ws, summe_row, 1, max_col) # --- Column widths --- ws.column_dimensions["A"].width = 6 @@ -341,16 +349,18 @@ def _write_sheet3_gutachten( wb: Workbook, data: dict[str, Any], jahr: int, + fallgruppen: list[str] | None = None, ) -> None: """Write the 'Auswertung Gutachten' sheet. Layout: Row 1: A1 = "Uebersicht nach Fallgruppen" - Row 3: Group headers: Gesamt (B3) + 5 Fallgruppen - Row 4: Sub-headers: KW | Gutachten | Alternative | Bestaetigung (x6) + Row 3: Group headers: Gesamt (B3) + Fallgruppen (dynamic) + Row 4: Sub-headers: KW | Gutachten | Alternative | Bestaetigung (per group) Row 5-56: KW 1-52 data Row 57: Summe row """ + fgs = fallgruppen or FALLGRUPPEN ws = wb.create_sheet(title="Auswertung Gutachten") weeks = _weeks_lookup(data.get("weekly", [])) @@ -360,22 +370,21 @@ def _write_sheet3_gutachten( ws["A1"].font = TITLE_FONT # --- Group headers (row 3) --- - # Gesamt: B3 (no merge since it's a single-column-start group header, - # but in the reference the Gesamt label sits in B3 without a merge) + # Gesamt: B3 cell = ws.cell(row=3, column=2, value="Gesamt") cell.fill = HEADER_FILL cell.font = HEADER_FONT cell.alignment = Alignment(horizontal="center") - # Fallgruppen start at columns E, H, K, N, Q (each 3 cols wide) - fg_start_cols = [5, 8, 11, 14, 17] - for fg_key, start_col in zip(FALLGRUPPEN, fg_start_cols): + # Fallgruppen start at column 5, each 3 cols wide + fg_start_cols = [5 + i * 3 for i in range(len(fgs))] + max_col = fg_start_cols[-1] + 2 if fg_start_cols else 4 + for fg_key, start_col in zip(fgs, fg_start_cols): label = FALLGRUPPEN_LABELS[fg_key] cell = ws.cell(row=3, column=start_col, value=label) cell.fill = HEADER_FILL cell.font = HEADER_FONT cell.alignment = Alignment(horizontal="center") - # Merge: the reference merges first 2 of 3 columns (E3:F3, H3:I3, etc.) end_col = start_col + 1 ws.merge_cells( start_row=3, start_column=start_col, @@ -396,11 +405,11 @@ def _write_sheet3_gutachten( ws.cell(row=4, column=start_col, value="Gutachten") ws.cell(row=4, column=start_col + 1, value="Alternative") ws.cell(row=4, column=start_col + 2, value="Best\u00e4tigung") - _apply_header_style(ws, 4, 1, 19) + _apply_header_style(ws, 4, 1, max_col) # --- Weekly data (rows 5-56) --- sums_gesamt = {"gutachten": 0, "alternative": 0, "bestaetigung": 0} - sums_fg = {fg: {"gutachten": 0, "alternative": 0, "bestaetigung": 0} for fg in FALLGRUPPEN} + sums_fg = {fg: {"gutachten": 0, "alternative": 0, "bestaetigung": 0} for fg in fgs} for kw in range(1, MAX_KW + 1): row = 4 + kw @@ -420,7 +429,7 @@ def _write_sheet3_gutachten( sums_gesamt["bestaetigung"] += g_best # Per Fallgruppe - for fg_key, start_col in zip(FALLGRUPPEN, fg_start_cols): + for fg_key, start_col in zip(fgs, fg_start_cols): fg_data = w.get(fg_key, {}) ga = _safe(fg_data.get("gutachten")) alt = _safe(fg_data.get("alternative")) @@ -437,15 +446,15 @@ def _write_sheet3_gutachten( ws.cell(row=summe_row, column=2, value=sums_gesamt["gutachten"]) ws.cell(row=summe_row, column=3, value=sums_gesamt["alternative"]) ws.cell(row=summe_row, column=4, value=sums_gesamt["bestaetigung"]) - for fg_key, start_col in zip(FALLGRUPPEN, fg_start_cols): + for fg_key, start_col in zip(fgs, fg_start_cols): ws.cell(row=summe_row, column=start_col, value=sums_fg[fg_key]["gutachten"]) ws.cell(row=summe_row, column=start_col + 1, value=sums_fg[fg_key]["alternative"]) ws.cell(row=summe_row, column=start_col + 2, value=sums_fg[fg_key]["bestaetigung"]) - _apply_header_style(ws, summe_row, 1, 19) + _apply_header_style(ws, summe_row, 1, max_col) # --- Column widths --- ws.column_dimensions["A"].width = 6 - for col in range(2, 20): + for col in range(2, max_col + 1): ws.column_dimensions[get_column_letter(col)].width = 12 diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index 88a63fc..befba11 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -27,6 +27,19 @@ logger = logging.getLogger(__name__) # Canonical Fallgruppen in display order FALLGRUPPEN = ("onko", "kardio", "intensiv", "galle", "sd") +# Report type definitions: each maps to a subset of Fallgruppen +REPORT_TYPES: dict[str, tuple[str, ...]] = { + "gesamt": FALLGRUPPEN, + "onko_intensiv": ("onko", "intensiv"), + "galle_schild": ("galle", "sd"), +} + +REPORT_TYPE_LABELS: dict[str, str] = { + "gesamt": "Gesamt", + "onko_intensiv": "Onko-Intensiv", + "galle_schild": "Galle-Schild", +} + # Number of calendar weeks to include (ISO weeks 1..52; 53 is rare) MAX_KW = 52 @@ -61,18 +74,18 @@ def _empty_weekly_row(kw: int) -> dict: } -def _empty_fg_weekly_row(kw: int) -> dict: +def _empty_fg_weekly_row(kw: int, fallgruppen: tuple[str, ...] = FALLGRUPPEN) -> dict: """Return a zeroed-out weekly row template for Sheet 2.""" row: dict[str, Any] = {"kw": kw} - for fg in FALLGRUPPEN: + for fg in fallgruppen: row[fg] = {"anzahl": 0, "gutachten": 0, "keine_rm": 0} return row -def _empty_gutachten_weekly_row(kw: int) -> dict: +def _empty_gutachten_weekly_row(kw: int, fallgruppen: tuple[str, ...] = FALLGRUPPEN) -> dict: """Return a zeroed-out weekly row template for Sheet 3.""" row: dict[str, Any] = {"kw": kw} - for group in ("gesamt",) + FALLGRUPPEN: + for group in ("gesamt",) + fallgruppen: row[group] = {"gutachten": 0, "alternative": 0, "bestaetigung": 0} return row @@ -94,7 +107,7 @@ def _empty_ta_weekly_row(kw: int) -> dict: # Sheet 1: Auswertung KW gesamt # --------------------------------------------------------------------------- -def calculate_sheet1_data(db: Session, jahr: int, max_kw: int | None = None) -> dict: +def calculate_sheet1_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict: """Calculate *Auswertung KW gesamt*. Returns:: @@ -126,6 +139,8 @@ def calculate_sheet1_data(db: Session, jahr: int, max_kw: int | None = None) -> """ # One query: group by kw, count the four flags filters = [Case.versicherung == settings.VERSICHERUNG_FILTER, Case.jahr == jahr] + if fallgruppen is not None: + filters.append(Case.fallgruppe.in_(fallgruppen)) if max_kw is not None: filters.append(Case.kw <= max_kw) rows = ( @@ -183,7 +198,7 @@ def calculate_sheet1_data(db: Session, jahr: int, max_kw: int | None = None) -> # Sheet 2: Auswertung nach Fachgebieten # --------------------------------------------------------------------------- -def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None) -> dict: +def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict: """Calculate *Auswertung nach Fachgebieten*. Per KW, per Fallgruppe: Anzahl, Gutachten, Keine RM/Ablehnung. @@ -207,8 +222,12 @@ def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None) -> Keine RM/Ablehnung = Anzahl - Gutachten (per the Excel formula). If *max_kw* is given, only data up to and including that KW is included. + If *fallgruppen* is given, only those Fallgruppen are included. """ + fgs = fallgruppen or FALLGRUPPEN filters = [Case.versicherung == settings.VERSICHERUNG_FILTER, Case.jahr == jahr] + if fallgruppen is not None: + filters.append(Case.fallgruppe.in_(fallgruppen)) if max_kw is not None: filters.append(Case.kw <= max_kw) rows = ( @@ -228,11 +247,11 @@ def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None) -> for row in rows: kw = _int(row.kw) fg = row.fallgruppe - if fg not in FALLGRUPPEN: + if fg not in fgs: logger.warning("Unknown fallgruppe '%s' in case data, skipping", fg) continue if kw not in kw_map: - kw_map[kw] = _empty_fg_weekly_row(kw) + kw_map[kw] = _empty_fg_weekly_row(kw, fgs) anzahl = _int(row.anzahl) gutachten = _int(row.gutachten) kw_map[kw][fg] = { @@ -243,7 +262,7 @@ def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None) -> weekly = [] for kw in range(1, MAX_KW + 1): - weekly.append(kw_map.get(kw, _empty_fg_weekly_row(kw))) + weekly.append(kw_map.get(kw, _empty_fg_weekly_row(kw, fgs))) return {"weekly": weekly} @@ -252,7 +271,7 @@ def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None) -> # Sheet 3: Auswertung Gutachten # --------------------------------------------------------------------------- -def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) -> dict: +def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict: """Calculate *Auswertung Gutachten*. Per KW, per group (gesamt + 5 Fallgruppen): @@ -281,12 +300,16 @@ def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) -> - Gesamt = sum across all Fallgruppen If *max_kw* is given, only data up to and including that KW is included. + If *fallgruppen* is given, only those Fallgruppen are included. """ + fgs = fallgruppen or FALLGRUPPEN filters = [ Case.versicherung == settings.VERSICHERUNG_FILTER, Case.jahr == jahr, Case.gutachten == True, # noqa: E712 ] + if fallgruppen is not None: + filters.append(Case.fallgruppe.in_(fallgruppen)) if max_kw is not None: filters.append(Case.kw <= max_kw) rows = ( @@ -307,10 +330,10 @@ def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) -> for row in rows: kw = _int(row.kw) fg = row.fallgruppe - if fg not in FALLGRUPPEN: + if fg not in fgs: continue if kw not in kw_map: - kw_map[kw] = _empty_gutachten_weekly_row(kw) + kw_map[kw] = _empty_gutachten_weekly_row(kw, fgs) gutachten = _int(row.gutachten) alternative = _int(row.alternative) @@ -320,10 +343,10 @@ def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) -> "bestaetigung": gutachten - alternative, } - # Compute gesamt (sum of all Fallgruppen per KW) + # Compute gesamt (sum of included Fallgruppen per KW) for kw_data in kw_map.values(): - total_g = sum(kw_data[fg]["gutachten"] for fg in FALLGRUPPEN) - total_a = sum(kw_data[fg]["alternative"] for fg in FALLGRUPPEN) + total_g = sum(kw_data[fg]["gutachten"] for fg in fgs) + total_a = sum(kw_data[fg]["alternative"] for fg in fgs) kw_data["gesamt"] = { "gutachten": total_g, "alternative": total_a, @@ -332,7 +355,7 @@ def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) -> weekly = [] for kw in range(1, MAX_KW + 1): - weekly.append(kw_map.get(kw, _empty_gutachten_weekly_row(kw))) + weekly.append(kw_map.get(kw, _empty_gutachten_weekly_row(kw, fgs))) return {"weekly": weekly} @@ -341,7 +364,7 @@ def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) -> # Sheet 4: Auswertung Therapieaenderungen # --------------------------------------------------------------------------- -def calculate_sheet4_data(db: Session, jahr: int, max_kw: int | None = None) -> dict: +def calculate_sheet4_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict: """Calculate *Auswertung Therapieaenderungen*. Per KW: Gutachten count, TA Ja, TA Nein, Diagnosekorrektur, @@ -365,12 +388,15 @@ def calculate_sheet4_data(db: Session, jahr: int, max_kw: int | None = None) -> } If *max_kw* is given, only data up to and including that KW is included. + If *fallgruppen* is given, only those Fallgruppen are included. """ filters = [ Case.versicherung == settings.VERSICHERUNG_FILTER, Case.jahr == jahr, Case.gutachten == True, # noqa: E712 ] + if fallgruppen is not None: + filters.append(Case.fallgruppe.in_(fallgruppen)) if max_kw is not None: filters.append(Case.kw <= max_kw) rows = ( @@ -416,7 +442,7 @@ def calculate_sheet4_data(db: Session, jahr: int, max_kw: int | None = None) -> # Sheet 5: Auswertung ICD onko # --------------------------------------------------------------------------- -def calculate_sheet5_data(db: Session, jahr: int, max_kw: int | None = None) -> dict: +def calculate_sheet5_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict: """Calculate *Auswertung ICD onko*. Returns sorted list of ICD codes from onko cases with counts. @@ -437,7 +463,13 @@ def calculate_sheet5_data(db: Session, jahr: int, max_kw: int | None = None) -> } If *max_kw* is given, only data up to and including that KW is included. + If *fallgruppen* is given and ``'onko'`` is not in the set, returns an + empty list (ICD codes are only relevant for onko cases). """ + fgs = fallgruppen or FALLGRUPPEN + if "onko" not in fgs: + return {"icd_codes": []} + filter_conditions = [ Case.versicherung == settings.VERSICHERUNG_FILTER, Case.fallgruppe == "onko", @@ -578,18 +610,23 @@ def calculate_dashboard_kpis(db: Session, jahr: int) -> dict: # Full report generation (all 5 sheets) # --------------------------------------------------------------------------- -def generate_full_report(db: Session, jahr: int, kw: int | None = None) -> dict: +def generate_full_report(db: Session, jahr: int, kw: int | None = None, report_type: str = "gesamt") -> dict: """Generate complete report data for all 5 sheets. If *kw* is given, only data up to and including that calendar week is included in the report. This allows generating historical reports that reflect the state at a specific point in the year. + If *report_type* is given, only the Fallgruppen belonging to that type + are included (e.g. ``"onko_intensiv"`` → only onko + intensiv). + Returns:: { "jahr": int, "kw": int | None, + "report_type": str, + "fallgruppen": list[str], "sheet1": {...}, "sheet2": {...}, "sheet3": {...}, @@ -597,14 +634,18 @@ def generate_full_report(db: Session, jahr: int, kw: int | None = None) -> dict: "sheet5": {...}, } """ - logger.info("Generating full report for jahr=%d, kw=%s", jahr, kw) + fallgruppen = REPORT_TYPES.get(report_type, FALLGRUPPEN) + fg_arg = fallgruppen if report_type != "gesamt" else None + logger.info("Generating full report for jahr=%d, kw=%s, report_type=%s", jahr, kw, report_type) return { "jahr": jahr, "kw": kw, - "sheet1": calculate_sheet1_data(db, jahr, max_kw=kw), - "sheet2": calculate_sheet2_data(db, jahr, max_kw=kw), - "sheet3": calculate_sheet3_data(db, jahr, max_kw=kw), - "sheet4": calculate_sheet4_data(db, jahr, max_kw=kw), - "sheet5": calculate_sheet5_data(db, jahr, max_kw=kw), + "report_type": report_type, + "fallgruppen": list(fallgruppen), + "sheet1": calculate_sheet1_data(db, jahr, max_kw=kw, fallgruppen=fg_arg), + "sheet2": calculate_sheet2_data(db, jahr, max_kw=kw, fallgruppen=fg_arg), + "sheet3": calculate_sheet3_data(db, jahr, max_kw=kw, fallgruppen=fg_arg), + "sheet4": calculate_sheet4_data(db, jahr, max_kw=kw, fallgruppen=fg_arg), + "sheet5": calculate_sheet5_data(db, jahr, max_kw=kw, fallgruppen=fg_arg), } diff --git a/frontend/src/components/ReportViewer.tsx b/frontend/src/components/ReportViewer.tsx index 74e70a3..c66534e 100644 --- a/frontend/src/components/ReportViewer.tsx +++ b/frontend/src/components/ReportViewer.tsx @@ -90,7 +90,7 @@ function Sheet1({ data }: { data: any }) { // --------------------------------------------------------------------------- // eslint-disable-next-line @typescript-eslint/no-explicit-any -function Sheet2({ data }: { data: any }) { +function Sheet2({ data, fallgruppen }: { data: any; fallgruppen: readonly string[] }) { if (!data?.weekly?.length) return const SUB_COLS = ['anzahl', 'gutachten', 'keine_rm'] as const @@ -102,14 +102,14 @@ function Sheet2({ data }: { data: any }) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const isRowEmpty = (row: any) => - FALLGRUPPEN_KEYS.every((fg) => + fallgruppen.every((fg) => SUB_COLS.every((sc) => (row[fg]?.[sc] ?? 0) === 0), ) // eslint-disable-next-line @typescript-eslint/no-explicit-any const rows = (data.weekly as any[]).filter((row) => !isRowEmpty(row)) - const totals = FALLGRUPPEN_KEYS.reduce( + const totals = fallgruppen.reduce( (acc, fg) => { acc[fg] = SUB_COLS.reduce( (sub, sc) => { @@ -129,14 +129,14 @@ function Sheet2({ data }: { data: any }) { KW - {FALLGRUPPEN_KEYS.map((fg) => ( + {fallgruppen.map((fg) => ( - {FALLGRUPPEN_LABELS[fg]} + {FALLGRUPPEN_LABELS[fg] ?? fg} ))} - {FALLGRUPPEN_KEYS.map((fg) => + {fallgruppen.map((fg) => SUB_COLS.map((sc) => ( {SUB_LABELS[sc]} @@ -149,7 +149,7 @@ function Sheet2({ data }: { data: any }) { {rows.map((row) => ( KW {row.kw} - {FALLGRUPPEN_KEYS.map((fg) => + {fallgruppen.map((fg) => SUB_COLS.map((sc) => ( {fmt(row[fg]?.[sc] ?? 0)} @@ -162,7 +162,7 @@ function Sheet2({ data }: { data: any }) { Gesamt - {FALLGRUPPEN_KEYS.map((fg) => + {fallgruppen.map((fg) => SUB_COLS.map((sc) => ( {fmt(totals[fg][sc])} @@ -180,7 +180,7 @@ function Sheet2({ data }: { data: any }) { // --------------------------------------------------------------------------- // eslint-disable-next-line @typescript-eslint/no-explicit-any -function Sheet3({ data }: { data: any }) { +function Sheet3({ data, fallgruppen }: { data: any; fallgruppen: readonly string[] }) { if (!data?.weekly?.length) return const SUB_COLS = ['gutachten', 'alternative', 'bestaetigung'] as const @@ -190,7 +190,7 @@ function Sheet3({ data }: { data: any }) { bestaetigung: 'Best\u00e4tigung', } - const GROUP_KEYS = ['gesamt', ...FALLGRUPPEN_KEYS] as const + const GROUP_KEYS = ['gesamt', ...fallgruppen] as const const GROUP_LABELS: Record = { gesamt: 'Gesamt', @@ -228,7 +228,7 @@ function Sheet3({ data }: { data: any }) { KW {GROUP_KEYS.map((grp) => ( - {GROUP_LABELS[grp]} + {GROUP_LABELS[grp] ?? grp} ))} @@ -379,6 +379,9 @@ function Sheet5({ data }: { data: any }) { // eslint-disable-next-line @typescript-eslint/no-explicit-any export function ReportViewer({ data }: { data: Record }) { + const fallgruppen: readonly string[] = data?.fallgruppen ?? FALLGRUPPEN_KEYS + const hasOnko = fallgruppen.includes('onko') + return ( @@ -386,7 +389,7 @@ export function ReportViewer({ data }: { data: Record }) { Fachgebiete Gutachten Therapie\u00e4nderungen - ICD onko + {hasOnko && ICD onko} @@ -394,20 +397,22 @@ export function ReportViewer({ data }: { data: Record }) { - + - + - - - + {hasOnko && ( + + + + )} ) } diff --git a/frontend/src/hooks/useReports.ts b/frontend/src/hooks/useReports.ts index f6e81fd..8dc41e2 100644 --- a/frontend/src/hooks/useReports.ts +++ b/frontend/src/hooks/useReports.ts @@ -18,7 +18,7 @@ export function useReports() { export function useGenerateReport() { const queryClient = useQueryClient() return useMutation({ - mutationFn: (params: { jahr: number; kw: number }) => + mutationFn: (params: { jahr: number; kw: number; report_type?: string }) => api.post('/reports/generate', null, { params }).then(r => r.data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['reports'] }) diff --git a/frontend/src/pages/ReportsPage.tsx b/frontend/src/pages/ReportsPage.tsx index be1caf3..bb9a433 100644 --- a/frontend/src/pages/ReportsPage.tsx +++ b/frontend/src/pages/ReportsPage.tsx @@ -12,9 +12,24 @@ import { Label } from '@/components/ui/label' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/components/ui/select' import { Skeleton } from '@/components/ui/skeleton' import { Alert, AlertDescription } from '@/components/ui/alert' +const REPORT_TYPES = [ + { value: 'gesamt', label: 'Gesamt' }, + { value: 'onko_intensiv', label: 'Onko-Intensiv' }, + { value: 'galle_schild', label: 'Galle-Schild' }, +] as const + +const REPORT_TYPE_LABELS: Record = { + gesamt: 'Gesamt', + onko_intensiv: 'Onko-Intensiv', + galle_schild: 'Galle-Schild', +} + export function ReportsPage() { const { isAdmin } = useAuth() const currentYear = new Date().getFullYear() @@ -31,6 +46,7 @@ export function ReportsPage() { // Report generation state const [genJahr, setGenJahr] = useState(currentYear) const [genKw, setGenKw] = useState(currentKw) + const [genReportType, setGenReportType] = useState('gesamt') const [genError, setGenError] = useState('') const [genSuccess, setGenSuccess] = useState('') @@ -45,8 +61,11 @@ export function ReportsPage() { setGenError('') setGenSuccess('') try { - const result = await generateMutation.mutateAsync({ jahr: genJahr, kw: genKw }) - setGenSuccess(`Bericht für KW ${result.kw}/${result.jahr} wurde generiert.`) + const result = await generateMutation.mutateAsync({ + jahr: genJahr, kw: genKw, report_type: genReportType, + }) + const typeLabel = REPORT_TYPE_LABELS[result.report_type] ?? result.report_type + setGenSuccess(`Bericht "${typeLabel}" für KW ${result.kw}/${result.jahr} wurde generiert.`) } catch { setGenError('Fehler beim Generieren des Berichts.') } @@ -150,6 +169,19 @@ export function ReportsPage() { max={53} /> +
+ + +