303 lines
36 KiB
Plaintext
303 lines
36 KiB
Plaintext
<!DOCTYPE html><html lang="nl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Dashboard — Mek-Tech Enterprise</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"><link rel="stylesheet" href="/css/style-v2.css"><link rel="stylesheet" href="/css/dynamic.css"><link rel="icon" href="/img/favicon.svg"><script src="https://unpkg.com/lucide@latest"></script><script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
:root{--sb-w:210px;--hdr-h:50px}
|
|
html,body{height:100%;margin:0;padding:0;scroll-behavior:smooth}
|
|
body{background:var(--bg);font-family:'Inter',-apple-system,sans-serif}
|
|
.app-layout{display:flex;min-height:100vh;padding-top:var(--hdr-h)}
|
|
|
|
.sidebar{position:fixed;top:var(--hdr-h);left:0;width:var(--sb-w);height:calc(100vh - var(--hdr-h));background:var(--bg-nav);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:50;overflow-y:auto;transition:all .3s ease}
|
|
.main-area{flex:1;margin-left:var(--sb-w);padding:1.5rem 2rem 3rem;max-width:1300px}
|
|
|
|
.sidebar .sb-brand{padding:1rem 1.1rem;border-bottom:1px solid var(--border);background:linear-gradient(135deg,transparent,rgba(88,166,255,.03))}
|
|
.sidebar .sb-brand h2{font-size:.82rem;font-weight:700;color:var(--text);letter-spacing:-.2px;display:flex;align-items:center;gap:.4rem}
|
|
.sidebar .sb-brand h2 .brand-dot{width:7px;height:7px;border-radius:50%;background:var(--accent);animation:pulse-glow 2s ease-in-out infinite}
|
|
.sidebar .sb-brand p{font-size:.6rem;color:var(--text-dim);margin-top:1px}
|
|
.sb-nav{flex:1;padding:.4rem 0}
|
|
.sb-sep{font-size:.55rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.6px;padding:.6rem 1rem .2rem;font-weight:600}
|
|
.sb-item{display:flex;align-items:center;gap:.55rem;padding:.5rem .9rem;margin:1px .45rem;border-radius:8px;cursor:pointer;font-size:.78rem;color:var(--text-muted);transition:all .18s cubic-bezier(.4,0,.2,1);border:none;background:none;width:calc(100% - .9rem);text-align:left;font-family:inherit;position:relative;overflow:hidden}
|
|
.sb-item::after{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent);border-radius:0 4px 4px 0;transform:translateX(-3px);transition:transform .2s ease;opacity:0}
|
|
.sb-item:hover{color:var(--text);background:var(--border-light)}
|
|
.sb-item.active{color:var(--accent);background:rgba(88,166,255,.08);font-weight:600}
|
|
.sb-item.active::after{transform:translateX(0);opacity:1}
|
|
.sb-item .sb-icon{width:20px;text-align:center;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
|
.sb-item .sb-icon svg{width:17px;height:17px;transition:transform .2s ease}
|
|
.sb-item:hover .sb-icon svg{transform:scale(1.1)}
|
|
.sb-item .sb-badge{margin-left:auto;background:var(--border-light);padding:.05rem .5rem;border-radius:999px;font-size:.58rem;font-weight:600}
|
|
.sb-item .sb-badge.warn{background:var(--red-bg);color:var(--red);animation:pulse 2s ease-in-out infinite}
|
|
.sb-item .sb-badge.ok{background:var(--green-bg);color:var(--green)}
|
|
|
|
.page-hdr{margin-bottom:1.5rem}.page-hdr h1{font-size:1.5rem;color:var(--text);font-weight:800;letter-spacing:-.4px}.page-hdr p{font-size:.78rem;color:var(--text-muted);margin-top:.1rem}
|
|
|
|
/* KPIs premium */
|
|
.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.65rem;margin-bottom:1.25rem}
|
|
.kpi-card{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:.9rem 1.1rem;display:flex;flex-direction:column;gap:.15rem;position:relative;overflow:hidden;transition:all .25s cubic-bezier(.4,0,.2,1);cursor:pointer}
|
|
.kpi-card:hover{transform:translateY(-2px);box-shadow:0 6px 25px rgba(0,0,0,.1),0 0 0 1px rgba(88,166,255,.15);border-color:rgba(88,166,255,.3)}
|
|
.kpi-card:hover .kpi-icon svg{opacity:.15;transform:scale(1.1)}
|
|
.kpi-card:active{transform:translateY(0)}
|
|
.kpi-card .kpi-num{font-size:1.7rem;font-weight:800;color:var(--text);line-height:1.1;letter-spacing:-.5px;transition:color .3s}
|
|
.kpi-card .kpi-label{font-size:.64rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;font-weight:600}
|
|
.kpi-card .kpi-sub{font-size:.62rem;color:var(--text-dim)}
|
|
.kpi-card .kpi-icon{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);transition:all .25s ease}
|
|
.kpi-card .kpi-icon svg{width:2.2rem;height:2.2rem;opacity:.06;transition:all .25s ease}
|
|
.kpi-card.accent .kpi-num{color:var(--accent)}.kpi-card.green .kpi-num{color:var(--green)}.kpi-card.orange .kpi-num{color:var(--orange)}.kpi-card.purple .kpi-num{color:var(--purple)}.kpi-card.red .kpi-num{color:var(--red)}.kpi-card.cyan .kpi-num{color:var(--cyan)}
|
|
|
|
.kpi-blink{position:relative}
|
|
.kpi-blink::after{content:'';position:absolute;top:.5rem;right:.5rem;width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse-glow 2s ease-in-out infinite}
|
|
|
|
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:.85rem}@media(max-width:768px){.grid-2{grid-template-columns:1fr}}
|
|
.card{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:1rem 1.1rem;transition:all .2s ease}
|
|
.card:hover{border-color:rgba(88,166,255,.15)}
|
|
.card-title{font-size:.66rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:.7rem;display:flex;justify-content:space-between;align-items:center;font-weight:600}
|
|
.card-title .ct-btn{font-size:.6rem;color:var(--accent);text-decoration:none;border:none;background:none;cursor:pointer;font-family:inherit;text-transform:none;letter-spacing:0;font-weight:500;transition:opacity .2s}
|
|
.card-title .ct-btn:hover{opacity:.8}
|
|
.tab-panel{display:none}.tab-panel.active{display:block}
|
|
|
|
/* Client grid */
|
|
.client-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:.7rem}
|
|
.client-card{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:1.1rem;text-decoration:none;color:inherit;transition:all .2s cubic-bezier(.4,0,.2,1);position:relative;overflow:hidden}
|
|
.client-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--accent),var(--purple));opacity:0;transition:opacity .25s}
|
|
.client-card:hover{border-color:rgba(88,166,255,.3);transform:translateY(-2px);box-shadow:0 4px 20px rgba(0,0,0,.08)}
|
|
.client-card:hover::before{opacity:1}
|
|
.client-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
|
|
.client-card-header h4{font-size:.9rem;color:var(--text);font-weight:600}
|
|
.client-meta{font-size:.78rem;color:var(--text-muted)}.client-meta.warn{color:var(--orange)}.client-meta.small{font-size:.68rem}
|
|
.status-badge{display:inline-block;padding:.15rem .55rem;border-radius:999px;font-size:.62rem;font-weight:600;border:1px solid transparent}
|
|
.status-badge.lead{border-color:var(--accent);color:var(--accent)}
|
|
.status-badge.active{border-color:var(--green);color:var(--green)}
|
|
.status-badge.paused{border-color:var(--orange);color:var(--orange)}
|
|
.status-badge.archived{border-color:var(--text-dim);color:var(--text-dim)}
|
|
.status-badge.planned,.status-badge.sent{border-color:var(--accent);color:var(--accent)}
|
|
.status-badge.in_progress{border-color:var(--green);color:var(--green)}
|
|
.status-badge.completed,.status-badge.paid{border-color:var(--purple);color:var(--purple)}
|
|
.status-badge.on_hold{border-color:var(--orange);color:var(--orange)}
|
|
.status-badge.todo{border-color:var(--text-dim);color:var(--text-dim)}
|
|
.status-badge.done{border-color:var(--green);color:var(--green)}
|
|
.status-badge.overdue{border-color:var(--red);color:var(--red)}
|
|
.status-badge.draft{border-color:var(--text-dim);color:var(--text-dim)}
|
|
.status-badge.cancelled{border-color:var(--orange);color:var(--orange)}
|
|
|
|
.engagement-type{display:inline-block;padding:.1rem .45rem;border-radius:4px;font-size:.62rem;font-weight:600}
|
|
.engagement-type.assessment{background:var(--cyan-bg);color:var(--cyan)}
|
|
.engagement-type.architecture{background:var(--purple-bg);color:var(--purple)}
|
|
.engagement-type.implementation{background:var(--green-bg);color:var(--green)}
|
|
.engagement-type.monitoring{background:var(--orange-bg);color:var(--orange)}
|
|
.engagement-type.consulting{background:var(--pink-bg);color:var(--pink)}
|
|
.engagement-type.training{background:var(--red-bg);color:var(--red)}
|
|
|
|
.data-table{width:100%;border-collapse:collapse;border-radius:8px;overflow:hidden}
|
|
.data-table th{text-align:left;padding:.55rem .8rem;font-size:.66rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--border);background:var(--bg);font-weight:600}
|
|
.data-table td{padding:.55rem .8rem;border-bottom:1px solid var(--border-light);font-size:.78rem}
|
|
.data-table tr{transition:background .15s}
|
|
.data-table tr:hover td{background:var(--border-light)}
|
|
.link{color:var(--accent);text-decoration:none;font-weight:500}.link:hover{text-decoration:underline}
|
|
|
|
.inline-form{display:flex;gap:.35rem;flex-wrap:wrap;align-items:flex-end;margin-bottom:.85rem}
|
|
.inline-form input,.inline-form select{padding:.38rem .55rem;font-size:.74rem;width:auto;min-width:115px}
|
|
.inline-form .btn{padding:.38rem .7rem;font-size:.72rem}
|
|
|
|
.advice-list{display:flex;flex-direction:column;gap:.4rem}
|
|
.advice-card{display:flex;align-items:flex-start;gap:.5rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:.6rem .8rem;border-left:4px solid var(--border);transition:all .2s ease}
|
|
.advice-card:hover{border-color:rgba(88,166,255,.2)}
|
|
.advice-card.critical{border-left-color:var(--red)}.advice-card.high{border-left-color:var(--orange)}.advice-card.medium{border-left-color:var(--accent)}.advice-card.low{border-left-color:var(--green)}
|
|
.advice-prio{font-size:.55rem;font-weight:700;text-transform:uppercase;padding:.08rem .38rem;border-radius:4px;white-space:nowrap}
|
|
.advice-prio.critical{background:var(--red-bg);color:var(--red)}.advice-prio.high{background:var(--orange-bg);color:var(--orange)}.advice-prio.medium{background:var(--cyan-bg);color:var(--cyan)}.advice-prio.low{background:var(--green-bg);color:var(--green)}
|
|
.advice-body{flex:1}.advice-body p{font-size:.78rem;color:var(--text);line-height:1.4}
|
|
|
|
.task-list{display:flex;flex-direction:column;gap:.4rem}
|
|
.task-item{display:flex;align-items:center;gap:.5rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:.55rem .8rem;transition:all .15s ease}
|
|
.task-item:hover{border-color:rgba(88,166,255,.15)}
|
|
.task-item.done{opacity:.5}.task-item.done .task-title{text-decoration:line-through}
|
|
.task-body{flex:1;display:flex;flex-direction:column;gap:.1rem}.task-title{font-size:.82rem;color:var(--text);font-weight:500}.task-desc{font-size:.72rem;color:var(--text-muted)}.task-meta{font-size:.68rem;color:var(--accent)}
|
|
.task-prio{font-size:.55rem;font-weight:700;text-transform:uppercase;padding:.08rem .38rem;border-radius:4px}
|
|
.task-prio.critical{background:var(--red-bg);color:var(--red)}.task-prio.high{background:var(--orange-bg);color:var(--orange)}.task-prio.medium{background:var(--cyan-bg);color:var(--cyan)}.task-prio.low{background:var(--green-bg);color:var(--green)}
|
|
.task-due{font-size:.68rem;color:var(--text-muted)}.task-due.overdue{color:var(--red);font-weight:600}
|
|
|
|
.rm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:.5rem}
|
|
.rm-card{display:flex;gap:.6rem;padding:.65rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;text-decoration:none;color:inherit;transition:all .2s cubic-bezier(.4,0,.2,1)}
|
|
.rm-card:hover{border-color:rgba(88,166,255,.3);transform:translateY(-1px);box-shadow:0 4px 16px rgba(0,0,0,.06)}
|
|
.rm-vis{width:14px;display:flex;flex-direction:column;gap:1px;flex-shrink:0}
|
|
.rm-u{width:100%;height:4px;border-radius:1px;background:var(--border)}.rm-u.filled{background:var(--accent)}
|
|
.nd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.5rem}
|
|
.nd-card{display:flex;align-items:center;gap:.5rem;padding:.6rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;text-decoration:none;color:inherit;transition:all .2s ease}
|
|
.nd-card:hover{border-color:rgba(88,166,255,.3);box-shadow:0 2px 12px rgba(0,0,0,.05)}
|
|
.nd-icon{width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:.6rem;font-weight:700;flex-shrink:0}
|
|
|
|
.quick-actions{display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:.4rem}
|
|
.qa-btn{display:flex;flex-direction:column;align-items:center;gap:.25rem;padding:.6rem .5rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);text-decoration:none;font-size:.63rem;font-weight:500;transition:all .2s ease;cursor:pointer}
|
|
.qa-btn:hover{border-color:var(--accent);color:var(--accent);background:rgba(88,166,255,.04);transform:translateY(-2px);box-shadow:0 4px 12px rgba(88,166,255,.08)}
|
|
.qa-btn .qa-icon svg{width:1.2rem;height:1.2rem;transition:transform .2s ease}
|
|
.qa-btn:hover .qa-icon svg{transform:scale(1.15)}
|
|
.empty-state-s{margin:.75rem 0;text-align:center;color:var(--text-dim);font-size:.75rem}
|
|
|
|
.progress-mini{height:3px;background:var(--border-light);border-radius:2px;overflow:hidden;margin-bottom:.05rem}
|
|
.progress-mini .pf{height:100%;border-radius:2px;transition:width .8s cubic-bezier(.4,0,.2,1)}
|
|
.timeline{display:flex;flex-direction:column;gap:.4rem}
|
|
.tl-item{display:flex;gap:.5rem;padding:.4rem 0;border-bottom:1px solid var(--border-light);font-size:.7rem;transition:all .15s}
|
|
.tl-item:hover{padding-left:.25rem}
|
|
.tl-item:last-child{border-bottom:none}.tl-item .tl-dot{width:8px;height:8px;border-radius:50%;margin-top:.3rem;flex-shrink:0}.tl-item .tl-body{flex:1}.tl-item .tl-title{color:var(--text);font-weight:500}.tl-item .tl-date{color:var(--text-dim);font-size:.64rem;margin-top:.05rem}
|
|
|
|
.alert{background:var(--green-bg);border:1px solid var(--green);color:var(--green);padding:.4rem .7rem;border-radius:8px;font-size:.76rem;margin-bottom:.75rem}
|
|
.alert-error{background:var(--red-bg);border:1px solid var(--red);color:var(--red);padding:.4rem .7rem;border-radius:8px;font-size:.76rem;margin-bottom:.75rem}
|
|
|
|
.time-tools{display:flex;gap:.5rem;margin-bottom:.85rem;flex-wrap:wrap}.time-tools .tt-btn{display:flex;align-items:center;gap:.35rem;padding:.42rem .75rem;border-radius:8px;font-size:.72rem;font-weight:500;cursor:pointer;border:1px solid var(--border);background:var(--bg-card);color:var(--text-muted);text-decoration:none;transition:all .2s ease;font-family:inherit}
|
|
.time-tools .tt-btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:0 2px 10px rgba(88,166,255,.06)}.time-tools .tt-btn svg{width:14px;height:14px}
|
|
|
|
.nav-link.logout{color:var(--red)}.nav-link.logout:hover{background:var(--red-bg);color:var(--red)}
|
|
.nav-link:hover{color:var(--text);background:var(--border-light)}
|
|
.nav-link.active{background:var(--border-light);font-weight:600}
|
|
</style></head><body>
|
|
<nav class="navbar"><div class="nav-inner" style="gap:.5rem">
|
|
<a href="/" class="nav-brand"><img src="/img/logo.svg" class="nav-logo" alt="Mek-Tech"></a>
|
|
<div class="nav-links">
|
|
<a href="/" class="nav-link active">Dashboard</a><a href="/clients" class="nav-link">Cliënten</a><a href="/engagements" class="nav-link">Engagements</a><a href="/finance" class="nav-link">Financiën</a><a href="/projects" class="nav-link">Projecten</a><a href="/market" class="nav-link">Markt</a><a href="/tasks" class="nav-link">Taken</a><a href="/reports" class="nav-link">Reports</a><a href="/quality" class="nav-link">Data</a><a href="/settings" class="nav-link">Instellingen</a><a href="/auth/logout" class="nav-link logout">Uitloggen</a>
|
|
</div>
|
|
<button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode" onclick="toggleTheme()"></button>
|
|
</div></nav>
|
|
|
|
<div class="app-layout">
|
|
<aside class="sidebar"><div class="sb-brand"><h2><span class="brand-dot"></span> Mek-Tech Console</h2><p>Enterprise Platform</p></div><nav class="sb-nav">
|
|
<div class="sb-sep">Kern</div>
|
|
<button class="sb-item active" data-tab="overview"><span class="sb-icon"><i data-lucide="layout-dashboard"></i></span>Overzicht</button>
|
|
<button class="sb-item" data-tab="clients"><span class="sb-icon"><i data-lucide="users"></i></span>Cliënten <span class="sb-badge"><%= clients.length %></span></button>
|
|
<button class="sb-item" data-tab="engagements"><span class="sb-icon"><i data-lucide="briefcase"></i></span>Engagements <span class="sb-badge"><%= stats.totalEngagements %></span></button>
|
|
<button class="sb-item" data-tab="finance"><span class="sb-icon"><i data-lucide="wallet"></i></span>Financiën</button>
|
|
<div class="sb-sep">Operaties</div>
|
|
<button class="sb-item" data-tab="infra"><span class="sb-icon"><i data-lucide="server"></i></span>Infrastructuur</button>
|
|
<button class="sb-item" data-tab="tasks"><span class="sb-icon"><i data-lucide="check-circle"></i></span>Taken <% if (overdueTasks > 0) { %><span class="sb-badge warn"><%= overdueTasks %></span><% } %></button>
|
|
<button class="sb-item" data-tab="advice"><span class="sb-icon"><i data-lucide="lightbulb"></i></span>Advies <% if (activeAdvice.length > 0) { %><span class="sb-badge ok"><%= activeAdvice.length %></span><% } %></button>
|
|
<div class="sb-sep">Intelligence</div>
|
|
<button class="sb-item" data-tab="market"><span class="sb-icon"><i data-lucide="target"></i></span>Markt Intel</button>
|
|
<button class="sb-item" data-tab="calendar"><span class="sb-icon"><i data-lucide="calendar"></i></span>Agenda</button>
|
|
<button class="sb-item" data-tab="data"><span class="sb-icon"><i data-lucide="shield-check"></i></span>Data Quality</button>
|
|
<div class="sb-sep">Tools</div>
|
|
<button class="sb-item" data-tab="email"><span class="sb-icon"><i data-lucide="mail"></i></span>Email</button>
|
|
<button class="sb-item" data-tab="activity"><span class="sb-icon"><i data-lucide="clock"></i></span>Activiteit</button>
|
|
</nav></aside>
|
|
|
|
<div class="main-area">
|
|
|
|
<!-- ===== OVERVIEW ===== -->
|
|
<div class="tab-panel active" id="panel-overview">
|
|
<div class="page-hdr fade-in-up"><h1><span class="gradient-text">Enterprise Dashboard</span></h1><p>Real-time consultancy intelligence platform</p></div>
|
|
<div class="kpi-row">
|
|
<div class="kpi-card accent kpi-blink" onclick="switchTab('clients')" style="cursor:pointer"><div class="kpi-icon"><i data-lucide="building-2"></i></div><div class="kpi-num"><%= stats.total %></div><div class="kpi-label">Cliënten</div><div class="kpi-sub"><%= stats.active %> actief · <%= stats.leads %> leads</div></div>
|
|
<div class="kpi-card green" onclick="switchTab('engagements')" style="cursor:pointer"><div class="kpi-icon"><i data-lucide="briefcase"></i></div><div class="kpi-num"><%= stats.activeEngagements %></div><div class="kpi-label">Actieve Engagements</div><div class="kpi-sub">van <%= stats.totalEngagements %> totaal</div></div>
|
|
<div class="kpi-card purple" onclick="switchTab('infra')" style="cursor:pointer"><div class="kpi-icon"><i data-lucide="layers"></i></div><div class="kpi-num"><%= totalRackDevices + totalNetworkDevices %></div><div class="kpi-label">Infra Devices</div><div class="kpi-sub"><%= totalRacks %> racks · <%= totalDiagrams %> diagrammen</div></div>
|
|
<div class="kpi-card <%= overdueTasks > 0 ? 'red' : 'green' %>" onclick="switchTab('tasks')" style="cursor:pointer"><div class="kpi-icon"><i data-lucide="check-circle"></i></div><div class="kpi-num"><%= overdueTasks %></div><div class="kpi-label">Overdue Taken</div><div class="kpi-sub"><%= completedTasks %>/<%= totalTasks %> voltooid</div></div>
|
|
<div class="kpi-card cyan" onclick="window.location='/quality'" style="cursor:pointer"><div class="kpi-icon"><i data-lucide="shield-check"></i></div><div class="kpi-num"><%= assessmentsCount %></div><div class="kpi-label">Assessments</div><div class="kpi-sub">Data quality engine</div></div>
|
|
</div>
|
|
<div class="grid-2">
|
|
<div class="card card-hover"><div class="card-title">Cliënten Status</div>
|
|
<% var sts={active:'Actief',lead:'Lead',paused:'Gepauzeerd',archived:'Gearchiveerd'};var clrs={active:'var(--green)',lead:'var(--accent)',paused:'var(--orange)',archived:'var(--text-dim)'};var maxC=Math.max(stats.active,stats.leads,stats.paused,stats.archived,1);
|
|
for(var k in sts){var cnt=stats[k]||0;%><div style="display:flex;justify-content:space-between;font-size:.74rem;margin:.25rem 0"><span style="color:var(--text-muted);font-weight:500"><%= sts[k] %></span><span style="color:var(--text);font-weight:600"><%= cnt %></span></div><div class="progress-mini" style="margin-bottom:.1rem"><div class="pf" style="width:<%= (cnt/maxC*100) %>%background:<%= clrs[k] %>"></div></div><% } %>
|
|
</div>
|
|
<div class="card card-hover"><div class="card-title">Engagements per Type</div>
|
|
<% var tCol={assessment:'var(--cyan)',architecture:'var(--purple)',implementation:'var(--green)',monitoring:'var(--orange)',consulting:'var(--pink)',training:'var(--red)'};var maxE=Math.max(...engagementByType.map(function(e){return e.cnt}),1);
|
|
engagementByType.forEach(function(e){ %><div style="display:flex;justify-content:space-between;font-size:.74rem;margin:.2rem 0"><span style="color:var(--text-muted);font-weight:500"><%= e.type %></span><span style="color:var(--text);font-weight:600"><%= e.cnt %></span></div><div class="progress-mini" style="margin-bottom:.05rem"><div class="pf" style="width:<%= (e.cnt/maxE*100) %>%background:<%= tCol[e.type]||'var(--accent)' %>"></div></div><% }) %>
|
|
</div></div>
|
|
<div class="grid-2" style="margin-top:.85rem">
|
|
<div class="card card-hover"><div class="card-title">Recente Assessments</div>
|
|
<% if(recentAssessments.length===0){ %><div class="empty-state-s">Nog geen assessments</div><% }else{ %><div class="timeline"><% recentAssessments.forEach(function(a){ %><div class="tl-item"><div class="tl-dot" style="background:var(--purple)"></div><div class="tl-body"><div class="tl-title">Assessment voor <strong><%= a.client_name %></strong></div><div class="tl-date"><%= new Date(a.created_at).toLocaleDateString('nl-NL',{hour:'2-digit',minute:'2-digit'}) %></div></div></div><% }) %></div><% } %>
|
|
</div>
|
|
<div class="card card-hover"><div class="card-title">Quick Actions</div>
|
|
<div class="quick-actions">
|
|
<a href="/clients/new" class="qa-btn"><span class="qa-icon"><i data-lucide="user-plus"></i></span>Cliënt</a>
|
|
<a href="/engagements/new" class="qa-btn"><span class="qa-icon"><i data-lucide="briefcase"></i></span>Engagement</a>
|
|
<a href="/time" class="qa-btn"><span class="qa-icon"><i data-lucide="clock"></i></span>Uren</a>
|
|
<a href="/invoices/new" class="qa-btn"><span class="qa-icon"><i data-lucide="receipt"></i></span>Factuur</a>
|
|
<a href="/quality" class="qa-btn"><span class="qa-icon"><i data-lucide="shield-check"></i></span>Data</a>
|
|
<a href="/assess" class="qa-btn"><span class="qa-icon"><i data-lucide="clipboard-list"></i></span>Assess</a>
|
|
</div></div></div></div>
|
|
|
|
<!-- ===== CLIENTS ===== -->
|
|
<div class="tab-panel" id="panel-clients"><div class="page-hdr fade-in-up"><h1>Cliënten</h1><p><%= stats.total %> totaal · <%= stats.active %> actief</p></div>
|
|
<% if (clients.length === 0) { %><div class="empty-state-s">Nog geen cliënten</div><% } else { %><div class="client-grid">
|
|
<% clients.forEach(function(c){ %><a href="/clients/<%= c.id %>" class="client-card fade-in-up"><div class="client-card-header"><h4><%= c.name %></h4><span class="status-badge <%= c.status %>"><%= c.status %></span></div><% if(c.industry){ %><p class="client-meta">Sector: <%= c.industry %></p><% } %><p class="client-meta"><%= c.engagement_count %> engagement(s) · <%= c.active_engagements %> actief</p><% if(c.last_assessment){ %><p class="client-meta small">Assessment: <%= new Date(c.last_assessment).toLocaleDateString('nl-NL') %></p><% }else{ %><p class="client-meta small warn">Nog geen assessment</p><% } %></a><% }) %>
|
|
</div><% } %></div>
|
|
|
|
<!-- ===== ENGAGEMENTS ===== -->
|
|
<div class="tab-panel" id="panel-engagements"><div class="page-hdr fade-in-up"><h1>Engagements</h1><p><%= stats.totalEngagements %> totaal · <%= stats.activeEngagements %> actief</p></div>
|
|
<div class="grid-2" style="margin-bottom:.85rem">
|
|
<div class="card card-hover"><div class="card-title">Per Type</div><div style="height:200px"><canvas id="engagementTypeChart"></canvas></div><% engagementByType.forEach(function(e){ %><div style="display:flex;justify-content:space-between;padding:.25rem 0;font-size:.74rem;border-bottom:1px solid var(--border-light)"><span style="color:var(--text-muted)"><%= e.type %></span><span style="color:var(--text);font-weight:600"><%= e.cnt %></span></div><% }) %></div>
|
|
<div class="card card-hover"><div class="card-title">Per Status</div><div style="height:200px"><canvas id="engagementStatusChart"></canvas></div><% engagementByStatus.forEach(function(e){ %><div style="display:flex;justify-content:space-between;padding:.25rem 0;font-size:.74rem;border-bottom:1px solid var(--border-light)"><span style="color:var(--text-muted)"><%= e.status.replace('_',' ') %></span><span style="color:var(--text);font-weight:600"><%= e.cnt %></span></div><% }) %></div></div>
|
|
<div class="card"><div class="card-title">Alle Engagements</div>
|
|
<% if(allEngagements.length===0){ %><div class="empty-state-s">Nog geen engagements</div><% }else{ %>
|
|
<table class="data-table"><thead><tr><th>Titel</th><th>Cliënt</th><th>Type</th><th>Status</th><th></th></tr></thead><tbody><% allEngagements.forEach(function(e){ %><tr><td><a href="/engagements/<%= e.id %>" class="link"><%= e.title %></a></td><td><%= e.client_name %></td><td><span class="engagement-type <%= e.type %>"><%= e.type %></span></td><td><span class="status-badge <%= e.status %>"><%= e.status %></span></td><td><a href="/engagements/<%= e.id %>" class="btn-small">Details</a></td></tr><% }) %></tbody></table><% } %>
|
|
</div></div>
|
|
|
|
<!-- ===== FINANCE ===== -->
|
|
<div class="tab-panel" id="panel-finance"><div class="page-hdr fade-in-up"><h1>Financiën</h1><p>Urenregistratie en facturatie</p></div>
|
|
<div class="kpi-row"><div class="kpi-card fade-in-up"><div class="kpi-num"><%= totalHours.toFixed(1) %></div><div class="kpi-label">Totaal uren</div><div class="kpi-sub"><%= billableHours.toFixed(1) %> facturabel</div></div><div class="kpi-card green fade-in-up"><div class="kpi-num">€<%= totalInvoiced.toFixed(0) %></div><div class="kpi-label">Gefactureerd</div><div class="kpi-sub">€<%= totalPaid.toFixed(0) %> betaald</div></div><div class="kpi-card orange fade-in-up"><div class="kpi-num">€<%= totalOutstanding.toFixed(0) %></div><div class="kpi-label">Openstaand</div></div></div>
|
|
<div class="time-tools"><a href="/time" class="tt-btn"><i data-lucide="plus"></i> Uren registreren</a><a href="/invoices/new" class="tt-btn"><i data-lucide="plus"></i> Factuur maken</a></div>
|
|
<div class="grid-2"><div class="card card-hover"><div class="card-title">Revenue Overview</div><div style="height:200px"><canvas id="revenueChart"></canvas></div>
|
|
<% if(timeEntries.length===0){ %><div class="empty-state-s">Nog geen uren</div><% }else{ %><table class="data-table"><thead><tr><th>Datum</th><th>Uren</th><th>Cliënt</th><th>Omschrijving</th></tr></thead><tbody><% timeEntries.forEach(function(t){ %><tr><td><%= t.date %></td><td><strong><%= t.hours %></strong></td><td><%= t.client_name||'-' %></td><td style="color:var(--text-muted);font-size:.7rem"><%= t.description||'' %></td></tr><% }) %></tbody></table><% } %></div>
|
|
<div class="card card-hover"><div class="card-title">Recente facturen</div>
|
|
<% if(invoices.length===0){ %><div class="empty-state-s">Nog geen facturen</div><% }else{ %><table class="data-table"><thead><tr><th>Nr</th><th>Cliënt</th><th>Totaal</th><th>Status</th></tr></thead><tbody><% invoices.forEach(function(i){ %><tr><td><a href="/invoices/<%= i.id %>" class="link"><%= i.number %></a></td><td><%= i.client_name %></td><td>€<%= (i.total||0).toFixed(2) %></td><td><span class="status-badge <%= i.status %>"><%= i.status %></span></td></tr><% }) %></tbody></table><% } %></div></div></div>
|
|
|
|
<!-- ===== INFRA ===== -->
|
|
<div class="tab-panel" id="panel-infra"><div class="page-hdr fade-in-up"><h1>Infrastructuur</h1><p>Racks, netwerk & architectuur</p></div>
|
|
<div class="kpi-row"><div class="kpi-card cyan"><div class="kpi-num"><%= totalRacks %></div><div class="kpi-label">Racks</div><div class="kpi-sub"><%= totalRackDevices %> devices</div></div><div class="kpi-card purple"><div class="kpi-num"><%= totalNetworkDevices %></div><div class="kpi-label">Netwerk</div></div><div class="kpi-card green"><div class="kpi-num"><%= totalDiagrams %></div><div class="kpi-label">Diagrammen</div></div></div>
|
|
<div class="grid-2"><div class="card card-hover"><div class="card-title">Server Racks</div>
|
|
<% if(racks.length===0){ %><div class="empty-state-s">Nog geen racks</div><% }else{ %><% racks.forEach(function(r){ %><a href="/racks/<%= r.id %>" class="rm-card"><div class="rm-vis"><% for(var i=0;i<Math.min(r.total_units||42,12);i++){%><div class="rm-u <%= i < (r.device_count||0) ? 'filled' : '' %>"></div><% } %></div><div><h4 style="font-size:.78rem;color:var(--text)"><%= r.name %></h4><p style="font-size:.66rem;color:var(--text-muted)"><%= r.client_name %> · <%= r.device_count||0 %>/<%= r.total_units||42 %>U</p></div></a><% }) %><% } %></div>
|
|
<div class="card card-hover"><div class="card-title">Netwerk Devices</div>
|
|
<% if(netDevices.length===0){ %><div class="empty-state-s">Nog geen devices</div><% }else{ %><% netDevices.forEach(function(d){ %><a href="/networking/devices/<%= d.id %>" class="nd-card"><div class="nd-icon" style="background:<%= d.device_type==='switch'?'rgba(188,140,255,.15)':d.device_type==='router'?'rgba(88,166,255,.15)':d.device_type==='firewall'?'rgba(248,81,73,.15)':'rgba(63,185,80,.15)' %>;color:<%= d.device_type==='switch'?'#bc8cff':d.device_type==='router'?'#58a6ff':d.device_type==='firewall'?'#f85149':'#3fb950' %>"><%= d.device_type.charAt(0).toUpperCase() %></div><div style="font-size:.7rem"><strong><%= d.name %></strong><br><span style="color:var(--text-muted);font-size:.63rem"><%= d.model||'' %> · <%= d.client_name %></span></div></a><% }) %><% } %></div></div></div>
|
|
|
|
<!-- ===== TASKS ===== -->
|
|
<div class="tab-panel" id="panel-tasks"><div class="page-hdr fade-in-up"><h1>Taken</h1><p><%= completedTasks %>/<%= totalTasks %> voltooid <% if(overdueTasks>0){ %><span style="color:var(--red);font-weight:600"><%= overdueTasks %> over tijd</span><% } %></p></div>
|
|
<div class="kpi-row"><div class="grid-2"><div class="card card-hover"><div class="card-title">Tasks Overview</div><div style="height:200px"><canvas id="taskChart"></canvas></div></div><div class="card card-hover"><div class="card-title">Clients by Industry</div><div style="height:200px"><canvas id="industryChart"></canvas></div></div></div><div class="kpi-card green"><div class="kpi-num"><%= completedTasks %></div><div class="kpi-label">Voltooid</div></div><div class="kpi-card accent"><div class="kpi-num"><%= totalTasks-completedTasks %></div><div class="kpi-label">Open</div></div><div class="kpi-card red"><div class="kpi-num"><%= overdueTasks %></div><div class="kpi-label">Over tijd</div></div></div>
|
|
<% if(allTasks.length===0){ %><div class="empty-state-s">Nog geen taken</div><% }else{ %><div class="task-list"><% allTasks.forEach(function(t){ var io=t.status!=='done'&&t.status!=='cancelled'&&t.due_date&&new Date(t.due_date)<new Date(); %><div class="task-item <%= t.status==='done'?'done':'' %> fade-in-up"><span class="task-prio <%= t.priority %>"><%= t.priority %></span><div class="task-body"><span class="task-title"><a href="/engagements/<%= t.eid %>" class="link" style="color:inherit;text-decoration:none"><%= t.title %></a></span><span class="task-desc"><%= t.client_name %> · <%= t.engagement_title %></span></div><span class="status-badge <%= t.status %>"><%= t.status.replace('_',' ') %></span><% if(t.due_date){ %><span class="task-due <%= io?'overdue':'' %>"><%= new Date(t.due_date).toLocaleDateString('nl-NL') %></span><% } %></div><% }) %></div><% } %></div>
|
|
|
|
<!-- ===== ADVICE ===== -->
|
|
<div class="tab-panel" id="panel-advice"><div class="page-hdr fade-in-up"><h1>Advies & Acties</h1><p>Automatisch advies op basis van cliënt data</p></div>
|
|
<% if(activeAdvice.length===0){ %><div class="empty-state-s">Geen advies items</div><% }else{ %><div class="advice-list"><% activeAdvice.forEach(function(a){ %><div class="advice-card <%= a.priority %> fade-in-up"><div class="advice-prio <%= a.priority %>"><%= a.priority %></div><div class="advice-body"><p><%= a.advice_text %></p></div></div><% }) %></div><% } %></div>
|
|
|
|
<!-- ===== MARKET ===== -->
|
|
<div class="tab-panel" id="panel-market"><div class="page-hdr fade-in-up"><h1>Markt Intelligence</h1><p>Datacenters & prospects — vind je volgende klant</p></div>
|
|
<div class="quick-actions"><a href="/market" class="qa-btn"><span class="qa-icon"><i data-lucide="target"></i></span>Markt verkenner openen</a></div></div>
|
|
|
|
<!-- ===== CALENDAR ===== -->
|
|
<div class="tab-panel" id="panel-calendar"><div class="page-hdr fade-in-up"><h1>Agenda</h1><p>Planning en afspraken</p></div>
|
|
<div class="quick-actions"><a href="/calendar" class="qa-btn"><span class="qa-icon"><i data-lucide="calendar"></i></span>Agenda openen</a></div></div>
|
|
|
|
<!-- ===== DATA ===== -->
|
|
<div class="tab-panel" id="panel-data"><div class="page-hdr fade-in-up"><h1>Data Quality</h1><p>Data profiling, kwaliteitschecks & dbt modellen</p></div>
|
|
<div class="quick-actions"><a href="/quality" class="qa-btn"><span class="qa-icon"><i data-lucide="shield-check"></i></span>Data Quality Engine</a></div></div>
|
|
|
|
<!-- ===== EMAIL ===== -->
|
|
<div class="tab-panel" id="panel-email"><div class="page-hdr fade-in-up"><h1>Email</h1><p>IMAP inbox & versturen</p></div>
|
|
<div class="quick-actions"><a href="/email" class="qa-btn"><span class="qa-icon"><i data-lucide="inbox"></i></span>Inbox</a><a href="/email/compose" class="qa-btn"><span class="qa-icon"><i data-lucide="send"></i></span>Opstellen</a></div></div>
|
|
|
|
<!-- ===== ACTIVITY ===== -->
|
|
<div class="tab-panel" id="panel-activity"><div class="page-hdr fade-in-up"><h1>Recente Activiteit</h1></div>
|
|
<div class="card card-hover"><div class="card-title">Recente Assessments</div>
|
|
<% if(recentAssessments.length===0){ %><div class="empty-state-s">Nog geen activiteit</div><% }else{ %><div class="timeline"><% recentAssessments.forEach(function(a){ %><div class="tl-item"><div class="tl-dot" style="background:var(--purple)"></div><div class="tl-body"><div class="tl-title">Rapport voor <strong><%= a.client_name %></strong></div><div class="tl-date"><%= new Date(a.created_at).toLocaleString('nl-NL') %></div></div></div><% }) %></div><% } %></div>
|
|
<div style="margin-top:.85rem" class="quick-actions"><a href="/audit" class="qa-btn"><span class="qa-icon"><i data-lucide="file-text"></i></span>Audit Log</a><a href="/notifications" class="qa-btn"><span class="qa-icon"><i data-lucide="bell"></i></span>Meldingen</a><a href="/users" class="qa-btn"><span class="qa-icon"><i data-lucide="users"></i></span>Gebruikers</a></div></div>
|
|
|
|
</div></div>
|
|
|
|
<script src="/js/dashboard-charts.js"></script><script>
|
|
function initTheme(){var m=localStorage.getItem('theme');if(m==='light'){document.body.classList.add('light-mode');document.getElementById('themeToggle').classList.add('light')}}
|
|
function toggleTheme(){var b=document.body;var t=document.getElementById('themeToggle');b.classList.toggle('light-mode');t.classList.toggle('light');localStorage.setItem('theme',b.classList.contains('light-mode')?'light':'dark')}
|
|
initTheme();
|
|
|
|
document.querySelectorAll('.sb-item').forEach(function(b){
|
|
b.addEventListener('click',function(){
|
|
var t=this.dataset.tab;
|
|
document.querySelectorAll('.sb-item').forEach(function(x){x.classList.remove('active')});
|
|
document.querySelectorAll('.tab-panel').forEach(function(x){x.classList.remove('active')});
|
|
this.classList.add('active');document.getElementById('panel-'+t).classList.add('active');
|
|
var u=new URL(window.location);u.searchParams.set('tab',t);window.history.replaceState({},'',u);
|
|
});
|
|
});
|
|
function switchTab(t){
|
|
document.querySelectorAll('.sb-item').forEach(function(x){x.classList.remove('active')});
|
|
document.querySelectorAll('.tab-panel').forEach(function(x){x.classList.remove('active')});
|
|
var b=document.querySelector('.sb-item[data-tab="'+t+'"]');if(b)b.classList.add('active');
|
|
document.getElementById('panel-'+t).classList.add('active');
|
|
}
|
|
var h=(new URL(window.location)).searchParams.get('tab');
|
|
if(h){switchTab(h)}else{switchTab('overview')}
|
|
lucide.createIcons();
|
|
</script>
|
|
</body></html>
|