Add consultant dashboard ajax tabs v1
This commit is contained in:
@@ -1,24 +1,14 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
{% include "modules/consultants/templates/consultants/_consultant_tabs.html" %}
|
||||
|
||||
<div class="space-y-6">
|
||||
{% if not payload.workspace %}
|
||||
<section class="rounded-3xl bg-gradient-to-r from-brand-700 to-slate-900 p-6 text-white shadow-soft">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-brand-100">Consultant Portal</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold">Set up your consultant workspace</h2>
|
||||
<p class="mt-2 max-w-3xl text-sm text-brand-100">Create your workspace to manage clients, communicate with firms and refer work.</p>
|
||||
</section>
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-6 text-sm text-amber-900 shadow-soft">
|
||||
Your consultant workspace is not configured yet. Open profile/workspace settings and complete setup.
|
||||
<div class="mt-4"><a href="/consultant/workspace/edit" class="rounded-xl bg-amber-700 px-4 py-2 text-sm font-semibold text-white hover:bg-amber-800">Configure Workspace</a></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<section class="rounded-3xl bg-gradient-to-r from-brand-700 to-slate-900 p-6 text-white shadow-soft">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-brand-100">Consultant Portal</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold">Managed clients, firm referrals and consultant assignments</h2>
|
||||
<p class="mt-2 max-w-3xl text-sm text-brand-100">Track bookkeeping clients, clarifications, due dates, service requests and conversion requests with audit firms.</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold">Consultant Workspace</h2>
|
||||
<p class="mt-2 max-w-3xl text-sm text-brand-100">Track assigned work, linked clients, documents, clarifications and service requests from one place.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/consultant/work" class="rounded-xl bg-white px-4 py-2 text-sm font-semibold text-brand-700 hover:bg-brand-50">Open Work Board</a>
|
||||
@@ -27,102 +17,60 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-6">
|
||||
<a href="/consultant/communications" class="af-metric-card border-purple-200 bg-purple-50 hover:border-purple-300"><div class="text-xs font-semibold uppercase text-purple-700">Clarifications</div><div class="mt-2 text-3xl font-semibold text-purple-700">{{ payload.stats.pending_clarifications or 0 }}</div><div class="mt-1 text-xs text-purple-700">Firm messages</div></a>
|
||||
<a href="/consultant/service-requests" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Service Requests</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ payload.stats.open_service_requests or 0 }}</div><div class="mt-1 text-xs text-slate-500">Open requests</div></a>
|
||||
<a href="/consultant/managed-clients" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Managed Clients</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ payload.stats.managed_clients or 0 }}</div><div class="mt-1 text-xs text-slate-500">Bookkeeping / support</div></a>
|
||||
<a href="/consultant/managed-clients" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Prospects</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ payload.stats.prospects or 0 }}</div><div class="mt-1 text-xs text-slate-500">Lead pipeline</div></a>
|
||||
<a href="/consultant/work" class="af-metric-card border-amber-200 bg-amber-50 hover:border-amber-300"><div class="text-xs font-semibold uppercase text-amber-700">Due Soon</div><div class="mt-2 text-3xl font-semibold text-amber-700">{{ payload.stats.upcoming_due or 0 }}</div><div class="mt-1 text-xs text-amber-700">Next 30 days</div></a>
|
||||
<a href="/consultant/work" class="af-metric-card border-red-200 bg-red-50 hover:border-red-300"><div class="text-xs font-semibold uppercase text-red-700">Overdue</div><div class="mt-2 text-3xl font-semibold text-red-700">{{ payload.stats.overdue or 0 }}</div><div class="mt-1 text-xs text-red-700">Linked firm work</div></a>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-6 xl:grid-cols-[380px_minmax(0,1fr)]">
|
||||
<aside class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-900">Workspace & Plan</h3>
|
||||
<p class="mt-1 text-sm text-slate-600">{{ payload.workspace.workspace_name }}</p>
|
||||
<p class="text-xs text-slate-500">{{ payload.workspace.workspace_type.replace('_',' ').title() }} • {{ payload.workspace.plan_code.replace('_',' ').title() }} • {{ payload.workspace.subscription_status.replace('_',' ').title() }}</p>
|
||||
</div>
|
||||
<a href="/consultant/workspace/edit" class="rounded-xl border border-slate-300 px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-50">Edit</a>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<div class="flex items-center justify-between text-xs text-slate-500">
|
||||
<span>Managed client usage</span>
|
||||
<span>{{ payload.workspace_summary.managed_clients_used or 0 }} / {{ payload.workspace_summary.managed_clients_limit or 'No limit' }}</span>
|
||||
</div>
|
||||
<div class="mt-2 h-2 rounded-full bg-slate-100"><div class="h-2 rounded-full bg-slate-900" style="width: {{ 100 if (payload.workspace_summary.usage_percent or 0) > 100 else (payload.workspace_summary.usage_percent or 0) }}%"></div></div>
|
||||
<div class="mt-2 text-xs text-slate-500">{% if payload.workspace_summary.managed_clients_remaining is none %}No client limit configured.{% else %}{{ payload.workspace_summary.managed_clients_remaining }} client slots remaining.{% endif %}</div>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-2 text-xs text-slate-600">
|
||||
<div class="flex justify-between"><span>Client portal</span><span>{{ 'Enabled' if payload.workspace.allow_client_portal else 'Disabled' }}</span></div>
|
||||
<div class="flex justify-between"><span>Firm referrals</span><span>{{ 'Enabled' if payload.workspace.allow_firm_referrals else 'Disabled' }}</span></div>
|
||||
<div class="flex justify-between"><span>Marketplace</span><span>{{ 'Enabled' if payload.workspace.allow_service_marketplace else 'Disabled' }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<h3 class="font-semibold text-slate-900">Quick Actions</h3>
|
||||
<div class="mt-4 grid gap-2 text-sm">
|
||||
<a href="/consultant/managed-clients" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Manage Clients</a>
|
||||
{% if payload.workspace_summary.managed_clients_remaining is none or payload.workspace_summary.managed_clients_remaining > 0 %}
|
||||
<a href="/consultant/managed-clients/new" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Add Managed Client</a>
|
||||
{% endif %}
|
||||
<a href="/consultant/communications" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Reply Clarifications</a>
|
||||
<a href="/consultant/service-requests" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Service Requests / Leads</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div><h3 class="text-lg font-semibold text-slate-900">Pending Consultant Clarifications</h3><p class="mt-1 text-sm text-slate-500">Firm messages visible to consultant. Internal and client-only notes are hidden.</p></div>
|
||||
<a href="/consultant/communications" class="text-sm font-semibold text-brand-700 hover:underline">View all</a>
|
||||
</div>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for comment, task, subscription, client, catalogue in payload.recent_firm_messages %}
|
||||
<a href="/consultant/communications/{{ comment.id }}" class="block rounded-2xl border border-slate-200 p-4 hover:bg-slate-50">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div><div class="font-semibold text-slate-900">{{ client.client_name }} — {{ catalogue.service_name }}</div><div class="text-xs text-slate-500">Task: {{ task.task_name }} • {{ comment.created_at_utc.strftime('%d-%m-%Y %H:%M') if comment.created_at_utc else '' }}</div></div>
|
||||
<span class="rounded-full bg-purple-50 px-2 py-1 text-xs font-semibold text-purple-700">{{ comment.comment_type.replace('_',' ').title() }}</span>
|
||||
</div>
|
||||
<div class="mt-3 line-clamp-3 whitespace-pre-line rounded-xl bg-slate-50 p-3 text-sm text-slate-700">{{ comment.message }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No pending consultant-visible firm messages.</div>
|
||||
<section class="rounded-3xl border border-slate-200 bg-white p-3 shadow-soft">
|
||||
{% set tabs = [
|
||||
('overview','Overview'),
|
||||
('assigned-work','Assigned Work'),
|
||||
('clients','Clients'),
|
||||
('documents','Documents'),
|
||||
('clarifications','Clarifications'),
|
||||
('requests','Requests'),
|
||||
('reports','Reports')
|
||||
] %}
|
||||
<div class="flex gap-2 overflow-x-auto" id="consultant-portal-tabs">
|
||||
{% for code, label in tabs %}
|
||||
<button type="button" data-tab="{{ code }}" class="consultant-portal-tab whitespace-nowrap rounded-2xl px-4 py-2 text-sm font-semibold transition {% if active_tab == code %}bg-brand-600 text-white{% else %}text-slate-600 hover:bg-slate-100{% endif %}">{{ label }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between"><h3 class="font-semibold text-slate-900">Pending Conversions</h3><a href="/consultant/managed-clients" class="text-xs font-semibold text-brand-700 hover:underline">View</a></div>
|
||||
<div class="mt-4 space-y-3">{% for client in payload.pending_conversions %}<a href="/consultant/managed-clients/{{ client.id }}" class="block rounded-xl border border-slate-200 p-3 hover:bg-slate-50"><div class="font-semibold text-slate-900">{{ client.client_name }}</div><div class="text-xs text-slate-500">{{ client.client_code or '-' }} • {{ client.conversion_status.replace('_',' ').title() }}</div></a>{% else %}<div class="text-sm text-slate-500">No pending firm-client conversion requests.</div>{% endfor %}</div>
|
||||
</div>
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between"><h3 class="font-semibold text-slate-900">Upcoming Due Dates</h3><span class="text-xs text-slate-500">30 days</span></div>
|
||||
<div class="mt-4 space-y-3">{% for subscription, client, catalogue in payload.due_items %}<div class="rounded-xl border border-slate-200 p-3"><div class="font-semibold text-slate-900">{{ client.client_name }}</div><div class="text-xs text-slate-500">{{ catalogue.service_name }} • Due {{ subscription.current_due_date.strftime('%d-%m-%Y') if subscription.current_due_date else '-' }}</div></div>{% else %}<div class="text-sm text-slate-500">No upcoming due dates.</div>{% endfor %}</div>
|
||||
</div>
|
||||
<div class="af-card border-red-200">
|
||||
<div class="flex items-center justify-between"><h3 class="font-semibold text-slate-900">Overdue Work</h3><span class="text-xs text-red-500">Attention</span></div>
|
||||
<div class="mt-4 space-y-3">{% for subscription, client, catalogue in payload.overdue_items %}<div class="rounded-xl border border-red-100 bg-red-50 p-3"><div class="font-semibold text-slate-900">{{ client.client_name }}</div><div class="text-xs text-red-600">{{ catalogue.service_name }} • Due {{ subscription.current_due_date.strftime('%d-%m-%Y') if subscription.current_due_date else '-' }}</div></div>{% else %}<div class="text-sm text-slate-500">No overdue linked firm engagements.</div>{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between"><h3 class="font-semibold text-slate-900">My Managed Clients</h3><a href="/consultant/managed-clients" class="text-xs font-semibold text-brand-700 hover:underline">View all</a></div>
|
||||
<div class="mt-4 space-y-3">{% for client in payload.managed_clients %}<a href="/consultant/managed-clients/{{ client.id }}" class="block rounded-xl border border-slate-200 p-3 hover:bg-slate-50"><div class="font-semibold text-slate-900">{{ client.client_name }}</div><div class="text-xs text-slate-500">{{ client.client_code or '-' }} • {{ client.relationship_stage.replace('_',' ').title() }} • {{ client.status.replace('_',' ').title() }}</div></a>{% else %}<div class="text-sm text-slate-500">No managed clients yet.</div>{% endfor %}</div>
|
||||
</div>
|
||||
<div class="af-card">
|
||||
<h3 class="font-semibold text-slate-900">Linked Firm Clients</h3><p class="mt-1 text-xs text-slate-500">Only explicitly linked clients are visible here.</p>
|
||||
<div class="mt-4 space-y-3">{% for link in payload.active_links %}<div class="rounded-xl border border-slate-200 p-3"><div class="font-semibold text-slate-900">{{ link.client.client_name if link.client else '-' }}</div><div class="text-xs text-slate-500">{{ link.client.client_code if link.client else '' }} • {{ link.relationship_type.replace('_',' ').title() }}</div><div class="mt-2 text-xs text-slate-600">Due dates {{ 'enabled' if link.can_view_due_dates else 'hidden' }} • Communications {{ 'enabled' if link.can_view_communications else 'hidden' }}</div></div>{% else %}<div class="text-sm text-slate-500">No firm clients are linked yet.</div>{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section id="consultant-portal-panel">
|
||||
{% include "modules/consultants/templates/consultants/portal_partials/overview.html" %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const buttons = Array.from(document.querySelectorAll('.consultant-portal-tab'));
|
||||
const panel = document.getElementById('consultant-portal-panel');
|
||||
if (!buttons.length || !panel) return;
|
||||
|
||||
function activate(tab) {
|
||||
buttons.forEach(btn => {
|
||||
if (btn.dataset.tab === tab) {
|
||||
btn.classList.add('bg-brand-600','text-white');
|
||||
btn.classList.remove('text-slate-600','hover:bg-slate-100');
|
||||
} else {
|
||||
btn.classList.remove('bg-brand-600','text-white');
|
||||
btn.classList.add('text-slate-600','hover:bg-slate-100');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTab(tab) {
|
||||
activate(tab);
|
||||
panel.innerHTML = '<div class="rounded-3xl border border-slate-200 bg-white p-8 text-center text-sm text-slate-500 shadow-soft">Loading...</div>';
|
||||
try {
|
||||
const res = await fetch('/consultant/dashboard/tab/' + encodeURIComponent(tab), {headers: {'X-Requested-With':'fetch'}});
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
panel.innerHTML = await res.text();
|
||||
history.replaceState(null, '', '/consultant/dashboard?tab=' + encodeURIComponent(tab));
|
||||
} catch (err) {
|
||||
panel.innerHTML = '<div class="rounded-3xl border border-red-200 bg-red-50 p-8 text-center text-sm text-red-700 shadow-soft">Unable to load tab. Please refresh the page.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
buttons.forEach(btn => btn.addEventListener('click', () => loadTab(btn.dataset.tab)));
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">Assigned Work</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Consultant-visible assignments and firm task communications.</p>
|
||||
</div>
|
||||
<a href="/consultant/work" class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800">Open Full Work Board</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set work_board = payload.work_board if payload.work_board is defined else {} %}
|
||||
{% if work_board.columns %}
|
||||
<section class="grid gap-4 xl:grid-cols-3">
|
||||
{% for key, column in work_board.columns.items() %}
|
||||
<div class="rounded-3xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-semibold text-slate-900">{{ column.label }}</h4>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-600">{{ column.items|length }}</span>
|
||||
</div>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for item in column.items[:5] %}
|
||||
{% set task = item.task %}{% set client = item.client %}{% set catalogue = item.catalogue %}{% set comment = item.comment %}
|
||||
<a href="/consultant/assignments/{{ task.id }}" class="block rounded-2xl border {% if item.is_overdue %}border-red-200 bg-red-50{% else %}border-slate-200 hover:bg-slate-50{% endif %} p-3">
|
||||
<div class="font-semibold text-slate-900">{{ client.client_name }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">{{ catalogue.service_name }} • {{ task.task_name }}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2 text-xs">
|
||||
<span class="rounded-full bg-white px-2 py-1 font-semibold text-slate-600">{{ task.status.replace('_',' ').title() if task.status else 'Pending' }}</span>
|
||||
{% if item.is_overdue %}<span class="rounded-full bg-red-100 px-2 py-1 font-semibold text-red-700">Overdue</span>{% endif %}
|
||||
</div>
|
||||
<div class="mt-2 line-clamp-2 text-xs text-slate-600">{{ comment.message }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-4 text-sm text-slate-500">No items in this column.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% else %}
|
||||
<div class="rounded-3xl border border-dashed border-slate-300 bg-white p-8 text-center text-sm text-slate-500 shadow-soft">No consultant-visible assigned work found.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div><h3 class="text-lg font-semibold text-slate-900">Clarifications</h3><p class="mt-1 text-sm text-slate-500">Firm messages visible to consultant and consultant replies.</p></div>
|
||||
<a href="/consultant/communications" class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800">Open Messages</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="grid gap-6 xl:grid-cols-2">
|
||||
<div class="af-card">
|
||||
<h4 class="font-semibold text-slate-900">Pending From Firm</h4>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for comment, task, subscription, client, catalogue in payload.recent_firm_messages %}
|
||||
<a href="/consultant/communications/{{ comment.id }}" class="block rounded-xl border border-slate-200 p-3 hover:bg-slate-50">
|
||||
<div class="font-semibold text-slate-900">{{ client.client_name }} — {{ catalogue.service_name }}</div>
|
||||
<div class="text-xs text-slate-500">{{ task.task_name }} • {{ comment.created_at_utc.strftime('%d-%m-%Y %H:%M') if comment.created_at_utc else '' }}</div>
|
||||
<div class="mt-2 line-clamp-2 text-xs text-slate-600">{{ comment.message }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No pending firm messages.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="af-card">
|
||||
<h4 class="font-semibold text-slate-900">My Recent Replies</h4>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for comment, task, subscription, client, catalogue in payload.recent_consultant_replies %}
|
||||
<a href="/consultant/communications/{{ comment.id }}" class="block rounded-xl border border-slate-200 p-3 hover:bg-slate-50">
|
||||
<div class="font-semibold text-slate-900">{{ client.client_name }} — {{ catalogue.service_name }}</div>
|
||||
<div class="text-xs text-slate-500">{{ task.task_name }} • {{ comment.created_at_utc.strftime('%d-%m-%Y %H:%M') if comment.created_at_utc else '' }}</div>
|
||||
<div class="mt-2 line-clamp-2 text-xs text-slate-600">{{ comment.message }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No consultant replies yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div><h3 class="text-lg font-semibold text-slate-900">Managed Clients</h3><p class="mt-1 text-sm text-slate-500">Clients maintained by the consultant workspace.</p></div>
|
||||
<a href="/consultant/managed-clients" class="text-sm font-semibold text-brand-700 hover:underline">View all</a>
|
||||
</div>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for client in payload.managed_clients %}
|
||||
<a href="/consultant/managed-clients/{{ client.id }}" class="block rounded-xl border border-slate-200 p-3 hover:bg-slate-50">
|
||||
<div class="font-semibold text-slate-900">{{ client.client_name }}</div>
|
||||
<div class="text-xs text-slate-500">{{ client.client_code or '-' }} • {{ client.relationship_stage.replace('_',' ').title() }} • {{ client.status.replace('_',' ').title() }}</div>
|
||||
<div class="mt-2 text-xs text-slate-600">Conversion: {{ client.conversion_status.replace('_',' ').title() if client.conversion_status else 'Not Requested' }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No managed clients yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div><h3 class="text-lg font-semibold text-slate-900">Linked Firm Clients</h3><p class="mt-1 text-sm text-slate-500">Only explicitly linked firm clients are visible.</p></div>
|
||||
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600">{{ payload.active_links|length }} active</span>
|
||||
</div>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for link in payload.active_links %}
|
||||
<div class="rounded-xl border border-slate-200 p-3">
|
||||
<div class="font-semibold text-slate-900">{{ link.client.client_name if link.client else '-' }}</div>
|
||||
<div class="text-xs text-slate-500">{{ link.client.client_code if link.client else '' }} • {{ link.relationship_type.replace('_',' ').title() }}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2 text-xs">
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 font-semibold text-slate-600">Client {{ 'visible' if link.can_view_client else 'hidden' }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 font-semibold text-slate-600">Services {{ 'visible' if link.can_view_services else 'hidden' }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 font-semibold text-slate-600">Due dates {{ 'visible' if link.can_view_due_dates else 'hidden' }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 font-semibold text-slate-600">Communications {{ 'visible' if link.can_view_communications else 'hidden' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No firm clients are linked yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div><h3 class="text-lg font-semibold text-slate-900">Documents</h3><p class="mt-1 text-sm text-slate-500">Shared firm documents and engagement documents visible to the consultant.</p></div>
|
||||
<a href="/consultant/documents" class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800">Open Document Centre</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set docs = payload.document_centre if payload.document_centre is defined else {} %}
|
||||
<section class="grid gap-6 xl:grid-cols-2">
|
||||
<div class="af-card">
|
||||
<h4 class="font-semibold text-slate-900">Engagement Documents</h4>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for doc in docs.engagement_documents[:8] if docs.engagement_documents is defined %}
|
||||
<div class="rounded-xl border border-slate-200 p-3">
|
||||
<div class="font-semibold text-slate-900">{{ doc.title }}</div>
|
||||
<div class="text-xs text-slate-500">{{ doc.document_code or '-' }} • {{ doc.document_type or '-' }} • {{ doc.client.client_name if doc.client else '-' }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No engagement documents available.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="af-card">
|
||||
<h4 class="font-semibold text-slate-900">Permanent Documents</h4>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for doc in docs.permanent_documents[:8] if docs.permanent_documents is defined %}
|
||||
<div class="rounded-xl border border-slate-200 p-3">
|
||||
<div class="font-semibold text-slate-900">{{ doc.title }}</div>
|
||||
<div class="text-xs text-slate-500">{{ doc.document_code or '-' }} • {{ doc.category or '-' }} • {{ doc.client.client_name if doc.client else '-' }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No permanent documents available.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,93 @@
|
||||
<div class="space-y-6">
|
||||
{% if not consultant or not payload.workspace %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-6 text-sm text-amber-900 shadow-soft">
|
||||
Your consultant workspace is not configured yet. Open profile/workspace settings and complete setup.
|
||||
<div class="mt-4"><a href="/consultant/workspace/edit" class="rounded-xl bg-amber-700 px-4 py-2 text-sm font-semibold text-white hover:bg-amber-800">Configure Workspace</a></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-6">
|
||||
<a href="/consultant/communications" class="af-metric-card border-purple-200 bg-purple-50 hover:border-purple-300"><div class="text-xs font-semibold uppercase text-purple-700">Clarifications</div><div class="mt-2 text-3xl font-semibold text-purple-700">{{ payload.stats.pending_clarifications or 0 }}</div><div class="mt-1 text-xs text-purple-700">Firm messages</div></a>
|
||||
<a href="/consultant/service-requests" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Open Requests</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ payload.stats.open_service_requests or 0 }}</div><div class="mt-1 text-xs text-slate-500">Service requests</div></a>
|
||||
<a href="/consultant/managed-clients" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Managed Clients</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ payload.stats.managed_clients or 0 }}</div><div class="mt-1 text-xs text-slate-500">Bookkeeping / support</div></a>
|
||||
<a href="/consultant/managed-clients" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Linked Firm Clients</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ payload.stats.linked_clients or 0 }}</div><div class="mt-1 text-xs text-slate-500">Visible clients</div></a>
|
||||
<a href="/consultant/work" class="af-metric-card border-amber-200 bg-amber-50 hover:border-amber-300"><div class="text-xs font-semibold uppercase text-amber-700">Due Soon</div><div class="mt-2 text-3xl font-semibold text-amber-700">{{ payload.stats.upcoming_due or 0 }}</div><div class="mt-1 text-xs text-amber-700">Next 30 days</div></a>
|
||||
<a href="/consultant/work" class="af-metric-card border-red-200 bg-red-50 hover:border-red-300"><div class="text-xs font-semibold uppercase text-red-700">Overdue</div><div class="mt-2 text-3xl font-semibold text-red-700">{{ payload.stats.overdue or 0 }}</div><div class="mt-1 text-xs text-red-700">Attention</div></a>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-6 xl:grid-cols-[380px_minmax(0,1fr)]">
|
||||
<aside class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-900">Workspace & Plan</h3>
|
||||
<p class="mt-1 text-sm text-slate-600">{{ payload.workspace.workspace_name }}</p>
|
||||
<p class="text-xs text-slate-500">{{ payload.workspace.workspace_type.replace('_',' ').title() }} • {{ payload.workspace.plan_code.replace('_',' ').title() }} • {{ payload.workspace.subscription_status.replace('_',' ').title() }}</p>
|
||||
</div>
|
||||
<a href="/consultant/workspace/edit" class="rounded-xl border border-slate-300 px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-50">Edit</a>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<div class="flex items-center justify-between text-xs text-slate-500">
|
||||
<span>Managed client usage</span>
|
||||
<span>{{ payload.workspace_summary.managed_clients_used or 0 }} / {{ payload.workspace_summary.managed_clients_limit or 'No limit' }}</span>
|
||||
</div>
|
||||
<div class="mt-2 h-2 rounded-full bg-slate-100"><div class="h-2 rounded-full bg-slate-900" style="width: {{ 100 if (payload.workspace_summary.usage_percent or 0) > 100 else (payload.workspace_summary.usage_percent or 0) }}%"></div></div>
|
||||
<div class="mt-2 text-xs text-slate-500">{% if payload.workspace_summary.managed_clients_remaining is none %}No client limit configured.{% else %}{{ payload.workspace_summary.managed_clients_remaining }} client slots remaining.{% endif %}</div>
|
||||
</div>
|
||||
{% if payload.workspace_alerts %}
|
||||
<div class="mt-4 space-y-2">
|
||||
{% for alert in payload.workspace_alerts %}<div class="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-semibold text-amber-800">{{ alert }}</div>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<h3 class="font-semibold text-slate-900">Quick Actions</h3>
|
||||
<div class="mt-4 grid gap-2 text-sm">
|
||||
<a href="/consultant/work" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Open Work Board</a>
|
||||
<a href="/consultant/managed-clients" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Manage Clients</a>
|
||||
{% if payload.workspace_summary.managed_clients_remaining is none or payload.workspace_summary.managed_clients_remaining > 0 %}<a href="/consultant/managed-clients/new" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Add Managed Client</a>{% endif %}
|
||||
<a href="/consultant/communications" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Reply Clarifications</a>
|
||||
<a href="/consultant/service-requests/new" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Raise Service Request</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div><h3 class="text-lg font-semibold text-slate-900">Pending Consultant Clarifications</h3><p class="mt-1 text-sm text-slate-500">Firm messages visible to consultant. Internal and client-only notes are hidden.</p></div>
|
||||
<a href="/consultant/communications" class="text-sm font-semibold text-brand-700 hover:underline">View all</a>
|
||||
</div>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for comment, task, subscription, client, catalogue in payload.recent_firm_messages %}
|
||||
<a href="/consultant/communications/{{ comment.id }}" class="block rounded-2xl border border-slate-200 p-4 hover:bg-slate-50">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div><div class="font-semibold text-slate-900">{{ client.client_name }} — {{ catalogue.service_name }}</div><div class="text-xs text-slate-500">Task: {{ task.task_name }} • {{ comment.created_at_utc.strftime('%d-%m-%Y %H:%M') if comment.created_at_utc else '' }}</div></div>
|
||||
<span class="rounded-full bg-purple-50 px-2 py-1 text-xs font-semibold text-purple-700">{{ comment.comment_type.replace('_',' ').title() }}</span>
|
||||
</div>
|
||||
<div class="mt-3 line-clamp-3 whitespace-pre-line rounded-xl bg-slate-50 p-3 text-sm text-slate-700">{{ comment.message }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No pending consultant-visible firm messages.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between"><h3 class="font-semibold text-slate-900">Upcoming Due Dates</h3><a href="/consultant/work" class="text-xs font-semibold text-brand-700 hover:underline">View</a></div>
|
||||
<div class="mt-4 space-y-3">{% for subscription, client, catalogue in payload.due_items %}<div class="rounded-xl border border-slate-200 p-3"><div class="font-semibold text-slate-900">{{ client.client_name }}</div><div class="text-xs text-slate-500">{{ catalogue.service_name }} • Due {{ subscription.current_due_date.strftime('%d-%m-%Y') if subscription.current_due_date else '-' }}</div></div>{% else %}<div class="text-sm text-slate-500">No upcoming due dates.</div>{% endfor %}</div>
|
||||
</div>
|
||||
<div class="af-card border-red-200">
|
||||
<div class="flex items-center justify-between"><h3 class="font-semibold text-slate-900">Overdue Work</h3><a href="/consultant/work" class="text-xs font-semibold text-red-600 hover:underline">View</a></div>
|
||||
<div class="mt-4 space-y-3">{% for subscription, client, catalogue in payload.overdue_items %}<div class="rounded-xl border border-red-100 bg-red-50 p-3"><div class="font-semibold text-slate-900">{{ client.client_name }}</div><div class="text-xs text-red-600">{{ catalogue.service_name }} • Due {{ subscription.current_due_date.strftime('%d-%m-%Y') if subscription.current_due_date else '-' }}</div></div>{% else %}<div class="text-sm text-slate-500">No overdue linked firm engagements.</div>{% endfor %}</div>
|
||||
</div>
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between"><h3 class="font-semibold text-slate-900">Open Requests</h3><a href="/consultant/service-requests" class="text-xs font-semibold text-brand-700 hover:underline">View</a></div>
|
||||
<div class="mt-4 space-y-3">{% for row in payload.open_service_requests %}<a href="/consultant/service-requests/{{ row.id }}" class="block rounded-xl border border-slate-200 p-3 hover:bg-slate-50"><div class="font-semibold text-slate-900">{{ row.title }}</div><div class="text-xs text-slate-500">{{ row.status.replace('_',' ').title() }} • {{ row.priority.replace('_',' ').title() if row.priority else 'Normal' }}</div></a>{% else %}<div class="text-sm text-slate-500">No open service requests.</div>{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<h3 class="text-lg font-semibold text-slate-900">Consultant Reports</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Quick access to consultant self-service reports. These use existing consultant portal pages and data visibility rules.</p>
|
||||
</div>
|
||||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<a href="/consultant/work" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:border-brand-300"><div class="text-sm font-semibold text-slate-900">Assigned Work Report</div><p class="mt-2 text-sm text-slate-500">View consultant-visible work board and assignments.</p></a>
|
||||
<a href="/consultant/communications" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:border-brand-300"><div class="text-sm font-semibold text-slate-900">Clarification Pending Report</div><p class="mt-2 text-sm text-slate-500">Firm messages and consultant replies.</p></a>
|
||||
<a href="/consultant/documents" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:border-brand-300"><div class="text-sm font-semibold text-slate-900">Document Centre Report</div><p class="mt-2 text-sm text-slate-500">Shared engagement and permanent documents.</p></a>
|
||||
<a href="/consultant/managed-clients" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:border-brand-300"><div class="text-sm font-semibold text-slate-900">Managed Client Report</div><p class="mt-2 text-sm text-slate-500">Consultant client book and conversion status.</p></a>
|
||||
<a href="/consultant/service-requests" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:border-brand-300"><div class="text-sm font-semibold text-slate-900">Service Request Report</div><p class="mt-2 text-sm text-slate-500">Requests raised to the firm.</p></a>
|
||||
<a href="/consultant/workspace" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:border-brand-300"><div class="text-sm font-semibold text-slate-900">Workspace Usage Report</div><p class="mt-2 text-sm text-slate-500">Workspace plan, usage and limits.</p></a>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div><h3 class="text-lg font-semibold text-slate-900">Service Requests</h3><p class="mt-1 text-sm text-slate-500">Requests and leads raised by consultant to the firm.</p></div>
|
||||
<div class="flex flex-wrap gap-2"><a href="/consultant/service-requests" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">View All</a><a href="/consultant/service-requests/new" class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800">New Request</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="grid gap-6 xl:grid-cols-2">
|
||||
<div class="af-card">
|
||||
<h4 class="font-semibold text-slate-900">Pending Requests</h4>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for row in payload.pending_service_requests %}
|
||||
<a href="/consultant/service-requests/{{ row.id }}" class="block rounded-xl border border-slate-200 p-3 hover:bg-slate-50">
|
||||
<div class="font-semibold text-slate-900">{{ row.title }}</div>
|
||||
<div class="text-xs text-slate-500">{{ row.status.replace('_',' ').title() }} • {{ row.priority.replace('_',' ').title() if row.priority else 'Normal' }}</div>
|
||||
<div class="mt-2 text-xs text-slate-600">{{ row.managed_client.client_name if row.managed_client else (row.firm_client.client_name if row.firm_client else 'General request') }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No pending service requests.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="af-card">
|
||||
<h4 class="font-semibold text-slate-900">Recent Requests</h4>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for row in payload.service_requests %}
|
||||
<a href="/consultant/service-requests/{{ row.id }}" class="block rounded-xl border border-slate-200 p-3 hover:bg-slate-50">
|
||||
<div class="font-semibold text-slate-900">{{ row.title }}</div>
|
||||
<div class="text-xs text-slate-500">{{ row.status.replace('_',' ').title() }} • {{ row.created_at_utc.strftime('%d-%m-%Y') if row.created_at_utc else '' }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No service requests created.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
+102
-12
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user