mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 17:13:42 +00:00
fix: add missing cases API, import router, and case schemas
These files were created by a subagent but not included in the commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3f8f96097d
commit
93884f3c8d
3 changed files with 796 additions and 0 deletions
403
backend/app/api/cases.py
Normal file
403
backend/app/api/cases.py
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
"""Cases API routes — list, detail, update, ICD entry, coding, template download."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.dependencies import get_current_user, require_admin
|
||||
from app.database import get_db
|
||||
from app.models.case import Case
|
||||
from app.models.user import User
|
||||
from app.schemas.case import (
|
||||
CaseListResponse,
|
||||
CaseResponse,
|
||||
CaseUpdate,
|
||||
CodingUpdate,
|
||||
ICDUpdate,
|
||||
)
|
||||
from app.services.audit_service import log_action
|
||||
from app.services.icd_service import generate_coding_template, get_pending_icd_cases, save_icd_for_case
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Static routes MUST be defined BEFORE the parameterised /{case_id} routes
|
||||
# to avoid FastAPI interpreting "pending-icd" etc. as a case_id.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/pending-icd", response_model=CaseListResponse)
|
||||
def list_pending_icd(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(50, ge=1, le=200),
|
||||
jahr: Optional[int] = Query(None),
|
||||
fallgruppe: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Return cases that still need ICD codes (icd IS NULL).
|
||||
|
||||
Accessible to both admin and dak_mitarbeiter users.
|
||||
"""
|
||||
cases, total = get_pending_icd_cases(
|
||||
db, jahr=jahr, fallgruppe=fallgruppe, page=page, per_page=per_page
|
||||
)
|
||||
return CaseListResponse(
|
||||
items=cases,
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/pending-coding", response_model=CaseListResponse)
|
||||
def list_pending_coding(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(50, ge=1, le=200),
|
||||
jahr: Optional[int] = Query(None),
|
||||
fallgruppe: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Return cases with gutachten=True but no gutachten_typ yet (admin only).
|
||||
|
||||
These cases have received a Gutachten but still need classification
|
||||
(Bestätigung/Alternative) and therapy-change coding.
|
||||
"""
|
||||
query = db.query(Case).filter(
|
||||
Case.gutachten == True, # noqa: E712
|
||||
Case.gutachten_typ == None, # noqa: E711
|
||||
)
|
||||
|
||||
if jahr:
|
||||
query = query.filter(Case.jahr == jahr)
|
||||
if fallgruppe:
|
||||
query = query.filter(Case.fallgruppe == fallgruppe)
|
||||
|
||||
total = query.count()
|
||||
cases = (
|
||||
query.order_by(Case.datum.desc())
|
||||
.offset((page - 1) * per_page)
|
||||
.limit(per_page)
|
||||
.all()
|
||||
)
|
||||
|
||||
return CaseListResponse(
|
||||
items=cases,
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/coding-template")
|
||||
def download_coding_template(
|
||||
jahr: Optional[int] = Query(None),
|
||||
fallgruppe: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Download an Excel template for ICD coding.
|
||||
|
||||
The template contains all cases that still need ICD codes, with an
|
||||
empty ICD column for DAK users to fill in.
|
||||
"""
|
||||
xlsx_bytes = generate_coding_template(db, jahr=jahr, fallgruppe=fallgruppe)
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
return StreamingResponse(
|
||||
BytesIO(xlsx_bytes),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": 'attachment; filename="ICD_Coding_Template.xlsx"'
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Paginated case list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/", response_model=CaseListResponse)
|
||||
def list_cases(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(50, ge=1, le=200),
|
||||
jahr: Optional[int] = Query(None),
|
||||
kw: Optional[int] = Query(None),
|
||||
fallgruppe: Optional[str] = Query(None),
|
||||
has_icd: Optional[bool] = Query(None),
|
||||
has_coding: Optional[bool] = Query(None),
|
||||
search: Optional[str] = Query(None, min_length=1, max_length=200),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Return a paginated, filterable list of cases.
|
||||
|
||||
Accessible to both admin and dak_mitarbeiter users.
|
||||
|
||||
Filters:
|
||||
- ``jahr``: calendar year
|
||||
- ``kw``: calendar week
|
||||
- ``fallgruppe``: one of onko, kardio, intensiv, galle, sd
|
||||
- ``has_icd``: True = only cases WITH icd; False = only cases WITHOUT
|
||||
- ``has_coding``: True = only with gutachten_typ; False = only without
|
||||
- ``search``: free-text search across nachname, vorname, fall_id, kvnr
|
||||
"""
|
||||
query = db.query(Case)
|
||||
|
||||
if jahr is not None:
|
||||
query = query.filter(Case.jahr == jahr)
|
||||
if kw is not None:
|
||||
query = query.filter(Case.kw == kw)
|
||||
if fallgruppe is not None:
|
||||
query = query.filter(Case.fallgruppe == fallgruppe)
|
||||
if has_icd is True:
|
||||
query = query.filter(Case.icd != None) # noqa: E711
|
||||
elif has_icd is False:
|
||||
query = query.filter(Case.icd == None) # noqa: E711
|
||||
if has_coding is True:
|
||||
query = query.filter(Case.gutachten_typ != None) # noqa: E711
|
||||
elif has_coding is False:
|
||||
query = query.filter(Case.gutachten_typ == None) # noqa: E711
|
||||
if search:
|
||||
like_pattern = f"%{search}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Case.nachname.ilike(like_pattern),
|
||||
Case.vorname.ilike(like_pattern),
|
||||
Case.fall_id.ilike(like_pattern),
|
||||
Case.kvnr.ilike(like_pattern),
|
||||
)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
cases = (
|
||||
query.order_by(Case.datum.desc(), Case.id.desc())
|
||||
.offset((page - 1) * per_page)
|
||||
.limit(per_page)
|
||||
.all()
|
||||
)
|
||||
|
||||
return CaseListResponse(
|
||||
items=cases,
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Single case detail
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/{case_id}", response_model=CaseResponse)
|
||||
def get_case(
|
||||
case_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Return a single case by its database ID."""
|
||||
case = db.query(Case).filter(Case.id == case_id).first()
|
||||
if not case:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Case not found",
|
||||
)
|
||||
return case
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Update case (admin only)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.put("/{case_id}", response_model=CaseResponse)
|
||||
def update_case(
|
||||
case_id: int,
|
||||
payload: CaseUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Update arbitrary case fields (admin only).
|
||||
|
||||
Only fields included in the request body are applied. Changes are
|
||||
recorded in the audit log.
|
||||
"""
|
||||
case = db.query(Case).filter(Case.id == case_id).first()
|
||||
if not case:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Case not found",
|
||||
)
|
||||
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
if not update_data:
|
||||
return case # Nothing to update
|
||||
|
||||
old_values: dict = {}
|
||||
new_values: dict = {}
|
||||
|
||||
for field, value in update_data.items():
|
||||
current = getattr(case, field)
|
||||
# Convert date objects to strings for JSON-safe audit log
|
||||
current_cmp = current
|
||||
value_cmp = value
|
||||
if current_cmp != value_cmp:
|
||||
old_values[field] = str(current) if current is not None else None
|
||||
new_values[field] = str(value) if value is not None else None
|
||||
setattr(case, field, value)
|
||||
|
||||
if new_values:
|
||||
case.updated_by = user.id
|
||||
db.commit()
|
||||
db.refresh(case)
|
||||
|
||||
log_action(
|
||||
db,
|
||||
user_id=user.id,
|
||||
action="case_updated",
|
||||
entity_type="case",
|
||||
entity_id=case.id,
|
||||
old_values=old_values,
|
||||
new_values=new_values,
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
)
|
||||
|
||||
return case
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ICD entry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.put("/{case_id}/icd", response_model=CaseResponse)
|
||||
def set_case_icd(
|
||||
case_id: int,
|
||||
payload: ICDUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Set ICD codes for a case. Accessible to both admin and dak_mitarbeiter.
|
||||
|
||||
Replaces any existing ICD codes. The raw ICD string is validated,
|
||||
normalised, and split into individual ``CaseICDCode`` entries.
|
||||
"""
|
||||
try:
|
||||
case = save_icd_for_case(db, case_id, payload.icd, user.id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=str(exc),
|
||||
)
|
||||
|
||||
log_action(
|
||||
db,
|
||||
user_id=user.id,
|
||||
action="icd_entered",
|
||||
entity_type="case",
|
||||
entity_id=case.id,
|
||||
new_values={"icd": payload.icd},
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
)
|
||||
|
||||
return case
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Coding (Gutachten classification)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.put("/{case_id}/coding", response_model=CaseResponse)
|
||||
def set_case_coding(
|
||||
case_id: int,
|
||||
payload: CodingUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Set coding fields on a case (admin only).
|
||||
|
||||
Records the Gutachten classification (Bestätigung/Alternative) and
|
||||
therapy-change flags.
|
||||
"""
|
||||
case = db.query(Case).filter(Case.id == case_id).first()
|
||||
if not case:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Case not found",
|
||||
)
|
||||
|
||||
# Validate gutachten_typ
|
||||
if payload.gutachten_typ not in ("Bestätigung", "Alternative"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="gutachten_typ must be 'Bestätigung' or 'Alternative'",
|
||||
)
|
||||
|
||||
# Validate therapieaenderung
|
||||
if payload.therapieaenderung not in ("Ja", "Nein"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="therapieaenderung must be 'Ja' or 'Nein'",
|
||||
)
|
||||
|
||||
old_values = {
|
||||
"gutachten_typ": case.gutachten_typ,
|
||||
"therapieaenderung": case.therapieaenderung,
|
||||
"ta_diagnosekorrektur": case.ta_diagnosekorrektur,
|
||||
"ta_unterversorgung": case.ta_unterversorgung,
|
||||
"ta_uebertherapie": case.ta_uebertherapie,
|
||||
}
|
||||
|
||||
case.gutachten_typ = payload.gutachten_typ
|
||||
case.therapieaenderung = payload.therapieaenderung
|
||||
case.ta_diagnosekorrektur = payload.ta_diagnosekorrektur
|
||||
case.ta_unterversorgung = payload.ta_unterversorgung
|
||||
case.ta_uebertherapie = payload.ta_uebertherapie
|
||||
case.coding_completed_by = user.id
|
||||
case.coding_completed_at = datetime.now(timezone.utc)
|
||||
case.updated_by = user.id
|
||||
|
||||
db.commit()
|
||||
db.refresh(case)
|
||||
|
||||
new_values = {
|
||||
"gutachten_typ": case.gutachten_typ,
|
||||
"therapieaenderung": case.therapieaenderung,
|
||||
"ta_diagnosekorrektur": case.ta_diagnosekorrektur,
|
||||
"ta_unterversorgung": case.ta_unterversorgung,
|
||||
"ta_uebertherapie": case.ta_uebertherapie,
|
||||
}
|
||||
|
||||
log_action(
|
||||
db,
|
||||
user_id=user.id,
|
||||
action="coding_completed",
|
||||
entity_type="case",
|
||||
entity_id=case.id,
|
||||
old_values=old_values,
|
||||
new_values=new_values,
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
)
|
||||
|
||||
return case
|
||||
268
backend/app/api/import_router.py
Normal file
268
backend/app/api/import_router.py
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
"""Import API routes — CSV upload/preview/confirm, ICD xlsx upload, import log."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.core.dependencies import get_current_user, require_admin
|
||||
from app.database import get_db
|
||||
from app.models.audit import ImportLog
|
||||
from app.models.user import User
|
||||
from app.schemas.import_schemas import ImportPreview, ImportResult
|
||||
from app.services.csv_parser import parse_csv
|
||||
from app.services.icd_service import import_icd_from_xlsx
|
||||
from app.services.import_service import confirm_import, preview_import
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Response schemas (co-located for simplicity)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ICDImportResponse(BaseModel):
|
||||
"""Result of importing ICD codes from an xlsx file."""
|
||||
|
||||
updated: int
|
||||
errors: list[str] = []
|
||||
|
||||
|
||||
class ImportLogResponse(BaseModel):
|
||||
"""Single import-log entry."""
|
||||
|
||||
id: int
|
||||
filename: str
|
||||
import_type: str
|
||||
cases_imported: int
|
||||
cases_skipped: int
|
||||
cases_updated: int
|
||||
errors: Optional[str] = None
|
||||
imported_by: Optional[int] = None
|
||||
imported_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class ImportLogListResponse(BaseModel):
|
||||
"""Paginated import-log response."""
|
||||
|
||||
items: list[ImportLogResponse]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CSV Import — preview (dry-run)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/csv", response_model=ImportPreview)
|
||||
async def upload_csv_preview(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Upload a CRM CSV file and preview what will be imported.
|
||||
|
||||
Returns a list of rows with duplicate detection. No data is written
|
||||
to the database — the caller must confirm via ``POST /csv/confirm``.
|
||||
"""
|
||||
if not file.filename or not file.filename.lower().endswith(".csv"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be a .csv file",
|
||||
)
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > settings.MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"File exceeds maximum size of {settings.MAX_UPLOAD_SIZE // (1024 * 1024)}MB",
|
||||
)
|
||||
|
||||
try:
|
||||
parsed = parse_csv(content, file.filename)
|
||||
except Exception as exc:
|
||||
logger.exception("CSV parsing failed for %s", file.filename)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"CSV parsing failed: {exc}",
|
||||
)
|
||||
|
||||
return preview_import(db, parsed, file.filename)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CSV Import — confirm (write to DB)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/csv/confirm", response_model=ImportResult)
|
||||
async def confirm_csv_import(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Re-upload the same CSV file to confirm the import.
|
||||
|
||||
The file is re-parsed and non-duplicate cases are inserted into the
|
||||
database. An entry is written to the ``import_log`` table.
|
||||
"""
|
||||
if not file.filename or not file.filename.lower().endswith(".csv"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be a .csv file",
|
||||
)
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > settings.MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"File exceeds maximum size of {settings.MAX_UPLOAD_SIZE // (1024 * 1024)}MB",
|
||||
)
|
||||
|
||||
try:
|
||||
parsed = parse_csv(content, file.filename)
|
||||
except Exception as exc:
|
||||
logger.exception("CSV parsing failed for %s", file.filename)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"CSV parsing failed: {exc}",
|
||||
)
|
||||
|
||||
return confirm_import(db, parsed, file.filename, user.id)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ICD Excel upload
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/icd-xlsx", response_model=ICDImportResponse)
|
||||
async def upload_icd_xlsx(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Upload a filled-in ICD coding Excel template.
|
||||
|
||||
Expects columns: Case_ID (col 1), ICD (col 7). Accessible to both
|
||||
admin and dak_mitarbeiter users.
|
||||
"""
|
||||
if not file.filename or not file.filename.lower().endswith((".xlsx", ".xls")):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be an Excel file (.xlsx)",
|
||||
)
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > settings.MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"File exceeds maximum size of {settings.MAX_UPLOAD_SIZE // (1024 * 1024)}MB",
|
||||
)
|
||||
|
||||
try:
|
||||
result = import_icd_from_xlsx(db, content, user.id)
|
||||
except Exception as exc:
|
||||
logger.exception("ICD xlsx import failed for %s", file.filename)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"ICD import failed: {exc}",
|
||||
)
|
||||
|
||||
# Log the import
|
||||
log = ImportLog(
|
||||
filename=file.filename,
|
||||
import_type="icd_xlsx",
|
||||
cases_imported=0,
|
||||
cases_skipped=0,
|
||||
cases_updated=result["updated"],
|
||||
errors="; ".join(result["errors"]) if result["errors"] else None,
|
||||
imported_by=user.id,
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
|
||||
return ICDImportResponse(**result)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Historical Excel upload (placeholder — full logic in Task 12)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/historical", response_model=ImportResult)
|
||||
async def upload_historical_excel(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Upload the Abrechnung_DAK.xlsx for historical data import (admin only).
|
||||
|
||||
This is a placeholder endpoint. The full import logic will be
|
||||
implemented in Task 12.
|
||||
"""
|
||||
if not file.filename or not file.filename.lower().endswith((".xlsx", ".xls")):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be an Excel file (.xlsx)",
|
||||
)
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > settings.MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"File exceeds maximum size of {settings.MAX_UPLOAD_SIZE // (1024 * 1024)}MB",
|
||||
)
|
||||
|
||||
# TODO: Implement historical import in Task 12
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Historical Excel import is not yet implemented",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Import log (history)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/log", response_model=ImportLogListResponse)
|
||||
def get_import_log(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
import_type: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Return a paginated list of past imports (admin only)."""
|
||||
query = db.query(ImportLog)
|
||||
|
||||
if import_type:
|
||||
query = query.filter(ImportLog.import_type == import_type)
|
||||
|
||||
total = query.count()
|
||||
items = (
|
||||
query.order_by(ImportLog.imported_at.desc())
|
||||
.offset((page - 1) * per_page)
|
||||
.limit(per_page)
|
||||
.all()
|
||||
)
|
||||
|
||||
return ImportLogListResponse(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
)
|
||||
125
backend/app/schemas/case.py
Normal file
125
backend/app/schemas/case.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
"""Pydantic schemas for case list, detail, update, ICD, and coding endpoints."""
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CaseResponse(BaseModel):
|
||||
"""Full representation of a case returned by list and detail endpoints."""
|
||||
|
||||
id: int
|
||||
fall_id: Optional[str] = None
|
||||
crm_ticket_id: Optional[str] = None
|
||||
jahr: int
|
||||
kw: int
|
||||
datum: date
|
||||
anrede: Optional[str] = None
|
||||
vorname: Optional[str] = None
|
||||
nachname: str
|
||||
geburtsdatum: Optional[date] = None
|
||||
kvnr: Optional[str] = None
|
||||
versicherung: str
|
||||
icd: Optional[str] = None
|
||||
fallgruppe: str
|
||||
strasse: Optional[str] = None
|
||||
plz: Optional[str] = None
|
||||
ort: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
ansprechpartner: Optional[str] = None
|
||||
telefonnummer: Optional[str] = None
|
||||
mobiltelefon: Optional[str] = None
|
||||
unterlagen: bool
|
||||
unterlagen_verschickt: Optional[date] = None
|
||||
erhalten: Optional[bool] = None
|
||||
unterlagen_erhalten: Optional[date] = None
|
||||
unterlagen_an_gutachter: Optional[date] = None
|
||||
gutachten: bool
|
||||
gutachter: Optional[str] = None
|
||||
gutachten_erstellt: Optional[date] = None
|
||||
gutachten_versendet: Optional[date] = None
|
||||
schweigepflicht: bool
|
||||
ablehnung: bool
|
||||
abbruch: bool
|
||||
abbruch_datum: Optional[date] = None
|
||||
gutachten_typ: Optional[str] = None
|
||||
therapieaenderung: Optional[str] = None
|
||||
ta_diagnosekorrektur: bool
|
||||
ta_unterversorgung: bool
|
||||
ta_uebertherapie: bool
|
||||
kurzbeschreibung: Optional[str] = None
|
||||
fragestellung: Optional[str] = None
|
||||
kommentar: Optional[str] = None
|
||||
sonstiges: Optional[str] = None
|
||||
abgerechnet: bool
|
||||
abrechnung_datum: Optional[date] = None
|
||||
import_source: Optional[str] = None
|
||||
imported_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class CaseListResponse(BaseModel):
|
||||
"""Paginated case list response."""
|
||||
|
||||
items: list[CaseResponse]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
||||
class CaseUpdate(BaseModel):
|
||||
"""Partial update for case fields (admin only).
|
||||
|
||||
All fields are optional; only provided fields are applied.
|
||||
"""
|
||||
|
||||
anrede: Optional[str] = None
|
||||
vorname: Optional[str] = None
|
||||
nachname: Optional[str] = None
|
||||
geburtsdatum: Optional[date] = None
|
||||
kvnr: Optional[str] = None
|
||||
strasse: Optional[str] = None
|
||||
plz: Optional[str] = None
|
||||
ort: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
ansprechpartner: Optional[str] = None
|
||||
telefonnummer: Optional[str] = None
|
||||
mobiltelefon: Optional[str] = None
|
||||
unterlagen: Optional[bool] = None
|
||||
unterlagen_verschickt: Optional[date] = None
|
||||
erhalten: Optional[bool] = None
|
||||
unterlagen_erhalten: Optional[date] = None
|
||||
unterlagen_an_gutachter: Optional[date] = None
|
||||
gutachten: Optional[bool] = None
|
||||
gutachter: Optional[str] = None
|
||||
gutachten_erstellt: Optional[date] = None
|
||||
gutachten_versendet: Optional[date] = None
|
||||
schweigepflicht: Optional[bool] = None
|
||||
ablehnung: Optional[bool] = None
|
||||
abbruch: Optional[bool] = None
|
||||
abbruch_datum: Optional[date] = None
|
||||
kurzbeschreibung: Optional[str] = None
|
||||
fragestellung: Optional[str] = None
|
||||
kommentar: Optional[str] = None
|
||||
sonstiges: Optional[str] = None
|
||||
abgerechnet: Optional[bool] = None
|
||||
abrechnung_datum: Optional[date] = None
|
||||
|
||||
|
||||
class ICDUpdate(BaseModel):
|
||||
"""Payload for setting ICD codes on a case."""
|
||||
|
||||
icd: str
|
||||
|
||||
|
||||
class CodingUpdate(BaseModel):
|
||||
"""Payload for setting coding/gutachten classification on a case."""
|
||||
|
||||
gutachten_typ: str # "Bestätigung" or "Alternative"
|
||||
therapieaenderung: str # "Ja" or "Nein"
|
||||
ta_diagnosekorrektur: bool = False
|
||||
ta_unterversorgung: bool = False
|
||||
ta_uebertherapie: bool = False
|
||||
Loading…
Reference in a new issue