mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 16:03:41 +00:00
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:
parent
3bbf5bf51a
commit
48939f01dd
11 changed files with 235 additions and 84 deletions
35
backend/alembic/versions/007_add_report_type.py
Normal file
35
backend/alembic/versions/007_add_report_type.py
Normal 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"])
|
||||||
|
|
@ -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)
|
||||||
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)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,19 +60,23 @@ 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
|
||||||
sheet5 = report_data.get("sheet5", {})
|
if "onko" in fgs:
|
||||||
icd_codes = sheet5.get("icd_codes", []) if isinstance(sheet5, dict) else []
|
sheet5 = report_data.get("sheet5", {})
|
||||||
_write_sheet4_icd_onko(wb, icd_codes, jahr)
|
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()
|
# Remove the default empty sheet created by Workbook()
|
||||||
if "Sheet" in wb.sheetnames:
|
if "Sheet" in wb.sheetnames:
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
<TabsContent value="sheet5">
|
{hasOnko && (
|
||||||
<Sheet5 data={data?.sheet5} />
|
<TabsContent value="sheet5">
|
||||||
</TabsContent>
|
<Sheet5 data={data?.sheet5} />
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'] })
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue