"""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 selectinload from app.api.deps import 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