from fastapi import APIRouter,Depends,HTTPException,Query from fastapi.responses import JSONResponse from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from db_setup import get_session from g_sheet.views import ( SheetFormData, SheetHealth, SheetImport, SheetRead, SheetWrite, ) from users.permissions import PermissionTag,require_permission from dotenv import load_dotenv load_dotenv() router = APIRouter() def _city_values(city: str | None): if not city or not str(city).strip(): return None parts=[p.strip() for p in str(city).split(",") if p.strip()] return parts or None class AppendRowsBody(BaseModel): rows: list[list[str]] class UpdateRangeBody(BaseModel): cell_range: str rows: list[list[str]] class ClearRangeBody(BaseModel): cell_range: str @router.get("/sheet/health") async def sheet_health(): """Liveness for the Sheets integration — credentials + spreadsheet reachability. Unauthenticated like GET /health in main.py, and never 500s: an unreachable sheet comes back as {"status":"error"} so a probe can read the reason. """ try: service=SheetHealth() data=await service.health_check() return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/sheet/metadata") async def fetch_sheet_metadata( spreadsheet_id: str | None = Query(None), current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_VIEW)), ): try: service=SheetRead(spreadsheet_id=spreadsheet_id) data=await service.get_metadata() return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/sheet/tabs") async def fetch_sheet_tabs( spreadsheet_id: str | None = Query(None), current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_VIEW)), ): try: service=SheetRead(spreadsheet_id=spreadsheet_id) items=await service.list_tabs() return JSONResponse(content={"data":items,"total":len(items),"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/sheet/fetch") async def fetch_sheet( tab: str | None = Query(None), cell_range: str | None = Query(None), raw: bool = Query(False), current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_VIEW)), spreadsheet_id: str | None = Query(None), ): """No tab -> every tab as records. With a tab -> that tab, header-mapped unless raw=true, which returns the rows exactly as the sheet stores them.""" try: service=SheetRead(spreadsheet_id=spreadsheet_id) if not tab: data=await service.read_all() return JSONResponse(content={"data":data["sheets"],"total":data["total"],"status_code":200}) if raw or cell_range: data=await service.read_range(tab,cell_range) return JSONResponse(content={"data":data,"total":data["row_count"],"status_code":200}) data=await service.read_records(tab) return JSONResponse(content={"data":data["records"],"total":data["total"],"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/sheet/import") async def import_all_sheets( tab: str | None = Query(None), current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_EDIT)), session: AsyncSession = Depends(get_session), ): """No tab -> every tab. With a tab -> that sheet only. Poll GET /sheet/import/fetch.""" try: service=SheetImport(session=session) data=await service.start_import(current_user=current_user,tab=tab) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/sheet/{tab}/import") async def import_one_sheet( tab: str, current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_EDIT)), session: AsyncSession = Depends(get_session), ): """Enqueue a single-tab import. Poll GET /sheet/import/fetch for status.""" try: service=SheetImport(session=session) data=await service.start_import(current_user=current_user,tab=tab) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/sheet/import/fetch") async def fetch_sheet_import( run_id: str | None = Query(None), current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=SheetImport(session=session) data=await service.get_import_run(run_id=run_id) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) # Form-data reads are shared by Settings (import UI) and Inbox (form applicants). _FORM_DATA_READ = require_permission( PermissionTag.INBOX_VIEW, PermissionTag.SETTINGS_VIEW, require_all=False, ) _FORM_DATA_EDIT = require_permission( PermissionTag.INBOX_EDIT, PermissionTag.SETTINGS_EDIT, require_all=False, ) class AssignFormJobPostBody(BaseModel): job_post_id: str | None = None class FormProcessingStateBody(BaseModel): processing_state: str class FormDuplicateBody(BaseModel): is_duplicate: bool @router.get("/sheet/form-data/sheets") async def fetch_form_data_sheets( current_user: dict = Depends(_FORM_DATA_READ), session: AsyncSession = Depends(get_session), ): try: service=SheetFormData(session=session) data=await service.get_imported_sheets() return JSONResponse(content={"data":data,"total":data["total"],"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/sheet/form-data/fetch") async def fetch_form_data( sheet: str | None = Query(None), search: str | None = Query(None), processing_state: str | None = Query(None), is_duplicate: bool | None = Query(None), has_linkedin: bool | None = Query(None), has_resume: bool | None = Query(None), city: str | None = Query(None), source: str | None = Query(None), assigned: bool | None = Query(None), no_suggestions: bool | None = Query(None), offset: int = Query(0,ge=0), limit: int | None = Query(None,ge=1,le=500), current_user: dict = Depends(_FORM_DATA_READ), session: AsyncSession = Depends(get_session), ): try: service=SheetFormData(session=session) items,total=await service.get_form_data( sheet=sheet,search=search,offset=offset,limit=limit, processing_state=processing_state,is_duplicate=is_duplicate, has_linkedin=has_linkedin,has_resume=has_resume, city=_city_values(city),source=(source or "").strip() or None, assigned=assigned,no_suggestions=no_suggestions, ) return JSONResponse(content={"data":items,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/sheet/form-data/counts") async def fetch_form_data_counts( sheet: str | None = Query(None), # The badges narrow with the list. Without these the tab counts describe the # whole sheet while the rows beneath them describe a filtered slice. # processing_state and is_duplicate are absent on purpose: those two ARE the # tabs, so passing them would make every badge report the current tab. search: str | None = Query(None), has_linkedin: bool | None = Query(None), has_resume: bool | None = Query(None), city: str | None = Query(None), source: str | None = Query(None), assigned: bool | None = Query(None), current_user: dict = Depends(_FORM_DATA_READ), session: AsyncSession = Depends(get_session), ): try: service=SheetFormData(session=session) data=await service.get_counts( sheet=sheet,search=search,has_linkedin=has_linkedin,has_resume=has_resume, city=_city_values(city),source=(source or "").strip() or None,assigned=assigned, ) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/sheet/form-data/count") async def count_form_data( sheet: str | None = Query(None), current_user: dict = Depends(_FORM_DATA_READ), session: AsyncSession = Depends(get_session), ): """Unfiltered form_data total for a sheet. Called once when Sheet Forms opens.""" try: service=SheetFormData(session=session) total=await service.count_rows(sheet=sheet) return JSONResponse(content={"data":{"total":total},"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/sheet/form-data/{record_id}") async def fetch_form_data_by_id( record_id: str, current_user: dict = Depends(_FORM_DATA_READ), session: AsyncSession = Depends(get_session), ): try: service=SheetFormData(session=session) data=await service.get_form_data_by_id(record_id) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/sheet/form-data/{record_id}/assign-job-post") async def assign_form_job_post( record_id: str, payload: AssignFormJobPostBody, current_user: dict = Depends(_FORM_DATA_EDIT), session: AsyncSession = Depends(get_session), ): try: service=SheetFormData(session=session) data=await service.assign_job_post(record_id,payload.job_post_id) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/sheet/form-data/{record_id}/processing-state") async def set_form_processing_state( record_id: str, payload: FormProcessingStateBody, current_user: dict = Depends(_FORM_DATA_EDIT), session: AsyncSession = Depends(get_session), ): try: service=SheetFormData(session=session) data=await service.set_processing_state(record_id,payload.processing_state,current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/sheet/form-data/{record_id}/duplicate") async def set_form_duplicate( record_id: str, payload: FormDuplicateBody, current_user: dict = Depends(_FORM_DATA_EDIT), session: AsyncSession = Depends(get_session), ): try: service=SheetFormData(session=session) data=await service.set_duplicate(record_id,payload.is_duplicate) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.delete("/sheet/form-data/{tab}/delete") async def delete_form_data_sheet( tab: str, current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_DELETE)), session: AsyncSession = Depends(get_session), ): try: service=SheetFormData(session=session) data=await service.delete_sheet_data(tab) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/sheet/{tab}/append") async def append_sheet_rows( tab: str, payload: AppendRowsBody, current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_EDIT)), spreadsheet_id: str | None = Query(None), ): try: service=SheetWrite(spreadsheet_id=spreadsheet_id) data=await service.append_rows(tab,payload.rows) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/sheet/{tab}/update") async def update_sheet_range( tab: str, payload: UpdateRangeBody, current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_EDIT)), spreadsheet_id: str | None = Query(None), ): try: service=SheetWrite(spreadsheet_id=spreadsheet_id) data=await service.update_range(tab,payload.cell_range,payload.rows) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/sheet/{tab}/clear") async def clear_sheet_range( tab: str, payload: ClearRangeBody, current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_EDIT)), spreadsheet_id: str | None = Query(None), ): try: service=SheetWrite(spreadsheet_id=spreadsheet_id) data=await service.clear_range(tab,payload.cell_range) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e))