style(orion): align report header footer and page layout

This commit is contained in:
Schubert Ferenc 2026-07-11 12:19:10 +02:00
parent 302e542fda
commit 503b343070
40 changed files with 2010 additions and 148 deletions

View file

@ -3,9 +3,12 @@ from __future__ import annotations
from typing import Any
import json
import logging
import shutil
import uuid
from pathlib import Path
from fastapi import APIRouter, Depends, Query, Response, UploadFile
from fastapi import APIRouter, Depends, File, Form, Query, Response, UploadFile
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
@ -19,6 +22,7 @@ from app.models.device import Device
from app.models.equipment import Equipment
from app.models.location import Location
from app.models.validation import Validation
from app.modules.helios.service import HeliosImportService
from app.modules.orion.service import OrionReportService
from app.schemas.common import PaginatedResponse
from app.schemas.domain import (
@ -37,6 +41,8 @@ from app.schemas.domain import (
LocationCreate,
LocationRead,
LocationUpdate,
MeasurementImportConfirmRequest,
MeasurementImportPreviewRead,
ValidationCreate,
ValidationImportPreview,
ValidationImportRequest,
@ -49,6 +55,25 @@ from app.services.domain_service import CrudService, DomainServices
from app.services.validation_workflow import ValidationWorkflowService
router = APIRouter(tags=["domain"], dependencies=[Depends(current_user)])
logger = logging.getLogger(__name__)
@router.get("/report-templates/default/checklists")
def default_report_checklists(session: Session = Depends(get_session)) -> list[dict]:
from app.modules.orion.template_service import ReportTemplateService
bundle = ReportTemplateService(session).ensure_default_template()
session.commit()
return [
{
"checklist_key": item.checklist_key,
"title": item.title,
"columns": item.columns,
"items": item.items,
"order_index": item.order_index,
}
for item in bundle.checklists
]
@router.get("/dashboard")
@ -60,12 +85,47 @@ def dashboard(session: Session = Depends(get_session)) -> dict[str, int]:
"contacts": session.scalar(select(func.count()).select_from(Contact)) or 0,
"devices": session.scalar(select(func.count()).select_from(Device)) or 0,
"equipment": session.scalar(select(func.count()).select_from(Equipment)) or 0,
"equipment_green": session.scalar(
select(func.count()).select_from(Equipment).where(Equipment.status == "green")
)
or 0,
"equipment_yellow": session.scalar(
select(func.count()).select_from(Equipment).where(Equipment.status == "yellow")
)
or 0,
"equipment_red": session.scalar(
select(func.count()).select_from(Equipment).where(Equipment.status == "red")
)
or 0,
"validations": session.scalar(select(func.count()).select_from(Validation)) or 0,
"validation_drafts": session.scalar(select(func.count()).select_from(Validation).where(Validation.status == "ENTWURF")) or 0,
"validation_ready": session.scalar(select(func.count()).select_from(Validation).where(Validation.status == "BEREIT_ZUR_PRUEFUNG")) or 0,
"validation_in_review": session.scalar(select(func.count()).select_from(Validation).where(Validation.status == "IN_PRUEFUNG")) or 0,
"validation_approved": session.scalar(select(func.count()).select_from(Validation).where(Validation.status == "FREIGEGEBEN")) or 0,
"validation_overdue": session.scalar(select(func.count()).select_from(Validation).where(Validation.next_validation_on < today)) or 0,
"validation_drafts": session.scalar(
select(func.count()).select_from(Validation).where(Validation.status == "ENTWURF")
)
or 0,
"validation_ready": session.scalar(
select(func.count())
.select_from(Validation)
.where(Validation.status == "BEREIT_ZUR_PRUEFUNG")
)
or 0,
"validation_in_review": session.scalar(
select(func.count()).select_from(Validation).where(Validation.status == "IN_PRUEFUNG")
)
or 0,
"validation_approved": session.scalar(
select(func.count()).select_from(Validation).where(Validation.status == "FREIGEGEBEN")
)
or 0,
"validation_completed": session.scalar(
select(func.count()).select_from(Validation).where(Validation.status == "ABGESCHLOSSEN")
)
or 0,
"validation_overdue": session.scalar(
select(func.count())
.select_from(Validation)
.where(Validation.next_validation_on < today)
)
or 0,
}
@ -89,7 +149,9 @@ def commit_create(session: Session, service: CrudService, payload):
session.rollback()
from fastapi import HTTPException
raise HTTPException(status_code=409, detail="Datensatz verletzt Datenbankbeziehungen.") from exc
raise HTTPException(
status_code=409, detail="Datensatz verletzt Datenbankbeziehungen."
) from exc
def commit_update(session: Session, service: CrudService, item_id: str, payload):
@ -104,7 +166,9 @@ def commit_update(session: Session, service: CrudService, item_id: str, payload)
session.rollback()
from fastapi import HTTPException
raise HTTPException(status_code=409, detail="Datensatz verletzt Datenbankbeziehungen.") from exc
raise HTTPException(
status_code=409, detail="Datensatz verletzt Datenbankbeziehungen."
) from exc
def commit_delete(session: Session, service: CrudService, item_id: str) -> Response:
@ -204,7 +268,9 @@ def create_equipment(payload: EquipmentCreate, session: Session = Depends(get_se
@router.put("/equipment/{item_id}", response_model=EquipmentRead)
def update_equipment(item_id: str, payload: EquipmentUpdate, session: Session = Depends(get_session)):
def update_equipment(
item_id: str, payload: EquipmentUpdate, session: Session = Depends(get_session)
):
return commit_update(session, DomainServices(session).equipment, item_id, payload)
@ -271,7 +337,9 @@ def create_validation(payload: ValidationCreate, session: Session = Depends(get_
@router.put("/validations/{item_id}", response_model=ValidationRead)
def update_validation(item_id: str, payload: ValidationUpdate, session: Session = Depends(get_session)):
def update_validation(
item_id: str, payload: ValidationUpdate, session: Session = Depends(get_session)
):
return commit_update(session, DomainServices(session).validations, item_id, payload)
@ -336,6 +404,96 @@ def cancel_validation(item_id: str, session: Session = Depends(get_session)):
return item
@router.post("/validations/{item_id}/attachments")
def upload_validation_attachment(
item_id: str,
category: str = Form(...),
description: str = Form(default=""),
order: int = Form(default=0),
file: UploadFile = File(...),
session: Session = Depends(get_session),
):
item = DomainServices(session).validations.repository.get(item_id)
if item is None:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Resource not found")
if item.status in {"FREIGEGEBEN", "ABGESCHLOSSEN"}:
from fastapi import HTTPException
raise HTTPException(status_code=409, detail="Freigegebene Berichte sind schreibgeschuetzt.")
original_name = Path(file.filename or "anlage").name
suffix = Path(original_name).suffix
stored_name = f"{uuid.uuid4().hex}{suffix}"
upload_dir = Path("/app/uploads/validations") / item_id
upload_dir.mkdir(parents=True, exist_ok=True)
storage_path = upload_dir / stored_name
with storage_path.open("wb") as target:
shutil.copyfileobj(file.file, target)
attachment = {
"category": category,
"filename": original_name,
"content_type": file.content_type,
"description": description,
"order": order,
"storage_path": str(storage_path),
"url": f"/uploads/validations/{item_id}/{stored_name}",
}
current = list(item.attachments or [])
current.append(attachment)
item.attachments = current
session.commit()
return attachment
@router.post(
"/validations/{item_id}/measurement-imports/winlog-pdf",
response_model=MeasurementImportPreviewRead,
)
async def upload_winlog_pdf_import(
item_id: str,
file: UploadFile = File(...),
attachment_only: bool = Form(default=False),
session: Session = Depends(get_session),
):
item = DomainServices(session).validations.repository.get(item_id)
if item is None:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Resource not found")
preview = HeliosImportService(session).save_winlog_pdf(
item_id,
file.filename or "winlog.pdf",
await file.read(),
attachment_only=attachment_only,
)
session.commit()
return preview
@router.post(
"/measurement-imports/{import_id}/confirm",
response_model=MeasurementImportPreviewRead,
)
def confirm_measurement_import(
import_id: str,
payload: MeasurementImportConfirmRequest,
session: Session = Depends(get_session),
):
try:
preview = HeliosImportService(session).confirm_values(
import_id, [item.model_dump() for item in payload.values]
)
except ValueError as exc:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail=str(exc)) from exc
session.commit()
return preview
@router.get("/validations/{item_id}/export.json")
def export_validation_json(item_id: str, session: Session = Depends(get_session)):
item = DomainServices(session).validations.repository.get(item_id)
@ -356,8 +514,12 @@ async def preview_validation_csv(file: UploadFile, session: Session = Depends(ge
@router.post("/validations/import/json", response_model=ValidationImportSummary)
def import_validation_json(payload: ValidationImportRequest, session: Session = Depends(get_session)):
summary = ValidationWorkflowService(session).import_rows(payload.rows, payload.duplicate_strategy)
def import_validation_json(
payload: ValidationImportRequest, session: Session = Depends(get_session)
):
summary = ValidationWorkflowService(session).import_rows(
payload.rows, payload.duplicate_strategy
)
session.commit()
return summary
@ -369,7 +531,16 @@ def validation_report_preview(item_id: str, session: Session = Depends(get_sessi
@router.get("/validations/{item_id}/report.pdf")
def validation_report_pdf(item_id: str, session: Session = Depends(get_session)):
report_path = OrionReportService(session, Path("/app/reports")).render_pdf(item_id)
try:
report_path = OrionReportService(session, Path("/app/reports")).render_pdf(item_id)
except Exception as exc:
logger.exception("PDF generation failed for validation %s", item_id)
from fastapi import HTTPException
raise HTTPException(
status_code=502,
detail="Der PDF-Bericht konnte nicht erzeugt werden. Die HTML-Vorschau ist weiterhin verfuegbar.",
) from exc
return FileResponse(
report_path,
media_type="application/pdf",