feat: gekleurde iconen in sidebar + dashboard (legacy icon_colors)

This commit is contained in:
mo
2026-07-21 22:00:39 +02:00
parent 3c49df64ae
commit 345bc84640
10 changed files with 558 additions and 489 deletions
+26 -24
View File
@@ -7,56 +7,58 @@ import {
Search, Bell, LogOut, Menu, X, ExternalLink, ShieldCheck, PenTool, FileSearch Search, Bell, LogOut, Menu, X, ExternalLink, ShieldCheck, PenTool, FileSearch
} from 'lucide-react'; } from 'lucide-react';
import { useAuth } from '../auth'; import { useAuth } from '../auth';
import { useSettings, iconColor, brandName } from '../settings';
import { api } from '../api'; import { api } from '../api';
const NAV = [ const NAV = [
{ {
label: 'CRM', label: 'CRM',
items: [ items: [
{ to: '/', icon: LayoutDashboard, label: 'Dashboard', end: true }, { to: '/', icon: LayoutDashboard, iconName: 'layout-dashboard', label: 'Dashboard', end: true },
{ to: '/clients', icon: Users, label: 'Cliënten' }, { to: '/clients', icon: Users, iconName: 'users', label: 'Cliënten' },
{ to: '/engagements', icon: Briefcase, label: 'Engagements' }, { to: '/engagements', icon: Briefcase, iconName: 'briefcase', label: 'Engagements' },
{ to: '/tasks', icon: CheckCircle2, label: 'Taken' }, { to: '/tasks', icon: CheckCircle2, iconName: 'check-circle', label: 'Taken' },
{ to: '/calendar', icon: Calendar, label: 'Agenda' }, { to: '/calendar', icon: Calendar, iconName: 'calendar', label: 'Agenda' },
{ to: '/email', icon: Mail, label: 'Email' } { to: '/email', icon: Mail, iconName: 'mail', label: 'Email' }
] ]
}, },
{ {
label: 'Delivery', label: 'Delivery',
items: [ items: [
{ to: '/projects', icon: FolderKanban, label: 'Projecten' }, { to: '/projects', icon: FolderKanban, iconName: 'folder-kanban', label: 'Projecten' },
{ to: '/time', icon: Clock, label: 'Uren' }, { to: '/time', icon: Clock, iconName: 'clock', label: 'Uren' },
{ to: '/invoices', icon: Receipt, label: 'Facturen' }, { to: '/invoices', icon: Receipt, iconName: 'receipt', label: 'Facturen' },
{ to: '/finance', icon: Wallet, label: 'Finance' }, { to: '/finance', icon: Wallet, iconName: 'wallet', label: 'Finance' },
{ to: '/reports', icon: BarChart3, label: 'Rapporten' } { to: '/reports', icon: BarChart3, iconName: 'bar-chart-3', label: 'Rapporten' }
] ]
}, },
{ {
label: 'Data & Infra', label: 'Data & Infra',
items: [ items: [
{ to: '/racks', icon: Server, label: 'Racks' }, { to: '/racks', icon: Server, iconName: 'server', label: 'Racks' },
{ to: '/networking', icon: Share2, label: 'Netwerk' }, { to: '/networking', icon: Share2, iconName: 'share-2', label: 'Netwerk' },
{ to: '/diagrams', icon: Network, label: 'Diagrammen' }, { to: '/diagrams', icon: Network, iconName: 'network', label: 'Diagrammen' },
{ to: '/market', icon: Radar, label: 'Market Intel' }, { to: '/market', icon: Radar, iconName: 'radar', label: 'Market Intel' },
{ to: '/ai', icon: Sparkles, label: 'AI Studio' } { to: '/ai', icon: Sparkles, iconName: 'sparkles', label: 'AI Studio' }
] ]
}, },
{ {
label: 'Platform', label: 'Platform',
items: [ items: [
{ to: '/settings', icon: Settings, label: 'Instellingen' } { to: '/settings', icon: Settings, iconName: 'settings', label: 'Instellingen' }
] ]
} }
]; ];
const LEGACY_LINKS = [ const LEGACY_LINKS = [
{ href: '/quality', icon: ShieldCheck, label: 'Data Quality' }, { href: '/quality', icon: ShieldCheck, iconName: 'shield-check', label: 'Data Quality' },
{ href: '/architecture/designer', icon: PenTool, label: 'Pro Designer' }, { href: '/architecture/designer', icon: PenTool, iconName: 'pen-tool', label: 'Pro Designer' },
{ href: '/assess', icon: FileSearch, label: 'Assessments' } { href: '/assess', icon: FileSearch, iconName: 'file-search', label: 'Assessments' }
]; ];
export default function Layout({ children }) { export default function Layout({ children }) {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const { settings } = useSettings();
const navigate = useNavigate(); const navigate = useNavigate();
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [unread, setUnread] = useState(0); const [unread, setUnread] = useState(0);
@@ -79,10 +81,10 @@ export default function Layout({ children }) {
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="px-5 py-5 flex items-center gap-3 border-b border-border-soft"> <div className="px-5 py-5 flex items-center gap-3 border-b border-border-soft">
<div className="w-9 h-9 rounded-lg bg-accent-soft flex items-center justify-center glow-accent"> <div className="w-9 h-9 rounded-lg bg-accent-soft flex items-center justify-center glow-accent">
<Network className="w-5 h-5 text-accent" /> <Network style={{ color: iconColor('network', settings) || '#00d4ff' }} className="w-5 h-5" />
</div> </div>
<div> <div>
<div className="font-bold text-[15px] tracking-tight">Mek-Tech</div> <div className="font-bold text-[15px] tracking-tight">{brandName(settings)}</div>
<div className="text-[11px] text-muted">Consultancy Platform</div> <div className="text-[11px] text-muted">Consultancy Platform</div>
</div> </div>
</div> </div>
@@ -108,7 +110,7 @@ export default function Layout({ children }) {
}` }`
} }
> >
<item.icon className="w-4 h-4 shrink-0" /> <item.icon className="w-4 h-4 shrink-0" style={{ color: iconColor(item.iconName, settings) || 'inherit' }} />
{item.label} {item.label}
</NavLink> </NavLink>
))} ))}
@@ -127,7 +129,7 @@ export default function Layout({ children }) {
href={item.href} href={item.href}
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[13px] text-muted hover:text-text hover:bg-card transition-colors" className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[13px] text-muted hover:text-text hover:bg-card transition-colors"
> >
<item.icon className="w-4 h-4 shrink-0" /> <item.icon className="w-4 h-4 shrink-0" style={{ color: iconColor(item.iconName, settings) || 'inherit' }} />
{item.label} {item.label}
<ExternalLink className="w-3 h-3 ml-auto opacity-50" /> <ExternalLink className="w-3 h-3 ml-auto opacity-50" />
</a> </a>
+2 -2
View File
@@ -57,7 +57,7 @@ export function Card({ title, subtitle, actions, children, className = '', paddi
); );
} }
export function KpiCard({ icon: Icon, label, value, sub, color = 'accent', to }) { export function KpiCard({ icon: Icon, label, value, sub, color = 'accent', to, iconColor: iconCol }) {
const colors = { const colors = {
accent: 'text-accent bg-accent-soft', accent: 'text-accent bg-accent-soft',
green: 'text-green bg-green/15', green: 'text-green bg-green/15',
@@ -76,7 +76,7 @@ export function KpiCard({ icon: Icon, label, value, sub, color = 'accent', to })
</div> </div>
{Icon && ( {Icon && (
<div className={`w-9 h-9 rounded-lg flex items-center justify-center ${colors[color] || colors.accent}`}> <div className={`w-9 h-9 rounded-lg flex items-center justify-center ${colors[color] || colors.accent}`}>
<Icon className="w-4.5 h-4.5 w-[18px] h-[18px]" /> <Icon className="w-[18px] h-[18px]" style={{ color: iconCol || 'currentColor' }} />
</div> </div>
)} )}
</div> </div>
+6 -3
View File
@@ -3,14 +3,17 @@ import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import App from './App'; import App from './App';
import { AuthProvider } from './auth'; import { AuthProvider } from './auth';
import { SettingsProvider } from './settings';
import './index.css'; import './index.css';
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter basename="/app"> <BrowserRouter basename="/app">
<AuthProvider> <SettingsProvider>
<App /> <AuthProvider>
</AuthProvider> <App />
</AuthProvider>
</SettingsProvider>
</BrowserRouter> </BrowserRouter>
</React.StrictMode> </React.StrictMode>
); );
+8 -6
View File
@@ -4,6 +4,7 @@ import {
Users, Briefcase, CheckCircle2, Clock, Receipt, Wallet, Users, Briefcase, CheckCircle2, Clock, Receipt, Wallet,
AlertTriangle, Lightbulb, Activity, ArrowRight AlertTriangle, Lightbulb, Activity, ArrowRight
} from 'lucide-react'; } from 'lucide-react';
import { useSettings, iconColor } from '../settings';
import { import {
ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid,
PieChart, Pie, Cell, Legend PieChart, Pie, Cell, Legend
@@ -29,6 +30,7 @@ const tooltipStyle = {
}; };
export default function Dashboard() { export default function Dashboard() {
const { settings } = useSettings();
const [data, setData] = useState(null); const [data, setData] = useState(null);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -62,12 +64,12 @@ export default function Dashboard() {
{/* KPI's */} {/* KPI's */}
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
<KpiCard icon={Users} label="Cliënten" value={stats.totalClients} sub={`${stats.activeClients} actief · ${stats.leads} leads`} color="accent" /> <KpiCard icon={Users} label="Cliënten" value={stats.totalClients} sub={`${stats.activeClients} actief · ${stats.leads} leads`} color="accent" iconColor={iconColor('users', settings)} />
<KpiCard icon={Briefcase} label="Lopende engagements" value={stats.activeEngagements} color="blue" /> <KpiCard icon={Briefcase} label="Lopende engagements" value={stats.activeEngagements} color="blue" iconColor={iconColor('briefcase', settings)} />
<KpiCard icon={CheckCircle2} label="Open taken" value={stats.totalTasks - stats.completedTasks} sub={stats.overdueTasks > 0 ? `${stats.overdueTasks} te laat` : undefined} color={stats.overdueTasks > 0 ? 'red' : 'green'} /> <KpiCard icon={CheckCircle2} label="Open taken" value={stats.totalTasks - stats.completedTasks} sub={stats.overdueTasks > 0 ? `${stats.overdueTasks} te laat` : undefined} color={stats.overdueTasks > 0 ? 'red' : 'green'} iconColor={iconColor('check-circle', settings)} />
<KpiCard icon={Clock} label="Uren totaal" value={fmt.hours(stats.totalHours)} sub={`${fmt.hours(stats.billableHours)} facturabel`} color="purple" /> <KpiCard icon={Clock} label="Uren totaal" value={fmt.hours(stats.totalHours)} sub={`${fmt.hours(stats.billableHours)} facturabel`} color="purple" iconColor={iconColor('clock', settings)} />
<KpiCard icon={Receipt} label="Openstaand" value={fmt.euro(stats.totalOutstanding)} color="yellow" /> <KpiCard icon={Receipt} label="Openstaand" value={fmt.euro(stats.totalOutstanding)} color="yellow" iconColor={iconColor('receipt', settings)} />
<KpiCard icon={Wallet} label="Betaald" value={fmt.euro(stats.totalPaid)} color="green" /> <KpiCard icon={Wallet} label="Betaald" value={fmt.euro(stats.totalPaid)} color="green" iconColor={iconColor('wallet', settings)} />
</div> </div>
{/* Grafieken */} {/* Grafieken */}
+62
View File
@@ -0,0 +1,62 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { api } from './api';
// Legacy standaard-icoonkleuren (fallback als gebruiker ze niet zelf heeft ingesteld)
const DEFAULT_ICON_COLORS = {
'layout-dashboard': '#58a6ff', 'users': '#3fb950', 'briefcase': '#d29922',
'server': '#bc8cff', 'check-circle': '#3fb950', 'lightbulb': '#d29922',
'clock': '#79c0ff', 'eye': '#f778ba', 'phone': '#3fb950', 'file-text': '#f778ba',
'edit-3': '#79c0ff', 'folder': '#d29922', 'clipboard-list': '#bc8cff',
'mail': '#58a6ff', 'calendar': '#79c0ff', 'receipt': '#d29922', 'wallet': '#3fb950',
'bar-chart-3': '#bc8cff', 'share-2': '#58a6ff', 'network': '#00d4ff', 'radar': '#d29922',
'sparkles': '#bc8cff', 'settings': '#8b9bb8', 'shield-check': '#3fb950',
'pen-tool': '#bc8cff', 'file-search': '#d29922', 'folder-kanban': '#d29922', 'cpu': '#58a6ff',
'building-2': '#bc8cff', 'activity': '#58a6ff', 'search': '#8b9bb8', 'bell': '#d29922',
'plus': '#00d4ff', 'trash-2': '#f85149', 'pencil': '#79c0ff', 'star': '#d29922',
'map-pin': '#58a6ff', 'globe': '#58a6ff', 'send': '#00d4ff', 'download': '#79c0ff',
'upload': '#79c0ff', 'save': '#3fb950', 'refresh-cw': '#58a6ff', 'filter': '#8b9bb8',
'more-vertical': '#8b9bb8', 'x': '#8b9bb8', 'menu': '#8b9bb8', 'external-link': '#8b9bb8',
'log-out': '#8b9bb8', 'trending-up': '#3fb950', 'pie-chart': '#bc8cff', 'target': '#d29922',
'shield': '#3fb950', 'zap': '#bc8cff', 'database': '#58a6ff', 'cloud': '#79c0ff',
'hard-drive': '#bc8cff', 'monitor': '#58a6ff', 'git-branch': '#f778ba', 'package': '#d29922',
'terminal': '#79c0ff', 'code': '#58a6ff', 'layers': '#bc8cff', 'link': '#58a6ff',
'unlink': '#8b9bb8', 'copy': '#79c0ff', 'check': '#3fb950', 'alert-triangle': '#d29922',
'info': '#58a6ff', 'help-circle': '#8b9bb8', 'type': '#79c0ff', 'columns': '#bc8cff',
'table': '#58a6ff', 'list': '#79c0ff', 'grid': '#bc8cff', 'tag': '#d29922',
'calendar-days': '#79c0ff', 'mail-open': '#58a6ff', 'inbox': '#58a6ff', 'send-horizontal': '#00d4ff',
'paperclip': '#8b9bb8', 'users-round': '#3fb950', 'projector': '#bc8cff', 'kanban-square': '#d29922',
'bar-chart': '#bc8cff', 'line-chart': '#58a6ff', 'activity-square': '#3fb950',
'server': '#bc8cff', 'wifi': '#58a6ff', 'ethernet-port': '#58a6ff', 'router': '#bc8cff',
'switch': '#3fb950', 'firewall': '#f85149', 'storage': '#bc8cff', 'ups': '#d29922',
'pdu': '#79c0ff', 'cloud-upload': '#58a6ff', 'cloud-download': '#58a6ff',
'brain-circuit': '#bc8cff', 'message-square': '#58a6ff', 'bot': '#bc8cff',
'key': '#f778ba', 'shield-check-big': '#3fb950', 'lock': '#79c0ff', 'unlock': '#8b9bb8'
};
const SettingsContext = createContext({ settings: null, loading: true });
export function SettingsProvider({ children }) {
const [settings, setSettings] = useState(null);
useEffect(() => {
api.get('/settings').then(setSettings).catch(() => setSettings(null));
}, []);
return (
<SettingsContext.Provider value={{ settings, loading: !settings }}>
{children}
</SettingsContext.Provider>
);
}
export function useSettings() {
return useContext(SettingsContext);
}
// Geef de kleur voor een icoon terug (user-instelling of legacy default)
export function iconColor(name, settings) {
if (settings?.icon_colors && settings.icon_colors[name]) return settings.icon_colors[name];
return DEFAULT_ICON_COLORS[name] || null;
}
export function brandName(settings) {
return settings?.brand_name || 'Mek-Tech';
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mek-Tech — Consultancy Platform</title> <title>Mek-Tech — Consultancy Platform</title>
<script type="module" crossorigin src="/app/assets/index-CQEBt6sl.js"></script> <script type="module" crossorigin src="/app/assets/index-BeV6U3Lw.js"></script>
<link rel="stylesheet" crossorigin href="/app/assets/index-A3FsIOhP.css"> <link rel="stylesheet" crossorigin href="/app/assets/index-Bc_GiMLN.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>