feat: add Onko-Intensiv and Galle-Schild report types

Adds report_type support across the full stack:
- Backend: REPORT_TYPES mapping, fallgruppen filter in all 5 sheet
  calculations, dynamic Excel columns, report_type DB column with
  Alembic migration 007
- Frontend: report type dropdown in generation form, type column in
  reports table, dynamic fallgruppen in ReportViewer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-27 12:47:59 +00:00
parent 3bbf5bf51a
commit 48939f01dd
11 changed files with 235 additions and 84 deletions

View file

@ -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"])

View file

@ -80,14 +80,21 @@ def weekly_report(
def generate_report( def generate_report(
jahr: int | None = Query(None), jahr: int | None = Query(None),
kw: int | None = Query(None), kw: int | None = Query(None),
report_type: str = Query("gesamt"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
user: User = Depends(require_admin), user: User = Depends(require_admin),
): ):
"""Generate a full Berichtswesen Excel report and persist it to disk + DB. """Generate a full Berichtswesen Excel report and persist it to disk + DB.
Admin only. Defaults to the current ISO year/week if not specified. 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: if not jahr:
from app.utils.kw_utils import date_to_jahr, date_to_kw 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.report_service import generate_full_report
from app.services.vorjahr_service import get_vorjahr_summary from app.services.vorjahr_service import get_vorjahr_summary
report_data = generate_full_report(db, jahr, kw) fallgruppen = list(REPORT_TYPES[report_type])
vorjahr = get_vorjahr_summary(db, jahr) report_data = generate_full_report(db, jahr, kw, report_type=report_type)
xlsx_bytes = generate_berichtswesen_xlsx(report_data, jahr, vorjahr)
# 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 # Persist Excel file to disk
reports_dir = os.path.join( reports_dir = os.path.join(
@ -115,15 +127,23 @@ def generate_report(
"reports", "reports",
) )
os.makedirs(reports_dir, exist_ok=True) os.makedirs(reports_dir, exist_ok=True)
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" filename = f"Berichtswesen_{jahr}_KW{kw:02d}.xlsx"
filepath = os.path.join(reports_dir, filename) filepath = os.path.join(reports_dir, filename)
with open(filepath, "wb") as f: with open(filepath, "wb") as f:
f.write(xlsx_bytes) 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 = ( report = (
db.query(WeeklyReport) db.query(WeeklyReport)
.filter(WeeklyReport.jahr == jahr, WeeklyReport.kw == kw) .filter(
WeeklyReport.jahr == jahr,
WeeklyReport.kw == kw,
WeeklyReport.report_type == report_type,
)
.first() .first()
) )
if report: if report:
@ -135,6 +155,7 @@ def generate_report(
report = WeeklyReport( report = WeeklyReport(
jahr=jahr, jahr=jahr,
kw=kw, kw=kw,
report_type=report_type,
report_date=date.today(), report_date=date.today(),
report_data=report_data, report_data=report_data,
generated_by=user.id, generated_by=user.id,
@ -151,7 +172,7 @@ def generate_report(
action="report_generated", action="report_generated",
entity_type="report", entity_type="report",
entity_id=report.id, 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) return ReportMeta.model_validate(report)

View file

@ -28,6 +28,9 @@ class WeeklyReport(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
jahr: Mapped[int] = mapped_column(SmallInteger, nullable=False) jahr: Mapped[int] = mapped_column(SmallInteger, nullable=False)
kw: 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_date: Mapped[dt.date] = mapped_column(Date, nullable=False)
report_file_path: Mapped[Optional[str]] = mapped_column( report_file_path: Mapped[Optional[str]] = mapped_column(
String(500), nullable=True String(500), nullable=True
@ -46,7 +49,7 @@ class WeeklyReport(Base):
) )
__table_args__ = ( __table_args__ = (
UniqueConstraint("jahr", "kw", name="uk_jahr_kw"), UniqueConstraint("jahr", "kw", "report_type", name="uk_jahr_kw_type"),
) )

View file

@ -40,6 +40,7 @@ class ReportMeta(BaseModel):
id: int id: int
jahr: int jahr: int
kw: int kw: int
report_type: str = "gesamt"
report_date: date report_date: date
generated_at: datetime generated_at: datetime
generated_by: Optional[int] = None generated_by: Optional[int] = None

View file

@ -49,6 +49,7 @@ def generate_berichtswesen_xlsx(
report_data: dict[str, Any], report_data: dict[str, Any],
jahr: int, jahr: int,
vorjahr_data: dict[str, Any] | None = None, vorjahr_data: dict[str, Any] | None = None,
fallgruppen: list[str] | None = None,
) -> bytes: ) -> bytes:
"""Generate a Berichtswesen Excel file. """Generate a Berichtswesen Excel file.
@ -59,16 +60,20 @@ def generate_berichtswesen_xlsx(
vorjahr_data: Previous-year summary data for the year-over-year vorjahr_data: Previous-year summary data for the year-over-year
comparison block on Sheet 1. Structure same as *report_data* comparison block on Sheet 1. Structure same as *report_data*
``sheet1`` (i.e. containing ``totals`` and ``weeks``). ``sheet1`` (i.e. containing ``totals`` and ``weeks``).
fallgruppen: Subset of Fallgruppen for filtered reports.
If *None*, all 5 Fallgruppen are used.
Returns: Returns:
The ``.xlsx`` file contents as *bytes*. The ``.xlsx`` file contents as *bytes*.
""" """
fgs = fallgruppen or FALLGRUPPEN
wb = Workbook() wb = Workbook()
_write_sheet1_kw_gesamt(wb, report_data.get("sheet1", {}), jahr, vorjahr_data) _write_sheet1_kw_gesamt(wb, report_data.get("sheet1", {}), jahr, vorjahr_data)
_write_sheet2_fachgebiete(wb, report_data.get("sheet2", {}), jahr) _write_sheet2_fachgebiete(wb, report_data.get("sheet2", {}), jahr, fgs)
_write_sheet3_gutachten(wb, report_data.get("sheet3", {}), jahr) _write_sheet3_gutachten(wb, report_data.get("sheet3", {}), jahr, fgs)
# ICD codes live inside sheet5 # ICD codes live inside sheet5 — only relevant if onko is included
if "onko" in fgs:
sheet5 = report_data.get("sheet5", {}) sheet5 = report_data.get("sheet5", {})
icd_codes = sheet5.get("icd_codes", []) if isinstance(sheet5, dict) else [] icd_codes = sheet5.get("icd_codes", []) if isinstance(sheet5, dict) else []
_write_sheet4_icd_onko(wb, icd_codes, jahr) _write_sheet4_icd_onko(wb, icd_codes, jahr)
@ -251,16 +256,18 @@ def _write_sheet2_fachgebiete(
wb: Workbook, wb: Workbook,
data: dict[str, Any], data: dict[str, Any],
jahr: int, jahr: int,
fallgruppen: list[str] | None = None,
) -> None: ) -> None:
"""Write the 'Auswertung nach Fachgebieten' sheet. """Write the 'Auswertung nach Fachgebieten' sheet.
Layout: Layout:
Row 1: A1 = "Uebersicht nach Fallgruppen" Row 1: A1 = "Uebersicht nach Fallgruppen"
Row 3: Merged group headers (B3:D3, E3:G3, H3:J3, K3:M3, N3:P3) Row 3: Merged group headers (dynamic based on fallgruppen)
Row 4: Sub-headers: KW | Anzahl | Gutachten | Keine RM/Ablehnung (x5) Row 4: Sub-headers: KW | Anzahl | Gutachten | Keine RM/Ablehnung (per fg)
Row 5-56: KW 1-52 data Row 5-56: KW 1-52 data
Row 57: Summe row Row 57: Summe row
""" """
fgs = fallgruppen or FALLGRUPPEN
ws = wb.create_sheet(title="Auswertung nach Fachgebieten") ws = wb.create_sheet(title="Auswertung nach Fachgebieten")
weeks = _weeks_lookup(data.get("weekly", [])) weeks = _weeks_lookup(data.get("weekly", []))
@ -270,8 +277,9 @@ def _write_sheet2_fachgebiete(
ws["A1"].font = TITLE_FONT ws["A1"].font = TITLE_FONT
# --- Group headers (row 3) with merged cells --- # --- Group headers (row 3) with merged cells ---
group_start_cols = [2, 5, 8, 11, 14] # B, E, H, K, N group_start_cols = [2 + i * 3 for i in range(len(fgs))]
for fg_key, start_col in zip(FALLGRUPPEN, group_start_cols): 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] label = FALLGRUPPEN_LABELS[fg_key]
cell = ws.cell(row=3, column=start_col, value=label) cell = ws.cell(row=3, column=start_col, value=label)
cell.fill = HEADER_FILL 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, value="Anzahl")
ws.cell(row=4, column=start_col + 1, value="Gutachten") ws.cell(row=4, column=start_col + 1, value="Gutachten")
ws.cell(row=4, column=start_col + 2, value="Keine R\u00fcckmeldung/Ablehnung") 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) --- # --- 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): for kw in range(1, MAX_KW + 1):
row = 4 + kw # row 5 = KW 1 row = 4 + kw # row 5 = KW 1
w = weeks.get(kw, {}) w = weeks.get(kw, {})
ws.cell(row=row, column=1, value=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, {}) fg_data = w.get(fg_key, {})
anz = _safe(fg_data.get("anzahl")) anz = _safe(fg_data.get("anzahl"))
ga = _safe(fg_data.get("gutachten")) ga = _safe(fg_data.get("gutachten"))
@ -319,11 +327,11 @@ def _write_sheet2_fachgebiete(
# --- Summe row (row 57) --- # --- Summe row (row 57) ---
summe_row = 4 + MAX_KW + 1 # 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, 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 + 1, value=sums[fg_key]["gutachten"])
ws.cell(row=summe_row, column=start_col + 2, value=sums[fg_key]["keine_rm"]) 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 --- # --- Column widths ---
ws.column_dimensions["A"].width = 6 ws.column_dimensions["A"].width = 6
@ -341,16 +349,18 @@ def _write_sheet3_gutachten(
wb: Workbook, wb: Workbook,
data: dict[str, Any], data: dict[str, Any],
jahr: int, jahr: int,
fallgruppen: list[str] | None = None,
) -> None: ) -> None:
"""Write the 'Auswertung Gutachten' sheet. """Write the 'Auswertung Gutachten' sheet.
Layout: Layout:
Row 1: A1 = "Uebersicht nach Fallgruppen" Row 1: A1 = "Uebersicht nach Fallgruppen"
Row 3: Group headers: Gesamt (B3) + 5 Fallgruppen Row 3: Group headers: Gesamt (B3) + Fallgruppen (dynamic)
Row 4: Sub-headers: KW | Gutachten | Alternative | Bestaetigung (x6) Row 4: Sub-headers: KW | Gutachten | Alternative | Bestaetigung (per group)
Row 5-56: KW 1-52 data Row 5-56: KW 1-52 data
Row 57: Summe row Row 57: Summe row
""" """
fgs = fallgruppen or FALLGRUPPEN
ws = wb.create_sheet(title="Auswertung Gutachten") ws = wb.create_sheet(title="Auswertung Gutachten")
weeks = _weeks_lookup(data.get("weekly", [])) weeks = _weeks_lookup(data.get("weekly", []))
@ -360,22 +370,21 @@ def _write_sheet3_gutachten(
ws["A1"].font = TITLE_FONT ws["A1"].font = TITLE_FONT
# --- Group headers (row 3) --- # --- Group headers (row 3) ---
# Gesamt: B3 (no merge since it's a single-column-start group header, # Gesamt: B3
# but in the reference the Gesamt label sits in B3 without a merge)
cell = ws.cell(row=3, column=2, value="Gesamt") cell = ws.cell(row=3, column=2, value="Gesamt")
cell.fill = HEADER_FILL cell.fill = HEADER_FILL
cell.font = HEADER_FONT cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal="center") cell.alignment = Alignment(horizontal="center")
# Fallgruppen start at columns E, H, K, N, Q (each 3 cols wide) # Fallgruppen start at column 5, each 3 cols wide
fg_start_cols = [5, 8, 11, 14, 17] fg_start_cols = [5 + i * 3 for i in range(len(fgs))]
for fg_key, start_col in zip(FALLGRUPPEN, fg_start_cols): 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] label = FALLGRUPPEN_LABELS[fg_key]
cell = ws.cell(row=3, column=start_col, value=label) cell = ws.cell(row=3, column=start_col, value=label)
cell.fill = HEADER_FILL cell.fill = HEADER_FILL
cell.font = HEADER_FONT cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal="center") cell.alignment = Alignment(horizontal="center")
# Merge: the reference merges first 2 of 3 columns (E3:F3, H3:I3, etc.)
end_col = start_col + 1 end_col = start_col + 1
ws.merge_cells( ws.merge_cells(
start_row=3, start_column=start_col, 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, value="Gutachten")
ws.cell(row=4, column=start_col + 1, value="Alternative") ws.cell(row=4, column=start_col + 1, value="Alternative")
ws.cell(row=4, column=start_col + 2, value="Best\u00e4tigung") 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) --- # --- Weekly data (rows 5-56) ---
sums_gesamt = {"gutachten": 0, "alternative": 0, "bestaetigung": 0} 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): for kw in range(1, MAX_KW + 1):
row = 4 + kw row = 4 + kw
@ -420,7 +429,7 @@ def _write_sheet3_gutachten(
sums_gesamt["bestaetigung"] += g_best sums_gesamt["bestaetigung"] += g_best
# Per Fallgruppe # 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, {}) fg_data = w.get(fg_key, {})
ga = _safe(fg_data.get("gutachten")) ga = _safe(fg_data.get("gutachten"))
alt = _safe(fg_data.get("alternative")) 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=2, value=sums_gesamt["gutachten"])
ws.cell(row=summe_row, column=3, value=sums_gesamt["alternative"]) ws.cell(row=summe_row, column=3, value=sums_gesamt["alternative"])
ws.cell(row=summe_row, column=4, value=sums_gesamt["bestaetigung"]) 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, 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 + 1, value=sums_fg[fg_key]["alternative"])
ws.cell(row=summe_row, column=start_col + 2, value=sums_fg[fg_key]["bestaetigung"]) 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 --- # --- Column widths ---
ws.column_dimensions["A"].width = 6 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 ws.column_dimensions[get_column_letter(col)].width = 12

View file

@ -27,6 +27,19 @@ logger = logging.getLogger(__name__)
# Canonical Fallgruppen in display order # Canonical Fallgruppen in display order
FALLGRUPPEN = ("onko", "kardio", "intensiv", "galle", "sd") 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) # Number of calendar weeks to include (ISO weeks 1..52; 53 is rare)
MAX_KW = 52 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.""" """Return a zeroed-out weekly row template for Sheet 2."""
row: dict[str, Any] = {"kw": kw} row: dict[str, Any] = {"kw": kw}
for fg in FALLGRUPPEN: for fg in fallgruppen:
row[fg] = {"anzahl": 0, "gutachten": 0, "keine_rm": 0} row[fg] = {"anzahl": 0, "gutachten": 0, "keine_rm": 0}
return row 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.""" """Return a zeroed-out weekly row template for Sheet 3."""
row: dict[str, Any] = {"kw": kw} row: dict[str, Any] = {"kw": kw}
for group in ("gesamt",) + FALLGRUPPEN: for group in ("gesamt",) + fallgruppen:
row[group] = {"gutachten": 0, "alternative": 0, "bestaetigung": 0} row[group] = {"gutachten": 0, "alternative": 0, "bestaetigung": 0}
return row return row
@ -94,7 +107,7 @@ def _empty_ta_weekly_row(kw: int) -> dict:
# Sheet 1: Auswertung KW gesamt # 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*. """Calculate *Auswertung KW gesamt*.
Returns:: 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 # One query: group by kw, count the four flags
filters = [Case.versicherung == settings.VERSICHERUNG_FILTER, Case.jahr == jahr] 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: if max_kw is not None:
filters.append(Case.kw <= max_kw) filters.append(Case.kw <= max_kw)
rows = ( rows = (
@ -183,7 +198,7 @@ def calculate_sheet1_data(db: Session, jahr: int, max_kw: int | None = None) ->
# Sheet 2: Auswertung nach Fachgebieten # 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*. """Calculate *Auswertung nach Fachgebieten*.
Per KW, per Fallgruppe: Anzahl, Gutachten, Keine RM/Ablehnung. 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). 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 *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] 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: if max_kw is not None:
filters.append(Case.kw <= max_kw) filters.append(Case.kw <= max_kw)
rows = ( rows = (
@ -228,11 +247,11 @@ def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None) ->
for row in rows: for row in rows:
kw = _int(row.kw) kw = _int(row.kw)
fg = row.fallgruppe fg = row.fallgruppe
if fg not in FALLGRUPPEN: if fg not in fgs:
logger.warning("Unknown fallgruppe '%s' in case data, skipping", fg) logger.warning("Unknown fallgruppe '%s' in case data, skipping", fg)
continue continue
if kw not in kw_map: 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) anzahl = _int(row.anzahl)
gutachten = _int(row.gutachten) gutachten = _int(row.gutachten)
kw_map[kw][fg] = { kw_map[kw][fg] = {
@ -243,7 +262,7 @@ def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None) ->
weekly = [] weekly = []
for kw in range(1, MAX_KW + 1): 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} return {"weekly": weekly}
@ -252,7 +271,7 @@ def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None) ->
# Sheet 3: Auswertung Gutachten # 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*. """Calculate *Auswertung Gutachten*.
Per KW, per group (gesamt + 5 Fallgruppen): 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 - Gesamt = sum across all Fallgruppen
If *max_kw* is given, only data up to and including that KW is included. 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 = [ filters = [
Case.versicherung == settings.VERSICHERUNG_FILTER, Case.versicherung == settings.VERSICHERUNG_FILTER,
Case.jahr == jahr, Case.jahr == jahr,
Case.gutachten == True, # noqa: E712 Case.gutachten == True, # noqa: E712
] ]
if fallgruppen is not None:
filters.append(Case.fallgruppe.in_(fallgruppen))
if max_kw is not None: if max_kw is not None:
filters.append(Case.kw <= max_kw) filters.append(Case.kw <= max_kw)
rows = ( rows = (
@ -307,10 +330,10 @@ def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) ->
for row in rows: for row in rows:
kw = _int(row.kw) kw = _int(row.kw)
fg = row.fallgruppe fg = row.fallgruppe
if fg not in FALLGRUPPEN: if fg not in fgs:
continue continue
if kw not in kw_map: 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) gutachten = _int(row.gutachten)
alternative = _int(row.alternative) alternative = _int(row.alternative)
@ -320,10 +343,10 @@ def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) ->
"bestaetigung": gutachten - alternative, "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(): for kw_data in kw_map.values():
total_g = sum(kw_data[fg]["gutachten"] 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 FALLGRUPPEN) total_a = sum(kw_data[fg]["alternative"] for fg in fgs)
kw_data["gesamt"] = { kw_data["gesamt"] = {
"gutachten": total_g, "gutachten": total_g,
"alternative": total_a, "alternative": total_a,
@ -332,7 +355,7 @@ def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) ->
weekly = [] weekly = []
for kw in range(1, MAX_KW + 1): 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} return {"weekly": weekly}
@ -341,7 +364,7 @@ def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None) ->
# Sheet 4: Auswertung Therapieaenderungen # 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*. """Calculate *Auswertung Therapieaenderungen*.
Per KW: Gutachten count, TA Ja, TA Nein, Diagnosekorrektur, 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 *max_kw* is given, only data up to and including that KW is included.
If *fallgruppen* is given, only those Fallgruppen are included.
""" """
filters = [ filters = [
Case.versicherung == settings.VERSICHERUNG_FILTER, Case.versicherung == settings.VERSICHERUNG_FILTER,
Case.jahr == jahr, Case.jahr == jahr,
Case.gutachten == True, # noqa: E712 Case.gutachten == True, # noqa: E712
] ]
if fallgruppen is not None:
filters.append(Case.fallgruppe.in_(fallgruppen))
if max_kw is not None: if max_kw is not None:
filters.append(Case.kw <= max_kw) filters.append(Case.kw <= max_kw)
rows = ( rows = (
@ -416,7 +442,7 @@ def calculate_sheet4_data(db: Session, jahr: int, max_kw: int | None = None) ->
# Sheet 5: Auswertung ICD onko # 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*. """Calculate *Auswertung ICD onko*.
Returns sorted list of ICD codes from onko cases with counts. 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 *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 = [ filter_conditions = [
Case.versicherung == settings.VERSICHERUNG_FILTER, Case.versicherung == settings.VERSICHERUNG_FILTER,
Case.fallgruppe == "onko", Case.fallgruppe == "onko",
@ -578,18 +610,23 @@ def calculate_dashboard_kpis(db: Session, jahr: int) -> dict:
# Full report generation (all 5 sheets) # 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. """Generate complete report data for all 5 sheets.
If *kw* is given, only data up to and including that calendar week is If *kw* is given, only data up to and including that calendar week is
included in the report. This allows generating historical reports included in the report. This allows generating historical reports
that reflect the state at a specific point in the year. 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:: Returns::
{ {
"jahr": int, "jahr": int,
"kw": int | None, "kw": int | None,
"report_type": str,
"fallgruppen": list[str],
"sheet1": {...}, "sheet1": {...},
"sheet2": {...}, "sheet2": {...},
"sheet3": {...}, "sheet3": {...},
@ -597,14 +634,18 @@ def generate_full_report(db: Session, jahr: int, kw: int | None = None) -> dict:
"sheet5": {...}, "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 { return {
"jahr": jahr, "jahr": jahr,
"kw": kw, "kw": kw,
"sheet1": calculate_sheet1_data(db, jahr, max_kw=kw), "report_type": report_type,
"sheet2": calculate_sheet2_data(db, jahr, max_kw=kw), "fallgruppen": list(fallgruppen),
"sheet3": calculate_sheet3_data(db, jahr, max_kw=kw), "sheet1": calculate_sheet1_data(db, jahr, max_kw=kw, fallgruppen=fg_arg),
"sheet4": calculate_sheet4_data(db, jahr, max_kw=kw), "sheet2": calculate_sheet2_data(db, jahr, max_kw=kw, fallgruppen=fg_arg),
"sheet5": calculate_sheet5_data(db, jahr, max_kw=kw), "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),
} }

View file

@ -90,7 +90,7 @@ function Sheet1({ data }: { data: any }) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-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 <NoData /> if (!data?.weekly?.length) return <NoData />
const SUB_COLS = ['anzahl', 'gutachten', 'keine_rm'] as const 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
const isRowEmpty = (row: any) => const isRowEmpty = (row: any) =>
FALLGRUPPEN_KEYS.every((fg) => fallgruppen.every((fg) =>
SUB_COLS.every((sc) => (row[fg]?.[sc] ?? 0) === 0), SUB_COLS.every((sc) => (row[fg]?.[sc] ?? 0) === 0),
) )
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows = (data.weekly as any[]).filter((row) => !isRowEmpty(row)) const rows = (data.weekly as any[]).filter((row) => !isRowEmpty(row))
const totals = FALLGRUPPEN_KEYS.reduce( const totals = fallgruppen.reduce(
(acc, fg) => { (acc, fg) => {
acc[fg] = SUB_COLS.reduce( acc[fg] = SUB_COLS.reduce(
(sub, sc) => { (sub, sc) => {
@ -129,14 +129,14 @@ function Sheet2({ data }: { data: any }) {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead rowSpan={2} className="align-bottom">KW</TableHead> <TableHead rowSpan={2} className="align-bottom">KW</TableHead>
{FALLGRUPPEN_KEYS.map((fg) => ( {fallgruppen.map((fg) => (
<TableHead key={fg} colSpan={SUB_COLS.length} className="text-center border-l"> <TableHead key={fg} colSpan={SUB_COLS.length} className="text-center border-l">
{FALLGRUPPEN_LABELS[fg]} {FALLGRUPPEN_LABELS[fg] ?? fg}
</TableHead> </TableHead>
))} ))}
</TableRow> </TableRow>
<TableRow> <TableRow>
{FALLGRUPPEN_KEYS.map((fg) => {fallgruppen.map((fg) =>
SUB_COLS.map((sc) => ( SUB_COLS.map((sc) => (
<TableHead key={`${fg}-${sc}`} className="text-right border-l first:border-l"> <TableHead key={`${fg}-${sc}`} className="text-right border-l first:border-l">
{SUB_LABELS[sc]} {SUB_LABELS[sc]}
@ -149,7 +149,7 @@ function Sheet2({ data }: { data: any }) {
{rows.map((row) => ( {rows.map((row) => (
<TableRow key={row.kw}> <TableRow key={row.kw}>
<TableCell>KW {row.kw}</TableCell> <TableCell>KW {row.kw}</TableCell>
{FALLGRUPPEN_KEYS.map((fg) => {fallgruppen.map((fg) =>
SUB_COLS.map((sc) => ( SUB_COLS.map((sc) => (
<TableCell key={`${fg}-${sc}`} className="text-right border-l"> <TableCell key={`${fg}-${sc}`} className="text-right border-l">
{fmt(row[fg]?.[sc] ?? 0)} {fmt(row[fg]?.[sc] ?? 0)}
@ -162,7 +162,7 @@ function Sheet2({ data }: { data: any }) {
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TableCell className="font-bold">Gesamt</TableCell> <TableCell className="font-bold">Gesamt</TableCell>
{FALLGRUPPEN_KEYS.map((fg) => {fallgruppen.map((fg) =>
SUB_COLS.map((sc) => ( SUB_COLS.map((sc) => (
<TableCell key={`${fg}-${sc}`} className="text-right font-bold border-l"> <TableCell key={`${fg}-${sc}`} className="text-right font-bold border-l">
{fmt(totals[fg][sc])} {fmt(totals[fg][sc])}
@ -180,7 +180,7 @@ function Sheet2({ data }: { data: any }) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-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 <NoData /> if (!data?.weekly?.length) return <NoData />
const SUB_COLS = ['gutachten', 'alternative', 'bestaetigung'] as const const SUB_COLS = ['gutachten', 'alternative', 'bestaetigung'] as const
@ -190,7 +190,7 @@ function Sheet3({ data }: { data: any }) {
bestaetigung: 'Best\u00e4tigung', bestaetigung: 'Best\u00e4tigung',
} }
const GROUP_KEYS = ['gesamt', ...FALLGRUPPEN_KEYS] as const const GROUP_KEYS = ['gesamt', ...fallgruppen] as const
const GROUP_LABELS: Record<string, string> = { const GROUP_LABELS: Record<string, string> = {
gesamt: 'Gesamt', gesamt: 'Gesamt',
@ -228,7 +228,7 @@ function Sheet3({ data }: { data: any }) {
<TableHead rowSpan={2} className="align-bottom">KW</TableHead> <TableHead rowSpan={2} className="align-bottom">KW</TableHead>
{GROUP_KEYS.map((grp) => ( {GROUP_KEYS.map((grp) => (
<TableHead key={grp} colSpan={SUB_COLS.length} className="text-center border-l"> <TableHead key={grp} colSpan={SUB_COLS.length} className="text-center border-l">
{GROUP_LABELS[grp]} {GROUP_LABELS[grp] ?? grp}
</TableHead> </TableHead>
))} ))}
</TableRow> </TableRow>
@ -379,6 +379,9 @@ function Sheet5({ data }: { data: any }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function ReportViewer({ data }: { data: Record<string, any> }) { export function ReportViewer({ data }: { data: Record<string, any> }) {
const fallgruppen: readonly string[] = data?.fallgruppen ?? FALLGRUPPEN_KEYS
const hasOnko = fallgruppen.includes('onko')
return ( return (
<Tabs defaultValue="sheet1" className="w-full"> <Tabs defaultValue="sheet1" className="w-full">
<TabsList className="w-full justify-start"> <TabsList className="w-full justify-start">
@ -386,7 +389,7 @@ export function ReportViewer({ data }: { data: Record<string, any> }) {
<TabsTrigger value="sheet2">Fachgebiete</TabsTrigger> <TabsTrigger value="sheet2">Fachgebiete</TabsTrigger>
<TabsTrigger value="sheet3">Gutachten</TabsTrigger> <TabsTrigger value="sheet3">Gutachten</TabsTrigger>
<TabsTrigger value="sheet4">Therapie\u00e4nderungen</TabsTrigger> <TabsTrigger value="sheet4">Therapie\u00e4nderungen</TabsTrigger>
<TabsTrigger value="sheet5">ICD onko</TabsTrigger> {hasOnko && <TabsTrigger value="sheet5">ICD onko</TabsTrigger>}
</TabsList> </TabsList>
<TabsContent value="sheet1"> <TabsContent value="sheet1">
@ -394,20 +397,22 @@ export function ReportViewer({ data }: { data: Record<string, any> }) {
</TabsContent> </TabsContent>
<TabsContent value="sheet2"> <TabsContent value="sheet2">
<Sheet2 data={data?.sheet2} /> <Sheet2 data={data?.sheet2} fallgruppen={fallgruppen} />
</TabsContent> </TabsContent>
<TabsContent value="sheet3"> <TabsContent value="sheet3">
<Sheet3 data={data?.sheet3} /> <Sheet3 data={data?.sheet3} fallgruppen={fallgruppen} />
</TabsContent> </TabsContent>
<TabsContent value="sheet4"> <TabsContent value="sheet4">
<Sheet4 data={data?.sheet4} /> <Sheet4 data={data?.sheet4} />
</TabsContent> </TabsContent>
{hasOnko && (
<TabsContent value="sheet5"> <TabsContent value="sheet5">
<Sheet5 data={data?.sheet5} /> <Sheet5 data={data?.sheet5} />
</TabsContent> </TabsContent>
)}
</Tabs> </Tabs>
) )
} }

View file

@ -18,7 +18,7 @@ export function useReports() {
export function useGenerateReport() { export function useGenerateReport() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (params: { jahr: number; kw: number }) => mutationFn: (params: { jahr: number; kw: number; report_type?: string }) =>
api.post<ReportMeta>('/reports/generate', null, { params }).then(r => r.data), api.post<ReportMeta>('/reports/generate', null, { params }).then(r => r.data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reports'] }) queryClient.invalidateQueries({ queryKey: ['reports'] })

View file

@ -12,9 +12,24 @@ import { Label } from '@/components/ui/label'
import { import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert' 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<string, string> = {
gesamt: 'Gesamt',
onko_intensiv: 'Onko-Intensiv',
galle_schild: 'Galle-Schild',
}
export function ReportsPage() { export function ReportsPage() {
const { isAdmin } = useAuth() const { isAdmin } = useAuth()
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
@ -31,6 +46,7 @@ export function ReportsPage() {
// Report generation state // Report generation state
const [genJahr, setGenJahr] = useState(currentYear) const [genJahr, setGenJahr] = useState(currentYear)
const [genKw, setGenKw] = useState(currentKw) const [genKw, setGenKw] = useState(currentKw)
const [genReportType, setGenReportType] = useState('gesamt')
const [genError, setGenError] = useState('') const [genError, setGenError] = useState('')
const [genSuccess, setGenSuccess] = useState('') const [genSuccess, setGenSuccess] = useState('')
@ -45,8 +61,11 @@ export function ReportsPage() {
setGenError('') setGenError('')
setGenSuccess('') setGenSuccess('')
try { try {
const result = await generateMutation.mutateAsync({ jahr: genJahr, kw: genKw }) const result = await generateMutation.mutateAsync({
setGenSuccess(`Bericht für KW ${result.kw}/${result.jahr} wurde generiert.`) 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 { } catch {
setGenError('Fehler beim Generieren des Berichts.') setGenError('Fehler beim Generieren des Berichts.')
} }
@ -150,6 +169,19 @@ export function ReportsPage() {
max={53} max={53}
/> />
</div> </div>
<div className="space-y-1">
<Label>Berichtstyp</Label>
<Select value={genReportType} onValueChange={setGenReportType}>
<SelectTrigger className="w-44">
<SelectValue />
</SelectTrigger>
<SelectContent>
{REPORT_TYPES.map((t) => (
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button onClick={generateReport} disabled={generateMutation.isPending}> <Button onClick={generateReport} disabled={generateMutation.isPending}>
{generateMutation.isPending ? ( {generateMutation.isPending ? (
<> <>
@ -224,6 +256,7 @@ export function ReportsPage() {
<TableHead>Berichtsdatum</TableHead> <TableHead>Berichtsdatum</TableHead>
<TableHead>Jahr</TableHead> <TableHead>Jahr</TableHead>
<TableHead>KW</TableHead> <TableHead>KW</TableHead>
<TableHead>Typ</TableHead>
<TableHead>Erstellt am</TableHead> <TableHead>Erstellt am</TableHead>
<TableHead className="text-right">Aktion</TableHead> <TableHead className="text-right">Aktion</TableHead>
</TableRow> </TableRow>
@ -231,7 +264,7 @@ export function ReportsPage() {
<TableBody> <TableBody>
{reports.map((r) => { {reports.map((r) => {
const isExpanded = expandedId === r.id const isExpanded = expandedId === r.id
const colCount = isAdmin ? 6 : 5 const colCount = isAdmin ? 7 : 6
return ( return (
<Fragment key={r.id}> <Fragment key={r.id}>
<TableRow <TableRow
@ -256,6 +289,7 @@ export function ReportsPage() {
</TableCell> </TableCell>
<TableCell>{r.jahr}</TableCell> <TableCell>{r.jahr}</TableCell>
<TableCell>KW {r.kw}</TableCell> <TableCell>KW {r.kw}</TableCell>
<TableCell>{REPORT_TYPE_LABELS[r.report_type] ?? r.report_type}</TableCell>
<TableCell>{formatDateTime(r.generated_at)}</TableCell> <TableCell>{formatDateTime(r.generated_at)}</TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}> <TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<Button <Button

View file

@ -211,6 +211,7 @@ export const mockReportMeta: ReportMeta = {
id: 1, id: 1,
jahr: 2026, jahr: 2026,
kw: 6, kw: 6,
report_type: 'gesamt',
report_date: '2026-02-09', report_date: '2026-02-09',
generated_at: '2026-02-10T08:30:00Z', generated_at: '2026-02-10T08:30:00Z',
} }

View file

@ -154,6 +154,7 @@ export interface ReportMeta {
id: number id: number
jahr: number jahr: number
kw: number kw: number
report_type: string
report_date: string report_date: string
generated_at: string generated_at: string
} }