from uuid import UUID from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.orm import Session from app.api import deps from app.api.utils import ( # adjust import path if different assert_project_exists_or_404, verify_id_list, ) from app.models import OrgRole, Sms, User, Volunteer, VolunteerGroup from app.schemas.requests import ( GroupMembershipRequest, GroupSmsRequest, VolunteerGroupCreateRequest, VolunteerGroupUpdateRequest, ) from app.schemas.responses import SMSResponse, VolunteerGroupResponse router = APIRouter(prefix="/project/{project_id}", tags=["volunteer_groups"]) MANAGE_GROUPS = (OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE) def _get_group_or_404(session: Session, project_id: UUID, group_id: UUID) -> VolunteerGroup: group = session.get(VolunteerGroup, group_id) if group is None or group.project_id != str(project_id): raise HTTPException(status_code=404, detail="Group not found") return group @router.get("/groups", response_model=list[VolunteerGroupResponse]) async def list_project_groups( project_id: UUID, current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)), session: Session = Depends(deps.get_session), ): """List volunteer groups from project""" assert_project_exists_or_404(session, project_id) results = session.execute(select(VolunteerGroup).where(VolunteerGroup.project_id == project_id)) return results.scalars().all() @router.post( "/group", response_model=VolunteerGroupResponse, openapi_extra={"sse_event": "volunteer_group_created"}, ) async def create_group( project_id: UUID, new_group: VolunteerGroupCreateRequest, current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)), session: Session = Depends(deps.get_session), ): """Create a new volunteer group in the project""" assert_project_exists_or_404(session, project_id) group = VolunteerGroup(project_id=project_id, **new_group.model_dump()) session.add(group) session.commit() session.refresh(group) return group @router.get( "/group/{group_id}", response_model=VolunteerGroupResponse, ) async def get_group( project_id: UUID, group_id: UUID, current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)), session: Session = Depends(deps.get_session), ): """Get a single volunteer group""" return _get_group_or_404(session, project_id, group_id) @router.post( "/group/{group_id}", response_model=VolunteerGroupResponse, openapi_extra={"sse_event": "volunteer_group_updated"}, ) async def update_group( project_id: UUID, group_id: UUID, edit_group: VolunteerGroupUpdateRequest, current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)), session: Session = Depends(deps.get_session), ): """Edit a volunteer group""" group = _get_group_or_404(session, project_id, group_id) group.name = edit_group.name group.color = edit_group.color session.commit() session.refresh(group) return group @router.delete( "/group/{group_id}", openapi_extra={"sse_event": "volunteer_group_deleted"}, ) async def delete_group( project_id: UUID, group_id: UUID, current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)), session: Session = Depends(deps.get_session), ): """Delete a volunteer group (does not delete its volunteers)""" group = _get_group_or_404(session, project_id, group_id) session.delete(group) session.commit() @router.post( "/group/{group_id}/volunteers", response_model=VolunteerGroupResponse, openapi_extra={"sse_event": "volunteer_group_updated"}, ) async def add_volunteers_to_group( project_id: UUID, group_id: UUID, payload: GroupMembershipRequest, current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)), session: Session = Depends(deps.get_session), ): """Bulk-add volunteers to a group (idempotent -- already-members are skipped)""" group = _get_group_or_404(session, project_id, group_id) await verify_id_list( session, payload.volunteer_ids, project_id, Volunteer, "Invalid volunteer list" ) existing_ids = {v.id for v in group.volunteers} for vid in payload.volunteer_ids: if str(vid) in existing_ids: continue volunteer = session.get(Volunteer, vid) group.volunteers.append(volunteer) session.commit() session.refresh(group) return group @router.delete( "/group/{group_id}/volunteer/{volunteer_id}", response_model=VolunteerGroupResponse, openapi_extra={"sse_event": "volunteer_group_updated"}, ) async def remove_volunteer_from_group( project_id: UUID, group_id: UUID, volunteer_id: UUID, current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)), session: Session = Depends(deps.get_session), ): """Remove a single volunteer from a group""" group = _get_group_or_404(session, project_id, group_id) volunteer = session.get(Volunteer, volunteer_id) if volunteer is not None and volunteer in group.volunteers: group.volunteers.remove(volunteer) session.commit() session.refresh(group) return group @router.post("/group/{group_id}/send-sms", response_model=list[SMSResponse]) async def send_sms_to_group( project_id: UUID, group_id: UUID, payload: GroupSmsRequest, current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)), session: Session = Depends(deps.get_session), ): """Sends an SMS immediately to every group member who has automatic_sms enabled and a phone number on file.""" group = _get_group_or_404(session, project_id, group_id) sms_list = [] for volunteer in group.volunteers: if not volunteer.automatic_sms or not volunteer.phone_number: continue content = payload.content.replace("{prenom}", volunteer.name).replace( "{nom}", volunteer.surname ) sms = Sms( project_id=project_id, volunteer_id=volunteer.id, content=content, phone_number=volunteer.phone_number, ) sms_list.append(sms) session.add_all(sms_list) session.commit() return sms_list