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 }) {