- Updated billing frontend views to use Jinja2 templates for rendering HTML pages. - Added support for displaying supplier invoices, template builder, and templates list with titles. - Introduced a new configuration setting for company CVR number. - Enhanced OllamaService to support credit notes in invoice extraction, including detailed JSON output format. - Improved PDF text extraction using pdfplumber for better layout handling. - Added a modal for editing vendor details with comprehensive fields and validation. - Implemented invoice loading and display functionality in vendor detail view. - Updated vendor management to remove priority handling and improve error messaging. - Added tests for AI analyze endpoint and CVR filtering to ensure correct behavior. - Created migration script to support credit notes in the database schema.
763 lines
38 KiB
HTML
763 lines
38 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);
|
|
}
|
|
|
|
.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);
|
|
}
|
|
</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="#">Sager</a></li>
|
|
<li><a class="dropdown-item py-2" href="#">Ny Sag</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>Kassekladde</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><a class="dropdown-item py-2" href="#">Rapporter</a></li>
|
|
</ul>
|
|
</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="/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>
|
|
|
|
<div class="container-fluid px-4 py-4">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.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
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const searchModal = new bootstrap.Modal(document.getElementById('globalSearchModal'));
|
|
const searchInput = document.getElementById('globalSearchInput');
|
|
|
|
// 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(() => {
|
|
searchInput.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', () => {
|
|
searchInput.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';
|
|
}
|
|
|
|
let searchTimeout;
|
|
let selectedEntity = null;
|
|
|
|
// 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
|
|
searchInput.addEventListener('input', (e) => {
|
|
const query = e.target.value.trim();
|
|
|
|
clearTimeout(searchTimeout);
|
|
|
|
// Reset empty state text
|
|
const emptyState = document.getElementById('emptyState');
|
|
emptyState.innerHTML = `
|
|
<i class="bi bi-search text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
|
|
<p class="text-muted mt-3">Begynd at skrive for at søge...</p>
|
|
`;
|
|
|
|
if (query.length < 2) {
|
|
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';
|
|
selectedEntity = null;
|
|
return;
|
|
}
|
|
|
|
searchTimeout = setTimeout(async () => {
|
|
try {
|
|
const response = await fetch(`/api/v1/dashboard/search?q=${encodeURIComponent(query)}`);
|
|
const data = await response.json();
|
|
|
|
emptyState.style.display = 'none';
|
|
|
|
// CRM Results (Customers + Contacts + Vendors)
|
|
const crmSection = document.getElementById('crmResults');
|
|
const allResults = [
|
|
...(data.customers || []).map(c => ({...c, entityType: 'customer', url: `/customers/${c.id}`, icon: 'building'})),
|
|
...(data.contacts || []).map(c => ({...c, entityType: 'contact', url: `/contacts/${c.id}`, icon: 'person'})),
|
|
...(data.vendors || []).map(c => ({...c, entityType: 'vendor', url: `/vendors/${c.id}`, icon: 'shop'}))
|
|
];
|
|
|
|
if (allResults.length > 0) {
|
|
crmSection.style.display = 'block';
|
|
crmSection.querySelector('.result-items').innerHTML = allResults.map(item => `
|
|
<div class="result-item p-3 mb-2 rounded" style="background: var(--bg-card); border: 1px solid transparent; transition: all 0.2s; cursor: pointer;"
|
|
onclick='showWorkflows("${item.entityType}", ${JSON.stringify(item).replace(/'/g, "'")})'>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="d-flex align-items-center">
|
|
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-3" style="width: 32px; height: 32px;">
|
|
<i class="bi bi-${item.icon} text-primary"></i>
|
|
</div>
|
|
<div>
|
|
<p class="mb-0 fw-bold" style="color: var(--text-primary);">${item.name}</p>
|
|
<p class="mb-0 small text-muted">${item.type} ${item.email ? '• ' + item.email : ''}</p>
|
|
</div>
|
|
</div>
|
|
<i class="bi bi-chevron-right" style="color: var(--accent);"></i>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Auto-select first result
|
|
if (allResults.length > 0) {
|
|
showWorkflows(allResults[0].entityType, allResults[0]);
|
|
}
|
|
} else {
|
|
crmSection.style.display = 'none';
|
|
emptyState.style.display = 'block';
|
|
emptyState.innerHTML = `
|
|
<i class="bi bi-search text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
|
|
<p class="text-muted mt-3">Ingen resultater fundet for "${query}"</p>
|
|
`;
|
|
}
|
|
|
|
// Hide other sections for now as we don't have real data for them yet
|
|
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';
|
|
|
|
} catch (error) {
|
|
console.error('Search error:', error);
|
|
}
|
|
}, 300); // Debounce 300ms
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
</script>
|
|
{% block extra_js %}{% endblock %}
|
|
</body>
|
|
</html> |