2025-12-06 02:22:01 +01:00
{% extends "shared/frontend/base.html" %}
{% block title %}Dashboard - BMC Hub{% endblock %}
2026-02-12 07:03:18 +01:00
{% block extra_css %}
< style >
/* Modern Dashboard Styling */
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
}
.dashboard-header {
background: linear-gradient(135deg, var(--accent) 0%, #1e5a8e 100%);
color: white;
padding: 2.5rem 0;
margin: -2rem -15px 2rem -15px;
border-radius: 0 0 24px 24px;
box-shadow: 0 10px 30px rgba(15, 76, 117, 0.15);
}
.dashboard-header h2 {
font-size: 2rem;
font-weight: 700;
margin: 0;
}
.dashboard-header p {
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
/* Stat Cards - Modern Gradient Design */
.stat-card {
position: relative;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-decoration: none;
display: block;
background: white;
border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--accent), #3b82f6);
opacity: 0;
transition: opacity 0.3s ease;
}
.stat-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 40px rgba(15, 76, 117, 0.15);
}
.stat-card:hover::before {
opacity: 1;
}
.stat-card-icon {
width: 56px;
height: 56px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.75rem;
margin-bottom: 1.25rem;
}
.stat-card-icon.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.stat-card-icon.success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.stat-card-icon.warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.stat-card-icon.info {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.stat-card-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
margin-bottom: 0.75rem;
}
.stat-card-value {
font-size: 2.5rem;
font-weight: 800;
color: #1e293b;
margin: 0.5rem 0 0.75rem 0;
line-height: 1;
}
.trend-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.trend-badge.positive {
background: linear-gradient(135deg, #d4fc79 0%, #96e6a1 100%);
color: #166534;
}
.trend-badge.negative {
background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
color: #991b1b;
}
.trend-badge.neutral {
background: #e2e8f0;
color: #64748b;
}
/* Content Cards */
.content-card {
background: white;
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
overflow: hidden;
border: 1px solid rgba(226, 232, 240, 0.8);
}
.card-header-modern {
padding: 1.5rem;
border-bottom: 1px solid #f1f5f9;
}
.card-header-modern h5 {
font-size: 1.125rem;
font-weight: 700;
color: #1e293b;
margin: 0;
}
/* Activity Items */
.activity-item {
padding: 1rem 1.5rem;
border-bottom: 1px solid #f1f5f9;
transition: all 0.2s ease;
cursor: pointer;
display: flex;
align-items: center;
gap: 1rem;
}
.activity-item:hover {
background: linear-gradient(90deg, rgba(99, 102, 241, 0.02) 0%, rgba(59, 130, 246, 0.05) 100%);
padding-left: 1.75rem;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
flex-shrink: 0;
}
.activity-icon.primary {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
color: #6366f1;
}
.activity-icon.warning {
background: linear-gradient(135deg, rgba(251, 146, 60, 0.1) 0%, rgba(245, 87, 108, 0.1) 100%);
color: #f59e0b;
}
.activity-icon.info {
background: linear-gradient(135deg, rgba(14, 165, 233, 0.1) 0%, rgba(6, 182, 212, 0.1) 100%);
color: #0ea5e9;
}
.activity-content {
flex: 1;
}
.activity-name {
font-weight: 600;
color: #1e293b;
margin-bottom: 0.25rem;
}
.activity-time {
font-size: 0.8rem;
color: #94a3b8;
}
/* Reminders */
.reminder-item {
padding: 1rem;
border-left: 4px solid;
border-radius: 12px;
margin-bottom: 0.75rem;
transition: all 0.2s ease;
}
.reminder-item:hover {
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.reminder-item.priority-high {
border-left-color: #ef4444;
background: linear-gradient(90deg, rgba(239, 68, 68, 0.05) 0%, rgba(239, 68, 68, 0.02) 100%);
}
.reminder-item.priority-medium {
border-left-color: #f59e0b;
background: linear-gradient(90deg, rgba(245, 158, 11, 0.05) 0%, rgba(245, 158, 11, 0.02) 100%);
}
.reminder-item.priority-low {
border-left-color: #10b981;
background: linear-gradient(90deg, rgba(16, 185, 129, 0.05) 0%, rgba(16, 185, 129, 0.02) 100%);
}
/* Quick Actions */
.quick-action-btn {
border-radius: 14px;
padding: 1rem 1.5rem;
font-weight: 600;
border: 2px solid transparent;
transition: all 0.3s ease;
background: linear-gradient(135deg, var(--accent) 0%, #1e5a8e 100%);
color: white;
}
.quick-action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(15, 76, 117, 0.25);
border-color: var(--accent);
color: white;
}
.quick-action-btn i {
font-size: 1.1rem;
}
/* Activity Feed Scrollbar */
.activity-feed {
max-height: 550px;
overflow-y: auto;
}
.activity-feed::-webkit-scrollbar {
width: 6px;
}
.activity-feed::-webkit-scrollbar-track {
background: #f1f5f9;
}
.activity-feed::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.activity-feed::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Skeleton Loading */
.skeleton {
background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
border-radius: 8px;
}
.skeleton-text {
height: 1rem;
margin-bottom: 0.5rem;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Alerts Modern Style */
.alert {
border-radius: 16px;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1.5rem;
color: #94a3b8;
}
.empty-state i {
font-size: 3rem;
opacity: 0.3;
margin-bottom: 1rem;
}
< / style >
{% endblock %}
2025-12-06 02:22:01 +01:00
{% block content %}
2026-02-12 07:03:18 +01:00
< div class = "container-fluid" >
<!-- Modern Header -->
< div class = "dashboard-header" >
< div class = "container-fluid" style = "max-width: 1400px;" >
< h2 > 📊 Dashboard< / h2 >
< p > Oversigt over BMC Hub - alt på ét sted< / p >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
2026-02-12 07:03:18 +01:00
< div class = "container-fluid" style = "max-width: 1400px;" >
<!-- Alerts -->
{% if bankruptcy_alerts %}
< div class = "alert alert-danger d-flex align-items-center mb-4" role = "alert" >
< i class = "bi bi-shield-exclamation flex-shrink-0 me-3 fs-2" > < / i >
< div class = "flex-grow-1" >
< h5 class = "alert-heading mb-1 fw-bold" > ⚠️ KONKURS ALARM< / h5 >
< div > Systemet har registreret < strong > {{ bankruptcy_alerts|length }}< / strong > potentiel(le) konkurssag(er).< / div >
< ul class = "mb-0 mt-2 small list-unstyled" >
{% for alert in bankruptcy_alerts %}
< li class = "mb-1" >
< span class = "badge bg-danger me-2" > ALARM< / span >
< strong > {{ alert.display_name }}:< / strong >
< a href = "/emails?id={{ alert.id }}" class = "alert-link text-decoration-underline" > {{ alert.subject }}< / a >
< / li >
{% endfor %}
< / ul >
< / div >
< div >
< a href = "/emails?filter=bankruptcy" class = "btn btn-sm btn-danger px-3" > Håndter Nu < i class = "bi bi-arrow-right ms-1" > < / i > < / a >
< / div >
2026-01-11 19:23:21 +01:00
< / div >
2026-02-12 07:03:18 +01:00
{% endif %}
2026-01-11 19:23:21 +01:00
2026-02-12 07:03:18 +01:00
{% if unknown_worklog_count > 0 %}
< div class = "alert alert-warning d-flex align-items-center mb-4" role = "alert" >
< i class = "bi bi-exclamation-triangle-fill flex-shrink-0 me-3 fs-4" > < / i >
< div >
< h5 class = "alert-heading mb-1" > Tidsregistreringer kræver handling< / h5 >
Der er < strong > {{ unknown_worklog_count }}< / strong > tidsregistrering(er) med typen "Ved ikke".
< a href = "/ticket/worklog/review" class = "alert-link" > Gå til godkendelse< / a > for at afklare dem.
< / div >
2026-01-10 21:09:29 +01:00
< / div >
2026-02-12 07:03:18 +01:00
{% endif %}
2026-01-10 21:09:29 +01:00
2026-02-12 07:03:18 +01:00
<!-- Stat Cards -->
< div class = "row g-4 mb-5" id = "statCards" >
< div class = "col-md-6 col-xl-3" >
< a href = "/customers" class = "stat-card p-4 h-100" >
< div class = "stat-card-icon primary" >
< i class = "bi bi-people-fill" > < / i >
< / div >
< div class = "stat-card-label" > Aktive Kunder< / div >
< div class = "stat-card-value skeleton skeleton-text" style = "width: 80px; height: 2.5rem;" id = "customerCount" > -< / div >
< div id = "customerTrend" >
< span class = "trend-badge neutral skeleton skeleton-text" style = "width: 120px; height: 24px;" > < / span >
< / div >
< / a >
2025-12-06 02:22:01 +01:00
< / div >
2026-02-12 07:03:18 +01:00
< div class = "col-md-6 col-xl-3" >
< a href = "/ticket/tickets?status=open" class = "stat-card p-4 h-100" >
< div class = "stat-card-icon warning" >
< i class = "bi bi-ticket-perforated-fill" > < / i >
< / div >
< div class = "stat-card-label" > Support Tickets< / div >
< div class = "stat-card-value skeleton skeleton-text" style = "width: 60px; height: 2.5rem;" id = "ticketCount" > -< / div >
< div id = "ticketUrgent" >
< span class = "skeleton skeleton-text" style = "width: 120px; height: 20px;" > < / span >
< / div >
< / a >
2025-12-06 02:22:01 +01:00
< / div >
2026-02-12 07:03:18 +01:00
< div class = "col-md-6 col-xl-3" >
< a href = "/billing" class = "stat-card p-4 h-100" >
< div class = "stat-card-icon success" >
< i class = "bi bi-graph-up-arrow" > < / i >
< / div >
< div class = "stat-card-label" > Omsætning< / div >
< div class = "stat-card-value skeleton skeleton-text" style = "width: 100px; height: 2.5rem;" id = "revenueCount" > -< / div >
< div id = "revenueTrend" >
< span class = "trend-badge neutral skeleton skeleton-text" style = "width: 110px; height: 24px;" > < / span >
< / div >
< / a >
2025-12-06 02:22:01 +01:00
< / div >
2026-02-12 07:03:18 +01:00
< div class = "col-md-6 col-xl-3" >
< a href = "/hardware" class = "stat-card p-4 h-100" >
< div class = "stat-card-icon info" >
< i class = "bi bi-hdd-rack-fill" > < / i >
< / div >
< div class = "stat-card-label" > Hardware< / div >
< div class = "stat-card-value skeleton skeleton-text" style = "width: 80px; height: 2.5rem;" id = "hardwareCount" > -< / div >
< small class = "text-muted" > Enheder registreret< / small >
< / a >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
2026-02-12 07:03:18 +01:00
<!-- Main Content -->
< div class = "row g-4" >
< div class = "col-lg-8" >
<!-- Quick Actions -->
< div class = "content-card mb-4" >
< div class = "card-header-modern" >
< h5 > ⚡ Hurtige handlinger< / h5 >
2025-12-16 15:36:11 +01:00
< / div >
2026-02-12 07:03:18 +01:00
< div class = "p-4" >
< div class = "row g-3" >
< div class = "col-md-4" >
< a href = "/customers/new" class = "btn quick-action-btn w-100" >
< i class = "bi bi-person-plus-fill me-2" > < / i > Ny kunde
< / a >
< / div >
< div class = "col-md-4" >
< a href = "/ticket/tickets/new" class = "btn quick-action-btn w-100" >
< i class = "bi bi-ticket-fill me-2" > < / i > Opret ticket
< / a >
< / div >
< div class = "col-md-4" >
< a href = "/sag/new" class = "btn quick-action-btn w-100" >
< i class = "bi bi-folder-plus me-2" > < / i > Ny sag
< / a >
< / div >
< / div >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
2026-02-12 07:03:18 +01:00
<!-- Recent Activity -->
< div class = "content-card" >
< div class = "card-header-modern" >
< h5 > 🕐 Seneste aktivitet< / h5 >
2025-12-16 15:36:11 +01:00
< / div >
2026-02-12 07:03:18 +01:00
< div class = "activity-feed" id = "activityFeed" >
< div class = "empty-state" >
< i class = "bi bi-arrow-clockwise" > < / i >
< div > Henter aktivitet...< / div >
< / div >
2025-12-16 15:36:11 +01:00
< / div >
< / div >
2026-02-12 07:03:18 +01:00
< / div >
< div class = "col-lg-4" >
<!-- Calendar / Reminders -->
< div class = "content-card" >
< div class = "card-header-modern" >
< h5 > 📅 Kommende påmindelser< / h5 >
< / div >
< div class = "p-4" id = "remindersWidget" >
< div class = "empty-state" >
< i class = "bi bi-arrow-clockwise" > < / i >
< div > Henter påmindelser...< / div >
< / div >
2025-12-16 15:36:11 +01:00
< / div >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
< / div >
< / div >
2026-02-12 07:03:18 +01:00
< / div >
{% endblock %}
{% block extra_js %}
< script >
// Format currency in Danish format
function formatCurrency(amount) {
return new Intl.NumberFormat('da-DK', {
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount) + ' kr';
}
// Format relative time
function timeAgo(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1 ) return ' Lige nu ' ;
if (diffMins < 60 ) return ` $ { diffMins } min siden ` ;
if (diffHours < 24 ) return ` $ { diffHours } timer siden ` ;
if (diffDays === 1) return 'I går';
if (diffDays < 7 ) return ` $ { diffDays } dage siden ` ;
return date.toLocaleDateString('da-DK', { day: 'numeric', month: 'short' });
}
// Get icon for activity type
function getActivityIcon(type, color) {
const icons = {
'customer': 'bi-building-fill',
'ticket': 'bi-ticket-perforated-fill',
'case': 'bi-folder-fill'
};
return `< div class = "activity-icon ${color}" > < i class = "bi ${icons[type] || 'bi-circle-fill'}" > < / i > < / div > `;
}
// Get link for activity type
function getActivityLink(type, id) {
const links = {
'customer': '/customers',
'ticket': '/ticket/tickets',
'case': '/sag'
};
return `${links[type] || '#'}/${id}`;
}
// Load dashboard stats
async function loadStats() {
try {
const response = await fetch('/api/v1/stats');
if (!response.ok) throw new Error('Failed to fetch stats');
const data = await response.json();
// Update customer stats
const customerEl = document.getElementById('customerCount');
customerEl.textContent = data.customers.total.toLocaleString('da-DK');
customerEl.classList.remove('skeleton', 'skeleton-text');
const customerTrendEl = document.getElementById('customerTrend');
const customerGrowth = data.customers.growth_pct;
const trendClass = customerGrowth > 0 ? 'positive' : customerGrowth < 0 ? ' negative ' : ' neutral ' ;
const trendIcon = customerGrowth > 0 ? '▲' : customerGrowth < 0 ? ' ▼ ' : ' — ' ;
customerTrendEl.innerHTML = `< span class = "trend-badge ${trendClass}" > ${trendIcon} ${Math.abs(customerGrowth)}% denne måned< / span > `;
// Update ticket stats
const ticketEl = document.getElementById('ticketCount');
ticketEl.textContent = data.tickets.open_count.toLocaleString('da-DK');
ticketEl.classList.remove('skeleton', 'skeleton-text');
const ticketUrgentEl = document.getElementById('ticketUrgent');
const urgentCount = data.tickets.urgent_count;
if (urgentCount > 0) {
ticketUrgentEl.innerHTML = `< span class = "badge bg-danger" > ${urgentCount} kræver handling< / span > `;
} else {
ticketUrgentEl.textContent = 'Ingen hastesager';
}
ticketUrgentEl.classList.remove('skeleton', 'skeleton-text');
// Update revenue stats
const revenueEl = document.getElementById('revenueCount');
revenueEl.textContent = formatCurrency(data.revenue.current_month);
revenueEl.classList.remove('skeleton', 'skeleton-text');
const revenueTrendEl = document.getElementById('revenueTrend');
const revenueGrowth = data.revenue.growth_pct;
const revTrendClass = revenueGrowth > 0 ? 'positive' : revenueGrowth < 0 ? ' negative ' : ' neutral ' ;
const revTrendIcon = revenueGrowth > 0 ? '▲' : revenueGrowth < 0 ? ' ▼ ' : ' — ' ;
revenueTrendEl.innerHTML = `< span class = "trend-badge ${revTrendClass}" > ${revTrendIcon} ${Math.abs(revenueGrowth)}% denne måned< / span > `;
// Update hardware stats
const hardwareEl = document.getElementById('hardwareCount');
hardwareEl.textContent = data.hardware.total.toLocaleString('da-DK');
hardwareEl.classList.remove('skeleton', 'skeleton-text');
} catch (error) {
console.error('Error loading stats:', error);
// Remove skeletons even on error
document.querySelectorAll('.skeleton').forEach(el => {
el.classList.remove('skeleton', 'skeleton-text');
el.textContent = 'Fejl';
});
}
}
// Load recent activity
async function loadActivity() {
try {
const response = await fetch('/api/v1/recent-activity');
if (!response.ok) throw new Error('Failed to fetch activity');
const activities = await response.json();
const feedEl = document.getElementById('activityFeed');
if (!activities || activities.length === 0) {
feedEl.innerHTML = '< div class = "empty-state" > < i class = "bi bi-inbox" > < / i > < div > Ingen aktivitet at vise< / div > < / div > ';
return;
}
feedEl.innerHTML = activities.map(activity => `
< div class = "activity-item" onclick = "window.location.href='${getActivityLink(activity.activity_type, activity.id)}'" >
${getActivityIcon(activity.activity_type, activity.color)}
< div class = "activity-content" >
< div class = "activity-name" > ${activity.name || 'Unavngivet'}< / div >
< div class = "activity-time" > ${timeAgo(activity.created_at)}< / div >
< / div >
< i class = "bi bi-chevron-right text-muted" > < / i >
< / div >
`).join('');
} catch (error) {
console.error('Error loading activity:', error);
document.getElementById('activityFeed').innerHTML = '< div class = "empty-state" > < i class = "bi bi-exclamation-triangle" > < / i > < div class = "text-danger" > Fejl ved indlæsning af aktivitet< / div > < / div > ';
}
}
// Load reminders
async function loadReminders() {
try {
const response = await fetch('/api/v1/reminders/upcoming');
if (!response.ok) throw new Error('Failed to fetch reminders');
const reminders = await response.json();
const widgetEl = document.getElementById('remindersWidget');
if (!reminders || reminders.length === 0) {
widgetEl.innerHTML = '< div class = "empty-state" > < i class = "bi bi-calendar-check" > < / i > < div > Ingen kommende påmindelser< / div > < / div > ';
return;
}
widgetEl.innerHTML = reminders.map(reminder => {
const dueDate = new Date(reminder.due_date);
const priorityClass = reminder.priority === 'high' ? 'priority-high' : reminder.priority === 'medium' ? 'priority-medium' : 'priority-low';
return `
< div class = "reminder-item ${priorityClass}" >
< div class = "d-flex justify-content-between align-items-start mb-1" >
< div class = "fw-semibold small" > ${reminder.title || reminder.case_title || 'Påmindelse'}< / div >
< small class = "text-muted ms-2" > ${dueDate.toLocaleDateString('da-DK', { day: 'numeric', month: 'short' })}< / small >
< / div >
${reminder.case_title ? `< div class = "small text-muted" > Sag: ${reminder.case_title}< / div > ` : ''}
< / div >
`;
}).join('');
// Add link to view all
widgetEl.innerHTML += '< div class = "text-center mt-3" > < a href = "/sag" class = "text-decoration-none small fw-semibold" > Se alle påmindelser →< / a > < / div > ';
} catch (error) {
console.error('Error loading reminders:', error);
document.getElementById('remindersWidget').innerHTML = '< div class = "empty-state" > < i class = "bi bi-exclamation-triangle" > < / i > < div class = "text-danger" > Fejl ved indlæsning< / div > < / div > ';
}
}
// Load all data on page load
document.addEventListener('DOMContentLoaded', function() {
loadStats();
loadActivity();
loadReminders();
});
< / script >
2025-12-16 15:36:11 +01:00
{% endblock %}