diff --git a/src/app.css b/src/app.css
index b0ba600..4d8275a 100644
--- a/src/app.css
+++ b/src/app.css
@@ -5330,3 +5330,155 @@ body {
flex-wrap: wrap;
gap: 6px;
}
+
+/* ── Dashboard Enhancements ────────────────────────────────────────────────── */
+
+.dashboard-widgets-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
+ gap: 20px;
+ margin-top: 24px;
+}
+
+.widget-card {
+ background: #fff;
+ border: 1px solid rgba(16, 11, 47, 0.08);
+ border-radius: 20px;
+ padding: 20px;
+ box-shadow: 0 4px 20px -10px rgba(2, 6, 23, 0.1);
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.widget-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.widget-header h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--ink);
+}
+
+.dashboard-tabs {
+ display: flex;
+ gap: 8px;
+ padding: 4px;
+ background: #f1f5f9;
+ border-radius: 12px;
+ width: fit-content;
+ margin-bottom: 24px;
+}
+
+.dashboard-tab {
+ padding: 8px 16px;
+ font-size: 13px;
+ font-weight: 600;
+ color: #64748b;
+ border: none;
+ background: transparent;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.dashboard-tab--active {
+ background: #fff;
+ color: var(--brand-orange);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.quick-actions-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+}
+
+.quick-action-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ padding: 16px;
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 16px;
+ text-decoration: none;
+ transition: all 0.2s ease;
+}
+
+.quick-action-btn:hover {
+ background: #fff;
+ border-color: var(--brand-orange);
+ transform: translateY(-2px);
+ box-shadow: 0 8px 20px -10px rgba(253, 97, 22, 0.2);
+}
+
+.action-icon {
+ font-size: 24px;
+}
+
+.quick-action-btn span:not(.action-icon) {
+ font-size: 12px;
+ font-weight: 700;
+ color: #1e293b;
+ text-align: center;
+}
+
+/* Activity Timeline */
+.activity-timeline {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ position: relative;
+ padding-left: 12px;
+}
+
+.activity-timeline::before {
+ content: '';
+ position: absolute;
+ left: 3px;
+ top: 8px;
+ bottom: 8px;
+ width: 2px;
+ background: #e2e8f0;
+}
+
+.activity-item {
+ display: flex;
+ gap: 16px;
+ position: relative;
+}
+
+.activity-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--brand-orange);
+ margin-top: 6px;
+ flex-shrink: 0;
+ z-index: 1;
+}
+
+.activity-text p {
+ margin: 0;
+ font-size: 13px;
+ line-height: 1.4;
+ color: #334155;
+}
+
+.activity-text small {
+ font-size: 11px;
+ color: #94a3b8;
+}
+
+.empty-state {
+ text-align: center;
+ color: #94a3b8;
+ font-size: 13px;
+ padding: 20px 0;
+}
diff --git a/src/components/dashboard/DashboardLayout.tsx b/src/components/dashboard/DashboardLayout.tsx
index a31d7eb..dc31bbc 100644
--- a/src/components/dashboard/DashboardLayout.tsx
+++ b/src/components/dashboard/DashboardLayout.tsx
@@ -1,7 +1,7 @@
import { Component, Show, createEffect, For, createSignal, onMount } from 'solid-js';
import { useNavigate, A, useSearchParams } from '@solidjs/router';
-import { authState, logout, switchRole, bootstrapAuth, setMockRuntimeConfig } from '~/lib/auth';
-import { shouldShowRoleSwitcher } from '~/lib/auth-flow';
+import { authState, logout, switchRole, bootstrapAuth, setMockRuntimeConfig, isModuleLocked } from '~/lib/auth';
+import { shouldShowRoleSwitcher, getRoleLabel } from '~/lib/auth-flow';
import {
getRoleTourStorageKey,
getWelcomeTourStorageKey,
@@ -66,6 +66,10 @@ const IconWallet = () => (
+);const IconLock = () => (
+
);
// ── Module → nav item mapping ─────────────────────────────────────────────────
@@ -115,6 +119,28 @@ const MODULE_NAV_MAP: Record(null);
const [tourStepIndex, setTourStepIndex] = createSignal(0);
@@ -348,6 +374,38 @@ export default function DashboardLayout(props: { children: any }) {
const navItems = () => {
const role = String(activeRole() || rc()?.role || '').toUpperCase();
+ const roleConfig = rc()?.role_config;
+
+ // 1. If admin-defined sidebar_items exist, use them as priority
+ if (Array.isArray(roleConfig?.sidebar_items) && roleConfig.sidebar_items.length > 0) {
+ return roleConfig.sidebar_items
+ .map((label: string) => {
+ const key = mapSidebarLabelToNavKey(label);
+ const base = key ? MODULE_NAV_MAP[key] : null;
+ if (!base) return null;
+ const isLocked = isModuleLocked(key || '');
+ let finalItem = { ...base, label, isLocked }; // Use the admin-saved label
+
+ if (isLocked) {
+ finalItem.href = '#'; // Disable navigation
+ }
+
+ if (key === 'MARKETPLACE' && role === 'CUSTOMER') {
+ finalItem = { ...finalItem, label: 'Post Requirement', href: '/users/requirements/new' };
+ }
+
+ if (role === 'COMPANY') {
+ if (key === 'profile') finalItem.href = '/companies/profile';
+ if (key === 'support') finalItem.href = '/companies/support';
+ if (key === 'settings') finalItem.href = '/companies/settings';
+ }
+
+ return finalItem;
+ })
+ .filter((item: any): item is NonNullable => Boolean(item));
+ }
+
+ // 2. Fallback to module-based logic
const moduleSet = new Set(
(Array.isArray(rc()?.enabled_modules) ? rc()!.enabled_modules : [])
.map((moduleKey) => String(moduleKey || '').toLowerCase()),
@@ -362,20 +420,25 @@ export default function DashboardLayout(props: { children: any }) {
.map((m) => {
const base = MODULE_NAV_MAP[m];
if (!base) return null;
+
+ const isLocked = isModuleLocked(m);
+ const finalItem = { ...base, isLocked };
+ if (isLocked) {
+ finalItem.href = '#';
+ }
- // Match Next.js role override: customer "leads" is "Post Requirement"
if (m === 'leads' && role === 'CUSTOMER') {
- return { ...base, label: 'Post Requirement', href: '/users/requirements/new', tourId: 'requirements' };
+ return { ...finalItem, label: 'Post Requirement', href: isLocked ? '#' : '/users/requirements/new', tourId: 'requirements' };
}
- // Match Next.js company route overrides.
if (role === 'COMPANY') {
- if (m === 'profile') return { ...base, href: '/companies/profile' };
- if (m === 'support') return { ...base, href: '/companies/support' };
- if (m === 'settings') return { ...base, href: '/companies/settings' };
+ const prefix = isLocked ? '#' : '';
+ if (m === 'profile') return { ...finalItem, href: prefix + '/companies/profile' };
+ if (m === 'support') return { ...finalItem, href: prefix + '/companies/support' };
+ if (m === 'settings') return { ...finalItem, href: prefix + '/companies/settings' };
}
- return base;
+ return finalItem;
})
.filter((item): item is NonNullable => Boolean(item))
.sort((left, right) => (left.order ?? 999) - (right.order ?? 999));
@@ -452,7 +515,7 @@ export default function DashboardLayout(props: { children: any }) {