1968 lines
77 KiB
Python
1968 lines
77 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Form, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
from sqlalchemy import select
|
|
|
|
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.clients.constants import CLIENT_TYPES
|
|
from app.modules.clients.models import Client
|
|
from app.modules.consultants.service import (
|
|
CONSULTANT_MANAGED_CLIENT_STAGES,
|
|
CONSULTANT_MANAGED_CLIENT_STATUSES,
|
|
CONSULTANT_RELATIONSHIP_TYPES,
|
|
CONSULTANT_TYPES,
|
|
CONSULTANT_BILLING_CYCLES,
|
|
CONSULTANT_ONBOARDING_STATUSES,
|
|
CONSULTANT_CLIENT_CONVERSION_STATUSES,
|
|
CONSULTANT_SERVICE_REQUEST_PRIORITIES,
|
|
CONSULTANT_SERVICE_REQUEST_STATUSES,
|
|
CONSULTANT_SUBSCRIPTION_STATUSES,
|
|
CONSULTANT_WORKSPACE_PLANS,
|
|
CONSULTANT_WORKSPACE_TYPES,
|
|
add_consultant_task_reply,
|
|
approve_managed_client_conversion,
|
|
consultant_can_add_managed_client,
|
|
consultant_dashboard_payload,
|
|
consultant_linked_clients_page,
|
|
create_consultant_service_request,
|
|
create_or_update_consultant,
|
|
create_or_update_managed_client,
|
|
ensure_consultant_login_user,
|
|
ensure_consultant_workspace,
|
|
get_consultant,
|
|
get_consultant_by_user,
|
|
get_consultant_managed_client,
|
|
get_client_conversion_request,
|
|
get_consultant_service_request,
|
|
get_consultant_visible_communication,
|
|
get_workspace_by_consultant,
|
|
link_client_to_consultant,
|
|
list_all_consultant_service_requests,
|
|
list_client_links_for_consultant,
|
|
list_consultant_conversion_requests,
|
|
list_clients_available_for_link,
|
|
list_consultant_requestable_services,
|
|
list_consultant_role_users,
|
|
list_consultant_managed_clients,
|
|
list_consultant_service_requests,
|
|
list_consultant_task_timeline,
|
|
list_consultant_visible_communications,
|
|
list_consultants,
|
|
mark_conversion_under_review,
|
|
parse_optional_date,
|
|
reject_managed_client_conversion,
|
|
request_managed_client_conversion,
|
|
set_link_active,
|
|
update_consultant_own_profile,
|
|
update_consultant_service_request_status,
|
|
update_consultant_workspace,
|
|
)
|
|
from app.modules.consultants.portal_service import (
|
|
get_consultant_assignment_detail,
|
|
get_consultant_engagement_progress,
|
|
update_consultant_assignment,
|
|
get_consultant_document_centre,
|
|
get_consultant_work_board,
|
|
)
|
|
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
|
from app.modules.core.rbac.permission_guard import require_permission
|
|
from app.modules.core.iam.profile_service import save_user_profile_photo, update_user_public_profile
|
|
|
|
router = APIRouter(prefix="/consultants", tags=["consultants-ui"])
|
|
portal_router = APIRouter(prefix="/consultant", tags=["consultant-portal-ui"])
|
|
|
|
|
|
def _base_ctx(request: Request, user, db, **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),
|
|
"consultant_types": CONSULTANT_TYPES,
|
|
"relationship_types": CONSULTANT_RELATIONSHIP_TYPES,
|
|
"managed_client_statuses": CONSULTANT_MANAGED_CLIENT_STATUSES,
|
|
"managed_client_stages": CONSULTANT_MANAGED_CLIENT_STAGES,
|
|
"client_types": CLIENT_TYPES,
|
|
"workspace_types": CONSULTANT_WORKSPACE_TYPES,
|
|
"workspace_plans": CONSULTANT_WORKSPACE_PLANS,
|
|
"subscription_statuses": CONSULTANT_SUBSCRIPTION_STATUSES,
|
|
"billing_cycles": CONSULTANT_BILLING_CYCLES,
|
|
"onboarding_statuses": CONSULTANT_ONBOARDING_STATUSES,
|
|
"service_request_statuses": CONSULTANT_SERVICE_REQUEST_STATUSES,
|
|
"service_request_priorities": CONSULTANT_SERVICE_REQUEST_PRIORITIES,
|
|
"conversion_statuses": CONSULTANT_CLIENT_CONVERSION_STATUSES,
|
|
}
|
|
base.update(ctx)
|
|
return base
|
|
|
|
|
|
def _render(request: Request, template_name: str, db, user, **ctx):
|
|
return templates.TemplateResponse(template_name, _base_ctx(request, user, db, **ctx))
|
|
|
|
|
|
def _redirect_denied():
|
|
from app.core.http_responses import ui_access_denied
|
|
return ui_access_denied()
|
|
|
|
|
|
def _has_perm(db, user, code: str) -> bool:
|
|
try:
|
|
require_permission(db, user, code)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _role_names(db, user) -> set[str]:
|
|
return set(get_user_roles(db, user.id))
|
|
|
|
|
|
def _is_system_admin(db, user) -> bool:
|
|
return "System Admin" in _role_names(db, user)
|
|
|
|
|
|
def _is_firm_admin(db, user) -> bool:
|
|
return "Firm Admin" in _role_names(db, user)
|
|
|
|
|
|
def _is_partner(db, user) -> bool:
|
|
return "Partner" in _role_names(db, user)
|
|
|
|
|
|
def _is_consultant(db, user) -> bool:
|
|
return "Consultant" in _role_names(db, user)
|
|
|
|
|
|
def _active_tenant_id(request: Request, user) -> int:
|
|
return int(
|
|
request.session.get("active_tenant_id")
|
|
or request.session.get("selected_tenant_id")
|
|
or request.session.get("tenant_id")
|
|
or user.tenant_id
|
|
)
|
|
|
|
|
|
def _active_branch_id(request: Request, user, db) -> int | None:
|
|
value = request.session.get("active_branch_id")
|
|
if value in (None, "", 0, "0"):
|
|
if _has_perm(db, user, "consultants.cross_branch") or _has_perm(db, user, "clients.cross_branch"):
|
|
return None
|
|
return int(getattr(user, "branch_id", 0) or 0) or None
|
|
return int(value)
|
|
|
|
|
|
def _form_bool(value) -> bool:
|
|
return value in ("1", "true", "True", "on", "yes")
|
|
|
|
|
|
def _can_manage_profiles(db, user) -> bool:
|
|
return _has_perm(db, user, "consultants.manage")
|
|
|
|
|
|
def _can_link_clients(db, user) -> bool:
|
|
return _has_perm(db, user, "consultants.link_clients")
|
|
|
|
|
|
def _can_manage_conversions(db, user) -> bool:
|
|
return _has_perm(db, user, "consultants.conversions.manage")
|
|
|
|
|
|
def _partner_filter_user_id(db, user) -> int | None:
|
|
return int(user.id) if _is_partner(db, user) and not _is_firm_admin(db, user) else None
|
|
|
|
|
|
def _build_payload(request: Request, *, tenant_id: int):
|
|
form = request._form
|
|
user_id_raw = form.get("user_id")
|
|
branch_raw = form.get("branch_id")
|
|
return {
|
|
"tenant_id": tenant_id,
|
|
"branch_id": int(branch_raw) if branch_raw not in (None, "", "None") else None,
|
|
"user_id": int(user_id_raw) if user_id_raw not in (None, "", "None") else None,
|
|
"consultant_type": form.get("consultant_type") or "external_consultant",
|
|
"firm_name": form.get("firm_name"),
|
|
"contact_person": (form.get("contact_person") or "").strip(),
|
|
"email": form.get("email"),
|
|
"mobile": form.get("mobile"),
|
|
"specialisation": form.get("specialisation"),
|
|
"gstin": form.get("gstin"),
|
|
"pan": form.get("pan"),
|
|
"address": form.get("address"),
|
|
"status": form.get("status") or "active",
|
|
"onboarding_status": form.get("onboarding_status") or "active",
|
|
"is_platform_partner": _form_bool(form.get("is_platform_partner")),
|
|
"is_franchise_partner": _form_bool(form.get("is_franchise_partner")),
|
|
"is_saas_customer": _form_bool(form.get("is_saas_customer")),
|
|
"is_active": _form_bool(form.get("is_active")),
|
|
"remarks": form.get("remarks"),
|
|
"create_login_user": _form_bool(form.get("create_login_user")),
|
|
"invite_login_user": _form_bool(form.get("invite_login_user")),
|
|
"login_email": form.get("login_email"),
|
|
"temporary_password": form.get("temporary_password"),
|
|
}
|
|
|
|
|
|
def _validate_payload(payload: dict) -> list[str]:
|
|
errors: list[str] = []
|
|
if not payload.get("contact_person"):
|
|
errors.append("Contact person/name is required.")
|
|
if not payload.get("email") and not payload.get("mobile"):
|
|
errors.append("Enter at least email or mobile for consultant communication.")
|
|
if payload.get("create_login_user") and payload.get("user_id"):
|
|
errors.append("Choose either an existing linked user or create a new login user, not both.")
|
|
if payload.get("create_login_user") and not (payload.get("login_email") or payload.get("email")):
|
|
errors.append("Login email is required for consultant login creation.")
|
|
if payload.get("create_login_user") and not payload.get("invite_login_user") and not (payload.get("temporary_password") or "").strip():
|
|
errors.append("Enter a temporary password or choose invite link for consultant login.")
|
|
if payload.get("temporary_password") and len((payload.get("temporary_password") or "").strip()) < 8:
|
|
errors.append("Temporary password must be at least 8 characters.")
|
|
return errors
|
|
|
|
|
|
def _build_own_profile_payload(request: Request) -> dict:
|
|
form = request._form
|
|
return {
|
|
"firm_name": form.get("firm_name"),
|
|
"contact_person": (form.get("contact_person") or "").strip(),
|
|
"email": form.get("email"),
|
|
"mobile": form.get("mobile"),
|
|
"specialisation": form.get("specialisation"),
|
|
"gstin": form.get("gstin"),
|
|
"pan": form.get("pan"),
|
|
"address": form.get("address"),
|
|
}
|
|
|
|
|
|
def _validate_own_profile_payload(payload: dict) -> list[str]:
|
|
errors: list[str] = []
|
|
if not payload.get("contact_person"):
|
|
errors.append("Contact person/name is required.")
|
|
if not payload.get("email") and not payload.get("mobile"):
|
|
errors.append("Enter at least email or mobile.")
|
|
return errors
|
|
|
|
|
|
def _maybe_create_consultant_login(db, *, payload: dict, actor_user, branch_id: int | None) -> tuple[int | None, str | None]:
|
|
if not payload.get("create_login_user"):
|
|
return payload.get("user_id"), None
|
|
login_user, invite_url = ensure_consultant_login_user(
|
|
db,
|
|
tenant_id=int(payload["tenant_id"]),
|
|
branch_id=payload.get("branch_id") or branch_id or getattr(actor_user, "branch_id", None),
|
|
email=(payload.get("login_email") or payload.get("email") or "").strip(),
|
|
full_name=payload.get("contact_person") or payload.get("firm_name") or "Consultant",
|
|
password=payload.get("temporary_password"),
|
|
invite_user=bool(payload.get("invite_login_user")),
|
|
)
|
|
return int(login_user.id), invite_url
|
|
|
|
|
|
def _get_own_consultant_or_dashboard(request: Request, db, user):
|
|
tenant_id = int(getattr(user, "tenant_id", 0) or 0)
|
|
consultant = get_consultant_by_user(db, tenant_id=tenant_id, user_id=user.id)
|
|
if consultant:
|
|
return consultant
|
|
return None
|
|
|
|
|
|
def _build_managed_client_payload(request: Request) -> dict:
|
|
form = request._form
|
|
return {
|
|
"client_code": form.get("client_code"),
|
|
"client_name": (form.get("client_name") or "").strip(),
|
|
"trade_name": form.get("trade_name"),
|
|
"client_type": form.get("client_type") or "Other",
|
|
"pan": form.get("pan"),
|
|
"gstin": form.get("gstin"),
|
|
"tan": form.get("tan"),
|
|
"contact_person_name": form.get("contact_person_name"),
|
|
"mobile": form.get("mobile"),
|
|
"email": form.get("email"),
|
|
"address_line_1": form.get("address_line_1"),
|
|
"address_line_2": form.get("address_line_2"),
|
|
"city": form.get("city"),
|
|
"state": form.get("state"),
|
|
"pincode": form.get("pincode"),
|
|
"country": form.get("country") or "India",
|
|
"service_interest": form.get("service_interest"),
|
|
"relationship_stage": form.get("relationship_stage") or "managed",
|
|
"status": form.get("status") or "active",
|
|
"is_active": _form_bool(form.get("is_active")),
|
|
"notes": form.get("notes"),
|
|
}
|
|
|
|
|
|
def _validate_managed_client_payload(payload: dict) -> list[str]:
|
|
errors: list[str] = []
|
|
if not payload.get("client_name"):
|
|
errors.append("Client name is required.")
|
|
if not payload.get("mobile") and not payload.get("email"):
|
|
errors.append("Enter at least mobile or email for the managed client.")
|
|
return errors
|
|
|
|
|
|
|
|
def _build_workspace_payload(request: Request) -> dict:
|
|
form = request._form
|
|
return {
|
|
"workspace_name": (form.get("workspace_name") or "").strip(),
|
|
"workspace_type": form.get("workspace_type") or "consultant_saas",
|
|
"plan_code": form.get("plan_code") or "starter",
|
|
"billing_cycle": form.get("billing_cycle") or "manual",
|
|
"subscription_status": form.get("subscription_status") or "trial",
|
|
"subscription_start_date": parse_optional_date(form.get("subscription_start_date")),
|
|
"subscription_end_date": parse_optional_date(form.get("subscription_end_date")),
|
|
"max_managed_clients": form.get("max_managed_clients") or 25,
|
|
"max_user_accounts": form.get("max_user_accounts") or 1,
|
|
"allow_client_portal": _form_bool(form.get("allow_client_portal")),
|
|
"allow_firm_referrals": _form_bool(form.get("allow_firm_referrals")),
|
|
"allow_service_marketplace": _form_bool(form.get("allow_service_marketplace")),
|
|
"is_active": _form_bool(form.get("is_active")),
|
|
"remarks": form.get("remarks"),
|
|
}
|
|
|
|
|
|
def _validate_workspace_payload(payload: dict) -> list[str]:
|
|
errors: list[str] = []
|
|
if not payload.get("workspace_name"):
|
|
errors.append("Workspace name is required.")
|
|
try:
|
|
if int(payload.get("max_managed_clients") or 0) < 0:
|
|
errors.append("Managed client limit cannot be negative.")
|
|
except (TypeError, ValueError):
|
|
errors.append("Managed client limit must be a number.")
|
|
try:
|
|
if int(payload.get("max_user_accounts") or 0) < 1:
|
|
errors.append("User account limit must be at least 1.")
|
|
except (TypeError, ValueError):
|
|
errors.append("User account limit must be a number.")
|
|
start = payload.get("subscription_start_date")
|
|
end = payload.get("subscription_end_date")
|
|
if start and end and end < start:
|
|
errors.append("Subscription end date cannot be earlier than start date.")
|
|
return errors
|
|
|
|
|
|
def _build_workspace_self_payload(request: Request) -> dict:
|
|
form = request._form
|
|
return {
|
|
"workspace_name": (form.get("workspace_name") or "").strip(),
|
|
"remarks": form.get("remarks"),
|
|
}
|
|
|
|
|
|
def _validate_workspace_self_payload(payload: dict) -> list[str]:
|
|
errors: list[str] = []
|
|
if not payload.get("workspace_name"):
|
|
errors.append("Workspace name is required.")
|
|
return errors
|
|
|
|
|
|
@router.get("")
|
|
def consultants_list(request: Request, q: str = "", include_inactive: bool = False):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not _has_perm(db, user, "consultants.view"):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
rows = list_consultants(db, tenant_id=tenant_id, branch_id=branch_id, q=q, include_inactive=include_inactive)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/list.html",
|
|
db,
|
|
user,
|
|
title="Consultants",
|
|
rows=rows,
|
|
q=q,
|
|
include_inactive=include_inactive,
|
|
can_manage=_can_manage_profiles(db, user),
|
|
can_link=_can_link_clients(db, user),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/new")
|
|
def consultant_new_page(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not _can_manage_profiles(db, user):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
users = list_consultant_role_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/form.html",
|
|
db,
|
|
user,
|
|
title="Add Consultant",
|
|
consultant=None,
|
|
consultant_users=users,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/new")
|
|
async def consultant_new_submit(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not _can_manage_profiles(db, user):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
payload = _build_payload(request, tenant_id=tenant_id)
|
|
if branch_id and payload.get("branch_id") not in (None, branch_id):
|
|
return _redirect_denied()
|
|
errors = _validate_payload(payload)
|
|
if errors:
|
|
users = list_consultant_role_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/form.html",
|
|
db,
|
|
user,
|
|
title="Add Consultant",
|
|
consultant=payload,
|
|
consultant_users=users,
|
|
errors=errors,
|
|
)
|
|
try:
|
|
linked_user_id, invite_url = _maybe_create_consultant_login(db, payload=payload, actor_user=user, branch_id=branch_id)
|
|
payload["user_id"] = linked_user_id
|
|
if invite_url:
|
|
payload["onboarding_status"] = "invited"
|
|
elif linked_user_id:
|
|
payload["onboarding_status"] = payload.get("onboarding_status") or "active"
|
|
consultant = create_or_update_consultant(db, payload=payload, user_id=user.id)
|
|
db.commit()
|
|
db.refresh(consultant)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
users = list_consultant_role_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/form.html",
|
|
db,
|
|
user,
|
|
title="Add Consultant",
|
|
consultant=payload,
|
|
consultant_users=users,
|
|
errors=[str(exc)],
|
|
)
|
|
if invite_url:
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/consultant_invite_link.html",
|
|
db,
|
|
user,
|
|
title="Consultant Invite Link",
|
|
consultant=consultant,
|
|
invite_url=invite_url,
|
|
invited_user=consultant.user,
|
|
)
|
|
return RedirectResponse(url=f"/consultants/{consultant.id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/service-requests")
|
|
def internal_consultant_service_requests_list(request: Request, status: str = ""):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.view") and _has_perm(db, user, "consultants.service_requests.manage")):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
rows = list_all_consultant_service_requests(db, tenant_id=tenant_id, branch_id=branch_id, status=status)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/internal_service_requests_list.html",
|
|
db,
|
|
user,
|
|
title="Consultant Service Requests",
|
|
rows=rows,
|
|
status=status,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/service-requests/{request_id}")
|
|
def internal_consultant_service_request_detail(request: Request, request_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.view") and _has_perm(db, user, "consultants.service_requests.manage")):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
row = get_consultant_service_request(db, tenant_id=tenant_id, request_id=request_id)
|
|
if not row:
|
|
return RedirectResponse(url="/consultants/service-requests", status_code=303)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/service_request_detail.html",
|
|
db,
|
|
user,
|
|
title="Consultant Service Request",
|
|
consultant=row.consultant,
|
|
request_row=row,
|
|
internal_view=True,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/service-requests/{request_id}/status")
|
|
def internal_consultant_service_request_status(
|
|
request: Request,
|
|
request_id: int,
|
|
status: str = Form(...),
|
|
firm_response: str = Form(""),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.view") and _has_perm(db, user, "consultants.service_requests.manage")):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
row = get_consultant_service_request(db, tenant_id=tenant_id, request_id=request_id)
|
|
if not row:
|
|
return RedirectResponse(url="/consultants/service-requests", status_code=303)
|
|
update_consultant_service_request_status(db, request=row, status=status, firm_response=firm_response, user_id=user.id)
|
|
db.commit()
|
|
return RedirectResponse(url=f"/consultants/service-requests/{request_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
@router.get("/conversion-requests")
|
|
def internal_consultant_conversion_requests_list(request: Request, status: str = ""):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.view") and _can_manage_conversions(db, user)):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
rows = list_consultant_conversion_requests(db, tenant_id=tenant_id, branch_id=branch_id, status=status)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/internal_conversion_requests_list.html",
|
|
db,
|
|
user,
|
|
title="Consultant Client Conversion Requests",
|
|
rows=rows,
|
|
status=status,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/conversion-requests/{managed_client_id}")
|
|
def internal_consultant_conversion_request_detail(request: Request, managed_client_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.view") and _can_manage_conversions(db, user)):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
row = get_client_conversion_request(db, tenant_id=tenant_id, managed_client_id=managed_client_id, branch_id=branch_id)
|
|
if not row:
|
|
return RedirectResponse(url="/consultants/conversion-requests", status_code=303)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/conversion_request_detail.html",
|
|
db,
|
|
user,
|
|
title="Conversion Request",
|
|
managed_client=row,
|
|
consultant=row.consultant,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/conversion-requests/{managed_client_id}/review")
|
|
def internal_consultant_conversion_review(
|
|
request: Request,
|
|
managed_client_id: int,
|
|
action: str = Form(...),
|
|
client_code: str = Form(""),
|
|
partner_user_id: str = Form(""),
|
|
firm_notes: str = Form(""),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.view") and _can_manage_conversions(db, user)):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
row = get_client_conversion_request(db, tenant_id=tenant_id, managed_client_id=managed_client_id, branch_id=branch_id)
|
|
if not row:
|
|
return RedirectResponse(url="/consultants/conversion-requests", status_code=303)
|
|
try:
|
|
if action == "under_review":
|
|
mark_conversion_under_review(db, managed_client=row, user_id=user.id, firm_notes=firm_notes)
|
|
elif action == "reject":
|
|
reject_managed_client_conversion(db, managed_client=row, user_id=user.id, firm_notes=firm_notes)
|
|
elif action == "approve":
|
|
partner_id = int(partner_user_id) if str(partner_user_id).strip() else None
|
|
firm_client = approve_managed_client_conversion(
|
|
db,
|
|
managed_client=row,
|
|
user_id=user.id,
|
|
partner_user_id=partner_id,
|
|
client_code=client_code,
|
|
firm_notes=firm_notes,
|
|
)
|
|
db.commit()
|
|
return RedirectResponse(url=f"/clients/{firm_client.id}", status_code=303)
|
|
else:
|
|
raise ValueError("Invalid review action.")
|
|
db.commit()
|
|
return RedirectResponse(url=f"/consultants/conversion-requests/{managed_client_id}", status_code=303)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/conversion_request_detail.html",
|
|
db,
|
|
user,
|
|
title="Conversion Request",
|
|
managed_client=row,
|
|
consultant=row.consultant,
|
|
errors=[str(exc)],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/{consultant_id}")
|
|
def consultant_detail(request: Request, consultant_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not _has_perm(db, user, "consultants.view"):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
consultant = get_consultant(db, tenant_id=tenant_id, consultant_id=consultant_id, branch_id=branch_id)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultants", status_code=303)
|
|
links = list_client_links_for_consultant(db, consultant_id=consultant.id)
|
|
clients = list_clients_available_for_link(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
branch_id=branch_id,
|
|
partner_user_id=_partner_filter_user_id(db, user),
|
|
)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/detail.html",
|
|
db,
|
|
user,
|
|
title="Consultant Detail",
|
|
consultant=consultant,
|
|
links=links,
|
|
clients=clients,
|
|
can_manage=_can_manage_profiles(db, user),
|
|
can_link=_can_link_clients(db, user),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/{consultant_id}/edit")
|
|
def consultant_edit_page(request: Request, consultant_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not _can_manage_profiles(db, user):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
consultant = get_consultant(db, tenant_id=tenant_id, consultant_id=consultant_id, branch_id=branch_id)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultants", status_code=303)
|
|
users = list_consultant_role_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/form.html",
|
|
db,
|
|
user,
|
|
title="Edit Consultant",
|
|
consultant=consultant,
|
|
consultant_users=users,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/{consultant_id}/edit")
|
|
async def consultant_edit_submit(request: Request, consultant_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not _can_manage_profiles(db, user):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
consultant = get_consultant(db, tenant_id=tenant_id, consultant_id=consultant_id, branch_id=branch_id)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultants", status_code=303)
|
|
payload = _build_payload(request, tenant_id=tenant_id)
|
|
if branch_id and payload.get("branch_id") not in (None, branch_id):
|
|
return _redirect_denied()
|
|
errors = _validate_payload(payload)
|
|
if errors:
|
|
users = list_consultant_role_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/form.html",
|
|
db,
|
|
user,
|
|
title="Edit Consultant",
|
|
consultant=consultant,
|
|
consultant_users=users,
|
|
errors=errors,
|
|
)
|
|
invite_url = None
|
|
try:
|
|
linked_user_id, invite_url = _maybe_create_consultant_login(db, payload=payload, actor_user=user, branch_id=branch_id)
|
|
payload["user_id"] = linked_user_id
|
|
if invite_url:
|
|
payload["onboarding_status"] = "invited"
|
|
create_or_update_consultant(db, payload=payload, user_id=user.id, consultant=consultant)
|
|
db.commit()
|
|
except Exception as exc:
|
|
db.rollback()
|
|
users = list_consultant_role_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/form.html",
|
|
db,
|
|
user,
|
|
title="Edit Consultant",
|
|
consultant=consultant,
|
|
consultant_users=users,
|
|
errors=[str(exc)],
|
|
)
|
|
if invite_url:
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/consultant_invite_link.html",
|
|
db,
|
|
user,
|
|
title="Consultant Invite Link",
|
|
consultant=consultant,
|
|
invite_url=invite_url,
|
|
invited_user=consultant.user,
|
|
)
|
|
return RedirectResponse(url=f"/consultants/{consultant.id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/{consultant_id}/links")
|
|
def consultant_link_submit(
|
|
request: Request,
|
|
consultant_id: int,
|
|
client_id: int = Form(...),
|
|
relationship_type: str = Form("accounts_consultant"),
|
|
is_primary: str | None = Form(None),
|
|
can_view_client: str | None = Form(None),
|
|
can_view_services: str | None = Form(None),
|
|
can_view_due_dates: str | None = Form(None),
|
|
can_view_communications: str | None = Form(None),
|
|
remarks: str = Form(""),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not _can_link_clients(db, user):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
branch_id = _active_branch_id(request, user, db)
|
|
consultant = get_consultant(db, tenant_id=tenant_id, consultant_id=consultant_id, branch_id=branch_id)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultants", status_code=303)
|
|
client_query = select(Client).where(Client.id == client_id, Client.tenant_id == tenant_id, Client.is_archived.is_(False))
|
|
if branch_id:
|
|
client_query = client_query.where(Client.branch_id == branch_id)
|
|
partner_id = _partner_filter_user_id(db, user)
|
|
if partner_id:
|
|
client_query = client_query.where(Client.partner_id == partner_id)
|
|
client = db.execute(client_query).scalar_one_or_none()
|
|
if not client:
|
|
return _redirect_denied()
|
|
link_client_to_consultant(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
consultant_id=consultant.id,
|
|
client=client,
|
|
relationship_type=relationship_type,
|
|
is_primary=_form_bool(is_primary),
|
|
can_view_client=can_view_client is not None,
|
|
can_view_services=can_view_services is not None,
|
|
can_view_due_dates=can_view_due_dates is not None,
|
|
can_view_communications=can_view_communications is not None,
|
|
remarks=remarks,
|
|
user_id=user.id,
|
|
)
|
|
db.commit()
|
|
return RedirectResponse(url=f"/consultants/{consultant.id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/links/{link_id}/toggle")
|
|
def consultant_link_toggle(request: Request, link_id: int, active: str = Form("0"), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not _can_link_clients(db, user):
|
|
return _redirect_denied()
|
|
tenant_id = _active_tenant_id(request, user)
|
|
link = set_link_active(db, tenant_id=tenant_id, link_id=link_id, active=_form_bool(active), user_id=user.id)
|
|
if not link:
|
|
return RedirectResponse(url="/consultants", status_code=303)
|
|
db.commit()
|
|
return RedirectResponse(url=f"/consultants/{link.consultant_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
@portal_router.get("/managed-clients")
|
|
def consultant_managed_clients_list(request: Request, q: str = "", status: str = "", include_inactive: bool = False):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
rows = list_consultant_managed_clients(
|
|
db,
|
|
tenant_id=consultant.tenant_id,
|
|
consultant_id=consultant.id,
|
|
q=q,
|
|
status=status,
|
|
include_inactive=include_inactive,
|
|
)
|
|
can_add_managed_client, workspace_summary = consultant_can_add_managed_client(db, consultant=consultant)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/managed_clients_list.html",
|
|
db,
|
|
user,
|
|
title="My Managed Clients",
|
|
consultant=consultant,
|
|
rows=rows,
|
|
q=q,
|
|
status=status,
|
|
include_inactive=include_inactive,
|
|
can_add_managed_client=can_add_managed_client,
|
|
workspace_summary=workspace_summary,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/managed-clients/new")
|
|
def consultant_managed_client_new_page(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
can_add_managed_client, workspace_summary = consultant_can_add_managed_client(db, consultant=consultant)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/managed_client_form.html",
|
|
db,
|
|
user,
|
|
title="Add Managed Client",
|
|
consultant=consultant,
|
|
managed_client=None,
|
|
errors=[] if can_add_managed_client else [
|
|
f"Managed client limit reached ({workspace_summary.get('managed_clients_used')}/{workspace_summary.get('managed_clients_limit')})."
|
|
],
|
|
can_add_managed_client=can_add_managed_client,
|
|
workspace_summary=workspace_summary,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/managed-clients/new")
|
|
async def consultant_managed_client_new_submit(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
payload = _build_managed_client_payload(request)
|
|
errors = _validate_managed_client_payload(payload)
|
|
can_add_managed_client, workspace_summary = consultant_can_add_managed_client(db, consultant=consultant)
|
|
if not can_add_managed_client:
|
|
errors.append(
|
|
f"Managed client limit reached ({workspace_summary.get('managed_clients_used')}/{workspace_summary.get('managed_clients_limit')})."
|
|
)
|
|
if errors:
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/managed_client_form.html",
|
|
db,
|
|
user,
|
|
title="Add Managed Client",
|
|
consultant=consultant,
|
|
managed_client=payload,
|
|
errors=errors,
|
|
can_add_managed_client=can_add_managed_client,
|
|
workspace_summary=workspace_summary,
|
|
)
|
|
try:
|
|
row = create_or_update_managed_client(db, consultant=consultant, payload=payload, user_id=user.id)
|
|
db.commit()
|
|
db.refresh(row)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/managed_client_form.html",
|
|
db,
|
|
user,
|
|
title="Add Managed Client",
|
|
consultant=consultant,
|
|
managed_client=payload,
|
|
errors=[str(exc)],
|
|
can_add_managed_client=can_add_managed_client,
|
|
workspace_summary=workspace_summary,
|
|
)
|
|
return RedirectResponse(url=f"/consultant/managed-clients/{row.id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/managed-clients/{managed_client_id}")
|
|
def consultant_managed_client_detail(request: Request, managed_client_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
row = get_consultant_managed_client(
|
|
db,
|
|
tenant_id=consultant.tenant_id,
|
|
consultant_id=consultant.id,
|
|
managed_client_id=managed_client_id,
|
|
)
|
|
if not row:
|
|
return RedirectResponse(url="/consultant/managed-clients", status_code=303)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/managed_client_detail.html",
|
|
db,
|
|
user,
|
|
title="Managed Client Detail",
|
|
consultant=consultant,
|
|
managed_client=row,
|
|
can_request_conversion=(not row.linked_firm_client_id and row.conversion_status not in {"requested", "under_review", "approved"}),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
@portal_router.post("/managed-clients/{managed_client_id}/request-conversion")
|
|
def consultant_managed_client_request_conversion(
|
|
request: Request,
|
|
managed_client_id: int,
|
|
conversion_notes: str = Form(""),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
row = get_consultant_managed_client(
|
|
db,
|
|
tenant_id=consultant.tenant_id,
|
|
consultant_id=consultant.id,
|
|
managed_client_id=managed_client_id,
|
|
)
|
|
if not row:
|
|
return RedirectResponse(url="/consultant/managed-clients", status_code=303)
|
|
try:
|
|
request_managed_client_conversion(
|
|
db,
|
|
consultant=consultant,
|
|
managed_client=row,
|
|
notes=conversion_notes,
|
|
user_id=user.id,
|
|
)
|
|
db.commit()
|
|
except Exception:
|
|
db.rollback()
|
|
return RedirectResponse(url=f"/consultant/managed-clients/{managed_client_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/managed-clients/{managed_client_id}/edit")
|
|
def consultant_managed_client_edit_page(request: Request, managed_client_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
row = get_consultant_managed_client(
|
|
db,
|
|
tenant_id=consultant.tenant_id,
|
|
consultant_id=consultant.id,
|
|
managed_client_id=managed_client_id,
|
|
)
|
|
if not row:
|
|
return RedirectResponse(url="/consultant/managed-clients", status_code=303)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/managed_client_form.html",
|
|
db,
|
|
user,
|
|
title="Edit Managed Client",
|
|
consultant=consultant,
|
|
managed_client=row,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/managed-clients/{managed_client_id}/edit")
|
|
async def consultant_managed_client_edit_submit(request: Request, managed_client_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
row = get_consultant_managed_client(
|
|
db,
|
|
tenant_id=consultant.tenant_id,
|
|
consultant_id=consultant.id,
|
|
managed_client_id=managed_client_id,
|
|
)
|
|
if not row:
|
|
return RedirectResponse(url="/consultant/managed-clients", status_code=303)
|
|
payload = _build_managed_client_payload(request)
|
|
errors = _validate_managed_client_payload(payload)
|
|
if errors:
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/managed_client_form.html",
|
|
db,
|
|
user,
|
|
title="Edit Managed Client",
|
|
consultant=consultant,
|
|
managed_client=row,
|
|
errors=errors,
|
|
)
|
|
create_or_update_managed_client(db, consultant=consultant, payload=payload, user_id=user.id, managed_client=row)
|
|
db.commit()
|
|
return RedirectResponse(url=f"/consultant/managed-clients/{row.id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/workspace")
|
|
def consultant_workspace_page(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
workspace = ensure_consultant_workspace(db, consultant=consultant, user_id=user.id)
|
|
db.commit()
|
|
db.refresh(workspace)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/workspace_detail.html",
|
|
db,
|
|
user,
|
|
title="Consultant Workspace",
|
|
consultant=consultant,
|
|
workspace=workspace,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/workspace/edit")
|
|
def consultant_workspace_edit_page(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.workspace.manage") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
workspace = ensure_consultant_workspace(db, consultant=consultant, user_id=user.id)
|
|
db.commit()
|
|
db.refresh(workspace)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/workspace_form.html",
|
|
db,
|
|
user,
|
|
title="Edit Consultant Workspace",
|
|
consultant=consultant,
|
|
workspace=workspace,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/workspace/edit")
|
|
async def consultant_workspace_edit_submit(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.workspace.manage") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
workspace = ensure_consultant_workspace(db, consultant=consultant, user_id=user.id)
|
|
payload = _build_workspace_self_payload(request)
|
|
errors = _validate_workspace_self_payload(payload)
|
|
if errors:
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/workspace_form.html",
|
|
db,
|
|
user,
|
|
title="Edit Consultant Workspace",
|
|
consultant=consultant,
|
|
workspace={**workspace.__dict__, **payload},
|
|
errors=errors,
|
|
)
|
|
update_consultant_workspace(db, workspace=workspace, payload=payload, user_id=user.id, admin_mode=False)
|
|
db.commit()
|
|
return RedirectResponse(url="/consultant/workspace", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/profile")
|
|
def consultant_profile_page(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/profile_form.html",
|
|
db,
|
|
user,
|
|
title="My Consultant Profile",
|
|
consultant=consultant,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/profile")
|
|
async def consultant_profile_submit(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
payload = _build_own_profile_payload(request)
|
|
errors = _validate_own_profile_payload(payload)
|
|
if errors:
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/profile_form.html",
|
|
db,
|
|
user,
|
|
title="My Consultant Profile",
|
|
consultant={**consultant.__dict__, **payload},
|
|
errors=errors,
|
|
)
|
|
form = request._form
|
|
profile_photo_path = await save_user_profile_photo(user, form.get("profile_photo"))
|
|
update_user_public_profile(
|
|
db,
|
|
user,
|
|
qualification=form.get("qualification"),
|
|
designation=form.get("public_designation") or "Consultant",
|
|
mobile=payload.get("mobile"),
|
|
bio=form.get("bio"),
|
|
profile_photo_path=profile_photo_path,
|
|
)
|
|
update_consultant_own_profile(db, consultant=consultant, payload=payload, user_id=user.id)
|
|
db.commit()
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
CONSULTANT_PORTAL_TABS = {
|
|
"overview": "modules/consultants/templates/consultants/portal_partials/overview.html",
|
|
"assigned-work": "modules/consultants/templates/consultants/portal_partials/assigned_work.html",
|
|
"clients": "modules/consultants/templates/consultants/portal_partials/clients.html",
|
|
"documents": "modules/consultants/templates/consultants/portal_partials/documents.html",
|
|
"clarifications": "modules/consultants/templates/consultants/portal_partials/clarifications.html",
|
|
"requests": "modules/consultants/templates/consultants/portal_partials/service_requests.html",
|
|
"reports": "modules/consultants/templates/consultants/portal_partials/reports.html",
|
|
}
|
|
|
|
|
|
def _consultant_empty_payload() -> dict:
|
|
return {
|
|
"links": [],
|
|
"active_links": [],
|
|
"linked_clients_page": {
|
|
"rows": [],
|
|
"groups": [],
|
|
"meta": {"total": 0, "page": 1, "per_page": 25, "pages": 1},
|
|
"filters": {"q": "", "client_group_id": None, "status": "active", "sort_by": "client_name", "sort_order": "asc"},
|
|
},
|
|
"comments": [],
|
|
"recent_firm_messages": [],
|
|
"recent_consultant_replies": [],
|
|
"managed_clients": [],
|
|
"workspace": None,
|
|
"workspace_alerts": [],
|
|
"service_requests": [],
|
|
"open_service_requests": [],
|
|
"pending_service_requests": [],
|
|
"conversion_requests": [],
|
|
"pending_conversions": [],
|
|
"due_items": [],
|
|
"overdue_items": [],
|
|
"workspace_summary": {
|
|
"workspace": None,
|
|
"managed_clients_used": 0,
|
|
"managed_clients_limit": None,
|
|
"managed_clients_remaining": None,
|
|
"usage_percent": 0,
|
|
},
|
|
"work_board": {"columns": {}, "column_options": [], "total_tasks": 0, "service_requests": []},
|
|
"document_centre": {"engagement_documents": [], "permanent_documents": [], "total": 0},
|
|
"stats": {
|
|
"linked_clients": 0,
|
|
"communication_enabled_clients": 0,
|
|
"due_date_enabled_clients": 0,
|
|
"pending_clarifications": 0,
|
|
"consultant_replies": 0,
|
|
"upcoming_due": 0,
|
|
"overdue": 0,
|
|
"open_service_requests": 0,
|
|
"pending_service_requests": 0,
|
|
"pending_conversions": 0,
|
|
"converted_clients": 0,
|
|
"managed_clients": 0,
|
|
"prospects": 0,
|
|
"referred_to_firm": 0,
|
|
},
|
|
}
|
|
|
|
|
|
def _build_consultant_portal_payload(
|
|
db,
|
|
consultant,
|
|
*,
|
|
linked_q: str = "",
|
|
linked_group_id: int | None = None,
|
|
linked_status: str = "active",
|
|
linked_page: int = 1,
|
|
linked_per_page: int = 25,
|
|
linked_sort_by: str = "client_name",
|
|
linked_sort_order: str = "asc",
|
|
) -> dict:
|
|
payload = consultant_dashboard_payload(db, consultant=consultant)
|
|
payload["linked_clients_page"] = consultant_linked_clients_page(
|
|
db,
|
|
consultant=consultant,
|
|
q=linked_q,
|
|
client_group_id=linked_group_id,
|
|
status=linked_status,
|
|
page=linked_page,
|
|
per_page=linked_per_page,
|
|
sort_by=linked_sort_by,
|
|
sort_order=linked_sort_order,
|
|
)
|
|
try:
|
|
payload["work_board"] = get_consultant_work_board(db, consultant=consultant)
|
|
except Exception:
|
|
payload["work_board"] = {"columns": {}, "column_options": [], "total_tasks": 0, "service_requests": []}
|
|
try:
|
|
payload["document_centre"] = get_consultant_document_centre(db, consultant=consultant)
|
|
except Exception:
|
|
payload["document_centre"] = {"engagement_documents": [], "permanent_documents": [], "total": 0}
|
|
return payload
|
|
|
|
|
|
|
|
@portal_router.get("/dashboard")
|
|
def consultant_dashboard(
|
|
request: Request,
|
|
tab: str = "overview",
|
|
linked_q: str = "",
|
|
linked_group_id: int | None = None,
|
|
linked_status: str = "active",
|
|
linked_page: int = 1,
|
|
linked_per_page: int = 25,
|
|
linked_sort_by: str = "client_name",
|
|
linked_sort_order: str = "asc",
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
tenant_id = int(getattr(user, "tenant_id", 0) or 0)
|
|
consultant = get_consultant_by_user(db, tenant_id=tenant_id, user_id=user.id)
|
|
active_tab = tab if tab in CONSULTANT_PORTAL_TABS else "overview"
|
|
payload = _build_consultant_portal_payload(
|
|
db,
|
|
consultant,
|
|
linked_q=linked_q,
|
|
linked_group_id=linked_group_id,
|
|
linked_status=linked_status,
|
|
linked_page=linked_page,
|
|
linked_per_page=linked_per_page,
|
|
linked_sort_by=linked_sort_by,
|
|
linked_sort_order=linked_sort_order,
|
|
) if consultant else _consultant_empty_payload()
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/portal_dashboard.html",
|
|
db,
|
|
user,
|
|
title="Consultant Portal",
|
|
consultant=consultant,
|
|
payload=payload,
|
|
active_tab=active_tab,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/dashboard/tab/{tab_name}")
|
|
def consultant_dashboard_tab(
|
|
request: Request,
|
|
tab_name: str,
|
|
linked_q: str = "",
|
|
linked_group_id: int | None = None,
|
|
linked_status: str = "active",
|
|
linked_page: int = 1,
|
|
linked_per_page: int = 25,
|
|
linked_sort_by: str = "client_name",
|
|
linked_sort_order: str = "asc",
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
tenant_id = int(getattr(user, "tenant_id", 0) or 0)
|
|
consultant = get_consultant_by_user(db, tenant_id=tenant_id, user_id=user.id)
|
|
active_tab = tab_name if tab_name in CONSULTANT_PORTAL_TABS else "overview"
|
|
payload = _build_consultant_portal_payload(
|
|
db,
|
|
consultant,
|
|
linked_q=linked_q,
|
|
linked_group_id=linked_group_id,
|
|
linked_status=linked_status,
|
|
linked_page=linked_page,
|
|
linked_per_page=linked_per_page,
|
|
linked_sort_by=linked_sort_by,
|
|
linked_sort_order=linked_sort_order,
|
|
) if consultant else _consultant_empty_payload()
|
|
return _render(
|
|
request,
|
|
CONSULTANT_PORTAL_TABS[active_tab],
|
|
db,
|
|
user,
|
|
title="Consultant Portal",
|
|
consultant=consultant,
|
|
payload=payload,
|
|
active_tab=active_tab,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/communications")
|
|
def consultant_communications_list(request: Request, q: str = ""):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
rows = list_consultant_visible_communications(db, consultant=consultant, q=q)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/communications_list.html",
|
|
db,
|
|
user,
|
|
title="Consultant Communications",
|
|
consultant=consultant,
|
|
rows=rows,
|
|
q=q,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/communications/{comment_id}")
|
|
def consultant_communication_detail(request: Request, comment_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
row = get_consultant_visible_communication(db, consultant=consultant, comment_id=comment_id)
|
|
if not row:
|
|
return RedirectResponse(url="/consultant/communications", status_code=303)
|
|
comment, task, subscription, client, catalogue = row
|
|
timeline = list_consultant_task_timeline(db, consultant=consultant, task_id=task.id)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/communication_detail.html",
|
|
db,
|
|
user,
|
|
title="Communication Detail",
|
|
consultant=consultant,
|
|
comment=comment,
|
|
task=task,
|
|
subscription=subscription,
|
|
client=client,
|
|
catalogue=catalogue,
|
|
timeline=timeline,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/communications/{comment_id}/reply")
|
|
async def consultant_communication_reply(request: Request, comment_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
form = await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
row = get_consultant_visible_communication(db, consultant=consultant, comment_id=comment_id)
|
|
if not row:
|
|
return RedirectResponse(url="/consultant/communications", status_code=303)
|
|
comment, task, subscription, client, catalogue = row
|
|
message = (form.get("message") or "").strip()
|
|
errors = [] if message else ["Reply message is required."]
|
|
if not errors:
|
|
reply = add_consultant_task_reply(db, consultant=consultant, task=task, message=message, user_id=user.id)
|
|
if not reply:
|
|
errors.append("Reply could not be saved. The task may be locked or access is restricted.")
|
|
if errors:
|
|
timeline = list_consultant_task_timeline(db, consultant=consultant, task_id=task.id)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/communication_detail.html",
|
|
db,
|
|
user,
|
|
title="Communication Detail",
|
|
consultant=consultant,
|
|
comment=comment,
|
|
task=task,
|
|
subscription=subscription,
|
|
client=client,
|
|
catalogue=catalogue,
|
|
timeline=timeline,
|
|
errors=errors,
|
|
)
|
|
db.commit()
|
|
return RedirectResponse(url=f"/consultant/communications/{comment_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/service-requests")
|
|
def consultant_service_requests_list(request: Request, status: str = ""):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
rows = list_consultant_service_requests(db, consultant=consultant, status=status)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/service_requests_list.html",
|
|
db,
|
|
user,
|
|
title="Service Requests",
|
|
consultant=consultant,
|
|
rows=rows,
|
|
status=status,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/service-requests/new")
|
|
def consultant_service_request_new_page(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
managed_clients = list_consultant_managed_clients(db, tenant_id=consultant.tenant_id, consultant_id=consultant.id)
|
|
firm_links = list_client_links_for_consultant(db, consultant_id=consultant.id)
|
|
services = list_consultant_requestable_services(db)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/service_request_form.html",
|
|
db,
|
|
user,
|
|
title="New Service Request",
|
|
consultant=consultant,
|
|
request_row=None,
|
|
managed_clients=managed_clients,
|
|
firm_links=[link for link in firm_links if link.is_active and link.can_view_client],
|
|
services=services,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/service-requests/new")
|
|
async def consultant_service_request_new_submit(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
payload = _build_service_request_payload(request)
|
|
errors = _validate_service_request_payload(payload)
|
|
if errors:
|
|
managed_clients = list_consultant_managed_clients(db, tenant_id=consultant.tenant_id, consultant_id=consultant.id)
|
|
firm_links = list_client_links_for_consultant(db, consultant_id=consultant.id)
|
|
services = list_consultant_requestable_services(db)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/service_request_form.html",
|
|
db,
|
|
user,
|
|
title="New Service Request",
|
|
consultant=consultant,
|
|
request_row=payload,
|
|
managed_clients=managed_clients,
|
|
firm_links=[link for link in firm_links if link.is_active and link.can_view_client],
|
|
services=services,
|
|
errors=errors,
|
|
)
|
|
try:
|
|
row = create_consultant_service_request(db, consultant=consultant, payload=payload, user_id=user.id)
|
|
db.commit()
|
|
db.refresh(row)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
managed_clients = list_consultant_managed_clients(db, tenant_id=consultant.tenant_id, consultant_id=consultant.id)
|
|
firm_links = list_client_links_for_consultant(db, consultant_id=consultant.id)
|
|
services = list_consultant_requestable_services(db)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/service_request_form.html",
|
|
db,
|
|
user,
|
|
title="New Service Request",
|
|
consultant=consultant,
|
|
request_row=payload,
|
|
managed_clients=managed_clients,
|
|
firm_links=[link for link in firm_links if link.is_active and link.can_view_client],
|
|
services=services,
|
|
errors=[str(exc)],
|
|
)
|
|
return RedirectResponse(url=f"/consultant/service-requests/{row.id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/service-requests/{request_id}")
|
|
def consultant_service_request_detail(request: Request, request_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
row = get_consultant_service_request(db, tenant_id=consultant.tenant_id, request_id=request_id, consultant_id=consultant.id)
|
|
if not row:
|
|
return RedirectResponse(url="/consultant/service-requests", status_code=303)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/service_request_detail.html",
|
|
db,
|
|
user,
|
|
title="Service Request Detail",
|
|
consultant=consultant,
|
|
request_row=row,
|
|
internal_view=False,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/work")
|
|
def consultant_work_board(request: Request, q: str = "", status: str = ""):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
board = get_consultant_work_board(db, consultant=consultant, q=q, status=status)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/work_board.html",
|
|
db,
|
|
user,
|
|
title="My Consultant Work",
|
|
consultant=consultant,
|
|
board=board,
|
|
q=q,
|
|
status=status,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/engagements/{subscription_id}", response_class=HTMLResponse)
|
|
def consultant_engagement_progress(request: Request, subscription_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
progress = get_consultant_engagement_progress(db, consultant=consultant, subscription_id=subscription_id)
|
|
if not progress:
|
|
return RedirectResponse(url="/consultant/work", status_code=303)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/engagement_progress.html",
|
|
db,
|
|
user,
|
|
title="Engagement Progress",
|
|
consultant=consultant,
|
|
**progress,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/assignments/{task_id}")
|
|
def consultant_assignment_detail(request: Request, task_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
detail = get_consultant_assignment_detail(db, consultant=consultant, task_id=task_id)
|
|
if not detail:
|
|
return RedirectResponse(url="/consultant/work", status_code=303)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/assignment_detail.html",
|
|
db,
|
|
user,
|
|
title="Consultant Work Detail",
|
|
consultant=consultant,
|
|
**detail,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/assignments/{task_id}/reply")
|
|
async def consultant_assignment_reply(request: Request, task_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
form = await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
detail = get_consultant_assignment_detail(db, consultant=consultant, task_id=task_id)
|
|
if not detail:
|
|
return RedirectResponse(url="/consultant/work", status_code=303)
|
|
message = (form.get("message") or "").strip()
|
|
errors = [] if message else ["Reply/submission note is required."]
|
|
if not errors:
|
|
reply = add_consultant_task_reply(db, consultant=consultant, task=detail["task"], message=message, user_id=user.id)
|
|
if not reply:
|
|
errors.append("Reply could not be saved. The task may be locked or access is restricted.")
|
|
if errors:
|
|
refreshed = get_consultant_assignment_detail(db, consultant=consultant, task_id=task_id) or detail
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/assignment_detail.html",
|
|
db,
|
|
user,
|
|
title="Consultant Work Detail",
|
|
consultant=consultant,
|
|
**refreshed,
|
|
errors=errors,
|
|
)
|
|
db.commit()
|
|
return RedirectResponse(url=f"/consultant/assignments/{task_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/documents")
|
|
def consultant_documents_page(request: Request, q: str = ""):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
docs = get_consultant_document_centre(db, consultant=consultant, q=q)
|
|
return _render(
|
|
request,
|
|
"modules/consultants/templates/consultants/documents.html",
|
|
db,
|
|
user,
|
|
title="Consultant Documents",
|
|
consultant=consultant,
|
|
docs=docs,
|
|
q=q,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/assignments/{task_id}/execution")
|
|
async def consultant_assignment_execution(request: Request, task_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
form = await request.form()
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
|
return _redirect_denied()
|
|
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
|
if not consultant:
|
|
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
|
detail = get_consultant_assignment_detail(db, consultant=consultant, task_id=task_id)
|
|
if not detail:
|
|
return RedirectResponse(url="/consultant/work", status_code=303)
|
|
ok, error = update_consultant_assignment(db, consultant=consultant, task=detail["task"], action=str(form.get("action") or ""), note=str(form.get("note") or ""), user_id=user.id)
|
|
if not ok:
|
|
refreshed = get_consultant_assignment_detail(db, consultant=consultant, task_id=task_id) or detail
|
|
return _render(request, "modules/consultants/templates/consultants/assignment_detail.html", db, user, title="Consultant Work Detail", consultant=consultant, **refreshed, errors=[error or "Action could not be completed."])
|
|
db.commit()
|
|
return RedirectResponse(url=f"/consultant/assignments/{task_id}?execution=updated", status_code=303)
|
|
except Exception:
|
|
db.rollback(); raise
|
|
finally:
|
|
db.close()
|