feat: Wahab demo prep — Cancelled status, phone persistence, admin delete, users picker, detail-page fixes
- Add Cancelled as terminal status reachable from all active states; exclude
from SLA breach reporting and dashboard active counts; add to status pickers
- Persist customer phone on ticket create/update (was silently dropped);
add tickets.phone migration + legacy self-heal guard
- Add admin-only DELETE /api/tickets/{id} (removes timeline/photos/escalations)
- Add GET /api/auth/users for the assign-technician dropdown (was hardcoded)
- TicketOut now returns nested unit/category so the detail page stops showing
'—' for Unit/Property/Category
- Ticket numbering uses max+1 so deletions never re-issue a number
- New-issue form: require Category and (standard mode) Priority client-side
- Tests: 11 new cases covering cancellation, SLA exemption, phone, delete,
users endpoint, numbering
This commit is contained in:
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -22,6 +23,21 @@ from app.services import auth as auth_service
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.get("/users", response_model=list[UserOut])
|
||||
async def list_users(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
) -> list[User]:
|
||||
"""List users (id, name, role) for assignment pickers.
|
||||
|
||||
Previously the frontend hard-coded technician ids/names in detail.html;
|
||||
this endpoint makes the assign dropdown data-driven so a seed change never
|
||||
silently breaks technician assignment.
|
||||
"""
|
||||
result = await db.execute(select(User).order_by(User.full_name))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserOut, status_code=201)
|
||||
async def register(
|
||||
body: RegisterRequest,
|
||||
|
||||
+20
-1
@@ -12,7 +12,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.core.security import get_current_user, require_roles
|
||||
from app.models.category import Category
|
||||
from app.models.ticket import Ticket, TicketPhoto
|
||||
from app.models.unit import Unit
|
||||
@@ -250,6 +250,25 @@ async def update_ticket(
|
||||
return ticket
|
||||
|
||||
|
||||
@router.delete("/{ticket_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_ticket(
|
||||
ticket_id: int,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(require_roles("Admin/Jerome", "Admin/Wahab"))],
|
||||
) -> None:
|
||||
"""Delete a ticket and its children (timeline, photos, escalations).
|
||||
|
||||
Admin-only: intended for removing test/scratch tickets from the demo
|
||||
database, never for routine workflow use.
|
||||
"""
|
||||
ticket = await ticket_service.delete_ticket(db, ticket_id)
|
||||
# Remove orphaned photo files from disk after the DB rows are gone.
|
||||
for photo in ticket.photos:
|
||||
if photo.photo_url:
|
||||
name = photo.photo_url.rsplit("/", 1)[-1]
|
||||
(UPLOADS_DIR / name).unlink(missing_ok=True)
|
||||
|
||||
|
||||
# ── Status Transitions (convenience endpoints) ───────────────────────
|
||||
@router.post("/{ticket_id}/status", response_model=TicketOut)
|
||||
async def change_ticket_status(
|
||||
|
||||
Reference in New Issue
Block a user