from __future__ import annotations from typing import Any import json from pathlib import Path from fastapi import APIRouter, Depends, Query, Response, UploadFile from fastapi.responses import FileResponse, HTMLResponse, JSONResponse from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.api.dependencies import current_user from app.db.session import get_session from app.models.contact import Contact from app.models.customer import Customer 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.orion.service import OrionReportService from app.schemas.common import PaginatedResponse from app.schemas.domain import ( ContactCreate, ContactRead, ContactUpdate, CustomerCreate, CustomerRead, CustomerUpdate, DeviceCreate, DeviceRead, DeviceUpdate, EquipmentCreate, EquipmentRead, EquipmentUpdate, LocationCreate, LocationRead, LocationUpdate, ValidationCreate, ValidationImportPreview, ValidationImportRequest, ValidationImportSummary, ValidationRead, ValidationReview, ValidationUpdate, ) from app.services.domain_service import CrudService, DomainServices from app.services.validation_workflow import ValidationWorkflowService router = APIRouter(tags=["domain"], dependencies=[Depends(current_user)]) @router.get("/dashboard") def dashboard(session: Session = Depends(get_session)) -> dict[str, int]: today = func.current_date() return { "customers": session.scalar(select(func.count()).select_from(Customer)) or 0, "locations": session.scalar(select(func.count()).select_from(Location)) or 0, "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, "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, } def paging( page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100), search: str | None = Query(default=None, max_length=120), ) -> dict[str, Any]: return {"page": page, "page_size": page_size, "search": search} def commit_create(session: Session, service: CrudService, payload): try: item = service.create(payload.model_dump()) if isinstance(item, Validation): ValidationWorkflowService(session).apply_revalidation_date(item) session.commit() session.refresh(item) return item except IntegrityError as exc: session.rollback() from fastapi import HTTPException raise HTTPException(status_code=409, detail="Datensatz verletzt Datenbankbeziehungen.") from exc def commit_update(session: Session, service: CrudService, item_id: str, payload): try: item = service.update(item_id, payload.model_dump()) if isinstance(item, Validation): ValidationWorkflowService(session).apply_revalidation_date(item) session.commit() session.refresh(item) return item except IntegrityError as exc: session.rollback() from fastapi import HTTPException raise HTTPException(status_code=409, detail="Datensatz verletzt Datenbankbeziehungen.") from exc def commit_delete(session: Session, service: CrudService, item_id: str) -> Response: service.delete(item_id) session.commit() return Response(status_code=204) @router.get("/customers", response_model=PaginatedResponse[CustomerRead]) def list_customers(params: dict = Depends(paging), session: Session = Depends(get_session)): return DomainServices(session).customers.list(**params) @router.post("/customers", response_model=CustomerRead, status_code=201) def create_customer(payload: CustomerCreate, session: Session = Depends(get_session)): return commit_create(session, DomainServices(session).customers, payload) @router.put("/customers/{item_id}", response_model=CustomerRead) def update_customer(item_id: str, payload: CustomerUpdate, session: Session = Depends(get_session)): return commit_update(session, DomainServices(session).customers, item_id, payload) @router.delete("/customers/{item_id}", status_code=204) def delete_customer(item_id: str, session: Session = Depends(get_session)): return commit_delete(session, DomainServices(session).customers, item_id) @router.get("/locations", response_model=PaginatedResponse[LocationRead]) def list_locations(params: dict = Depends(paging), session: Session = Depends(get_session)): return DomainServices(session).locations.list(**params) @router.post("/locations", response_model=LocationRead, status_code=201) def create_location(payload: LocationCreate, session: Session = Depends(get_session)): return commit_create(session, DomainServices(session).locations, payload) @router.put("/locations/{item_id}", response_model=LocationRead) def update_location(item_id: str, payload: LocationUpdate, session: Session = Depends(get_session)): return commit_update(session, DomainServices(session).locations, item_id, payload) @router.delete("/locations/{item_id}", status_code=204) def delete_location(item_id: str, session: Session = Depends(get_session)): return commit_delete(session, DomainServices(session).locations, item_id) @router.get("/contacts", response_model=PaginatedResponse[ContactRead]) def list_contacts(params: dict = Depends(paging), session: Session = Depends(get_session)): return DomainServices(session).contacts.list(**params) @router.post("/contacts", response_model=ContactRead, status_code=201) def create_contact(payload: ContactCreate, session: Session = Depends(get_session)): return commit_create(session, DomainServices(session).contacts, payload) @router.put("/contacts/{item_id}", response_model=ContactRead) def update_contact(item_id: str, payload: ContactUpdate, session: Session = Depends(get_session)): return commit_update(session, DomainServices(session).contacts, item_id, payload) @router.delete("/contacts/{item_id}", status_code=204) def delete_contact(item_id: str, session: Session = Depends(get_session)): return commit_delete(session, DomainServices(session).contacts, item_id) @router.get("/devices", response_model=PaginatedResponse[DeviceRead]) def list_devices(params: dict = Depends(paging), session: Session = Depends(get_session)): return DomainServices(session).devices.list(**params) @router.post("/devices", response_model=DeviceRead, status_code=201) def create_device(payload: DeviceCreate, session: Session = Depends(get_session)): return commit_create(session, DomainServices(session).devices, payload) @router.put("/devices/{item_id}", response_model=DeviceRead) def update_device(item_id: str, payload: DeviceUpdate, session: Session = Depends(get_session)): return commit_update(session, DomainServices(session).devices, item_id, payload) @router.delete("/devices/{item_id}", status_code=204) def delete_device(item_id: str, session: Session = Depends(get_session)): return commit_delete(session, DomainServices(session).devices, item_id) @router.get("/equipment", response_model=PaginatedResponse[EquipmentRead]) def list_equipment(params: dict = Depends(paging), session: Session = Depends(get_session)): return DomainServices(session).equipment.list(**params) @router.post("/equipment", response_model=EquipmentRead, status_code=201) def create_equipment(payload: EquipmentCreate, session: Session = Depends(get_session)): return commit_create(session, DomainServices(session).equipment, payload) @router.put("/equipment/{item_id}", response_model=EquipmentRead) def update_equipment(item_id: str, payload: EquipmentUpdate, session: Session = Depends(get_session)): return commit_update(session, DomainServices(session).equipment, item_id, payload) @router.delete("/equipment/{item_id}", status_code=204) def delete_equipment(item_id: str, session: Session = Depends(get_session)): return commit_delete(session, DomainServices(session).equipment, item_id) @router.get("/validations", response_model=PaginatedResponse[ValidationRead]) def list_validations( search: str | None = Query(default=None, max_length=120), page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100), sort_by: str = Query(default="updated_at"), sort_order: str = Query(default="desc", pattern="^(asc|desc)$"), status: str | None = None, customer_id: str | None = None, device_id: str | None = None, validation_type: str | None = None, result: str | None = None, date_from: str | None = None, date_to: str | None = None, overdue_only: bool = False, session: Session = Depends(get_session), ): return ValidationWorkflowService(session).query_validations( search=search, page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order, filters={ "status": status, "customer_id": customer_id, "device_id": device_id, "validation_type": validation_type, "result": result, "date_from": date_from, "date_to": date_to, "overdue_only": overdue_only, }, ) @router.get("/validations/next-report-number") def next_report_number(session: Session = Depends(get_session)) -> dict[str, str]: count = session.scalar(select(func.count()).select_from(Validation)) or 0 return {"report_number": f"VAL-{count + 1:05d}"} @router.get("/validations/{item_id}", response_model=ValidationRead) def get_validation(item_id: str, 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") return item @router.post("/validations", response_model=ValidationRead, status_code=201) def create_validation(payload: ValidationCreate, session: Session = Depends(get_session)): return commit_create(session, DomainServices(session).validations, payload) @router.put("/validations/{item_id}", response_model=ValidationRead) def update_validation(item_id: str, payload: ValidationUpdate, session: Session = Depends(get_session)): return commit_update(session, DomainServices(session).validations, item_id, payload) @router.delete("/validations/{item_id}", status_code=204) def delete_validation(item_id: str, session: Session = Depends(get_session)): item = DomainServices(session).validations.repository.get(item_id) if item and item.status != "ENTWURF": from fastapi import HTTPException raise HTTPException(status_code=409, detail="Only draft validations can be deleted") return commit_delete(session, DomainServices(session).validations, item_id) @router.post("/validations/{item_id}/review", response_model=ValidationReview) def review_validation(item_id: str, 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") review = ValidationWorkflowService(session).mark_ready_for_review(item) session.commit() return review @router.post("/validations/{item_id}/duplicate", response_model=ValidationRead, status_code=201) def duplicate_validation(item_id: str, 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") clone = ValidationWorkflowService(session).duplicate(item) session.commit() session.refresh(clone) return clone @router.post("/validations/{item_id}/new-version", response_model=ValidationRead, status_code=201) def new_validation_version(item_id: str, 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") clone = ValidationWorkflowService(session).create_new_version(item) session.commit() session.refresh(clone) return clone @router.post("/validations/{item_id}/cancel", response_model=ValidationRead) def cancel_validation(item_id: str, 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") item.status = "STORNIERT" session.commit() session.refresh(item) return item @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) if item is None: from fastapi import HTTPException raise HTTPException(status_code=404, detail="Resource not found") data = ValidationWorkflowService(session).export_json(item) return JSONResponse( content=json.loads(json.dumps(data, default=str)), headers={"Content-Disposition": f'attachment; filename="{item.report_number}.json"'}, ) @router.post("/validations/import/csv-preview", response_model=ValidationImportPreview) async def preview_validation_csv(file: UploadFile, session: Session = Depends(get_session)): return ValidationWorkflowService(session).preview_csv(await file.read()) @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) session.commit() return summary @router.get("/validations/{item_id}/report.html", response_class=HTMLResponse) def validation_report_preview(item_id: str, session: Session = Depends(get_session)): return OrionReportService(session, Path("/app/reports")).render_html(item_id) @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) return FileResponse( report_path, media_type="application/pdf", filename=report_path.name, )