update: Add Global Search on Header, Add Global Audit log for all actions.

This commit is contained in:
2026-04-19 15:41:29 +03:00
parent 4f35bef6e3
commit 6a958a8d7d
27 changed files with 2086 additions and 267 deletions

View File

@@ -1,20 +1,40 @@
// frontend/src/components/layout/Sidebar.jsx
// Primary navigation sidebar — 224px wide, fixed, full height.
//
// Visual style (matches Stitch reference):
// - Brand header with logo at top
// - Section labels: plain uppercase text, generous padding, no rule lines
// - Nav items: px-6 py-3, full width, 3px left bar + primary-subtle bg when active
// - Collapsible groups: same row height as nav items
// - Children: darker inset bg, deep left-indent, text-color hover
// - Console Settings: pinned at bottom
import { useState } from 'react'
import { NavLink, useLocation } from 'react-router-dom'
import { useAuth } from '@/hooks/useAuth'
import logoDark from '@/assets/logos/bell_systems_horizontal_darkMode.png'
// ─── Icon set ─────────────────────────────────────────────────────────────────
// ─── SVG file icons (vite-plugin-svgr v4 — ?react query) ─────────────────────
import IcoDevices from '@/assets/side-menu-icons/devices.svg?react'
import IcoDeviceOverview from '@/assets/side-menu-icons/device-overview.svg?react'
import IcoFleet from '@/assets/side-menu-icons/fleet.svg?react'
import IcoBlackbox from '@/assets/side-menu-icons/blackbox.svg?react'
import IcoMelodies from '@/assets/side-menu-icons/melodies.svg?react'
import IcoMelodiesEditor from '@/assets/side-menu-icons/melodies-editor.svg?react'
import IcoComposer from '@/assets/side-menu-icons/composer.svg?react'
import IcoArchetypes from '@/assets/side-menu-icons/archetypes.svg?react'
import IcoMelodySettings from '@/assets/side-menu-icons/melody-settings.svg?react'
import IcoCommunications from '@/assets/side-menu-icons/communications.svg?react'
import IcoWhatsapp from '@/assets/side-menu-icons/whatsapp.svg?react'
import IcoSms from '@/assets/side-menu-icons/sms.svg?react'
import IcoHelpdesk from '@/assets/side-menu-icons/helpdesk.svg?react'
import IcoCommsLog from '@/assets/side-menu-icons/communications-log.svg?react'
import IcoCustomers from '@/assets/side-menu-icons/customers.svg?react'
import IcoCustomerOverview from '@/assets/side-menu-icons/customer-overview.svg?react'
import IcoOrders from '@/assets/side-menu-icons/orders.svg?react'
import IcoProducts from '@/assets/side-menu-icons/products.svg?react'
import IcoCatalog from '@/assets/side-menu-icons/product-catalog.svg?react'
import IcoSnManager from '@/assets/side-menu-icons/sn-manager.svg?react'
import IcoManufacturing from '@/assets/side-menu-icons/manufacturing.svg?react'
import IcoInventory from '@/assets/side-menu-icons/inventory.svg?react'
import IcoProvisioning from '@/assets/side-menu-icons/provision.svg?react'
import IcoFirmware from '@/assets/side-menu-icons/firmware.svg?react'
import IcoApi from '@/assets/side-menu-icons/api.svg?react'
// ─── Inline-only icons (no SVG file equivalent) ───────────────────────────────
const S = ({ children, ...p }) => (
<svg
@@ -29,38 +49,50 @@ const S = ({ children, ...p }) => (
</svg>
)
// Wrapper to normalise imported SVG file components to 16×16
function SvgIcon({ Component }) {
return (
<Component
width="16" height="16"
aria-hidden="true" focusable="false"
style={{ flexShrink: 0 }}
/>
)
}
const Icons = {
dashboard: () => <S><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/></S>,
devices: () => <S><rect x="2" y="3" width="12" height="8" rx="1.5"/><path d="M5 14h6M8 11v3"/></S>,
deviceOverview: () => <S><circle cx="8" cy="6" r="3"/><path d="M3 13c0-2.76 2.24-5 5-5s5 2.24 5 5"/></S>,
fleet: () => <S><path d="M2 5h12M2 8h9M2 11h6"/></S>,
devices: () => <SvgIcon Component={IcoDevices} />,
deviceOverview: () => <SvgIcon Component={IcoDeviceOverview} />,
fleet: () => <SvgIcon Component={IcoFleet} />,
commandCenter: () => <S><rect x="2" y="2" width="12" height="12" rx="1.5"/><path d="M5 6l2 2-2 2M9 10h2"/></S>,
blackBox: () => <S><rect x="2" y="4" width="12" height="8" rx="1"/><path d="M5 8h6"/></S>,
blackBox: () => <SvgIcon Component={IcoBlackbox} />,
deviceSettings: () => <SvgIcon Component={IcoMelodySettings} />,
appUsers: () => <S><circle cx="6" cy="5" r="2.5"/><path d="M2 13c0-2.2 1.8-4 4-4"/><circle cx="12" cy="7" r="2"/><path d="M9.5 13c0-1.65 1.12-3 2.5-3s2.5 1.35 2.5 3"/></S>,
melodies: () => <S><path d="M9 3v7"/><path d="M9 3l4-1v7"/><circle cx="7" cy="10" r="2"/><circle cx="11" cy="9" r="2"/></S>,
library: () => <S><rect x="2" y="2" width="4" height="12" rx="1"/><rect x="7" y="4" width="4" height="10" rx="1"/><rect x="12" y="2" width="2" height="12" rx="1"/></S>,
composer: () => <S><path d="M2 12L10 4l2 2-8 8H2v-2z"/><path d="M8 6l2 2"/></S>,
archetypes: () => <S><path d="M8 2l2 4h4l-3 3 1 4-4-2.5L4 13l1-4-3-3h4z"/></S>,
melodySettings: () => <S><path d="M2 4h2"/><path d="M6 4h8"/><circle cx="5" cy="4" r="1.5" fill="currentColor" stroke="none"/><path d="M2 8h6"/><path d="M10 8h4"/><circle cx="9" cy="8" r="1.5" fill="currentColor" stroke="none"/><path d="M2 12h9"/><path d="M13 12h1"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/></S>,
communications: () => <S><path d="M2 3h12v8H2z"/><path d="M5 14l3-3 3 3"/></S>,
melodies: () => <SvgIcon Component={IcoMelodies} />,
library: () => <SvgIcon Component={IcoMelodiesEditor} />,
composer: () => <SvgIcon Component={IcoComposer} />,
archetypes: () => <SvgIcon Component={IcoArchetypes} />,
melodySettings: () => <SvgIcon Component={IcoMelodySettings} />,
communications: () => <SvgIcon Component={IcoCommunications} />,
mail: () => <S><rect x="2" y="4" width="12" height="9" rx="1"/><path d="M2 5l6 4 6-4"/></S>,
whatsapp: () => <S><path d="M8 2a6 6 0 0 1 6 6c0 3.31-2.69 6-6 6a5.97 5.97 0 0 1-3.1-.86L2 14l.86-2.9A5.97 5.97 0 0 1 2 8a6 6 0 0 1 6-6z"/></S>,
sms: () => <S><path d="M2 3h12v8H8l-3 2.5V11H2z"/></S>,
helpdesk: () => <S><path d="M8 2a4 4 0 0 0-4 4c0 1.5.82 2.8 2 3.46V11h4V9.46A4 4 0 0 0 8 2z"/><path d="M6 13h4"/><path d="M8 11v2"/></S>,
commsLog: () => <S><circle cx="8" cy="8" r="6"/><path d="M8 5v3l2 2"/></S>,
customers: () => <S><rect x="3" y="2" width="10" height="7" rx="1"/><path d="M1 14c0-2.76 3.13-5 7-5s7 2.24 7 5"/></S>,
customerOverview: () => <S><path d="M2 12l3-5 3 3 2-4 4 6"/></S>,
orders: () => <S><rect x="3" y="2" width="10" height="12" rx="1"/><path d="M6 6h4M6 9h4M6 12h2"/></S>,
whatsapp: () => <SvgIcon Component={IcoWhatsapp} />,
sms: () => <SvgIcon Component={IcoSms} />,
helpdesk: () => <SvgIcon Component={IcoHelpdesk} />,
commsLog: () => <SvgIcon Component={IcoCommsLog} />,
customers: () => <SvgIcon Component={IcoCustomers} />,
customerOverview: () => <SvgIcon Component={IcoCustomerOverview} />,
orders: () => <SvgIcon Component={IcoOrders} />,
quotations: () => <S><rect x="3" y="2" width="10" height="12" rx="1"/><path d="M6 5h4M6 8h3M9 11l1-1 1 1 1-3"/></S>,
products: () => <S><path d="M8 2L2 5v6l6 3 6-3V5z"/><path d="M8 2v9M2 5l6 3 6-3"/></S>,
catalog: () => <S><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/></S>,
snManager: () => <S><rect x="2" y="5" width="12" height="6" rx="1"/><path d="M4 8h1M6 8h1M8 8h1M10 8h1"/></S>,
products: () => <SvgIcon Component={IcoProducts} />,
catalog: () => <SvgIcon Component={IcoCatalog} />,
snManager: () => <SvgIcon Component={IcoSnManager} />,
staffLog: () => <S><rect x="4" y="2" width="8" height="2" rx="1"/><rect x="2" y="3" width="12" height="11" rx="1"/><path d="M5 8h6M5 11h4"/></S>,
manufacturing: () => <S><path d="M2 12l3-6 3 3 2-5 4 8H2z"/><circle cx="5" cy="5" r="1.5"/></S>,
inventory: () => <S><rect x="2" y="7" width="5" height="7" rx="1"/><rect x="5.5" y="4" width="5" height="10" rx="1"/><rect x="9" y="2" width="5" height="12" rx="1"/></S>,
provisioning: () => <S><path d="M8 2v8"/><path d="M5 8l3 3 3-3"/><path d="M3 13h10"/></S>,
firmware: () => <S><rect x="3" y="4" width="10" height="8" rx="1"/><path d="M6 7h4M7 10h2"/><path d="M6 2h4M6 14h4"/></S>,
api: () => <S><path d="M4 6l-2 2 2 2M12 6l2 2-2 2M9 4l-2 8"/></S>,
manufacturing: () => <SvgIcon Component={IcoManufacturing} />,
inventory: () => <SvgIcon Component={IcoInventory} />,
provisioning: () => <SvgIcon Component={IcoProvisioning} />,
firmware: () => <SvgIcon Component={IcoFirmware} />,
api: () => <SvgIcon Component={IcoApi} />,
settings: () => <S><circle cx="8" cy="8" r="2.5"/><path d="M8 2v1.5M8 12.5V14M2 8h1.5M12.5 8H14M3.5 3.5l1 1M11.5 11.5l1 1M3.5 12.5l1-1M11.5 4.5l1-1"/></S>,
staff: () => <S><circle cx="8" cy="5" r="3"/><path d="M2 14c0-3.31 2.69-6 6-6s6 2.69 6 6"/></S>,
publicFeatures: () => <S><path d="M2 8a6 6 0 1 0 12 0A6 6 0 0 0 2 8z"/><path d="M8 2a9 9 0 0 0 0 12M8 2a9 9 0 0 1 0 12M2 8h12"/></S>,
@@ -99,7 +131,7 @@ const navSections = [
{ to: '/devices', label: 'Fleet', icon: 'fleet', exact: true },
{ to: '/mqtt/commands', label: 'Command Center', icon: 'commandCenter' },
{ to: '/equipment/notes', label: 'BlackBox', icon: 'blackBox' },
{ to: '/devices/settings', label: 'Device Settings', icon: 'settings', placeholder: true },
{ to: '/devices/settings', label: 'Device Settings', icon: 'deviceSettings', placeholder: true },
],
},
{ to: '/users', label: 'App Users', icon: 'appUsers', permission: 'app_users' },
@@ -117,11 +149,11 @@ const navSections = [
{
label: 'Communications', icon: 'communications', permission: 'crm',
children: [
{ to: '/mail', label: 'Mailbox', icon: 'mail' },
{ to: '/comms/whatsapp', label: 'WhatsApp', icon: 'whatsapp', placeholder: true },
{ to: '/comms/sms', label: 'SMS', icon: 'sms', placeholder: true },
{ to: '/crm/comms/helpdesk', label: 'Helpdesk', icon: 'helpdesk', exact: true },
{ to: '/crm/comms', label: 'Comms Log', icon: 'commsLog', exact: true },
{ to: '/mail', label: 'Mailbox', icon: 'mail' },
{ to: '/comms/whatsapp', label: 'WhatsApp', icon: 'whatsapp', placeholder: true },
{ to: '/comms/sms', label: 'SMS', icon: 'sms', placeholder: true },
{ to: '/crm/comms/helpdesk', label: 'Helpdesk', icon: 'helpdesk', exact: true },
{ to: '/crm/comms', label: 'Comms Log', icon: 'commsLog', exact: true },
],
},
{
@@ -225,7 +257,7 @@ function CollapsibleGroup({ label, icon, children, currentPath, locked, open, on
end={child.exact === true}
className={({ isActive }) => `nav-child-link${isActive ? ' active' : ''}`}
>
{({ isActive }) => {
{() => {
const ChildIcon = Icons[child.icon] ?? Icons.placeholder
return (
<>
@@ -274,7 +306,7 @@ export default function Sidebar() {
<img
src={logoDark}
alt="BellSystems"
style={{ height: '18px', width: 'auto', objectFit: 'contain' }}
className="sidebar-brand-logo"
/>
</div>