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