Add navigation workspace switcher v1

This commit is contained in:
A R R R Associates
2026-07-06 12:28:55 +05:30
parent 47f5080d43
commit bfb1fca43a
6 changed files with 235 additions and 20 deletions
@@ -0,0 +1 @@
"""Workspace navigation module."""
+139
View File
@@ -0,0 +1,139 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
@dataclass(frozen=True)
class WorkspaceCard:
code: str
title: str
description: str
url: str
badge: str
priority: int
def _role_set(roles: Iterable[str] | None) -> set[str]:
return {str(role).strip() for role in (roles or []) if str(role).strip()}
def _has_employee_portal_access(roles: set[str], permissions: set[str]) -> bool:
employee_permissions = {
"employees.ess.view",
"employees.work.view_self",
"employees.attendance.view_self",
"employees.leave.view_self",
"employees.documents.view_self",
"employees.payroll.view_self",
}
return bool({"Staff", "Employee"}.intersection(roles) or employee_permissions.intersection(permissions))
def build_workspace_cards(roles: Iterable[str] | None, permissions: Iterable[str] | None) -> list[WorkspaceCard]:
role_set = _role_set(roles)
permission_set = {str(permission).strip() for permission in (permissions or []) if str(permission).strip()}
cards: list[WorkspaceCard] = []
if "System Admin" in role_set:
cards.append(WorkspaceCard(
code="system-admin",
title="System Admin",
description="Platform control, firms, SMTP, storage, reports and setup health.",
url="/system-admin/dashboard",
badge="Platform",
priority=10,
))
if "Firm Admin" in role_set:
cards.append(WorkspaceCard(
code="firm-admin",
title="Firm Administration",
description="Firm settings, branches, users, roles, services, FY and setup reports.",
url="/firm-admin/dashboard",
badge="Firm setup",
priority=20,
))
if "Partner" in role_set:
cards.append(WorkspaceCard(
code="partner",
title="Partner Operations",
description="Branch work, clients, staff workload, review, billing and partner reports.",
url="/partner/dashboard",
badge="Branch control",
priority=30,
))
if {"Manager", "Branch Manager"}.intersection(role_set):
cards.append(WorkspaceCard(
code="manager",
title="Manager Workspace",
description="Team work, review queue, client pending, documents, escalations and reports.",
url="/manager/dashboard",
badge="Execution control",
priority=40,
))
if _has_employee_portal_access(role_set, permission_set):
cards.append(WorkspaceCard(
code="employee",
title="Employee Portal",
description="Attendance, my work board, leave, documents, payslips, alerts and my reports.",
url="/employee/dashboard",
badge="My work",
priority=50,
))
if "Client" in role_set:
cards.append(WorkspaceCard(
code="client",
title="Client Portal",
description="Pending documents, services, billing, messages and client reports.",
url="/client/dashboard",
badge="Client view",
priority=60,
))
if "Consultant" in role_set:
cards.append(WorkspaceCard(
code="consultant",
title="Consultant Workspace",
description="Assigned work, clients, documents, clarifications, requests and consultant reports.",
url="/consultant/dashboard",
badge="External work",
priority=70,
))
# Reports centre is available as a role-aware workspace for every authenticated user.
cards.append(WorkspaceCard(
code="reports",
title="Reports Centre",
description="Role-aware reports grouped by work, clients, billing, HR, system and audit areas.",
url="/reports",
badge="Reports",
priority=90,
))
return sorted(cards, key=lambda card: card.priority)
def current_workspace_code(current_path: str | None) -> str:
path = current_path or ""
if path.startswith("/system-admin"):
return "system-admin"
if path.startswith("/firm-admin"):
return "firm-admin"
if path.startswith("/partner"):
return "partner"
if path.startswith("/manager"):
return "manager"
if path.startswith("/employee") or path.startswith("/employees"):
return "employee"
if path.startswith("/client"):
return "client"
if path.startswith("/consultant"):
return "consultant"
if path.startswith("/reports"):
return "reports"
return ""
@@ -0,0 +1,34 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<section class="mx-auto max-w-7xl space-y-6 px-4 py-6 sm:px-6 lg:px-8">
<div class="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600">Navigation Centre</p>
<div class="mt-2 flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 class="text-2xl font-bold text-slate-900">My Workspaces</h1>
<p class="mt-1 max-w-3xl text-sm text-slate-500">Open the workspace allowed for your role. This page does not replace any existing module; it only gives a clean entry point to dashboards and reports.</p>
</div>
<a href="/reports" class="inline-flex items-center justify-center rounded-2xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-slate-800">Open Reports Centre</a>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{% for card in workspace_cards %}
<a href="{{ card.url }}" class="group rounded-3xl border {% if current_workspace_code == card.code %}border-brand-400 bg-brand-50{% else %}border-slate-200 bg-white{% endif %} p-5 shadow-sm transition hover:-translate-y-0.5 hover:border-brand-300 hover:shadow-md">
<div class="flex items-start justify-between gap-3">
<div>
<span class="inline-flex rounded-full {% if current_workspace_code == card.code %}bg-brand-600 text-white{% else %}bg-slate-100 text-slate-600{% endif %} px-3 py-1 text-xs font-semibold">{{ card.badge }}</span>
<h2 class="mt-4 text-lg font-bold text-slate-900">{{ card.title }}</h2>
</div>
<span class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-slate-500 ring-1 ring-slate-200 group-hover:text-brand-700">Open</span>
</div>
<p class="mt-3 text-sm leading-6 text-slate-500">{{ card.description }}</p>
{% if current_workspace_code == card.code %}
<p class="mt-4 text-xs font-semibold uppercase tracking-wide text-brand-700">Current workspace</p>
{% endif %}
</a>
{% endfor %}
</div>
</section>
{% endblock %}
+43
View File
@@ -0,0 +1,43 @@
from __future__ import annotations
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse
from app.core.db.common import CommonSessionLocal
from app.core.security.csrf import get_or_create_csrf_token
from app.core.security.session_auth import get_current_user
from app.core.templating import templates
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.workspace_navigation.service import build_workspace_cards, current_workspace_code
router = APIRouter(tags=["workspace-navigation-ui"])
@router.get("/workspaces")
def workspaces(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return RedirectResponse(url="/login", status_code=303)
roles = get_user_roles(db, current_user.id)
permissions = get_user_permissions(db, current_user.id)
cards = build_workspace_cards(roles, permissions)
current_path = request.url.path
return templates.TemplateResponse(
"modules/workspace_navigation/templates/workspace_navigation/index.html",
{
"request": request,
"current_user": current_user,
"current_user_roles": roles,
"current_user_permissions": permissions,
"csrf_token": get_or_create_csrf_token(request),
"title": "My Workspaces",
"workspace_cards": cards,
"current_workspace_code": current_workspace_code(current_path),
},
)
finally:
db.close()
+2
View File
@@ -28,6 +28,7 @@ from app.modules.wizards.ui import router as wizards_ui_router
from app.modules.system_admin_dashboard.ui import router as system_admin_dashboard_router
from app.ui.routes.auth import router as auth_router
from app.modules.unified_reports.ui import router as unified_reports_router
from app.modules.workspace_navigation.ui import router as workspace_navigation_router
from app.modules.firm_admin_dashboard.ui import router as firm_admin_dashboard_router
@@ -51,6 +52,7 @@ def mount_ui(app: FastAPI) -> None:
app.include_router(documents_ui_router)
app.include_router(alerts_ui_router)
app.include_router(unified_reports_router)
app.include_router(workspace_navigation_router)
app.include_router(notice_cases_router)
app.include_router(wizards_ui_router)
app.include_router(system_admin_dashboard_router)
@@ -1,25 +1,21 @@
{% if full_auth %}
{% set ws = namespace(count=0) %}
{% if 'System Admin' in ui_roles %}{% set ws.count = ws.count + 1 %}{% endif %}
{% if 'Firm Admin' in ui_roles %}{% set ws.count = ws.count + 1 %}{% endif %}
{% if 'Partner' in ui_roles %}{% set ws.count = ws.count + 1 %}{% endif %}
{% if 'Manager' in ui_roles or 'Branch Manager' in ui_roles %}{% set ws.count = ws.count + 1 %}{% endif %}
{% if 'Staff' in ui_roles or can_view_employee_portal(current_user, ui_perms, ui_roles) %}{% set ws.count = ws.count + 1 %}{% endif %}
{% if 'Client' in ui_roles %}{% set ws.count = ws.count + 1 %}{% endif %}
{% if 'Consultant' in ui_roles %}{% set ws.count = ws.count + 1 %}{% endif %}
{% if ws.count > 1 %}
<div class="mt-2 flex justify-end">
{% set roles = ui_roles if ui_roles is defined else [] %}
{% set perms = ui_perms if ui_perms is defined else [] %}
{% set current = current_path if current_path is defined else request.url.path %}
{% set can_employee = ('Staff' in roles) or ('Employee' in roles) or ('employees.ess.view' in perms) or ('employees.work.view_self' in perms) or ('employees.attendance.view_self' in perms) or ('employees.leave.view_self' in perms) or ('employees.documents.view_self' in perms) or ('employees.payroll.view_self' in perms) %}
<div class="mt-2 flex flex-wrap justify-end gap-2">
<label class="sr-only" for="workspace_switcher">Workspace</label>
<select id="workspace_switcher" onchange="if(this.value){window.location.href=this.value;}" class="max-w-xs rounded-lg border border-brand-200 bg-brand-50 px-3 py-1.5 text-xs font-semibold text-brand-800 shadow-sm hover:bg-brand-100">
<option value="">Workspace: switch view</option>
{% if 'System Admin' in ui_roles %}<option value="/system-admin/dashboard" {% if current_path.startswith('/system-admin') %}selected{% endif %}>System Admin - Platform Control</option>{% endif %}
{% if 'Partner' in ui_roles %}<option value="/partner/dashboard" {% if current_path.startswith('/partner') %}selected{% endif %}>Partner Operations</option>{% endif %}
{% if 'Firm Admin' in ui_roles %}<option value="/firm-admin/dashboard" {% if current_path.startswith('/firm-admin') %}selected{% endif %}>Firm Administration</option>{% endif %}
{% if 'Manager' in ui_roles or 'Branch Manager' in ui_roles %}<option value="/manager/dashboard" {% if current_path.startswith('/manager') %}selected{% endif %}>Manager / Team Workspace</option>{% endif %}
{% if 'Staff' in ui_roles or can_view_employee_portal(current_user, ui_perms, ui_roles) %}<option value="/employee/dashboard" {% if current_path.startswith('/employee') %}selected{% endif %}>My Staff Workspace</option>{% endif %}
{% if 'Client' in ui_roles %}<option value="/client/dashboard" {% if current_path.startswith('/client') %}selected{% endif %}>Client Portal</option>{% endif %}
{% if 'Consultant' in ui_roles %}<option value="/consultant/dashboard" {% if current_path.startswith('/consultant') %}selected{% endif %}>Consultant Workspace</option>{% endif %}
<select id="workspace_switcher" onchange="if(this.value){window.location.href=this.value;}" class="min-w-[230px] max-w-xs rounded-xl border border-brand-200 bg-brand-50 px-3 py-1.5 text-xs font-semibold text-brand-900 shadow-sm outline-none transition hover:bg-brand-100 focus:border-brand-400 focus:ring-2 focus:ring-brand-200">
<option value="/workspaces" {% if current.startswith('/workspaces') %}selected{% endif %}>Workspace Centre</option>
{% if 'System Admin' in roles %}<option value="/system-admin/dashboard" {% if current.startswith('/system-admin') %}selected{% endif %}>System Admin - Platform Control</option>{% endif %}
{% if 'Partner' in roles %}<option value="/partner/dashboard" {% if current.startswith('/partner') %}selected{% endif %}>Partner Operations</option>{% endif %}
{% if 'Firm Admin' in roles %}<option value="/firm-admin/dashboard" {% if current.startswith('/firm-admin') %}selected{% endif %}>Firm Administration</option>{% endif %}
{% if 'Manager' in roles or 'Branch Manager' in roles %}<option value="/manager/dashboard" {% if current.startswith('/manager') %}selected{% endif %}>Manager Workspace</option>{% endif %}
{% if can_employee %}<option value="/employee/dashboard" {% if current.startswith('/employee') or current.startswith('/employees') %}selected{% endif %}>Employee Portal</option>{% endif %}
{% if 'Client' in roles %}<option value="/client/dashboard" {% if current.startswith('/client') %}selected{% endif %}>Client Portal</option>{% endif %}
{% if 'Consultant' in roles %}<option value="/consultant/dashboard" {% if current.startswith('/consultant') %}selected{% endif %}>Consultant Workspace</option>{% endif %}
<option value="/reports" {% if current.startswith('/reports') %}selected{% endif %}>Reports Centre</option>
</select>
<a href="/workspaces" class="inline-flex items-center rounded-xl border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600 shadow-sm hover:border-brand-200 hover:text-brand-700">All workspaces</a>
</div>
{% endif %}
{% endif %}