| from typing import Optional |
| from uuid import UUID |
| from datetime import datetime |
| from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form |
| from fastapi.security import OAuth2PasswordBearer |
| from pydantic import BaseModel |
| from sqlalchemy import select |
| from sqlalchemy.ext.asyncio import AsyncSession |
| from sqlalchemy.orm import selectinload |
| import jwt |
| from jwt import PyJWTError |
|
|
| from Backend.database.connection import get_db |
| from Backend.database.models import Issue, Member |
| from Backend.core.logging import get_logger |
| from Backend.core.config import settings |
| from Backend.utils.storage import save_upload, get_upload_url |
|
|
| logger = get_logger(__name__) |
| router = APIRouter() |
|
|
| oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/admin/login") |
|
|
| async def get_current_worker( |
| token: str = Depends(oauth2_scheme), |
| db: AsyncSession = Depends(get_db) |
| ) -> Member: |
| try: |
| payload = jwt.decode(token, settings.supabase_jwt_secret, algorithms=["HS256"]) |
| member_id = payload.get("sub") |
| if not member_id: |
| raise HTTPException(status_code=401, detail="Invalid token") |
| |
| member = await db.get(Member, UUID(member_id)) |
| if not member or not member.is_active: |
| raise HTTPException(status_code=401, detail="User not found or inactive") |
| |
| if member.role not in ["worker", "admin"]: |
| raise HTTPException(status_code=403, detail="Not a worker") |
| |
| return member |
| except PyJWTError: |
| raise HTTPException(status_code=401, detail="Invalid token") |
|
|
|
|
| class TaskResponse(BaseModel): |
| id: UUID |
| description: Optional[str] |
| priority: Optional[int] |
| state: str |
| city: Optional[str] |
| locality: Optional[str] |
| full_address: Optional[str] |
| latitude: float |
| longitude: float |
| image_url: Optional[str] |
| annotated_url: Optional[str] |
| created_at: datetime |
| sla_deadline: Optional[datetime] |
| category: Optional[str] = None |
|
|
|
|
| @router.get("/tasks", response_model=list[TaskResponse]) |
| async def get_worker_tasks( |
| db: AsyncSession = Depends(get_db), |
| current_worker: Member = Depends(get_current_worker), |
| ): |
| result = await db.execute( |
| select(Issue) |
| .options(selectinload(Issue.images), selectinload(Issue.classification)) |
| .where(Issue.assigned_member_id == current_worker.id) |
| .where(Issue.state.in_(["assigned", "in_progress", "pending_verification", "resolved"])) |
| .order_by(Issue.priority.asc().nullslast(), Issue.created_at.asc()) |
| ) |
| issues = result.scalars().all() |
| |
| tasks = [] |
| for issue in issues: |
| image_url = None |
| annotated_url = None |
| if issue.images: |
| image_url = get_upload_url(issue.images[0].file_path) |
| if issue.images[0].annotated_path: |
| annotated_url = get_upload_url(issue.images[0].annotated_path) |
| |
| tasks.append(TaskResponse( |
| id=issue.id, |
| description=issue.description, |
| priority=issue.priority, |
| state=issue.state, |
| city=issue.city, |
| locality=issue.locality, |
| full_address=issue.full_address, |
| latitude=issue.latitude, |
| longitude=issue.longitude, |
| image_url=image_url, |
| annotated_url=annotated_url, |
| created_at=issue.created_at, |
| sla_deadline=issue.sla_deadline, |
| category=issue.classification.primary_category if issue.classification else None, |
| )) |
| |
| return tasks |
|
|
|
|
| @router.post("/tasks/{task_id}/start") |
| async def start_task( |
| task_id: UUID, |
| db: AsyncSession = Depends(get_db), |
| current_worker: Member = Depends(get_current_worker), |
| ): |
| issue = await db.get(Issue, task_id) |
| if not issue: |
| raise HTTPException(status_code=404, detail="Task not found") |
| |
| if issue.assigned_member_id != current_worker.id: |
| raise HTTPException(status_code=403, detail="Not assigned to this task") |
| |
| issue.state = "in_progress" |
| await db.commit() |
| |
| logger.info(f"Worker {current_worker.id} started task {task_id}") |
| return {"status": "started"} |
|
|
|
|
| @router.post("/tasks/{task_id}/complete") |
| async def complete_task( |
| task_id: UUID, |
| notes: Optional[str] = Form(None), |
| proof_image: UploadFile = File(...), |
| db: AsyncSession = Depends(get_db), |
| current_worker: Member = Depends(get_current_worker), |
| ): |
| issue = await db.get(Issue, task_id) |
| if not issue: |
| raise HTTPException(status_code=404, detail="Task not found") |
| |
| if issue.assigned_member_id != current_worker.id: |
| raise HTTPException(status_code=403, detail="Not assigned to this task") |
| |
| proof_path = await save_upload(proof_image, f"proofs/{task_id}") |
| |
| issue.state = "pending_verification" |
| issue.proof_image_path = proof_path |
| issue.resolution_notes = notes |
| issue.resolved_at = datetime.utcnow() |
| |
| |
| |
| await db.commit() |
| |
| logger.info(f"Worker {current_worker.id} completed task {task_id}") |
| |
| return { |
| "status": "completed", |
| "proof_url": get_upload_url(proof_path), |
| } |
|
|
|
|
| @router.get("/tasks/{task_id}") |
| async def get_task_detail( |
| task_id: UUID, |
| db: AsyncSession = Depends(get_db), |
| current_worker: Member = Depends(get_current_worker), |
| ): |
| result = await db.execute( |
| select(Issue) |
| .options(selectinload(Issue.images), selectinload(Issue.classification)) |
| .where(Issue.id == task_id) |
| ) |
| issue = result.scalar_one_or_none() |
| |
| if not issue: |
| raise HTTPException(status_code=404, detail="Task not found") |
| |
| if issue.assigned_member_id != current_worker.id: |
| raise HTTPException(status_code=403, detail="Not assigned to this task") |
| |
| image_url = None |
| annotated_url = None |
| if issue.images: |
| image_url = get_upload_url(issue.images[0].file_path) |
| if issue.images[0].annotated_path: |
| annotated_url = get_upload_url(issue.images[0].annotated_path) |
| |
| return { |
| "id": str(issue.id), |
| "description": issue.description, |
| "priority": issue.priority, |
| "state": issue.state, |
| "city": issue.city, |
| "locality": issue.locality, |
| "full_address": issue.full_address, |
| "latitude": issue.latitude, |
| "longitude": issue.longitude, |
| "image_url": image_url, |
| "annotated_url": annotated_url, |
| "created_at": issue.created_at, |
| "sla_deadline": issue.sla_deadline, |
| "category": issue.classification.primary_category if issue.classification else None, |
| "proof_image_url": get_upload_url(issue.proof_image_path) if issue.proof_image_path else None, |
| "resolution_notes": issue.resolution_notes, |
| "resolved_at": issue.resolved_at, |
| } |
|
|