style(orion): align report header footer and page layout
This commit is contained in:
parent
302e542fda
commit
503b343070
40 changed files with 2010 additions and 148 deletions
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue