Files
arrr-erp/app/modules/work_detail/ui.py
T
2026-07-23 11:37:04 +05:30

277 lines
12 KiB
Python

from __future__ import annotations
from datetime import date
from fastapi import APIRouter, Form, Request
from fastapi.responses import RedirectResponse
from app.core.http_responses import forbidden_response, not_found_response
from app.core.db.common import CommonSessionLocal
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
from app.core.security.session_auth import get_current_user
from app.core.templating import templates
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.services.models import ServiceTaskDocumentRequest
from app.modules.work_detail.service import assign_task_to_consultant, create_document_request, get_task_for_action, load_unified_engagement_detail, review_consultant_submission, save_task_comment, save_task_status, update_document_request
router = APIRouter(prefix="/work", tags=["unified-work-detail-ui"])
def _csrf_rejected(request: Request):
from app.core.http_responses import forbidden_response
return forbidden_response(request, "CSRF validation failed")
def _base_ctx(request: Request, db, user, **ctx):
base = {
"request": request,
"current_user": user,
"current_user_roles": get_user_roles(db, user.id),
"current_user_permissions": get_user_permissions(db, user.id),
"csrf_token": get_or_create_csrf_token(request),
}
base.update(ctx)
return base
def _fallback_for_user(db, user) -> str:
roles = set(get_user_roles(db, user.id))
if "Client" in roles:
return "/client/compliance"
if "Consultant" in roles:
return "/consultant/work"
if "Partner" in roles:
return "/partner/reviews"
if roles.intersection({"Manager", "Branch Manager"}):
return "/manager/work"
return "/employee/work"
@router.get("")
def unified_work_board(request: Request):
"""Compatibility alias for the common /work route.
Existing functionality remains in the Services Work Tracker module.
This route only prevents stale bookmarks/tests from receiving a 404.
"""
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
return RedirectResponse(url="/services/work-tracker", status_code=303)
finally:
db.close()
# Phase 3 security hardening: /work/engagements is a listing alias, not a create endpoint.
# Existing engagement creation remains in the services module.
@router.post("/engagements")
def unified_engagements_post_rejected(request: Request):
return _csrf_rejected(request)
@router.get("/engagements")
def unified_engagements_index(request: Request):
"""Compatibility alias for /work/engagements.
Engagement listing is maintained under /services/engagements.
"""
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
return RedirectResponse(url="/services/engagements", status_code=303)
finally:
db.close()
@router.get("/engagements/{engagement_id}")
def unified_engagement_detail(request: Request, engagement_id: int):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
if "Client" in set(get_user_roles(db, user.id)):
return forbidden_response(request, "Access denied")
detail = load_unified_engagement_detail(db, request=request, user=user, engagement_id=engagement_id)
if not detail:
return not_found_response(request, "Work engagement not found or access denied")
return templates.TemplateResponse(
"modules/work_detail/templates/work_detail/engagement_detail.html",
_base_ctx(request, db, user, title="Work Details", **detail),
)
finally:
db.close()
@router.post("/tasks/{task_id}/status")
async def unified_task_status_update(request: Request, task_id: int, csrf_token: str = Form(...), status: str = Form(...), priority: str = Form("")):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
task, access = get_task_for_action(db, user=user, task_id=task_id)
if not task or not access:
return RedirectResponse(url=_fallback_for_user(db, user), status_code=303)
if not access.can_update_tasks:
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?error=not_allowed", status_code=303)
save_task_status(db, task=task, status=status, priority=priority, user_id=user.id)
db.commit()
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?updated=1", status_code=303)
except Exception:
db.rollback()
raise
finally:
db.close()
@router.post("/tasks/{task_id}/comment")
async def unified_task_comment_add(
request: Request,
task_id: int,
csrf_token: str = Form(...),
comment_type: str = Form("internal_note"),
visibility: str = Form("internal"),
message: str = Form(""),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
task, access = get_task_for_action(db, user=user, task_id=task_id)
if not task or not access:
return RedirectResponse(url=_fallback_for_user(db, user), status_code=303)
if not access.can_comment:
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?error=comment_not_allowed", status_code=303)
ok = save_task_comment(db, task=task, access=access, comment_type=comment_type, visibility=visibility, message=message, user_id=user.id)
if ok:
db.commit()
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?comment=sent", status_code=303)
db.rollback()
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?error=empty_comment", status_code=303)
except Exception:
db.rollback()
raise
finally:
db.close()
@router.post("/tasks/{task_id}/document-requests")
async def unified_document_request_add(request: Request, task_id: int, csrf_token: str = Form(...), title: str = Form(...), description: str = Form(""), requested_from: str = Form("client_and_consultant"), due_date: str = Form(""), request_type: str = Form("document")):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
task, access = get_task_for_action(db, user=user, task_id=task_id)
if not task or not access:
return RedirectResponse(url=_fallback_for_user(db, user), status_code=303)
parsed_due = date.fromisoformat(due_date) if due_date else None
row = create_document_request(db, task=task, access=access, title=title, description=description, requested_from=requested_from, due_date=parsed_due, request_type=request_type, user_id=user.id)
if not row:
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?error=request_not_allowed", status_code=303)
db.commit()
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?request=created", status_code=303)
except Exception:
db.rollback(); raise
finally:
db.close()
@router.post("/document-requests/{request_id}/status")
async def unified_document_request_status(request: Request, request_id: int, csrf_token: str = Form(...), status: str = Form(...), response_note: str = Form("")):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
row = db.get(ServiceTaskDocumentRequest, request_id)
if not row or not row.is_active:
return not_found_response(request, "Document request not found")
task, access = get_task_for_action(db, user=user, task_id=row.task_instance_id)
if not task or not access or not update_document_request(db, request_row=row, access=access, status=status, response_note=response_note, user_id=user.id):
return RedirectResponse(url=_fallback_for_user(db, user), status_code=303)
db.commit()
return RedirectResponse(url=f"/work/engagements/{row.subscription_id}?request=updated", status_code=303)
except Exception:
db.rollback(); raise
finally:
db.close()
@router.post("/tasks/{task_id}/consultant-assignment")
async def unified_consultant_assignment_update(request: Request, task_id: int, csrf_token: str = Form(...), consultant_id: str = Form(""), consultant_due_date: str = Form("")):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
task, access = get_task_for_action(db, user=user, task_id=task_id)
if not task or not access:
return RedirectResponse(url=_fallback_for_user(db, user), status_code=303)
parsed_consultant_id = int(consultant_id) if consultant_id.strip() else None
parsed_due = date.fromisoformat(consultant_due_date) if consultant_due_date else None
if not assign_task_to_consultant(db, task=task, access=access, consultant_id=parsed_consultant_id, due_date=parsed_due, user_id=user.id):
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?error=consultant_assignment_not_allowed", status_code=303)
db.commit()
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?consultant_assignment=updated", status_code=303)
except (ValueError, TypeError):
db.rollback()
return RedirectResponse(url=f"/work/engagements/{task.subscription_id if 'task' in locals() and task else ''}?error=invalid_consultant_assignment", status_code=303)
except Exception:
db.rollback(); raise
finally:
db.close()
@router.post("/tasks/{task_id}/consultant-review")
async def unified_consultant_submission_review(request: Request, task_id: int, csrf_token: str = Form(...), action: str = Form(...), note: str = Form("")):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
task, access = get_task_for_action(db, user=user, task_id=task_id)
if not task or not access:
return RedirectResponse(url=_fallback_for_user(db, user), status_code=303)
if not review_consultant_submission(db, task=task, access=access, action=action, note=note, user_id=user.id):
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?error=consultant_review_not_allowed", status_code=303)
db.commit()
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?consultant_review=updated", status_code=303)
except Exception:
db.rollback(); raise
finally:
db.close()