- Updated dashboard stats to include new customer counts and trends, ticket counts, hardware counts, and revenue growth percentages. - Added a new endpoint for fetching upcoming reminders for the dashboard calendar widget. - Improved recent activity fetching to include recent tickets and cases. - Enhanced frontend with modern styling for dashboard components, including stat cards and activity feed. - Implemented loading states and error handling for stats, activity, and reminders in the frontend. - Refactored HTML structure for better organization and responsiveness. feat(hardware): support for new hardware_assets table in contact hardware listing - Modified the endpoint to list hardware by contact to support both new hardware_assets and legacy hardware tables. - Merged results from both tables, prioritizing the new hardware_assets table for better data accuracy. style(eset_import): improve device display options in ESET import template - Added toggle functionality for switching between tablet view and table view for device listings. - Enhanced the layout and visibility of device cards and tables for better user experience.
677 lines
24 KiB
HTML
677 lines
24 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Dashboard - BMC Hub{% endblock %}
|
|
|
|
{% 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 %}
|
|
|
|
{% block content %}
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% 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>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- 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>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Activity -->
|
|
<div class="content-card">
|
|
<div class="card-header-modern">
|
|
<h5>🕐 Seneste aktivitet</h5>
|
|
</div>
|
|
<div class="activity-feed" id="activityFeed">
|
|
<div class="empty-state">
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
<div>Henter aktivitet...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
{% endblock %} |