feat(repairs): add repair estimates

This commit is contained in:
Schubert Ferenc 2026-07-05 00:57:02 +02:00
parent 6e7e75f864
commit 4436fe5f73
25 changed files with 1802 additions and 9 deletions

View file

@ -11,6 +11,7 @@ from app.models.audit import AuditLog
from app.models.user import User
from app.repositories.customer_repository import CustomerRepository
from app.repositories.repair_repository import RepairRepository
from app.repositories.repair_estimate_repository import RepairEstimateRepository
from app.repositories.user_repository import UserRepository
from app.schemas.dashboard import DashboardSummary, EmptyWidget, MetricCard, SystemStatusItem
from app.services.system_settings_service import SystemSettingsService
@ -73,6 +74,14 @@ def get_dashboard_summary(
MetricCard(label="Reparaturdokumente", value=RepairRepository.count_documents(db)),
]
if "repair_estimates.read" in permissions:
repairs.extend([
MetricCard(label="Offene KVs", value=RepairEstimateRepository.count_open(db)),
MetricCard(label="Warten auf Freigabe", value=RepairEstimateRepository.count_waiting(db)),
MetricCard(label="KVs freigegeben heute", value=RepairEstimateRepository.count_approved_today(db)),
MetricCard(label="KVs abgelehnt", value=RepairEstimateRepository.count_declined(db)),
])
if "system_settings.manage" in permissions:
smtp_config = SystemSettingsService.get_smtp_runtime_config(db)
public_links = SystemSettingsService.get_public_links_settings(db)

View file

@ -0,0 +1,181 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from starlette.requests import Request
from app.core.rbac import require_permission
from app.db.database import get_db
from app.models.repair import Repair
from app.models.repair_estimate import RepairEstimate
from app.models.user import User
from app.repositories.repair_estimate_repository import RepairEstimateRepository
from app.repositories.repair_repository import RepairRepository
from app.schemas.repair_estimate import (
PublicEstimateDecisionRequest,
PublicEstimateResponse,
RepairEstimateCreate,
RepairEstimateEventResponse,
RepairEstimateResponse,
RepairEstimateUpdate,
)
from app.services.repair_estimate_service import RepairEstimateService
from app.services.repair_public_link_service import RepairPublicLinkService
router = APIRouter(tags=["Repair Estimates"])
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
def get_estimate_or_404(db: Session, repair_id: int, estimate_id: int) -> RepairEstimate:
estimate = RepairEstimateRepository.get(db, repair_id=repair_id, estimate_id=estimate_id)
if estimate is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kostenvoranschlag nicht gefunden")
return estimate
@router.get("/repairs/{repair_id}/estimates", response_model=list[RepairEstimateResponse])
def list_estimates(
repair_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("repair_estimates.read")),
):
get_repair_or_404(db, repair_id)
return RepairEstimateRepository.list_by_repair(db, repair_id)
@router.post("/repairs/{repair_id}/estimates", response_model=RepairEstimateResponse, status_code=status.HTTP_201_CREATED)
def create_estimate(
repair_id: int,
payload: RepairEstimateCreate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("repair_estimates.create")),
):
repair = get_repair_or_404(db, repair_id)
return RepairEstimateService.create(db, repair, payload, actor=current_user, request=request)
@router.get("/repairs/{repair_id}/estimates/{estimate_id}", response_model=RepairEstimateResponse)
def get_estimate(
repair_id: int,
estimate_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("repair_estimates.read")),
):
get_repair_or_404(db, repair_id)
return get_estimate_or_404(db, repair_id, estimate_id)
@router.put("/repairs/{repair_id}/estimates/{estimate_id}", response_model=RepairEstimateResponse)
def update_estimate(
repair_id: int,
estimate_id: int,
payload: RepairEstimateUpdate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("repair_estimates.update")),
):
repair = get_repair_or_404(db, repair_id)
estimate = get_estimate_or_404(db, repair_id, estimate_id)
return RepairEstimateService.update(db, repair, estimate, payload, actor=current_user, request=request)
@router.delete("/repairs/{repair_id}/estimates/{estimate_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_estimate(
repair_id: int,
estimate_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("repair_estimates.delete")),
):
repair = get_repair_or_404(db, repair_id)
estimate = get_estimate_or_404(db, repair_id, estimate_id)
RepairEstimateService.delete(db, repair, estimate, actor=current_user, request=request)
return None
@router.post("/repairs/{repair_id}/estimates/{estimate_id}/send", response_model=RepairEstimateResponse)
def send_estimate(
repair_id: int,
estimate_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("repair_estimates.send")),
):
repair = get_repair_or_404(db, repair_id)
estimate = get_estimate_or_404(db, repair_id, estimate_id)
return RepairEstimateService.send(db, repair, estimate, actor=current_user, request=request)
@router.post("/repairs/{repair_id}/estimates/{estimate_id}/cancel", response_model=RepairEstimateResponse)
def cancel_estimate(
repair_id: int,
estimate_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("repair_estimates.update")),
):
repair = get_repair_or_404(db, repair_id)
estimate = get_estimate_or_404(db, repair_id, estimate_id)
return RepairEstimateService.cancel(db, repair, estimate, actor=current_user, request=request)
@router.get("/repairs/{repair_id}/estimates/{estimate_id}/events", response_model=list[RepairEstimateEventResponse])
def list_estimate_events(
repair_id: int,
estimate_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("repair_estimates.read")),
):
estimate = get_estimate_or_404(db, repair_id, estimate_id)
return estimate.events
def get_public_repair_and_estimate(db: Session, token: str) -> tuple[Repair, RepairEstimate]:
public_link = RepairPublicLinkService.get_public_link_or_404(db, token)
repair = public_link.repair
estimate = RepairEstimateRepository.get_latest_public(db, repair.id)
if estimate is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kostenvoranschlag nicht gefunden")
RepairRepository.mark_public_link_used(db, public_link)
return repair, estimate
@router.post("/public/repairs/status/{token}/estimate/approve", response_model=PublicEstimateResponse)
def approve_public_estimate(
token: str,
payload: PublicEstimateDecisionRequest,
request: Request,
db: Session = Depends(get_db),
):
repair, estimate = get_public_repair_and_estimate(db, token)
updated = RepairEstimateService.customer_decision(db, repair, estimate, "approve", payload, request=request)
return RepairEstimateService.public_response(updated)
@router.post("/public/repairs/status/{token}/estimate/decline", response_model=PublicEstimateResponse)
def decline_public_estimate(
token: str,
payload: PublicEstimateDecisionRequest,
request: Request,
db: Session = Depends(get_db),
):
repair, estimate = get_public_repair_and_estimate(db, token)
updated = RepairEstimateService.customer_decision(db, repair, estimate, "decline", payload, request=request)
return RepairEstimateService.public_response(updated)
@router.post("/public/repairs/status/{token}/estimate/question", response_model=PublicEstimateResponse)
def question_public_estimate(
token: str,
payload: PublicEstimateDecisionRequest,
request: Request,
db: Session = Depends(get_db),
):
repair, estimate = get_public_repair_and_estimate(db, token)
updated = RepairEstimateService.customer_decision(db, repair, estimate, "question", payload, request=request)
return RepairEstimateService.public_response(updated)