270 lines
9.0 KiB
Python
270 lines
9.0 KiB
Python
|
|
"""Crew CRUD endpoints."""
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||
|
|
from sqlalchemy import select, func, or_
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
from sqlalchemy.orm import joinedload, selectinload
|
||
|
|
|
||
|
|
from app.api.deps import get_current_user, require_permission
|
||
|
|
from app.db.session import get_async_session
|
||
|
|
from app.models import User, Crew, CrewAvailability
|
||
|
|
from app.schemas.crew import (
|
||
|
|
CrewCreateRequest,
|
||
|
|
CrewUpdateRequest,
|
||
|
|
CrewResponse,
|
||
|
|
CrewListResponse,
|
||
|
|
CrewAvailabilityCreateRequest,
|
||
|
|
CrewAvailabilityUpdateRequest,
|
||
|
|
CrewAvailabilityResponse,
|
||
|
|
CrewAvailabilityListResponse,
|
||
|
|
)
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/crew", tags=["crew"])
|
||
|
|
avail_router = APIRouter(prefix="/crew-availabilities", tags=["crew-availabilities"])
|
||
|
|
|
||
|
|
# ===== Crew Endpoints =====
|
||
|
|
|
||
|
|
@router.get("", response_model=CrewListResponse)
|
||
|
|
async def list_crew(
|
||
|
|
page: int = Query(1, ge=1),
|
||
|
|
size: int = Query(20, ge=1, le=100),
|
||
|
|
search: str | None = Query(None, description="Search in name, email, role"),
|
||
|
|
is_active: bool | None = Query(None, description="Filter by active status"),
|
||
|
|
current_user: User = Depends(require_permission("crew:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""List crew members with search, filter, pagination."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
base_q = select(Crew).where(Crew.account_id == account_id)
|
||
|
|
|
||
|
|
if search:
|
||
|
|
pattern = f"%{search}%"
|
||
|
|
base_q = base_q.where(
|
||
|
|
or_(
|
||
|
|
Crew.first_name.ilike(pattern),
|
||
|
|
Crew.last_name.ilike(pattern),
|
||
|
|
Crew.email.ilike(pattern),
|
||
|
|
Crew.role_title.ilike(pattern),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
if is_active is not None:
|
||
|
|
base_q = base_q.where(Crew.is_active == is_active)
|
||
|
|
|
||
|
|
count_q = select(func.count()).select_from(base_q.subquery())
|
||
|
|
total = (await session.execute(count_q)).scalar() or 0
|
||
|
|
|
||
|
|
q = (
|
||
|
|
base_q
|
||
|
|
.options(selectinload(Crew.availabilities))
|
||
|
|
.order_by(Crew.last_name, Crew.first_name)
|
||
|
|
.offset((page - 1) * size)
|
||
|
|
.limit(size)
|
||
|
|
)
|
||
|
|
result = await session.execute(q)
|
||
|
|
items = result.unique().scalars().all()
|
||
|
|
|
||
|
|
return CrewListResponse(
|
||
|
|
items=[CrewResponse.model_validate(i) for i in items],
|
||
|
|
total=total,
|
||
|
|
page=page,
|
||
|
|
size=size,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("", response_model=CrewResponse, status_code=status.HTTP_201_CREATED)
|
||
|
|
async def create_crew(
|
||
|
|
body: CrewCreateRequest,
|
||
|
|
current_user: User = Depends(require_permission("crew:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Create a new crew member."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
member = Crew(account_id=account_id, **body.model_dump())
|
||
|
|
session.add(member)
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(member)
|
||
|
|
return CrewResponse.model_validate(member)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{member_id}", response_model=CrewResponse)
|
||
|
|
async def get_crew(
|
||
|
|
member_id: str,
|
||
|
|
current_user: User = Depends(require_permission("crew:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Get a specific crew member with availabilities."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
result = await session.execute(
|
||
|
|
select(Crew)
|
||
|
|
.options(selectinload(Crew.availabilities))
|
||
|
|
.where(Crew.id == member_id, Crew.account_id == account_id)
|
||
|
|
)
|
||
|
|
member = result.unique().scalars().first()
|
||
|
|
if not member:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found")
|
||
|
|
return CrewResponse.model_validate(member)
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/{member_id}", response_model=CrewResponse)
|
||
|
|
async def update_crew(
|
||
|
|
member_id: str,
|
||
|
|
body: CrewUpdateRequest,
|
||
|
|
current_user: User = Depends(require_permission("crew:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Update a crew member."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
result = await session.execute(
|
||
|
|
select(Crew)
|
||
|
|
.options(selectinload(Crew.availabilities))
|
||
|
|
.where(Crew.id == member_id, Crew.account_id == account_id)
|
||
|
|
)
|
||
|
|
member = result.unique().scalars().first()
|
||
|
|
if not member:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found")
|
||
|
|
|
||
|
|
update_data = body.model_dump(exclude_unset=True)
|
||
|
|
for field, value in update_data.items():
|
||
|
|
setattr(member, field, value)
|
||
|
|
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(member)
|
||
|
|
return CrewResponse.model_validate(member)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/{member_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
|
|
async def delete_crew(
|
||
|
|
member_id: str,
|
||
|
|
current_user: User = Depends(require_permission("crew:delete")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Delete a crew member."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
result = await session.execute(
|
||
|
|
select(Crew).where(Crew.id == member_id, Crew.account_id == account_id)
|
||
|
|
)
|
||
|
|
member = result.scalars().first()
|
||
|
|
if not member:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found")
|
||
|
|
|
||
|
|
await session.delete(member)
|
||
|
|
await session.commit()
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
# ===== CrewAvailability Endpoints =====
|
||
|
|
|
||
|
|
@avail_router.get("", response_model=CrewAvailabilityListResponse)
|
||
|
|
async def list_availabilities(
|
||
|
|
page: int = Query(1, ge=1),
|
||
|
|
size: int = Query(50, ge=1, le=200),
|
||
|
|
crew_id: str | None = Query(None, description="Filter by crew member"),
|
||
|
|
status: str | None = Query(None, description="Filter by status"),
|
||
|
|
current_user: User = Depends(require_permission("crew:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""List availabilities with filters."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
base_q = (
|
||
|
|
select(CrewAvailability)
|
||
|
|
.join(Crew)
|
||
|
|
.where(Crew.account_id == account_id)
|
||
|
|
)
|
||
|
|
|
||
|
|
if crew_id:
|
||
|
|
base_q = base_q.where(CrewAvailability.crew_id == crew_id)
|
||
|
|
if status:
|
||
|
|
base_q = base_q.where(CrewAvailability.status == status)
|
||
|
|
|
||
|
|
count_q = select(func.count()).select_from(base_q.subquery())
|
||
|
|
total = (await session.execute(count_q)).scalar() or 0
|
||
|
|
|
||
|
|
q = base_q.order_by(CrewAvailability.start_date.desc()).offset((page - 1) * size).limit(size)
|
||
|
|
result = await session.execute(q)
|
||
|
|
items = result.scalars().all()
|
||
|
|
|
||
|
|
return CrewAvailabilityListResponse(
|
||
|
|
items=[CrewAvailabilityResponse.model_validate(i) for i in items],
|
||
|
|
total=total,
|
||
|
|
page=page,
|
||
|
|
size=size,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@avail_router.post("", response_model=CrewAvailabilityResponse, status_code=status.HTTP_201_CREATED)
|
||
|
|
async def create_availability(
|
||
|
|
body: CrewAvailabilityCreateRequest,
|
||
|
|
current_user: User = Depends(require_permission("crew:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Create a new availability entry."""
|
||
|
|
# Verify crew member belongs to this tenant
|
||
|
|
crew_result = await session.execute(
|
||
|
|
select(Crew).where(
|
||
|
|
Crew.id == body.crew_id,
|
||
|
|
Crew.account_id == current_user.account_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
if not crew_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found")
|
||
|
|
|
||
|
|
avail = CrewAvailability(**body.model_dump())
|
||
|
|
session.add(avail)
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(avail)
|
||
|
|
return CrewAvailabilityResponse.model_validate(avail)
|
||
|
|
|
||
|
|
|
||
|
|
@avail_router.put("/{avail_id}", response_model=CrewAvailabilityResponse)
|
||
|
|
async def update_availability(
|
||
|
|
avail_id: str,
|
||
|
|
body: CrewAvailabilityUpdateRequest,
|
||
|
|
current_user: User = Depends(require_permission("crew:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Update an availability entry."""
|
||
|
|
result = await session.execute(
|
||
|
|
select(CrewAvailability)
|
||
|
|
.join(Crew)
|
||
|
|
.where(
|
||
|
|
CrewAvailability.id == avail_id,
|
||
|
|
Crew.account_id == current_user.account_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
avail = result.scalars().first()
|
||
|
|
if not avail:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found")
|
||
|
|
|
||
|
|
update_data = body.model_dump(exclude_unset=True)
|
||
|
|
for field, value in update_data.items():
|
||
|
|
setattr(avail, field, value)
|
||
|
|
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(avail)
|
||
|
|
return CrewAvailabilityResponse.model_validate(avail)
|
||
|
|
|
||
|
|
|
||
|
|
@avail_router.delete("/{avail_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
|
|
async def delete_availability(
|
||
|
|
avail_id: str,
|
||
|
|
current_user: User = Depends(require_permission("crew:delete")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Delete an availability entry."""
|
||
|
|
result = await session.execute(
|
||
|
|
select(CrewAvailability)
|
||
|
|
.join(Crew)
|
||
|
|
.where(
|
||
|
|
CrewAvailability.id == avail_id,
|
||
|
|
Crew.account_id == current_user.account_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
avail = result.scalars().first()
|
||
|
|
if not avail:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found")
|
||
|
|
|
||
|
|
await session.delete(avail)
|
||
|
|
await session.commit()
|
||
|
|
return None
|