Spaces:
Sleeping
Sleeping
| """ | |
| Bulk Invitation Management Endpoints - CSV bulk user invitations | |
| """ | |
| from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form | |
| from sqlalchemy.orm import Session | |
| from typing import Optional | |
| from uuid import UUID | |
| from app.api.deps import get_db, get_current_active_user | |
| from app.models.user import User | |
| from app.schemas.bulk_invitation import ( | |
| BulkAnalysisResult, | |
| BulkExecutionRequest, | |
| BulkExecutionResult | |
| ) | |
| from app.services.bulk_invitation_service import BulkInvitationService | |
| from app.core.permissions import require_permission | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter(prefix="/invitations/bulk", tags=["Bulk Invitations"]) | |
| # Initialize service | |
| bulk_invitation_service = BulkInvitationService() | |
| async def analyze_bulk_invitation_csv( | |
| csv_file: UploadFile = File(..., description="CSV file with user data"), | |
| project_id: Optional[UUID] = Form(None, description="Project context (for project-level bulk invite)"), | |
| client_id: Optional[UUID] = Form(None, description="Client context (for org-level bulk invite)"), | |
| contractor_id: Optional[UUID] = Form(None, description="Contractor context (for org-level bulk invite)"), | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| """ | |
| Upload and analyze CSV file for bulk invitations | |
| **CSV Format:** | |
| - Required columns: email, role | |
| - Optional columns: first_name, last_name, phone, invitation_method | |
| - Project-specific columns: project_role, region, subcontractor | |
| **Context:** | |
| - Provide ONE of: project_id, client_id, or contractor_id | |
| - project_id: Bulk invite to specific project | |
| - client_id/contractor_id: Bulk invite to organization | |
| **Authorization:** | |
| - platform_admin: Can bulk invite to any context | |
| - project_manager: Can bulk invite to their projects | |
| - dispatcher/sales_manager: Can bulk invite to their contractor's projects | |
| - client_admin: Can bulk invite to their client | |
| - contractor_admin: Can bulk invite to their contractor | |
| **Returns:** | |
| - Analysis of CSV with categorized users (in-memory, not persisted) | |
| - Frontend should store this and send back selected users for execution | |
| """ | |
| # Validate file type | |
| if not csv_file.filename.endswith('.csv'): | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="File must be a CSV file" | |
| ) | |
| # Validate context (one and only one) | |
| contexts = [project_id, client_id, contractor_id] | |
| non_none_count = sum(1 for ctx in contexts if ctx is not None) | |
| if non_none_count == 0: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Must provide either project_id, client_id, or contractor_id" | |
| ) | |
| if non_none_count > 1: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Can only provide one context: project_id, client_id, or contractor_id" | |
| ) | |
| result = await bulk_invitation_service.analyze_csv( | |
| csv_file=csv_file, | |
| project_id=str(project_id) if project_id else None, | |
| client_id=str(client_id) if client_id else None, | |
| contractor_id=str(contractor_id) if contractor_id else None, | |
| current_user=current_user, | |
| db=db | |
| ) | |
| return result | |
| async def execute_bulk_invitation( | |
| execution_request: BulkExecutionRequest, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| """ | |
| Execute bulk invitation import | |
| **Process:** | |
| 1. Add existing users to project (if project context) | |
| 2. Send invitations to new users | |
| 3. Return detailed results | |
| **Request Body:** | |
| - users_to_add: List of existing users to add to project | |
| - users_to_invite: List of new users to invite | |
| - Context: project_id, client_id, or contractor_id | |
| **Authorization:** | |
| - Same as analyze endpoint | |
| **Returns:** | |
| - Detailed results with success/failure counts | |
| - Individual results for each user | |
| **Note:** | |
| - Bulk operations are tracked in invitation_metadata.bulk_operation_id | |
| - No session persistence - frontend manages the flow | |
| """ | |
| result = await bulk_invitation_service.execute_bulk_import( | |
| execution_request=execution_request, | |
| current_user=current_user, | |
| db=db | |
| ) | |
| return result | |
| async def download_csv_template( | |
| template_type: str = "organization" | |
| ): | |
| """ | |
| Download CSV template for bulk invitations | |
| **Template Types:** | |
| - organization: For org-level bulk invites (simpler) | |
| - project: For project-level bulk invites (includes project fields) | |
| **Returns:** | |
| - CSV file with headers and example row | |
| """ | |
| from fastapi.responses import StreamingResponse | |
| import io | |
| if template_type == "project": | |
| headers = [ | |
| "email", "first_name", "last_name", "phone", "role", | |
| "invitation_method", "project_role", "region", "subcontractor" | |
| ] | |
| example_row = [ | |
| "john@example.com", "John", "Doe", "+254712345678", "field_agent", | |
| "whatsapp", "Installer", "Nairobi West", "" | |
| ] | |
| else: | |
| headers = [ | |
| "email", "first_name", "last_name", "phone", "role", "invitation_method" | |
| ] | |
| example_row = [ | |
| "john@example.com", "John", "Doe", "+254712345678", "field_agent", "whatsapp" | |
| ] | |
| # Create CSV in memory | |
| output = io.StringIO() | |
| import csv | |
| writer = csv.writer(output) | |
| writer.writerow(headers) | |
| writer.writerow(example_row) | |
| # Return as downloadable file | |
| output.seek(0) | |
| return StreamingResponse( | |
| iter([output.getvalue()]), | |
| media_type="text/csv", | |
| headers={ | |
| "Content-Disposition": f"attachment; filename=bulk_invitation_template_{template_type}.csv" | |
| } | |
| ) | |