- Implemented a new simplified time tracking wizard (wizard2) for approval processes. - Added a registrations view to list all time tracking entries. - Enhanced the existing wizard.html to include a billable checkbox for entries. - Updated JavaScript logic to handle billable state and travel status for time entries. - Introduced a cleanup step in the deployment script to remove old images. - Created a new HTML template for registrations with filtering and pagination capabilities.
1122 lines
53 KiB
HTML
1122 lines
53 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="da" data-bs-theme="light">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}BMC Hub{% endblock %}</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
|
<style>
|
|
:root {
|
|
--bg-body: #f8f9fa;
|
|
--bg-card: #ffffff;
|
|
--text-primary: #2c3e50;
|
|
--text-secondary: #6c757d;
|
|
--accent: #0f4c75;
|
|
--accent-light: #eef2f5;
|
|
--border-radius: 12px;
|
|
}
|
|
|
|
[data-bs-theme="dark"] {
|
|
--bg-body: #212529;
|
|
--bg-card: #2c3034;
|
|
--text-primary: #f8f9fa;
|
|
--text-secondary: #adb5bd;
|
|
--accent: #3d8bfd; /* Lighter blue for dark mode */
|
|
--accent-light: #373b3e;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-body);
|
|
color: var(--text-primary);
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
padding-top: 80px;
|
|
transition: background-color 0.3s, color 0.3s;
|
|
}
|
|
|
|
.navbar {
|
|
background: var(--bg-card);
|
|
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
|
|
padding: 1rem 0;
|
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.navbar-brand {
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.nav-link {
|
|
color: var(--text-secondary);
|
|
padding: 0.6rem 1.2rem !important;
|
|
border-radius: var(--border-radius);
|
|
transition: all 0.2s;
|
|
font-weight: 500;
|
|
margin: 0 0.2rem;
|
|
}
|
|
|
|
.nav-link:hover, .nav-link.active {
|
|
background-color: var(--accent-light);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.card {
|
|
border: none;
|
|
border-radius: var(--border-radius);
|
|
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
|
|
transition: transform 0.2s, background-color 0.3s;
|
|
background: var(--bg-card);
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.stat-card h3 {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.stat-card p {
|
|
color: var(--text-secondary);
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.table {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.table th {
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
border-bottom-width: 1px;
|
|
font-size: 0.85rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: var(--accent);
|
|
border-color: var(--accent);
|
|
padding: 0.6rem 1.5rem;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: #0a3655;
|
|
border-color: #0a3655;
|
|
}
|
|
|
|
.header-search {
|
|
background: var(--bg-body);
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
padding: 0.6rem 1.2rem;
|
|
border-radius: 8px;
|
|
width: 300px;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.header-search:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.dropdown-menu {
|
|
border: none;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
|
border-radius: 12px;
|
|
padding: 0.5rem;
|
|
background-color: var(--bg-card);
|
|
}
|
|
|
|
/* Nested dropdown support - simplified click-based approach */
|
|
.dropdown-submenu {
|
|
position: relative;
|
|
}
|
|
|
|
.dropdown-submenu .dropdown-menu {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 100%;
|
|
margin-left: 0.1rem;
|
|
margin-top: -0.5rem;
|
|
display: none;
|
|
}
|
|
|
|
.dropdown-submenu .dropdown-menu.show {
|
|
display: block;
|
|
}
|
|
|
|
.dropdown-submenu .dropdown-toggle::after {
|
|
display: none;
|
|
}
|
|
|
|
.dropdown-submenu > a {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.dropdown-item {
|
|
border-radius: 8px;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.dropdown-item:hover {
|
|
background-color: var(--accent-light);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.result-item {
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 8px;
|
|
margin-bottom: 0.5rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
transition: all 0.2s;
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
.result-item:hover {
|
|
background-color: var(--accent-light);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.result-item.selected {
|
|
background-color: var(--accent-light);
|
|
border-color: var(--accent);
|
|
box-shadow: 0 2px 8px rgba(15, 76, 117, 0.1);
|
|
}
|
|
</style>
|
|
{% block extra_css %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
|
|
<nav class="navbar navbar-expand-lg fixed-top">
|
|
<div class="container-fluid px-4">
|
|
<a class="navbar-brand d-flex align-items-center" href="/">
|
|
<div class="bg-primary text-white rounded p-1 me-2 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; background-color: var(--accent) !important;">
|
|
<i class="bi bi-hdd-network-fill" style="font-size: 16px;"></i>
|
|
</div>
|
|
BMC Hub
|
|
</a>
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="collapse navbar-collapse" id="navbarNav">
|
|
<ul class="navbar-nav mx-auto">
|
|
<li class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<i class="bi bi-people me-2"></i>CRM
|
|
</a>
|
|
<ul class="dropdown-menu mt-2">
|
|
<li><a class="dropdown-item py-2" href="/customers">Kunder</a></li>
|
|
<li><a class="dropdown-item py-2" href="/contacts">Kontakter</a></li>
|
|
<li><a class="dropdown-item py-2" href="/vendors">Leverandører</a></li>
|
|
<li><a class="dropdown-item py-2" href="#">Leads</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item py-2" href="#">Rapporter</a></li>
|
|
</ul>
|
|
</li>
|
|
<li class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<i class="bi bi-headset me-2"></i>Support
|
|
</a>
|
|
<ul class="dropdown-menu mt-2">
|
|
<li><a class="dropdown-item py-2" href="/ticket/dashboard"><i class="bi bi-speedometer2 me-2"></i>Dashboard</a></li>
|
|
<li><a class="dropdown-item py-2" href="/ticket/tickets"><i class="bi bi-ticket-detailed me-2"></i>Alle Tickets</a></li>
|
|
<li><a class="dropdown-item py-2" href="/ticket/worklog/review"><i class="bi bi-clock-history me-2"></i>Godkend Worklog</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item py-2" href="#">Ny Ticket</a></li>
|
|
<li><a class="dropdown-item py-2" href="/prepaid-cards"><i class="bi bi-credit-card-2-front me-2"></i>Prepaid Cards</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item py-2" href="#">Knowledge Base</a></li>
|
|
</ul>
|
|
</li>
|
|
<li class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<i class="bi bi-cart3 me-2"></i>Salg
|
|
</a>
|
|
<ul class="dropdown-menu mt-2">
|
|
<li><a class="dropdown-item py-2" href="#">Tilbud</a></li>
|
|
<li><a class="dropdown-item py-2" href="#">Ordre</a></li>
|
|
<li><a class="dropdown-item py-2" href="#">Produkter</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item py-2" href="#">Pipeline</a></li>
|
|
</ul>
|
|
</li>
|
|
<li class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<i class="bi bi-currency-dollar me-2"></i>Økonomi
|
|
</a>
|
|
<ul class="dropdown-menu mt-2">
|
|
<li><a class="dropdown-item py-2" href="#">Fakturaer</a></li>
|
|
<li><a class="dropdown-item py-2" href="/billing/supplier-invoices"><i class="bi bi-receipt me-2"></i>Leverandør fakturaer</a></li>
|
|
<li><a class="dropdown-item py-2" href="#">Abonnementer</a></li>
|
|
<li><a class="dropdown-item py-2" href="#">Betalinger</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li class="dropdown-submenu">
|
|
<a class="dropdown-item dropdown-toggle py-2" href="#" data-submenu-toggle="timetracking">
|
|
<span><i class="bi bi-clock-history me-2"></i>Timetracking</span>
|
|
<i class="bi bi-chevron-right small opacity-75"></i>
|
|
</a>
|
|
<ul class="dropdown-menu" data-submenu="timetracking">
|
|
<li><a class="dropdown-item py-2" href="/timetracking"><i class="bi bi-speedometer2 me-2"></i>Dashboard</a></li>
|
|
<li><a class="dropdown-item py-2" href="/timetracking/registrations"><i class="bi bi-list-columns-reverse me-2"></i>Registreringer</a></li>
|
|
<li><a class="dropdown-item py-2" href="/timetracking/wizard"><i class="bi bi-magic me-2"></i>Godkend Timer</a></li>
|
|
<li><a class="dropdown-item py-2" href="/timetracking/orders"><i class="bi bi-receipt me-2"></i>Ordrer</a></li>
|
|
<li><a class="dropdown-item py-2" href="/timetracking/customers"><i class="bi bi-people me-2"></i>Kunder</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item py-2" href="#">Rapporter</a></li>
|
|
</ul>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/emails">
|
|
<i class="bi bi-envelope me-2"></i>Email
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<button class="btn btn-light rounded-circle border-0" id="darkModeToggle" style="background: var(--accent-light); color: var(--accent);">
|
|
<i class="bi bi-moon-fill"></i>
|
|
</button>
|
|
<button class="btn btn-light rounded-circle border-0" style="background: var(--accent-light); color: var(--accent);"><i class="bi bi-bell"></i></button>
|
|
<div class="dropdown">
|
|
<a href="#" class="d-flex align-items-center text-decoration-none text-dark dropdown-toggle" data-bs-toggle="dropdown">
|
|
<img src="https://ui-avatars.com/api/?name=CT&background=0f4c75&color=fff" class="rounded-circle me-2" width="32">
|
|
<span class="small fw-bold" style="color: var(--text-primary)">Christian</span>
|
|
</a>
|
|
<ul class="dropdown-menu dropdown-menu-end mt-2">
|
|
<li><a class="dropdown-item py-2" href="#">Profil</a></li>
|
|
<li><a class="dropdown-item py-2" href="/settings"><i class="bi bi-gear me-2"></i>Indstillinger</a></li>
|
|
<li><a class="dropdown-item py-2" href="/backups"><i class="bi bi-hdd-stack me-2"></i>Backup System</a></li>
|
|
<li><a class="dropdown-item py-2" href="/devportal"><i class="bi bi-code-square me-2"></i>DEV Portal</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item py-2 text-danger" href="#">Log ud</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Global Search Modal (Cmd+K) -->
|
|
<div class="modal fade" id="globalSearchModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered" style="max-width: 85vw; width: 85vw;">
|
|
<div class="modal-content" style="border: none; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); height: 85vh;">
|
|
<div class="modal-body p-0 d-flex flex-column" style="height: 100%;">
|
|
<div class="p-4 border-bottom" style="background: var(--bg-card);">
|
|
<div class="position-relative">
|
|
<i class="bi bi-search position-absolute" style="left: 20px; top: 50%; transform: translateY(-50%); font-size: 1.5rem; color: var(--text-secondary);"></i>
|
|
<input
|
|
type="text"
|
|
id="globalSearchInput"
|
|
class="form-control form-control-lg ps-5"
|
|
placeholder="Søg efter kunder, sager, produkter... (tryk Esc for at lukke)"
|
|
style="border: none; background: var(--bg-body); font-size: 1.25rem; padding: 1rem 1rem 1rem 4rem; border-radius: 12px;"
|
|
autofocus
|
|
>
|
|
</div>
|
|
<div class="d-flex gap-2 mt-3">
|
|
<span class="badge bg-secondary bg-opacity-10 text-secondary">⌘K for at åbne</span>
|
|
<span class="badge bg-secondary bg-opacity-10 text-secondary">ESC for at lukke</span>
|
|
<span class="badge bg-secondary bg-opacity-10 text-secondary">↑↓ for at navigere</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-0 flex-grow-1" style="overflow-y: auto;">
|
|
<!-- Search Results & Workflows (3/4 width) -->
|
|
<div class="col-lg-9 p-4" style="border-right: 1px solid rgba(0,0,0,0.1);">
|
|
<!-- Contextual Workflows Section -->
|
|
<div id="workflowActions" style="display: none;" class="mb-4">
|
|
<h6 class="text-muted text-uppercase small fw-bold mb-3">
|
|
<i class="bi bi-lightning-charge me-2"></i>Hurtige Handlinger
|
|
</h6>
|
|
<div id="workflowButtons" class="d-flex flex-wrap gap-2">
|
|
<!-- Dynamic workflow buttons -->
|
|
</div>
|
|
</div>
|
|
|
|
<div id="searchResults">
|
|
<!-- Empty State -->
|
|
<div id="emptyState" class="text-center py-5">
|
|
<i class="bi bi-search text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
|
|
<p class="text-muted mt-3">Tryk <kbd>⌘K</kbd> eller begynd at skrive...</p>
|
|
</div>
|
|
|
|
<!-- CRM Results -->
|
|
<div id="crmResults" class="result-section mb-4" style="display: none;">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="text-muted text-uppercase small fw-bold mb-0">
|
|
<i class="bi bi-people me-2"></i>CRM
|
|
</h6>
|
|
<a href="/crm/workflow" class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-diagram-3 me-1"></i>Se Workflow
|
|
</a>
|
|
</div>
|
|
<div class="result-items">
|
|
<!-- Dynamic results will be inserted here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Support Results -->
|
|
<div id="supportResults" class="result-section mb-4" style="display: none;">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="text-muted text-uppercase small fw-bold mb-0">
|
|
<i class="bi bi-headset me-2"></i>Support
|
|
</h6>
|
|
<a href="/support/workflow" class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-diagram-3 me-1"></i>Se Workflow
|
|
</a>
|
|
</div>
|
|
<div class="result-items">
|
|
<!-- Dynamic results will be inserted here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sales Results -->
|
|
<div id="salesResults" class="result-section mb-4" style="display: none;">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="text-muted text-uppercase small fw-bold mb-0">
|
|
<i class="bi bi-cart3 me-2"></i>Salg
|
|
</h6>
|
|
<a href="/sales/workflow" class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-diagram-3 me-1"></i>Se Workflow
|
|
</a>
|
|
</div>
|
|
<div class="result-items">
|
|
<!-- Dynamic results will be inserted here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Finance Results -->
|
|
<div id="financeResults" class="result-section mb-4" style="display: none;">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="text-muted text-uppercase small fw-bold mb-0">
|
|
<i class="bi bi-currency-dollar me-2"></i>Økonomi
|
|
</h6>
|
|
<a href="/finance/workflow" class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-diagram-3 me-1"></i>Se Workflow
|
|
</a>
|
|
</div>
|
|
<div class="result-items">
|
|
<!-- Dynamic results will be inserted here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Boxes Sidebar (1/4 width) -->
|
|
<div class="col-lg-3 p-3" style="background: var(--bg-body); overflow-y: auto;">
|
|
<!-- Recent Activity Section -->
|
|
<div class="mb-4">
|
|
<h6 class="text-uppercase small fw-bold mb-3" style="color: var(--text-primary);">
|
|
<i class="bi bi-clock-history me-2"></i>Seneste Aktivitet
|
|
</h6>
|
|
<div id="recentActivityList">
|
|
<!-- Dynamic activity items -->
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-3">
|
|
|
|
<!-- Sales Box -->
|
|
<div class="live-box mb-3 p-3 rounded" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
|
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
<h6 class="text-uppercase small fw-bold mb-0">
|
|
<i class="bi bi-cart3 me-2"></i>Sales
|
|
</h6>
|
|
<i class="bi bi-arrow-up-right"></i>
|
|
</div>
|
|
<div id="salesBox">
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Aktive ordrer</p>
|
|
<h4 class="mb-0 fw-bold">-</h4>
|
|
</div>
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Månedens salg</p>
|
|
<h5 class="mb-0 fw-bold">- kr</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Support Box -->
|
|
<div class="live-box mb-3 p-3 rounded" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white;">
|
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
<h6 class="text-uppercase small fw-bold mb-0">
|
|
<i class="bi bi-headset me-2"></i>Support
|
|
</h6>
|
|
<i class="bi bi-arrow-up-right"></i>
|
|
</div>
|
|
<div id="supportBox">
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Åbne sager</p>
|
|
<h4 class="mb-0 fw-bold">-</h4>
|
|
</div>
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Gns. svartid</p>
|
|
<h5 class="mb-0 fw-bold">- min</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Økonomi Box -->
|
|
<div class="live-box mb-3 p-3 rounded" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white;">
|
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
<h6 class="text-uppercase small fw-bold mb-0">
|
|
<i class="bi bi-currency-dollar me-2"></i>Økonomi
|
|
</h6>
|
|
<i class="bi bi-arrow-up-right"></i>
|
|
</div>
|
|
<div id="financeBox">
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Ubetalte fakturaer</p>
|
|
<h4 class="mb-0 fw-bold">-</h4>
|
|
</div>
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Samlet beløb</p>
|
|
<h5 class="mb-0 fw-bold">- kr</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% block content_wrapper %}
|
|
<div class="container-fluid px-4 py-4">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="/static/js/tag-picker.js"></script>
|
|
<script>
|
|
// Dark Mode Toggle Logic
|
|
const darkModeToggle = document.getElementById('darkModeToggle');
|
|
const htmlElement = document.documentElement;
|
|
const icon = darkModeToggle.querySelector('i');
|
|
|
|
// Check local storage
|
|
if (localStorage.getItem('theme') === 'dark') {
|
|
htmlElement.setAttribute('data-bs-theme', 'dark');
|
|
icon.classList.replace('bi-moon-fill', 'bi-sun-fill');
|
|
}
|
|
|
|
darkModeToggle.addEventListener('click', () => {
|
|
if (htmlElement.getAttribute('data-bs-theme') === 'dark') {
|
|
htmlElement.setAttribute('data-bs-theme', 'light');
|
|
localStorage.setItem('theme', 'light');
|
|
icon.classList.replace('bi-sun-fill', 'bi-moon-fill');
|
|
} else {
|
|
htmlElement.setAttribute('data-bs-theme', 'dark');
|
|
localStorage.setItem('theme', 'dark');
|
|
icon.classList.replace('bi-moon-fill', 'bi-sun-fill');
|
|
}
|
|
});
|
|
|
|
// Global Search Modal (Cmd+K) - Initialize after DOM is ready
|
|
let selectedResultIndex = -1;
|
|
let allResults = [];
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const searchModal = new bootstrap.Modal(document.getElementById('globalSearchModal'));
|
|
const globalSearchInput = document.getElementById('globalSearchInput');
|
|
|
|
// Search input listener with debounce
|
|
let searchTimeout;
|
|
if (globalSearchInput) {
|
|
globalSearchInput.addEventListener('input', (e) => {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
selectedResultIndex = -1;
|
|
performGlobalSearch(e.target.value);
|
|
}, 300);
|
|
});
|
|
|
|
// Keyboard navigation
|
|
globalSearchInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
navigateResults(1);
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
navigateResults(-1);
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
selectCurrentResult();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Keyboard shortcut: Cmd+K or Ctrl+K
|
|
document.addEventListener('keydown', (e) => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
console.log('Cmd+K pressed - opening search modal'); // Debug
|
|
searchModal.show();
|
|
setTimeout(() => {
|
|
if (globalSearchInput) {
|
|
globalSearchInput.focus();
|
|
}
|
|
loadLiveStats();
|
|
loadRecentActivity();
|
|
}, 300);
|
|
}
|
|
|
|
// ESC to close
|
|
if (e.key === 'Escape') {
|
|
searchModal.hide();
|
|
}
|
|
});
|
|
|
|
// Reset search when modal is closed
|
|
document.getElementById('globalSearchModal').addEventListener('hidden.bs.modal', () => {
|
|
if (globalSearchInput) {
|
|
globalSearchInput.value = '';
|
|
}
|
|
selectedEntity = null;
|
|
document.getElementById('emptyState').style.display = 'block';
|
|
document.getElementById('workflowActions').style.display = 'none';
|
|
document.getElementById('crmResults').style.display = 'none';
|
|
document.getElementById('supportResults').style.display = 'none';
|
|
if (document.getElementById('salesResults')) document.getElementById('salesResults').style.display = 'none';
|
|
if (document.getElementById('financeResults')) document.getElementById('financeResults').style.display = 'none';
|
|
});
|
|
});
|
|
|
|
// Load live statistics for the three boxes
|
|
async function loadLiveStats() {
|
|
try {
|
|
const response = await fetch('/api/v1/dashboard/live-stats');
|
|
const data = await response.json();
|
|
|
|
// Update Sales Box
|
|
const salesBox = document.getElementById('salesBox');
|
|
salesBox.innerHTML = `
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Aktive ordrer</p>
|
|
<h4 class="mb-0 fw-bold">${data.sales.active_orders}</h4>
|
|
</div>
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Månedens salg</p>
|
|
<h5 class="mb-0 fw-bold">${data.sales.monthly_sales.toLocaleString('da-DK')} kr</h5>
|
|
</div>
|
|
`;
|
|
|
|
// Update Support Box
|
|
const supportBox = document.getElementById('supportBox');
|
|
supportBox.innerHTML = `
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Åbne sager</p>
|
|
<h4 class="mb-0 fw-bold">${data.support.open_tickets}</h4>
|
|
</div>
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Gns. svartid</p>
|
|
<h5 class="mb-0 fw-bold">${data.support.avg_response_time} min</h5>
|
|
</div>
|
|
`;
|
|
|
|
// Update Finance Box
|
|
const financeBox = document.getElementById('financeBox');
|
|
financeBox.innerHTML = `
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Ubetalte fakturaer</p>
|
|
<h4 class="mb-0 fw-bold">${data.finance.unpaid_invoices_count}</h4>
|
|
</div>
|
|
<div class="mb-2">
|
|
<p class="mb-0 small opacity-75">Samlet beløb</p>
|
|
<h5 class="mb-0 fw-bold">${data.finance.unpaid_invoices_amount.toLocaleString('da-DK')} kr</h5>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
console.error('Error loading live stats:', error);
|
|
}
|
|
}
|
|
|
|
// Load recent activity
|
|
async function loadRecentActivity() {
|
|
try {
|
|
const response = await fetch('/api/v1/dashboard/recent-activity');
|
|
const activities = await response.json();
|
|
|
|
const activityList = document.getElementById('recentActivityList');
|
|
|
|
if (activities.length === 0) {
|
|
activityList.innerHTML = '<p class="small text-muted">Ingen nylig aktivitet</p>';
|
|
return;
|
|
}
|
|
|
|
activityList.innerHTML = activities.map(activity => {
|
|
const timeAgo = getTimeAgo(new Date(activity.created_at));
|
|
const label = activity.activity_type === 'customer' ? 'Kunde' :
|
|
activity.activity_type === 'contact' ? 'Kontakt' : 'Leverandør';
|
|
|
|
return `
|
|
<div class="activity-item mb-2 p-2 rounded" style="background: var(--bg-card); border-left: 3px solid var(--accent);">
|
|
<div class="d-flex align-items-start">
|
|
<i class="${activity.icon} text-${activity.color} me-2" style="font-size: 1.1rem;"></i>
|
|
<div class="flex-grow-1">
|
|
<p class="mb-0 small fw-bold" style="color: var(--text-primary);">${activity.name}</p>
|
|
<p class="mb-0 small text-muted">${label} • ${timeAgo}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (error) {
|
|
console.error('Error loading recent activity:', error);
|
|
}
|
|
}
|
|
|
|
// Helper function to format time ago
|
|
function getTimeAgo(date) {
|
|
const seconds = Math.floor((new Date() - date) / 1000);
|
|
|
|
let interval = seconds / 31536000;
|
|
if (interval > 1) return Math.floor(interval) + ' år siden';
|
|
|
|
interval = seconds / 2592000;
|
|
if (interval > 1) return Math.floor(interval) + ' mdr siden';
|
|
|
|
interval = seconds / 86400;
|
|
if (interval > 1) return Math.floor(interval) + ' dage siden';
|
|
|
|
interval = seconds / 3600;
|
|
if (interval > 1) return Math.floor(interval) + ' timer siden';
|
|
|
|
interval = seconds / 60;
|
|
if (interval > 1) return Math.floor(interval) + ' min siden';
|
|
|
|
return 'Lige nu';
|
|
}
|
|
|
|
// Helper function to escape HTML
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
let selectedEntity = null;
|
|
|
|
// Navigate through search results
|
|
function navigateResults(direction) {
|
|
const resultItems = document.querySelectorAll('.result-item');
|
|
allResults = Array.from(resultItems);
|
|
|
|
if (allResults.length === 0) return;
|
|
|
|
// Remove previous selection
|
|
if (selectedResultIndex >= 0 && allResults[selectedResultIndex]) {
|
|
allResults[selectedResultIndex].classList.remove('selected');
|
|
}
|
|
|
|
// Update index
|
|
selectedResultIndex += direction;
|
|
|
|
// Wrap around
|
|
if (selectedResultIndex < 0) {
|
|
selectedResultIndex = allResults.length - 1;
|
|
} else if (selectedResultIndex >= allResults.length) {
|
|
selectedResultIndex = 0;
|
|
}
|
|
|
|
// Add selection
|
|
if (allResults[selectedResultIndex]) {
|
|
allResults[selectedResultIndex].classList.add('selected');
|
|
allResults[selectedResultIndex].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
}
|
|
}
|
|
|
|
// Select current result and navigate
|
|
function selectCurrentResult() {
|
|
if (selectedResultIndex >= 0 && allResults[selectedResultIndex]) {
|
|
allResults[selectedResultIndex].click();
|
|
} else if (allResults.length > 0) {
|
|
// No selection, select first result
|
|
allResults[0].click();
|
|
}
|
|
}
|
|
|
|
// Global search function
|
|
async function performGlobalSearch(query) {
|
|
if (!query || query.trim().length < 2) {
|
|
document.getElementById('emptyState').style.display = 'block';
|
|
document.getElementById('crmResults').style.display = 'none';
|
|
document.getElementById('supportResults').style.display = 'none';
|
|
if (document.getElementById('salesResults')) document.getElementById('salesResults').style.display = 'none';
|
|
if (document.getElementById('financeResults')) document.getElementById('financeResults').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
console.log('🔍 Performing global search:', query);
|
|
document.getElementById('emptyState').style.display = 'none';
|
|
|
|
let hasResults = false;
|
|
|
|
try {
|
|
// Search customers
|
|
const customerResponse = await fetch(`/api/v1/customers?search=${encodeURIComponent(query)}&limit=5`);
|
|
const customerData = await customerResponse.json();
|
|
|
|
const crmResults = document.getElementById('crmResults');
|
|
if (customerData.customers && customerData.customers.length > 0) {
|
|
hasResults = true;
|
|
crmResults.style.display = 'block';
|
|
const resultsList = crmResults.querySelector('.result-items');
|
|
if (resultsList) {
|
|
resultsList.innerHTML = customerData.customers.map(customer => `
|
|
<div class="result-item" onclick="window.location.href='/customers/${customer.id}'" style="cursor: pointer;">
|
|
<div>
|
|
<div class="fw-bold">${escapeHtml(customer.name)}</div>
|
|
<div class="small text-muted">
|
|
<i class="bi bi-building me-1"></i>Kunde
|
|
${customer.cvr_number ? ` • CVR: ${customer.cvr_number}` : ''}
|
|
${customer.email ? ` • ${customer.email}` : ''}
|
|
</div>
|
|
</div>
|
|
<i class="bi bi-arrow-right"></i>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
} else {
|
|
crmResults.style.display = 'none';
|
|
}
|
|
|
|
// Search contacts
|
|
try {
|
|
const contactsResponse = await fetch(`/api/v1/contacts?search=${encodeURIComponent(query)}&limit=5`);
|
|
const contactsData = await contactsResponse.json();
|
|
|
|
if (contactsData.contacts && contactsData.contacts.length > 0) {
|
|
hasResults = true;
|
|
const supportResults = document.getElementById('supportResults');
|
|
supportResults.style.display = 'block';
|
|
const supportList = supportResults.querySelector('.result-items');
|
|
if (supportList) {
|
|
supportList.innerHTML = contactsData.contacts.map(contact => `
|
|
<div class="result-item" onclick="window.location.href='/contacts/${contact.id}'" style="cursor: pointer;">
|
|
<div>
|
|
<div class="fw-bold">${escapeHtml(contact.first_name)} ${escapeHtml(contact.last_name)}</div>
|
|
<div class="small text-muted">
|
|
<i class="bi bi-person me-1"></i>Kontakt
|
|
${contact.email ? ` • ${contact.email}` : ''}
|
|
${contact.title ? ` • ${contact.title}` : ''}
|
|
</div>
|
|
</div>
|
|
<i class="bi bi-arrow-right"></i>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
} else {
|
|
document.getElementById('supportResults').style.display = 'none';
|
|
}
|
|
} catch (e) {
|
|
console.log('Contacts search not available');
|
|
}
|
|
|
|
// Search hardware
|
|
try {
|
|
const hardwareResponse = await fetch(`/api/v1/hardware?search=${encodeURIComponent(query)}&limit=5`);
|
|
const hardwareData = await hardwareResponse.json();
|
|
|
|
if (hardwareData.hardware && hardwareData.hardware.length > 0) {
|
|
hasResults = true;
|
|
const salesResults = document.getElementById('salesResults');
|
|
if (salesResults) {
|
|
salesResults.style.display = 'block';
|
|
const salesList = salesResults.querySelector('.result-items');
|
|
if (salesList) {
|
|
salesList.innerHTML = hardwareData.hardware.map(hw => `
|
|
<div class="result-item" onclick="window.location.href='/hardware/${hw.id}'" style="cursor: pointer;">
|
|
<div>
|
|
<div class="fw-bold">${escapeHtml(hw.serial_number || hw.name)}</div>
|
|
<div class="small text-muted">
|
|
<i class="bi bi-pc-display me-1"></i>Hardware
|
|
${hw.type ? ` • ${hw.type}` : ''}
|
|
${hw.customer_name ? ` • ${hw.customer_name}` : ''}
|
|
</div>
|
|
</div>
|
|
<i class="bi bi-arrow-right"></i>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('Hardware search not available');
|
|
}
|
|
|
|
// Search vendors
|
|
try {
|
|
const vendorsResponse = await fetch(`/api/v1/vendors?search=${encodeURIComponent(query)}&limit=5`);
|
|
const vendorsData = await vendorsResponse.json();
|
|
|
|
if (vendorsData.vendors && vendorsData.vendors.length > 0) {
|
|
hasResults = true;
|
|
const financeResults = document.getElementById('financeResults');
|
|
if (financeResults) {
|
|
financeResults.style.display = 'block';
|
|
const financeList = financeResults.querySelector('.result-items');
|
|
if (financeList) {
|
|
financeList.innerHTML = vendorsData.vendors.map(vendor => `
|
|
<div class="result-item" onclick="window.location.href='/vendors/${vendor.id}'" style="cursor: pointer;">
|
|
<div>
|
|
<div class="fw-bold">${escapeHtml(vendor.name)}</div>
|
|
<div class="small text-muted">
|
|
<i class="bi bi-cart me-1"></i>Leverandør
|
|
${vendor.cvr_number ? ` • CVR: ${vendor.cvr_number}` : ''}
|
|
${vendor.email ? ` • ${vendor.email}` : ''}
|
|
</div>
|
|
</div>
|
|
<i class="bi bi-arrow-right"></i>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('Vendors search not available');
|
|
}
|
|
|
|
// Show empty state if no results
|
|
if (!hasResults) {
|
|
document.getElementById('emptyState').style.display = 'block';
|
|
document.getElementById('emptyState').innerHTML = `
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-search" style="font-size: 3rem; opacity: 0.3;"></i>
|
|
<p class="text-muted mt-3">Ingen resultater for "${escapeHtml(query)}"</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Search error:', error);
|
|
}
|
|
}
|
|
|
|
// Workflow definitions per entity type
|
|
const workflows = {
|
|
customer: [
|
|
{ label: 'Opret ordre', icon: 'cart-plus', action: (data) => window.location.href = `/orders/new?customer=${data.id}` },
|
|
{ label: 'Opret sag', icon: 'ticket-detailed', action: (data) => window.location.href = `/tickets/new?customer=${data.id}` },
|
|
{ label: 'Ring til kontakt', icon: 'telephone', action: (data) => alert('Ring til: ' + (data.phone || 'Intet telefonnummer')) },
|
|
{ label: 'Vis kunde', icon: 'eye', action: (data) => window.location.href = `/customers/${data.id}` }
|
|
],
|
|
contact: [
|
|
{ label: 'Ring op', icon: 'telephone', action: (data) => alert('Ring til: ' + (data.mobile_phone || data.phone || 'Intet telefonnummer')) },
|
|
{ label: 'Send email', icon: 'envelope', action: (data) => window.location.href = `mailto:${data.email}` },
|
|
{ label: 'Opret møde', icon: 'calendar-event', action: (data) => alert('Opret møde funktionalitet kommer snart') },
|
|
{ label: 'Vis kontakt', icon: 'eye', action: (data) => window.location.href = `/contacts/${data.id}` }
|
|
],
|
|
vendor: [
|
|
{ label: 'Opret ordre', icon: 'cart-plus', action: (data) => window.location.href = `/orders/new?vendor=${data.id}` },
|
|
{ label: 'Se produkter', icon: 'box-seam', action: (data) => window.location.href = `/vendors/${data.id}/products` },
|
|
{ label: 'Vis leverandør', icon: 'eye', action: (data) => window.location.href = `/vendors/${data.id}` }
|
|
],
|
|
invoice: [
|
|
{ label: 'Vis faktura', icon: 'eye', action: (data) => window.location.href = `/invoices/${data.id}` },
|
|
{ label: 'Udskriv faktura', icon: 'printer', action: (data) => window.print() },
|
|
{ label: 'Opret kassekladde', icon: 'journal-text', action: (data) => alert('Kassekladde funktionalitet kommer snart') },
|
|
{ label: 'Opret kreditnota', icon: 'file-earmark-minus', action: (data) => window.location.href = `/invoices/${data.id}/credit-note` }
|
|
],
|
|
ticket: [
|
|
{ label: 'Åbn sag', icon: 'folder2-open', action: (data) => window.location.href = `/tickets/${data.id}` },
|
|
{ label: 'Luk sag', icon: 'check-circle', action: (data) => alert('Luk sag funktionalitet kommer snart') },
|
|
{ label: 'Tildel medarbejder', icon: 'person-plus', action: (data) => alert('Tildel funktionalitet kommer snart') }
|
|
],
|
|
rodekasse: [
|
|
{ label: 'Behandle', icon: 'pencil-square', action: (data) => window.location.href = `/rodekasse/${data.id}` },
|
|
{ label: 'Arkiver', icon: 'archive', action: (data) => alert('Arkiver funktionalitet kommer snart') },
|
|
{ label: 'Slet', icon: 'trash', action: (data) => confirm('Er du sikker?') && alert('Slet funktionalitet kommer snart') }
|
|
]
|
|
};
|
|
|
|
// Show contextual workflows based on entity
|
|
function showWorkflows(entityType, entityData) {
|
|
selectedEntity = entityData;
|
|
const workflowSection = document.getElementById('workflowActions');
|
|
const workflowButtons = document.getElementById('workflowButtons');
|
|
|
|
const entityWorkflows = workflows[entityType];
|
|
if (!entityWorkflows) {
|
|
workflowSection.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
workflowButtons.innerHTML = entityWorkflows.map(wf => `
|
|
<button class="btn btn-outline-primary" onclick="executeWorkflow('${entityType}', '${wf.label}')">
|
|
<i class="bi bi-${wf.icon} me-2"></i>${wf.label}
|
|
</button>
|
|
`).join('');
|
|
|
|
workflowSection.style.display = 'block';
|
|
}
|
|
|
|
// Execute workflow action
|
|
window.executeWorkflow = function(entityType, label) {
|
|
const workflow = workflows[entityType].find(w => w.label === label);
|
|
if (workflow && selectedEntity) {
|
|
workflow.action(selectedEntity);
|
|
}
|
|
};
|
|
|
|
// Search function already implemented in DOMContentLoaded above - duplicate removed
|
|
|
|
// Hover effects for result items
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.result-item:hover {
|
|
border-color: var(--accent) !important;
|
|
background: var(--accent-light) !important;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
});
|
|
|
|
// Nested dropdown support - simple click-based approach that works reliably
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Find all submenu toggle links
|
|
document.querySelectorAll('.dropdown-submenu > a').forEach((toggle) => {
|
|
toggle.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Get the submenu
|
|
const submenu = this.nextElementSibling;
|
|
if (!submenu || !submenu.classList.contains('dropdown-menu')) return;
|
|
|
|
// Close all other submenus first
|
|
document.querySelectorAll('.dropdown-submenu .dropdown-menu').forEach((menu) => {
|
|
if (menu !== submenu) {
|
|
menu.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Toggle this submenu
|
|
submenu.classList.toggle('show');
|
|
});
|
|
});
|
|
|
|
// Close all submenus when parent dropdown closes
|
|
document.querySelectorAll('.dropdown').forEach((dropdown) => {
|
|
dropdown.addEventListener('hide.bs.dropdown', () => {
|
|
dropdown.querySelectorAll('.dropdown-submenu .dropdown-menu').forEach((submenu) => {
|
|
submenu.classList.remove('show');
|
|
});
|
|
});
|
|
});
|
|
|
|
// Close submenu when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.dropdown-submenu')) {
|
|
document.querySelectorAll('.dropdown-submenu .dropdown-menu.show').forEach((submenu) => {
|
|
submenu.classList.remove('show');
|
|
});
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<!-- Maintenance Mode Overlay -->
|
|
<div id="maintenance-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 9999; backdrop-filter: blur(5px);">
|
|
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: white; max-width: 500px; padding: 2rem;">
|
|
<div style="font-size: 4rem; margin-bottom: 1rem;">🔧</div>
|
|
<h2 style="font-weight: 700; margin-bottom: 1rem;">System under vedligeholdelse</h2>
|
|
<p id="maintenance-message" style="font-size: 1.1rem; margin-bottom: 1.5rem; opacity: 0.9;">Systemet er midlertidigt utilgængeligt på grund af vedligeholdelse.</p>
|
|
<div id="maintenance-eta" style="font-size: 1rem; margin-bottom: 2rem; opacity: 0.8;"></div>
|
|
<div class="spinner-border text-light" role="status" style="width: 3rem; height: 3rem;">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p style="margin-top: 1.5rem; font-size: 0.9rem; opacity: 0.7;">
|
|
Siden opdateres automatisk når systemet er klar igen.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Check maintenance mode status
|
|
let maintenanceCheckInterval = null;
|
|
|
|
function checkMaintenanceMode() {
|
|
fetch('/api/v1/system/maintenance')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
// Silently ignore 404 - maintenance endpoint not implemented yet
|
|
return null;
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (!data) return; // Skip if endpoint doesn't exist
|
|
|
|
const overlay = document.getElementById('maintenance-overlay');
|
|
const messageEl = document.getElementById('maintenance-message');
|
|
const etaEl = document.getElementById('maintenance-eta');
|
|
|
|
if (data.maintenance_mode) {
|
|
// Show overlay
|
|
overlay.style.display = 'block';
|
|
|
|
// Update message
|
|
if (data.maintenance_message) {
|
|
messageEl.textContent = data.maintenance_message;
|
|
}
|
|
|
|
// Update ETA
|
|
if (data.maintenance_eta_minutes) {
|
|
etaEl.textContent = `Estimeret tid: ${data.maintenance_eta_minutes} minutter`;
|
|
} else {
|
|
etaEl.textContent = '';
|
|
}
|
|
|
|
// Start polling every 5 seconds if not already polling
|
|
if (!maintenanceCheckInterval) {
|
|
maintenanceCheckInterval = setInterval(checkMaintenanceMode, 5000);
|
|
}
|
|
} else {
|
|
// Hide overlay
|
|
overlay.style.display = 'none';
|
|
|
|
// Stop polling if maintenance is over
|
|
if (maintenanceCheckInterval) {
|
|
clearInterval(maintenanceCheckInterval);
|
|
maintenanceCheckInterval = null;
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
// Silently ignore errors - maintenance check is not critical
|
|
});
|
|
}
|
|
|
|
// Check on page load (optional feature, don't block if not available)
|
|
checkMaintenanceMode();
|
|
|
|
// Check periodically (every 30 seconds when not in maintenance)
|
|
setInterval(() => {
|
|
if (!maintenanceCheckInterval) {
|
|
checkMaintenanceMode();
|
|
}
|
|
}, 30000);
|
|
</script>
|
|
|
|
{% block extra_js %}{% endblock %}
|
|
</body>
|
|
</html> |