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, Slot, Sms, User, Volunteer, VolunteerGroup from app.schemas.requests import ( GroupMembershipRequest, GroupSmsRequest, VolunteerGroupCreateRequest, VolunteerGroupUpdateRequest, ) from app.schemas.responses import SMSResponse, VolunteerGroupResponse, VolunteerResponse 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) 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) 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}") 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) 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) 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}/add-to-slot/{slot_id}", response_model=list[VolunteerResponse]) async def add_group_to_slot( project_id: UUID, group_id: UUID, slot_id: UUID, current_user: User = Depends(deps.require_org_role(*MANAGE_GROUPS)), session: Session = Depends(deps.get_session), ): """Bulk-assigns every member of the group to the slot in one call. This is a one-time copy, not a live link: adding someone to the group later does NOT retroactively add them to slots already assigned.""" group = _get_group_or_404(session, project_id, group_id) slot = session.get(Slot, slot_id) if slot is None or slot.project_id != str(project_id): raise HTTPException(status_code=404, detail="Slot not found in this project") existing_ids = {v.id for v in slot.volunteers} for volunteer in group.volunteers: if volunteer.id not in existing_ids: slot.volunteers.append(volunteer) session.commit() session.refresh(slot) return slot.volunteers @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