diff --git a/app/modules/consultants/templates/consultants/portal_dashboard.html b/app/modules/consultants/templates/consultants/portal_dashboard.html index 2c01742..b1533a5 100644 --- a/app/modules/consultants/templates/consultants/portal_dashboard.html +++ b/app/modules/consultants/templates/consultants/portal_dashboard.html @@ -1,24 +1,14 @@ {% extends "ui/templates/base/layout.html" %} {% block content %} {% include "modules/consultants/templates/consultants/_consultant_tabs.html" %} +
- {% if not payload.workspace %} -
-

Consultant Portal

-

Set up your consultant workspace

-

Create your workspace to manage clients, communicate with firms and refer work.

-
-
- Your consultant workspace is not configured yet. Open profile/workspace settings and complete setup. - -
- {% else %}

Consultant Portal

-

Managed clients, firm referrals and consultant assignments

-

Track bookkeeping clients, clarifications, due dates, service requests and conversion requests with audit firms.

+

Consultant Workspace

+

Track assigned work, linked clients, documents, clarifications and service requests from one place.

Open Work Board @@ -27,102 +17,60 @@
-
-
Clarifications
{{ payload.stats.pending_clarifications or 0 }}
Firm messages
-
Service Requests
{{ payload.stats.open_service_requests or 0 }}
Open requests
-
Managed Clients
{{ payload.stats.managed_clients or 0 }}
Bookkeeping / support
-
Prospects
{{ payload.stats.prospects or 0 }}
Lead pipeline
-
Due Soon
{{ payload.stats.upcoming_due or 0 }}
Next 30 days
-
Overdue
{{ payload.stats.overdue or 0 }}
Linked firm work
-
- -
- - -
-
-
-

Pending Consultant Clarifications

Firm messages visible to consultant. Internal and client-only notes are hidden.

- View all -
- -
- -
-
-

Pending Conversions

View
-
{% for client in payload.pending_conversions %}
{{ client.client_name }}
{{ client.client_code or '-' }} • {{ client.conversion_status.replace('_',' ').title() }}
{% else %}
No pending firm-client conversion requests.
{% endfor %}
-
-
-

Upcoming Due Dates

30 days
-
{% for subscription, client, catalogue in payload.due_items %}
{{ client.client_name }}
{{ catalogue.service_name }} • Due {{ subscription.current_due_date.strftime('%d-%m-%Y') if subscription.current_due_date else '-' }}
{% else %}
No upcoming due dates.
{% endfor %}
-
-
-

Overdue Work

Attention
-
{% for subscription, client, catalogue in payload.overdue_items %}
{{ client.client_name }}
{{ catalogue.service_name }} • Due {{ subscription.current_due_date.strftime('%d-%m-%Y') if subscription.current_due_date else '-' }}
{% else %}
No overdue linked firm engagements.
{% endfor %}
-
-
- -
- -
-

Linked Firm Clients

Only explicitly linked clients are visible here.

-
{% for link in payload.active_links %}
{{ link.client.client_name if link.client else '-' }}
{{ link.client.client_code if link.client else '' }} • {{ link.relationship_type.replace('_',' ').title() }}
Due dates {{ 'enabled' if link.can_view_due_dates else 'hidden' }} • Communications {{ 'enabled' if link.can_view_communications else 'hidden' }}
{% else %}
No firm clients are linked yet.
{% endfor %}
-
-
+
+ {% set tabs = [ + ('overview','Overview'), + ('assigned-work','Assigned Work'), + ('clients','Clients'), + ('documents','Documents'), + ('clarifications','Clarifications'), + ('requests','Requests'), + ('reports','Reports') + ] %} +
+ {% for code, label in tabs %} + + {% endfor %}
- {% endif %} + +
+ {% include "modules/consultants/templates/consultants/portal_partials/overview.html" %} +
+ + {% endblock %} diff --git a/app/modules/consultants/templates/consultants/portal_partials/assigned_work.html b/app/modules/consultants/templates/consultants/portal_partials/assigned_work.html new file mode 100644 index 0000000..087a0d8 --- /dev/null +++ b/app/modules/consultants/templates/consultants/portal_partials/assigned_work.html @@ -0,0 +1,43 @@ +
+
+
+
+

Assigned Work

+

Consultant-visible assignments and firm task communications.

+
+ Open Full Work Board +
+
+ + {% set work_board = payload.work_board if payload.work_board is defined else {} %} + {% if work_board.columns %} +
+ {% for key, column in work_board.columns.items() %} +
+
+

{{ column.label }}

+ {{ column.items|length }} +
+
+ {% for item in column.items[:5] %} + {% set task = item.task %}{% set client = item.client %}{% set catalogue = item.catalogue %}{% set comment = item.comment %} + +
{{ client.client_name }}
+
{{ catalogue.service_name }} • {{ task.task_name }}
+
+ {{ task.status.replace('_',' ').title() if task.status else 'Pending' }} + {% if item.is_overdue %}Overdue{% endif %} +
+
{{ comment.message }}
+
+ {% else %} +
No items in this column.
+ {% endfor %} +
+
+ {% endfor %} +
+ {% else %} +
No consultant-visible assigned work found.
+ {% endif %} +
diff --git a/app/modules/consultants/templates/consultants/portal_partials/clarifications.html b/app/modules/consultants/templates/consultants/portal_partials/clarifications.html new file mode 100644 index 0000000..fe25b83 --- /dev/null +++ b/app/modules/consultants/templates/consultants/portal_partials/clarifications.html @@ -0,0 +1,39 @@ +
+
+
+

Clarifications

Firm messages visible to consultant and consultant replies.

+ Open Messages +
+
+ +
+
+

Pending From Firm

+
+ {% for comment, task, subscription, client, catalogue in payload.recent_firm_messages %} + +
{{ client.client_name }} — {{ catalogue.service_name }}
+
{{ task.task_name }} • {{ comment.created_at_utc.strftime('%d-%m-%Y %H:%M') if comment.created_at_utc else '' }}
+
{{ comment.message }}
+
+ {% else %} +
No pending firm messages.
+ {% endfor %} +
+
+
+

My Recent Replies

+
+ {% for comment, task, subscription, client, catalogue in payload.recent_consultant_replies %} + +
{{ client.client_name }} — {{ catalogue.service_name }}
+
{{ task.task_name }} • {{ comment.created_at_utc.strftime('%d-%m-%Y %H:%M') if comment.created_at_utc else '' }}
+
{{ comment.message }}
+
+ {% else %} +
No consultant replies yet.
+ {% endfor %} +
+
+
+
diff --git a/app/modules/consultants/templates/consultants/portal_partials/clients.html b/app/modules/consultants/templates/consultants/portal_partials/clients.html new file mode 100644 index 0000000..0cc9f7f --- /dev/null +++ b/app/modules/consultants/templates/consultants/portal_partials/clients.html @@ -0,0 +1,42 @@ +
+ + +
+
+

Linked Firm Clients

Only explicitly linked firm clients are visible.

+ {{ payload.active_links|length }} active +
+
+ {% for link in payload.active_links %} +
+
{{ link.client.client_name if link.client else '-' }}
+
{{ link.client.client_code if link.client else '' }} • {{ link.relationship_type.replace('_',' ').title() }}
+
+ Client {{ 'visible' if link.can_view_client else 'hidden' }} + Services {{ 'visible' if link.can_view_services else 'hidden' }} + Due dates {{ 'visible' if link.can_view_due_dates else 'hidden' }} + Communications {{ 'visible' if link.can_view_communications else 'hidden' }} +
+
+ {% else %} +
No firm clients are linked yet.
+ {% endfor %} +
+
+
diff --git a/app/modules/consultants/templates/consultants/portal_partials/documents.html b/app/modules/consultants/templates/consultants/portal_partials/documents.html new file mode 100644 index 0000000..65d19f6 --- /dev/null +++ b/app/modules/consultants/templates/consultants/portal_partials/documents.html @@ -0,0 +1,38 @@ +
+
+
+

Documents

Shared firm documents and engagement documents visible to the consultant.

+ Open Document Centre +
+
+ + {% set docs = payload.document_centre if payload.document_centre is defined else {} %} +
+
+

Engagement Documents

+
+ {% for doc in docs.engagement_documents[:8] if docs.engagement_documents is defined %} +
+
{{ doc.title }}
+
{{ doc.document_code or '-' }} • {{ doc.document_type or '-' }} • {{ doc.client.client_name if doc.client else '-' }}
+
+ {% else %} +
No engagement documents available.
+ {% endfor %} +
+
+
+

Permanent Documents

+
+ {% for doc in docs.permanent_documents[:8] if docs.permanent_documents is defined %} +
+
{{ doc.title }}
+
{{ doc.document_code or '-' }} • {{ doc.category or '-' }} • {{ doc.client.client_name if doc.client else '-' }}
+
+ {% else %} +
No permanent documents available.
+ {% endfor %} +
+
+
+
diff --git a/app/modules/consultants/templates/consultants/portal_partials/overview.html b/app/modules/consultants/templates/consultants/portal_partials/overview.html new file mode 100644 index 0000000..4503783 --- /dev/null +++ b/app/modules/consultants/templates/consultants/portal_partials/overview.html @@ -0,0 +1,93 @@ +
+ {% if not consultant or not payload.workspace %} +
+ Your consultant workspace is not configured yet. Open profile/workspace settings and complete setup. + +
+ {% else %} +
+
Clarifications
{{ payload.stats.pending_clarifications or 0 }}
Firm messages
+
Open Requests
{{ payload.stats.open_service_requests or 0 }}
Service requests
+
Managed Clients
{{ payload.stats.managed_clients or 0 }}
Bookkeeping / support
+
Linked Firm Clients
{{ payload.stats.linked_clients or 0 }}
Visible clients
+
Due Soon
{{ payload.stats.upcoming_due or 0 }}
Next 30 days
+
Overdue
{{ payload.stats.overdue or 0 }}
Attention
+
+ +
+ + +
+
+
+

Pending Consultant Clarifications

Firm messages visible to consultant. Internal and client-only notes are hidden.

+ View all +
+ +
+ +
+
+

Upcoming Due Dates

View
+
{% for subscription, client, catalogue in payload.due_items %}
{{ client.client_name }}
{{ catalogue.service_name }} • Due {{ subscription.current_due_date.strftime('%d-%m-%Y') if subscription.current_due_date else '-' }}
{% else %}
No upcoming due dates.
{% endfor %}
+
+
+

Overdue Work

View
+
{% for subscription, client, catalogue in payload.overdue_items %}
{{ client.client_name }}
{{ catalogue.service_name }} • Due {{ subscription.current_due_date.strftime('%d-%m-%Y') if subscription.current_due_date else '-' }}
{% else %}
No overdue linked firm engagements.
{% endfor %}
+
+
+

Open Requests

View
+
{% for row in payload.open_service_requests %}
{{ row.title }}
{{ row.status.replace('_',' ').title() }} • {{ row.priority.replace('_',' ').title() if row.priority else 'Normal' }}
{% else %}
No open service requests.
{% endfor %}
+
+
+
+
+ {% endif %} +
diff --git a/app/modules/consultants/templates/consultants/portal_partials/reports.html b/app/modules/consultants/templates/consultants/portal_partials/reports.html new file mode 100644 index 0000000..7a2aea8 --- /dev/null +++ b/app/modules/consultants/templates/consultants/portal_partials/reports.html @@ -0,0 +1,14 @@ + diff --git a/app/modules/consultants/templates/consultants/portal_partials/service_requests.html b/app/modules/consultants/templates/consultants/portal_partials/service_requests.html new file mode 100644 index 0000000..37fb339 --- /dev/null +++ b/app/modules/consultants/templates/consultants/portal_partials/service_requests.html @@ -0,0 +1,38 @@ +
+
+
+

Service Requests

Requests and leads raised by consultant to the firm.

+ +
+
+ +
+ +
+

Recent Requests

+
+ {% for row in payload.service_requests %} + +
{{ row.title }}
+
{{ row.status.replace('_',' ').title() }} • {{ row.created_at_utc.strftime('%d-%m-%Y') if row.created_at_utc else '' }}
+
+ {% else %} +
No service requests created.
+ {% endfor %} +
+
+
+
diff --git a/app/modules/consultants/ui.py b/app/modules/consultants/ui.py index 5d0f107..0ef8f3e 100644 --- a/app/modules/consultants/ui.py +++ b/app/modules/consultants/ui.py @@ -1330,8 +1330,79 @@ async def consultant_profile_submit(request: Request, csrf_token: str = Form(... 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": [], + "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) -> dict: + payload = consultant_dashboard_payload(db, consultant=consultant) + 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): +def consultant_dashboard(request: Request, tab: str = "overview"): db = CommonSessionLocal() try: user = get_current_user(request, db=db) @@ -1341,17 +1412,8 @@ def consultant_dashboard(request: Request): 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) - if not consultant: - return _render( - request, - "modules/consultants/templates/consultants/portal_dashboard.html", - db, - user, - title="Consultant Portal", - consultant=None, - payload={"links": [], "active_links": [], "comments": [], "managed_clients": [], "workspace": None, "workspace_summary": {"workspace": None, "managed_clients_used": 0, "managed_clients_limit": None, "managed_clients_remaining": None, "usage_percent": 0}, "stats": {"linked_clients": 0, "managed_clients": 0, "prospects": 0, "referred_to_firm": 0, "pending_clarifications": 0, "upcoming_due": 0, "overdue": 0}}, - ) - payload = consultant_dashboard_payload(db, consultant=consultant) + active_tab = tab if tab in CONSULTANT_PORTAL_TABS else "overview" + payload = _build_consultant_portal_payload(db, consultant) if consultant else _consultant_empty_payload() return _render( request, "modules/consultants/templates/consultants/portal_dashboard.html", @@ -1360,6 +1422,34 @@ def consultant_dashboard(request: Request): 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): + 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) 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()