Unify Client navigation and align full-width header

This commit is contained in:
A R R R Associates
2026-07-11 08:38:50 +05:30
parent 2fbd1b5069
commit 49f0ce15f6
3 changed files with 132 additions and 52 deletions
@@ -2,7 +2,6 @@
{% block content %} {% block content %}
<div class="space-y-6"> <div class="space-y-6">
{% include "ui/templates/components/client_navigation_v2.html" %}
<section class="rounded-3xl bg-gradient-to-r from-brand-700 to-slate-900 p-6 text-white shadow-soft"> <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 md:flex-row md:items-center md:justify-between"> <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div> <div>
@@ -25,59 +24,87 @@
We could not find a client master linked to your login email in the current audit firm. Please contact your firm admin to map this login to the correct client record. We could not find a client master linked to your login email in the current audit firm. Please contact your firm admin to map this login to the correct client record.
</div> </div>
{% else %} {% else %}
{% set client_tabs = [ {# Dashboard sections are now part of the unified Client Dashboard submenu. #}
('overview','Overview'),
('action-centre','Action Centre'),
('services-documents','Services & Documents'),
('billing-messages','Billing & Messages'),
('reports','Reports')
] %}
<section class="rounded-3xl border border-slate-200 bg-white p-2 shadow-soft"> <section id="client-dashboard-panel"
<div class="flex gap-2 overflow-x-auto" id="client-dashboard-tabs"> data-initial-tab="{{ active_tab|default('overview') }}"
{% for code, label in client_tabs %} class="min-h-[240px]">
<button type="button" data-tab="{{ code }}" class="client-dashboard-tab whitespace-nowrap rounded-2xl px-4 py-2 text-sm font-semibold transition {% if (active_tab|default('overview')) == code %}bg-brand-600 text-white{% else %}text-slate-600 hover:bg-slate-100{% endif %}">{{ label }}</button> <div class="rounded-3xl border border-slate-200 bg-white p-8 text-center text-sm text-slate-500 shadow-soft">Loading dashboard...</div>
{% endfor %}
</div>
</section>
<section id="client-dashboard-panel" class="min-h-[240px]">
{% include "modules/clients/templates/clients/portal_partials/overview.html" %}
</section> </section>
<script> <script>
(function(){ (function () {
const panel = document.getElementById('client-dashboard-panel'); "use strict";
const buttons = Array.from(document.querySelectorAll('.client-dashboard-tab'));
if (!panel || !buttons.length) return;
function setActive(tab) { const panel = document.getElementById('client-dashboard-panel');
buttons.forEach((btn) => { if (!panel) return;
const active = btn.dataset.tab === tab;
btn.classList.toggle('bg-brand-600', active); const dashboardLinks = Array.from(document.querySelectorAll(
btn.classList.toggle('text-white', active); '[data-uiux-v2-pilot="client"] a[href^="/client/dashboard?tab="]'
btn.classList.toggle('text-slate-600', !active); ));
btn.classList.toggle('hover:bg-slate-100', !active);
function tabFromLink(link) {
try {
return new URL(link.href, window.location.origin).searchParams.get('tab') || 'overview';
} catch (error) {
return 'overview';
}
}
function setActive(tabName) {
dashboardLinks.forEach((link) => {
const active = tabFromLink(link) === tabName;
link.classList.toggle('bg-brand-50', active);
link.classList.toggle('text-brand-700', active);
link.classList.toggle('text-slate-700', !active);
if (active) {
link.setAttribute('aria-current', 'page');
} else {
link.removeAttribute('aria-current');
}
}); });
} }
async function loadTab(tab) { async function loadTab(tabName, pushState) {
setActive(tab); setActive(tabName);
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>'; 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 { try {
const res = await fetch('/client/dashboard/tab/' + encodeURIComponent(tab), {headers: {'X-Requested-With':'fetch'}}); const response = await fetch('/client/dashboard/tab/' + encodeURIComponent(tabName), {
if (!res.ok) throw new Error('HTTP ' + res.status); headers: { 'X-Requested-With': 'fetch' }
panel.innerHTML = await res.text(); });
if (history && history.replaceState) { if (!response.ok) throw new Error('Tab load failed');
history.replaceState(null, '', '/client/dashboard?tab=' + encodeURIComponent(tab));
panel.innerHTML = await response.text();
if (pushState) {
const url = new URL(window.location.href);
url.searchParams.set('tab', tabName);
history.pushState({ tab: tabName }, '', url.toString());
} }
} catch (err) {
panel.innerHTML = '<div class="rounded-3xl border border-red-200 bg-red-50 p-6 text-sm text-red-700 shadow-soft">Unable to load this tab. Please refresh the page or open the linked full page.</div>'; document.querySelectorAll('[data-uiux-v2-pilot="client"] details[open]').forEach((details) => {
details.open = false;
});
} catch (error) {
panel.innerHTML = '<div class="rounded-3xl border border-red-200 bg-red-50 p-6 text-sm text-red-700 shadow-soft">Unable to load this dashboard section. Please refresh the page.</div>';
} }
} }
window.loadClientDashboardTab = loadTab; dashboardLinks.forEach((link) => {
buttons.forEach((btn) => btn.addEventListener('click', () => loadTab(btn.dataset.tab || 'overview'))); link.addEventListener('click', function (event) {
if (window.location.pathname !== '/client/dashboard') return;
event.preventDefault();
loadTab(tabFromLink(link), true);
});
});
window.addEventListener('popstate', function () {
const url = new URL(window.location.href);
loadTab(url.searchParams.get('tab') || 'overview', false);
});
loadTab(panel.dataset.initialTab || 'overview', false);
})(); })();
</script> </script>
{% endif %} {% endif %}
@@ -1,19 +1,23 @@
{% if navigation_render_location_v2|default('page') == 'global-host' or not global_role_navigation_host_v2|default(false) %} {% if navigation_render_location_v2|default('page') == 'global-host' or not global_role_navigation_host_v2|default(false) %}
{# {#
UI/UX V2 Phase 1C — Client pilot navigation UI/UX V2 Phase 1C.2Unified Client navigation.
Presentation only. Existing routes and route-level permissions remain authoritative.
This component preserves the existing client portal URLs and visibility.
It changes presentation only and does not grant access, change routes, or
replace the client dashboard's existing AJAX content tabs.
#} #}
{% set _client_path = request.url.path %} {% set _client_path = request.url.path %}
{% set _client_tab = request.query_params.get('tab', 'overview') %}
{% set navigation_aria_label_v2 = 'Client workspace navigation' %} {% set navigation_aria_label_v2 = 'Client workspace navigation' %}
{% set navigation_items_v2 = [ {% set navigation_items_v2 = [
{ {
'label': 'Dashboard', 'label': 'Dashboard',
'url': '/client/dashboard',
'visible': true, 'visible': true,
'active': _client_path == '/client/dashboard' 'active': _client_path.startswith('/client/dashboard'),
'children': [
{'label': 'Overview', 'url': '/client/dashboard?tab=overview', 'active': _client_path.startswith('/client/dashboard') and _client_tab == 'overview'},
{'label': 'Action Centre', 'url': '/client/dashboard?tab=action-centre', 'active': _client_path.startswith('/client/dashboard') and _client_tab == 'action-centre'},
{'label': 'Services & Documents', 'url': '/client/dashboard?tab=services-documents', 'active': _client_path.startswith('/client/dashboard') and _client_tab == 'services-documents'},
{'label': 'Billing & Messages', 'url': '/client/dashboard?tab=billing-messages', 'active': _client_path.startswith('/client/dashboard') and _client_tab == 'billing-messages'},
{'label': 'Reports', 'url': '/client/dashboard?tab=reports', 'active': _client_path.startswith('/client/dashboard') and _client_tab == 'reports'}
]
}, },
{ {
'label': 'Compliance', 'label': 'Compliance',
@@ -54,8 +58,57 @@
} }
] %} ] %}
<div class="mb-6 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-soft" data-uiux-v2-pilot="client"> <div class="border-b border-slate-200 bg-white shadow-sm" data-uiux-v2-pilot="client">
{% include "ui/templates/components/navigation_v2.html" %} {% include "ui/templates/components/navigation_v2.html" %}
{% include "ui/templates/components/mobile_navigation_v2.html" %} {% include "ui/templates/components/mobile_navigation_v2.html" %}
</div> </div>
<script>
(function () {
"use strict";
function initialiseClientDropdowns() {
var root = document.querySelector('[data-uiux-v2-pilot="client"]');
if (!root || root.dataset.dropdownAutoCloseInitialised === 'true') return;
var desktopNav = root.querySelector('nav[data-uiux-v2="desktop-navigation"]');
if (!desktopNav) return;
root.dataset.dropdownAutoCloseInitialised = 'true';
var dropdowns = Array.from(desktopNav.querySelectorAll('details'));
function closeAllExcept(exception) {
dropdowns.forEach(function (details) {
if (details !== exception) details.removeAttribute('open');
});
}
dropdowns.forEach(function (details) {
details.addEventListener('toggle', function () {
if (details.open) closeAllExcept(details);
});
details.querySelectorAll('a[href]').forEach(function (link) {
link.addEventListener('click', function () {
details.removeAttribute('open');
});
});
});
document.addEventListener('click', function (event) {
if (!root.contains(event.target)) closeAllExcept(null);
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') closeAllExcept(null);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialiseClientDropdowns, { once: true });
} else {
initialiseClientDropdowns();
}
})();
</script>
{% endif %} {% endif %}
@@ -1,4 +1,4 @@
{% set _uiux_full_width_header = ('System Admin' in (ui_roles|default([], true))) or ('Firm Admin' in (ui_roles|default([], true))) or ('Partner' in (ui_roles|default([], true))) or ('Manager' in (ui_roles|default([], true))) or ('Branch Manager' in (ui_roles|default([], true))) or ('Staff' in (ui_roles|default([], true))) or ('Employee' in (ui_roles|default([], true))) %} {% set _uiux_full_width_header = ('System Admin' in (ui_roles|default([], true))) or ('Firm Admin' in (ui_roles|default([], true))) or ('Partner' in (ui_roles|default([], true))) or ('Manager' in (ui_roles|default([], true))) or ('Branch Manager' in (ui_roles|default([], true))) or ('Staff' in (ui_roles|default([], true))) or ('Employee' in (ui_roles|default([], true))) or ('Client' in (ui_roles|default([], true))) %}
<header class="sticky top-0 z-40 border-b border-slate-200/70 {{ 'bg-white/95' if _uiux_full_width_header else 'bg-slate-100/85' }} backdrop-blur-xl"> <header class="sticky top-0 z-40 border-b border-slate-200/70 {{ 'bg-white/95' if _uiux_full_width_header else 'bg-slate-100/85' }} backdrop-blur-xl">
<div class="{{ 'w-full px-4 py-3 sm:px-6 lg:px-8' if _uiux_full_width_header else 'mx-auto max-w-[1500px] px-3 py-3 sm:px-6 lg:px-8' }}"> <div class="{{ 'w-full px-4 py-3 sm:px-6 lg:px-8' if _uiux_full_width_header else 'mx-auto max-w-[1500px] px-3 py-3 sm:px-6 lg:px-8' }}">
<div class="{{ 'border-0 bg-transparent px-0 py-0 shadow-none' if _uiux_full_width_header else 'rounded-3xl border border-slate-200 bg-white/95 px-3 py-3 shadow-soft sm:px-4' }}"> <div class="{{ 'border-0 bg-transparent px-0 py-0 shadow-none' if _uiux_full_width_header else 'rounded-3xl border border-slate-200 bg-white/95 px-3 py-3 shadow-soft sm:px-4' }}">