from fastapi import APIRouter, Depends, File, Form, Header, HTTPException, Query, UploadFile, status from fastapi.responses import FileResponse from sqlalchemy.orm import Session from starlette.requests import Request from typing import cast from app.core.config import settings from app.core.rbac import require_permission from app.db.database import get_db from app.models.repair import Repair from app.models.user import User from app.repositories.repair_repository import RepairRepository from app.schemas.repair import ( RepairCreate, RepairDocumentResponse, RepairDocumentType, RepairDocumentUpdate, RepairDocumentVisibility, RepairIntakePayload, RepairIntakeResponse, RepairNotificationEventResponse, RepairListResponse, RepairNotificationOverviewResponse, RepairPublicLinkCreateResponse, RepairPublicLinkResponse, RepairPublicStatusResponse, RepairResponse, RepairStatus, RepairStatusHistoryResponse, RepairStatusUpdate, RepairUpdate, ) from app.services.repair_notification_service import RepairNotificationService from app.services.repair_document_service import RepairDocumentService from app.services.repair_public_link_service import RepairPublicLinkService from app.services.repair_service import RepairService router = APIRouter(tags=["Repairs"]) def get_repair_or_404(db: Session, repair_id: int) -> Repair: repair = RepairRepository.get_by_id(db, repair_id) if repair is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Reparatur nicht gefunden") return repair @router.get("/repairs", response_model=RepairListResponse) @router.get("/repairs/", response_model=RepairListResponse, include_in_schema=False) def get_repairs( status_filter: str | None = Query(default=None, alias="status"), q: str | None = None, customer_id: int | None = None, source: str | None = None, priority: str | None = None, limit: int = Query(default=50, ge=1, le=100), offset: int = Query(default=0, ge=0), db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.read")), ): items, total = RepairRepository.list( db, status=status_filter, q=q, customer_id=customer_id, source=source, priority=priority, limit=limit, offset=offset, ) return RepairListResponse(items=items, total=total, limit=limit, offset=offset) @router.get("/repairs/{repair_id}", response_model=RepairResponse) def get_repair( repair_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.read")), ): return get_repair_or_404(db, repair_id) @router.post("/repairs", response_model=RepairResponse, status_code=status.HTTP_201_CREATED) @router.post("/repairs/", response_model=RepairResponse, status_code=status.HTTP_201_CREATED, include_in_schema=False) def create_repair( repair: RepairCreate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.create")), ): return RepairService.create(db, repair, actor=current_user, request=request) @router.put("/repairs/{repair_id}", response_model=RepairResponse) def update_repair( repair_id: int, repair: RepairUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.update")), ): db_repair = get_repair_or_404(db, repair_id) return RepairService.update(db, db_repair, repair, actor=current_user, request=request) @router.delete("/repairs/{repair_id}", response_model=RepairResponse) def delete_repair( repair_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.delete")), ): db_repair = get_repair_or_404(db, repair_id) return RepairService.cancel(db, db_repair, actor=current_user, request=request) @router.put("/repairs/{repair_id}/status", response_model=RepairResponse) def update_repair_status( repair_id: int, payload: RepairStatusUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.status.update")), ): db_repair = get_repair_or_404(db, repair_id) return RepairService.update_status(db, db_repair, payload, actor=current_user, request=request) @router.get("/repairs/{repair_id}/history", response_model=list[RepairStatusHistoryResponse]) def get_repair_history( repair_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.read")), ): get_repair_or_404(db, repair_id) return RepairRepository.get_history(db, repair_id) @router.get("/repairs/{repair_id}/documents", response_model=list[RepairDocumentResponse]) def list_repair_documents( repair_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.read")), ): get_repair_or_404(db, repair_id) return RepairRepository.list_documents(db, repair_id) @router.post("/repairs/{repair_id}/documents/upload", response_model=RepairDocumentResponse, status_code=status.HTTP_201_CREATED) async def upload_repair_document( repair_id: int, request: Request, file: UploadFile = File(...), title: str = Form(..., min_length=1, max_length=255), document_type: RepairDocumentType = Form("other"), visibility: RepairDocumentVisibility = Form("internal"), note: str | None = Form(None), db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.update")), ): db_repair = get_repair_or_404(db, repair_id) normalized_title = title.strip() normalized_note = note.strip() if note else None if not normalized_title: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Titel ist erforderlich") return await RepairDocumentService.upload_document( db, db_repair, file=file, title=normalized_title, document_type=document_type, visibility=visibility, note=normalized_note, actor=current_user, request=request, ) @router.get("/repairs/{repair_id}/documents/{document_id}", response_model=RepairDocumentResponse) def get_repair_document( repair_id: int, document_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.read")), ): get_repair_or_404(db, repair_id) document = RepairRepository.get_document(db, repair_id=repair_id, document_id=document_id) if document is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument nicht gefunden") return document @router.put("/repairs/{repair_id}/documents/{document_id}", response_model=RepairDocumentResponse) def update_repair_document( repair_id: int, document_id: int, payload: RepairDocumentUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.update")), ): db_repair = get_repair_or_404(db, repair_id) document = RepairRepository.get_document(db, repair_id=repair_id, document_id=document_id) if document is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument nicht gefunden") return RepairDocumentService.update_document(db, db_repair, document, payload, actor=current_user, request=request) @router.get("/repairs/{repair_id}/documents/{document_id}/download") def download_repair_document( repair_id: int, document_id: int, disposition: str = Query(default="inline", pattern="^(inline|attachment)$"), db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.read")), ): get_repair_or_404(db, repair_id) document = RepairRepository.get_document(db, repair_id=repair_id, document_id=document_id) if document is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument nicht gefunden") file_path = RepairDocumentService.open_document_file(document) return FileResponse( file_path, media_type=document.mime_type or "application/octet-stream", headers={ "Content-Disposition": RepairDocumentService.content_disposition( document, mode="attachment" if disposition == "attachment" else "inline", ), }, ) @router.delete("/repairs/{repair_id}/documents/{document_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_repair_document( repair_id: int, document_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.update")), ): db_repair = get_repair_or_404(db, repair_id) document = RepairRepository.get_document(db, repair_id=repair_id, document_id=document_id) if document is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument nicht gefunden") RepairDocumentService.delete_document(db, db_repair, document, actor=current_user, request=request) return None @router.get("/repairs/{repair_id}/public-link", response_model=RepairPublicLinkResponse) def get_public_link( repair_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.public_link.manage")), ): db_repair = get_repair_or_404(db, repair_id) return RepairPublicLinkService.get(db, db_repair) @router.post("/repairs/{repair_id}/public-link", response_model=RepairPublicLinkCreateResponse, status_code=status.HTTP_201_CREATED) def create_public_link( repair_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.public_link.manage")), ): db_repair = get_repair_or_404(db, repair_id) return RepairPublicLinkService.create(db, db_repair, actor=current_user, request=request) @router.delete("/repairs/{repair_id}/public-link", response_model=RepairPublicLinkResponse) def revoke_public_link( repair_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.public_link.manage")), ): db_repair = get_repair_or_404(db, repair_id) return RepairPublicLinkService.revoke(db, db_repair, actor=current_user, request=request) @router.get("/repairs/{repair_id}/notifications", response_model=RepairNotificationOverviewResponse) def get_repair_notifications( repair_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.read")), ): db_repair = get_repair_or_404(db, repair_id) return RepairNotificationService.overview(db, db_repair) @router.post("/repairs/{repair_id}/send-status-mail", response_model=RepairNotificationEventResponse) def send_repair_status_mail( repair_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("repairs.update")), ): db_repair = get_repair_or_404(db, repair_id) return RepairNotificationService.send_status_mail(db, db_repair, actor=current_user, request=request, manual=True) @router.post("/public/repair-intake", response_model=RepairIntakeResponse, status_code=status.HTTP_201_CREATED) def create_repair_intake( payload: RepairIntakePayload, request: Request, x_olympus_intake_token: str | None = Header(default=None, alias="X-Olympus-Intake-Token"), db: Session = Depends(get_db), ): expected_token = settings.olympus_repair_intake_token if not expected_token or not x_olympus_intake_token or x_olympus_intake_token != expected_token: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Nicht autorisiert") repair = RepairService.create_from_intake(db, payload, request=request) return RepairIntakeResponse( repair_number=repair.repair_number, status=cast(RepairStatus, repair.status), message="Reparaturanfrage übernommen", ) @router.get("/public/repairs/status/{token}", response_model=RepairPublicStatusResponse) def get_public_repair_status( token: str, db: Session = Depends(get_db), ): return RepairPublicLinkService.public_status(db, token)