feat: gekleurde iconen in sidebar + dashboard (legacy icon_colors)
This commit is contained in:
@@ -7,56 +7,58 @@ import {
|
||||
Search, Bell, LogOut, Menu, X, ExternalLink, ShieldCheck, PenTool, FileSearch
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../auth';
|
||||
import { useSettings, iconColor, brandName } from '../settings';
|
||||
import { api } from '../api';
|
||||
|
||||
const NAV = [
|
||||
{
|
||||
label: 'CRM',
|
||||
items: [
|
||||
{ to: '/', icon: LayoutDashboard, label: 'Dashboard', end: true },
|
||||
{ to: '/clients', icon: Users, label: 'Cliënten' },
|
||||
{ to: '/engagements', icon: Briefcase, label: 'Engagements' },
|
||||
{ to: '/tasks', icon: CheckCircle2, label: 'Taken' },
|
||||
{ to: '/calendar', icon: Calendar, label: 'Agenda' },
|
||||
{ to: '/email', icon: Mail, label: 'Email' }
|
||||
{ to: '/', icon: LayoutDashboard, iconName: 'layout-dashboard', label: 'Dashboard', end: true },
|
||||
{ to: '/clients', icon: Users, iconName: 'users', label: 'Cliënten' },
|
||||
{ to: '/engagements', icon: Briefcase, iconName: 'briefcase', label: 'Engagements' },
|
||||
{ to: '/tasks', icon: CheckCircle2, iconName: 'check-circle', label: 'Taken' },
|
||||
{ to: '/calendar', icon: Calendar, iconName: 'calendar', label: 'Agenda' },
|
||||
{ to: '/email', icon: Mail, iconName: 'mail', label: 'Email' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Delivery',
|
||||
items: [
|
||||
{ to: '/projects', icon: FolderKanban, label: 'Projecten' },
|
||||
{ to: '/time', icon: Clock, label: 'Uren' },
|
||||
{ to: '/invoices', icon: Receipt, label: 'Facturen' },
|
||||
{ to: '/finance', icon: Wallet, label: 'Finance' },
|
||||
{ to: '/reports', icon: BarChart3, label: 'Rapporten' }
|
||||
{ to: '/projects', icon: FolderKanban, iconName: 'folder-kanban', label: 'Projecten' },
|
||||
{ to: '/time', icon: Clock, iconName: 'clock', label: 'Uren' },
|
||||
{ to: '/invoices', icon: Receipt, iconName: 'receipt', label: 'Facturen' },
|
||||
{ to: '/finance', icon: Wallet, iconName: 'wallet', label: 'Finance' },
|
||||
{ to: '/reports', icon: BarChart3, iconName: 'bar-chart-3', label: 'Rapporten' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Data & Infra',
|
||||
items: [
|
||||
{ to: '/racks', icon: Server, label: 'Racks' },
|
||||
{ to: '/networking', icon: Share2, label: 'Netwerk' },
|
||||
{ to: '/diagrams', icon: Network, label: 'Diagrammen' },
|
||||
{ to: '/market', icon: Radar, label: 'Market Intel' },
|
||||
{ to: '/ai', icon: Sparkles, label: 'AI Studio' }
|
||||
{ to: '/racks', icon: Server, iconName: 'server', label: 'Racks' },
|
||||
{ to: '/networking', icon: Share2, iconName: 'share-2', label: 'Netwerk' },
|
||||
{ to: '/diagrams', icon: Network, iconName: 'network', label: 'Diagrammen' },
|
||||
{ to: '/market', icon: Radar, iconName: 'radar', label: 'Market Intel' },
|
||||
{ to: '/ai', icon: Sparkles, iconName: 'sparkles', label: 'AI Studio' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Platform',
|
||||
items: [
|
||||
{ to: '/settings', icon: Settings, label: 'Instellingen' }
|
||||
{ to: '/settings', icon: Settings, iconName: 'settings', label: 'Instellingen' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const LEGACY_LINKS = [
|
||||
{ href: '/quality', icon: ShieldCheck, label: 'Data Quality' },
|
||||
{ href: '/architecture/designer', icon: PenTool, label: 'Pro Designer' },
|
||||
{ href: '/assess', icon: FileSearch, label: 'Assessments' }
|
||||
{ href: '/quality', icon: ShieldCheck, iconName: 'shield-check', label: 'Data Quality' },
|
||||
{ href: '/architecture/designer', icon: PenTool, iconName: 'pen-tool', label: 'Pro Designer' },
|
||||
{ href: '/assess', icon: FileSearch, iconName: 'file-search', label: 'Assessments' }
|
||||
];
|
||||
|
||||
export default function Layout({ children }) {
|
||||
const { user, logout } = useAuth();
|
||||
const { settings } = useSettings();
|
||||
const navigate = useNavigate();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [unread, setUnread] = useState(0);
|
||||
@@ -79,10 +81,10 @@ export default function Layout({ children }) {
|
||||
<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="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 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>
|
||||
</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}
|
||||
</NavLink>
|
||||
))}
|
||||
@@ -127,7 +129,7 @@ export default function Layout({ children }) {
|
||||
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"
|
||||
>
|
||||
<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}
|
||||
<ExternalLink className="w-3 h-3 ml-auto opacity-50" />
|
||||
</a>
|
||||
|
||||
@@ -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 = {
|
||||
accent: 'text-accent bg-accent-soft',
|
||||
green: 'text-green bg-green/15',
|
||||
@@ -76,7 +76,7 @@ export function KpiCard({ icon: Icon, label, value, sub, color = 'accent', to })
|
||||
</div>
|
||||
{Icon && (
|
||||
<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>
|
||||
|
||||
@@ -3,14 +3,17 @@ import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import { AuthProvider } from './auth';
|
||||
import { SettingsProvider } from './settings';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter basename="/app">
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
<SettingsProvider>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</SettingsProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Users, Briefcase, CheckCircle2, Clock, Receipt, Wallet,
|
||||
AlertTriangle, Lightbulb, Activity, ArrowRight
|
||||
} from 'lucide-react';
|
||||
import { useSettings, iconColor } from '../settings';
|
||||
import {
|
||||
ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid,
|
||||
PieChart, Pie, Cell, Legend
|
||||
@@ -29,6 +30,7 @@ const tooltipStyle = {
|
||||
};
|
||||
|
||||
export default function Dashboard() {
|
||||
const { settings } = useSettings();
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
@@ -62,12 +64,12 @@ export default function Dashboard() {
|
||||
|
||||
{/* KPI's */}
|
||||
<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={Briefcase} label="Lopende engagements" value={stats.activeEngagements} color="blue" />
|
||||
<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={Clock} label="Uren totaal" value={fmt.hours(stats.totalHours)} sub={`${fmt.hours(stats.billableHours)} facturabel`} color="purple" />
|
||||
<KpiCard icon={Receipt} label="Openstaand" value={fmt.euro(stats.totalOutstanding)} color="yellow" />
|
||||
<KpiCard icon={Wallet} label="Betaald" value={fmt.euro(stats.totalPaid)} color="green" />
|
||||
<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" 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'} iconColor={iconColor('check-circle', settings)} />
|
||||
<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" iconColor={iconColor('receipt', settings)} />
|
||||
<KpiCard icon={Wallet} label="Betaald" value={fmt.euro(stats.totalPaid)} color="green" iconColor={iconColor('wallet', settings)} />
|
||||
</div>
|
||||
|
||||
{/* Grafieken */}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
Reference in New Issue
Block a user