bmc_hub/app/shared/frontend/base.html

3695 lines
181 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230f4c75'/%3E%3Ctext x='32' y='42' text-anchor='middle' font-size='30' font-family='Arial, sans-serif' fill='white'%3EB%3C/text%3E%3C/svg%3E">
<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;
--bg-card-rgb: 255, 255, 255;
--text-primary: #2c3e50;
--text-primary-rgb: 44, 62, 80;
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
--frame-border: rgba(15, 76, 117, 0.18);
--frame-border-strong: rgba(15, 76, 117, 0.28);
--frame-shadow: 0 4px 12px rgba(15, 76, 117, 0.10);
--border-radius: 12px;
--bottom-bar-height: 50px;
--bottom-bar-expanded-height: 68vh;
--bottom-bar-zindex: 1030;
}
/* Keep the content viewport stable when a tall dropdown opens/closes.
Without a reserved scrollbar gutter, wide case hero sections visibly
jump a few pixels on smaller monitors. */
html {
scrollbar-gutter: stable;
}
@supports not (scrollbar-gutter: stable) {
html { overflow-y: scroll; }
}
[data-bs-theme="dark"] {
--bg-body: #212529;
--bg-card: #2c3034;
--bg-card-rgb: 44, 48, 52;
--text-primary: #f8f9fa;
--text-primary-rgb: 248, 249, 250;
--text-secondary: #adb5bd;
--accent: #3d8bfd; /* Lighter blue for dark mode */
--accent-light: #373b3e;
--frame-border: rgba(117, 167, 204, 0.30);
--frame-border-strong: rgba(117, 167, 204, 0.45);
--frame-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding-top: 68px;
transition: background-color 0.3s, color 0.3s;
}
body.bottom-bar-visible {
padding-bottom: calc(var(--bottom-bar-height) + 10px);
}
body.bottom-bar-visible.bottom-bar-expanded {
padding-bottom: calc(var(--bottom-bar-height) + var(--bottom-bar-expanded-height));
}
.global-bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: var(--bottom-bar-zindex);
background: rgba(var(--bg-card-rgb), 0.96);
backdrop-filter: blur(18px) saturate(1.15);
-webkit-backdrop-filter: blur(18px) saturate(1.15);
border: 1px solid rgba(var(--text-primary-rgb), 0.1);
border-bottom: 0;
box-shadow: 0 -12px 38px rgba(15, 23, 42, 0.14);
border-top-left-radius: 20px;
border-top-right-radius: 20px;
min-height: var(--bottom-bar-height);
padding: 0.5rem 1rem calc(0.5rem + env(safe-area-inset-bottom, 0px));
transform: translateY(calc(100% + 12px));
opacity: 0;
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
isolation: isolate;
}
.global-bottom-bar::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
background: linear-gradient(120deg, rgba(15, 76, 117, 0.08), rgba(15, 76, 117, 0.02) 35%, rgba(255, 255, 255, 0));
z-index: -1;
}
.global-bottom-bar.is-visible {
transform: translateY(0);
opacity: 1;
}
.global-bottom-bar .bb-header {
min-height: calc(var(--bottom-bar-height) - 10px);
display: flex;
align-items: center;
gap: 0.6rem;
justify-content: space-between;
}
.global-bottom-bar .bb-zone {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.global-bottom-bar .bb-zone-left {
flex: 1;
justify-content: flex-start;
overflow-x: auto;
scrollbar-width: none;
}
.global-bottom-bar .bb-zone-left .bb-back-btn {
position: sticky;
left: 0;
z-index: 3;
background: var(--bg-card);
border-color: rgba(var(--text-primary-rgb), 0.2);
margin-right: 0.2rem;
flex: 0 0 auto;
}
.global-bottom-bar .bb-zone-left::-webkit-scrollbar {
display: none;
}
.global-bottom-bar .bb-zone-center {
flex: 0 0 auto;
justify-content: center;
}
.global-bottom-bar .bb-zone-right {
flex: 0 0 auto;
justify-content: flex-end;
}
.global-bottom-bar .bb-count-line {
flex: 1;
background: transparent;
color: var(--text-primary);
padding: 0 0.25rem;
font-size: 0.84rem;
font-weight: 600;
line-height: 1.35;
white-space: nowrap;
overflow-x: auto;
scrollbar-width: none; /* Cleaner look without scrollbar */
display: flex;
align-items: center;
gap: 0.6rem;
}
.global-bottom-bar .bb-count-line::-webkit-scrollbar {
display: none;
}
.global-bottom-bar .bb-sheet-toggle {
border: 1px solid rgba(15, 76, 117, 0.18);
background: var(--accent-light);
color: var(--text-primary);
border-radius: 10px;
width: 38px;
height: 34px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
transition: all 0.2s ease;
}
.global-bottom-bar .bb-sheet-toggle:hover {
background: var(--accent);
color: #fff;
transform: translateY(-1px);
}
.global-bottom-bar .bb-sheet-toggle span { display: none; }
.global-bottom-bar .bb-sheet-toggle i {
transition: transform 0.25s ease;
}
.global-bottom-bar .bb-action-btn {
border: 1px solid rgba(var(--text-primary-rgb), 0.12);
background: var(--bg-card);
color: var(--text-primary);
border-radius: 9px;
padding: 0.3rem 0.75rem;
font-size: 0.8rem;
font-weight: 600;
line-height: 1.2;
transition: all 0.2s ease;
}
.global-bottom-bar .dropdown-menu {
z-index: calc(var(--bottom-bar-zindex) + 40);
}
.global-bottom-bar .bb-action-btn:hover {
border-color: rgba(var(--text-primary-rgb), 0.25);
background: var(--accent-light);
color: var(--accent);
}
.global-bottom-bar .bb-search-btn {
min-width: 40px;
justify-content: center;
}
.global-bottom-bar .bb-activity-chip {
border: 1px solid rgba(var(--text-primary-rgb), 0.12);
border-radius: 999px;
padding: 0.28rem 0.7rem;
font-size: 0.8rem;
background: var(--bg-card);
color: var(--text-primary);
display: inline-flex;
align-items: center;
gap: 0.3rem;
white-space: nowrap;
}
.global-bottom-bar .bb-activity-chip.is-hidden {
display: none;
}
.global-bottom-bar .bb-activity-chip.is-paused {
border-color: rgba(245, 158, 11, 0.45);
background: rgba(245, 158, 11, 0.12);
color: #9a6700;
}
.global-bottom-bar .bb-notification-count {
background: var(--accent);
color: #fff;
border-radius: 999px;
min-width: 1.2rem;
height: 1.2rem;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
padding: 0 0.3rem;
}
.global-bottom-bar.is-expanded .bb-sheet-toggle i {
transform: rotate(180deg);
}
.global-bottom-bar .bb-chip {
border: 1px solid rgba(var(--text-primary-rgb), 0.12);
background: rgba(var(--text-primary-rgb), 0.045);
color: var(--text-primary);
border-radius: 10px;
padding: 0.34rem 0.65rem;
font-size: 0.78rem;
font-weight: 600;
line-height: 1.2;
cursor: pointer;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 0.35rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
transition: all 0.2s ease;
}
.global-bottom-bar .bb-chip .bb-chip-label {
opacity: 0.95;
}
.global-bottom-bar .bb-chip .bb-chip-bubble {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.35rem;
height: 1.35rem;
border-radius: 999px;
padding: 0 0.35rem;
font-size: 0.72rem;
font-weight: 700;
background: rgba(var(--text-primary-rgb), 0.15);
color: currentColor;
}
.global-bottom-bar .bb-chip:hover {
transform: translateY(-1px);
box-shadow: 0 3px 8px rgba(0,0,0,0.08);
border-color: rgba(var(--text-primary-rgb), 0.2);
}
.global-bottom-bar .bb-chip.is-active {
background: var(--accent);
color: white;
border-color: var(--accent);
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.3);
}
.global-bottom-bar .bb-chip.sev-ok {
background: rgba(25, 135, 84, 0.14);
border-color: rgba(25, 135, 84, 0.35);
color: #146c43;
}
.global-bottom-bar .bb-chip.sev-ok .bb-chip-bubble {
background: rgba(25, 135, 84, 0.2);
}
.global-bottom-bar .bb-chip.sev-warn {
background: rgba(255, 193, 7, 0.22);
border-color: rgba(255, 193, 7, 0.5);
color: #8a6d00;
}
.global-bottom-bar .bb-chip.sev-warn .bb-chip-bubble {
background: rgba(255, 193, 7, 0.3);
}
.global-bottom-bar .bb-chip.sev-critical {
background: rgba(220, 53, 69, 0.14);
border-color: rgba(220, 53, 69, 0.4);
color: #b02a37;
}
.global-bottom-bar .bb-chip.sev-critical .bb-chip-bubble {
background: rgba(220, 53, 69, 0.22);
}
[data-bs-theme="dark"] .global-bottom-bar .bb-chip.sev-ok {
color: #75d39a;
}
[data-bs-theme="dark"] .global-bottom-bar .bb-chip.sev-warn {
color: #ffd166;
}
[data-bs-theme="dark"] .global-bottom-bar .bb-chip.sev-critical {
color: #ff9aa2;
}
[data-bs-theme="dark"] .global-bottom-bar .bb-chip .bb-chip-bubble {
background: rgba(255, 255, 255, 0.12);
}
[data-bs-theme="dark"] .global-bottom-bar .bb-chip.is-active {
color: #fff;
}
.global-bottom-bar .bb-chip:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.global-bottom-bar .bb-detail-line {
display: none;
margin-top: 0.5rem;
background: transparent;
padding: 0 0.5rem;
font-size: 0.82rem;
color: var(--text-secondary);
white-space: nowrap;
overflow-x: auto;
scrollbar-width: none;
}
.global-bottom-bar.is-expanded .bb-detail-line {
display: block;
}
.global-bottom-bar:not(.is-expanded) .bb-header {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: none;
}
.global-bottom-bar:not(.is-expanded) .bb-header::-webkit-scrollbar {
display: none;
}
.global-bottom-bar .bb-sheet-panel {
margin-top: 0.4rem;
max-height: 0;
opacity: 0;
overflow: hidden;
transition: max-height 0.28s ease, opacity 0.2s ease;
}
.global-bottom-bar.is-expanded .bb-sheet-panel {
max-height: min(var(--bottom-bar-expanded-height), 590px);
opacity: 1;
}
.global-bottom-bar .bb-sheet-inner {
background: var(--bg-card);
border: 1px solid rgba(var(--text-primary-rgb), 0.1);
border-radius: 16px;
display: flex;
flex-direction: row;
min-height: 320px;
max-height: min(var(--bottom-bar-expanded-height), 540px);
height: min(var(--bottom-bar-expanded-height), 540px);
overflow: hidden;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
margin-top: 0.5rem;
}
.global-bottom-bar .bb-sheet-inner > * {
min-height: 0;
}
.global-bottom-bar .bb-side-tabs {
width: 176px;
flex: 0 0 176px;
border-right: 1px solid rgba(var(--text-primary-rgb), 0.08);
border-bottom: 0;
background: color-mix(in srgb, var(--accent) 4%, var(--bg-card));
padding: 0.85rem 0.65rem;
display: flex;
flex-direction: column;
gap: 0.3rem;
align-items: stretch;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
}
.global-bottom-bar .bb-side-tabs::-webkit-scrollbar { display: none; }
.global-bottom-bar .bb-tab-btn {
border: 1px solid transparent;
background: transparent;
color: var(--text-secondary);
border-radius: 9px;
text-align: left;
font-size: 0.84rem;
font-weight: 600;
padding: 0.62rem 0.7rem;
line-height: 1.3;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
flex: 0 0 auto;
width: 100%;
}
.global-bottom-bar .bb-tab-btn i {
font-size: 1rem;
opacity: 0.7;
}
.global-bottom-bar .bb-tab-btn .bb-tab-badge {
display: none;
align-items: center;
justify-content: center;
min-width: 1.2rem;
height: 1.2rem;
border-radius: 999px;
padding: 0 0.35rem;
margin-left: auto;
font-size: 0.7rem;
font-weight: 700;
background: rgba(220, 53, 69, 0.14);
color: #b02a37;
}
.global-bottom-bar .bb-tab-btn.has-unread .bb-tab-badge {
display: inline-flex;
}
.global-bottom-bar .bb-tab-btn:hover {
background: rgba(var(--text-primary-rgb), 0.05);
color: var(--text-primary);
}
.global-bottom-bar .bb-tab-btn.is-active {
background: var(--accent);
color: #fff;
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.22);
font-weight: 700;
}
.global-bottom-bar .bb-tab-btn.is-active i {
opacity: 1;
color: inherit;
}
[data-bs-theme="dark"] .global-bottom-bar .bb-tab-btn .bb-tab-badge {
background: rgba(255, 138, 148, 0.18);
color: #ffb3ba;
}
.global-bottom-bar .bb-tab-content {
padding: 1rem 1.15rem 1.1rem;
overflow: hidden;
display: flex;
flex-direction: column;
flex: 1 1 0;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
}
.global-bottom-bar .bb-tab-title {
font-size: 1.05rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.15rem;
display: flex;
align-items: center;
gap: 0.5rem;
line-height: 1.15;
}
.global-bottom-bar .bb-tab-heading { flex: 0 0 auto; padding-bottom: 0.75rem; margin-bottom: 0.75rem; border-bottom: 1px solid rgba(var(--text-primary-rgb), 0.08); }
.global-bottom-bar .bb-tab-description { color: var(--text-secondary); font-size: 0.78rem; line-height: 1.35; }
.global-bottom-bar .bb-tab-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.45rem;
}
.global-bottom-bar .bb-tab-list li {
border: 1px solid rgba(var(--text-primary-rgb), 0.08);
background: color-mix(in srgb, var(--bg-card) 96%, var(--text-primary));
border-radius: 11px;
padding: 0.68rem 0.78rem;
font-size: 0.88rem;
line-height: 1.4;
color: var(--text-primary);
box-shadow: none;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.global-bottom-bar .bb-tab-list li:hover {
transform: translateY(-1px);
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
}
.global-bottom-bar .bb-tab-content .bb-tab-list { overflow-y: auto; min-height: 0; padding-right: 0.25rem; }
.global-bottom-bar .bb-task-actions { display: flex; justify-content: flex-end; margin: 0 0 0.75rem !important; width: 100%; }
.global-bottom-bar .bb-task-actions .btn { width: auto !important; box-shadow: none !important; }
.global-bottom-bar .bb-boss-hero {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.8rem 0.9rem;
border-radius: 12px;
color: #fff;
background: linear-gradient(120deg, #0f4c75, #176b87);
}
.global-bottom-bar .bb-boss-eyebrow { font-size: 0.72rem; opacity: 0.8; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
.global-bottom-bar .bb-boss-heading { font-weight: 700; font-size: 0.95rem; margin-top: 0.1rem; }
.global-bottom-bar .bb-boss-hero .btn { background: #fff; color: #0f4c75; border: 0; white-space: nowrap; }
.global-bottom-bar .bb-boss-kpis .border { border-color: rgba(var(--text-primary-rgb), 0.08) !important; border-radius: 10px !important; }
.global-bottom-bar .bb-support-queue-card {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.6rem;
padding: 0.65rem 0.7rem;
border: 1px solid rgba(245, 158, 11, 0.28);
border-left: 4px solid #f59e0b;
border-radius: 10px;
background: rgba(245, 158, 11, 0.055);
margin-bottom: 0.45rem;
}
.global-bottom-bar .bb-support-queue-main { min-width: 0; }
.global-bottom-bar .bb-support-queue-main strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.global-bottom-bar .bb-support-queue-actions { display: flex; align-items: center; gap: 0.35rem; flex: 0 0 auto; }
.global-bottom-bar .bb-support-queue-actions select { width: 175px; }
.global-bottom-bar .bb-boss-empty { padding: 0.75rem; border-radius: 10px; color: #146c43; background: rgba(25, 135, 84, 0.1); font-weight: 600; }
.global-bottom-bar .bb-messages-layout {
display: flex;
flex-direction: column;
gap: 0.75rem;
min-height: 0;
}
.global-bottom-bar .bb-message-threads {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding: 0.1rem 0 0.45rem;
scrollbar-width: none;
}
.global-bottom-bar .bb-message-threads::-webkit-scrollbar {
display: none;
}
.global-bottom-bar .bb-message-thread {
border: 1px solid rgba(var(--text-primary-rgb), 0.1);
background: var(--bg-card);
color: var(--text-primary);
border-radius: 999px;
padding: 0.42rem 0.75rem;
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-size: 0.78rem;
font-weight: 600;
white-space: nowrap;
}
.global-bottom-bar .bb-message-thread.is-active {
background: var(--accent);
color: #fff;
border-color: transparent;
box-shadow: 0 8px 20px rgba(15, 76, 117, 0.18);
}
.global-bottom-bar .bb-message-thread-count {
min-width: 1.2rem;
height: 1.2rem;
border-radius: 999px;
background: rgba(220, 53, 69, 0.14);
color: #b42318;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
padding: 0 0.35rem;
}
.global-bottom-bar .bb-message-thread.is-active .bb-message-thread-count {
background: rgba(255, 255, 255, 0.18);
color: #fff;
}
.global-bottom-bar .bb-messages-list {
max-height: none;
flex: 1 1 auto;
overflow-y: auto;
padding-right: 0.2rem;
}
.global-bottom-bar #bbTabInnerContent {
flex: 1 1 auto;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
}
.global-bottom-bar #bbTabInnerContent > * { width: 100%; min-width: 0; }
.global-bottom-bar .bb-panel-overview,
.global-bottom-bar .bb-panel-tasks,
.global-bottom-bar .bb-panel-boss { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); align-content: start; }
.global-bottom-bar .bb-panel-timer { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); align-content: start; }
.global-bottom-bar .bb-notes-layout {
display: flex;
flex-direction: column;
gap: 0.75rem;
height: 100%;
min-height: 0;
overflow: hidden;
}
.global-bottom-bar .bb-notes-editor {
min-width: 0;
}
.global-bottom-bar .bb-notes-list {
min-height: 0;
overflow-y: auto;
padding-right: 0.2rem;
}
.global-bottom-bar .bb-messages-composer {
border: 1px solid rgba(var(--text-primary-rgb), 0.08);
background: color-mix(in srgb, var(--accent) 3%, var(--bg-card));
border-radius: 12px;
padding: 0.65rem;
margin-top: 0.15rem;
}
.global-bottom-bar .bb-messages-layout {
height: 100%;
overflow: hidden;
}
.global-bottom-bar .bb-messages-list li { border: 0; background: transparent; padding: 0.2rem 0.1rem; }
.global-bottom-bar .bb-messages-list li:hover { transform: none; box-shadow: none; }
.global-bottom-bar .bb-notes-editor > li { border: 0; background: transparent; padding: 0; }
.global-bottom-bar .bb-notes-editor > li > .border { border: 1px solid color-mix(in srgb, var(--accent) 18%, transparent) !important; border-radius: 12px !important; background: color-mix(in srgb, var(--accent) 4%, var(--bg-card)) !important; padding: 0.8rem !important; margin: 0 !important; }
.global-bottom-bar .bb-boss-kpis { display: grid; grid-template-columns: repeat(4, minmax(100px, 1fr)); }
.global-bottom-bar .bb-boss-kpis > [class*="col-"] { width: auto; }
.global-bottom-bar .bb-panel-timer li { border-color: rgba(25, 135, 84, 0.18); background: rgba(25, 135, 84, 0.045); }
.global-bottom-bar .bb-timer-case-link { cursor: pointer; }
.global-bottom-bar .bb-timer-case-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 8px; }
.global-bottom-bar .bb-timer-work-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.3rem; overflow-y: auto; min-height: 0; }
.global-bottom-bar .bb-timer-work-card { display: flex; align-items: center; justify-content: space-between; gap: 0.55rem; min-height: 38px; padding: 0.28rem 0.45rem; border: 1px solid rgba(var(--text-primary-rgb), 0.1); border-radius: 8px; background: var(--bg-card); cursor: pointer; font-size: 0.72rem; }
.global-bottom-bar .bb-timer-work-main { display: flex; align-items: center; gap: 0.45rem; min-width: 0; flex: 1 1 auto; white-space: nowrap; overflow: hidden; }
.global-bottom-bar .bb-timer-work-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.76rem; }
.global-bottom-bar .bb-timer-work-meta { color: var(--text-secondary); flex: 0 0 auto; font-size: 0.68rem; white-space: nowrap; }
.global-bottom-bar .bb-timer-work-actions { display: flex; gap: 0.25rem; flex: 0 0 auto; }
.global-bottom-bar .bb-timer-work-card .btn { --bs-btn-padding-y: 0.18rem; --bs-btn-padding-x: 0.4rem; --bs-btn-font-size: 0.68rem; }
.global-bottom-bar .bb-panel-tasks li { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.global-bottom-bar .bb-panel-empty { min-height: 140px; display: grid; place-items: center; text-align: center; color: var(--text-secondary); }
.global-bottom-bar .bb-panel-empty i { display: block; font-size: 1.8rem; color: color-mix(in srgb, var(--accent) 55%, var(--text-secondary)); margin-bottom: 0.35rem; }
.global-bottom-bar .bb-location-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0.5rem; overflow-y: auto; min-height: 0; padding-right: 0.25rem; }
.global-bottom-bar .bb-location-card { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; min-height: 58px; padding: 0.55rem 0.65rem; color: inherit; text-decoration: none; border: 1px solid rgba(var(--text-primary-rgb), 0.1); border-radius: 10px; background: var(--bg-card); }
.global-bottom-bar .bb-location-card:hover { color: inherit; border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: color-mix(in srgb, var(--accent) 5%, var(--bg-card)); }
.global-bottom-bar .bb-live-indicator { width: 0.58rem; height: 0.58rem; border-radius: 50%; background: #198754; box-shadow: 0 0 0 5px rgba(25, 135, 84, 0.12); display: inline-block; margin-right: 0.55rem; }
.global-bottom-bar .bb-detail-line {
min-height: 28px;
padding: 0.2rem 0.2rem 0;
font-size: 0.8rem;
line-height: 1.2;
}
.global-bottom-bar.is-expanded .bb-detail-line {
padding-bottom: 0.1rem;
}
#bbSwitchCaseModal .modal-content {
border: 1px solid rgba(var(--text-primary-rgb), 0.12);
border-radius: 14px;
background: var(--bg-card);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.18);
}
#bbSwitchCaseModal .modal-header {
border-bottom: 1px solid rgba(var(--text-primary-rgb), 0.08);
}
#bbSwitchCaseModal .modal-dialog { max-width: 1080px; }
#bbSwitchCaseModal .modal-body { padding: 1rem 1.15rem 1.2rem; }
#bbSwitchCaseModal .bb-switch-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.7rem 0.8rem; margin-bottom: 1rem; border: 1px solid rgba(var(--text-primary-rgb), 0.08); border-radius: 12px; background: color-mix(in srgb, var(--accent) 4%, var(--bg-card)); }
#bbSwitchCaseModal .bb-switch-search { max-width: 360px; }
#bbSwitchCaseModal .bb-switch-section-title { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary); }
#bbSwitchCaseModal .bb-switch-timers { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.35rem; margin-bottom: 1rem; }
#bbSwitchCaseModal .bb-switch-timer, #bbSwitchCaseModal .bb-switch-case { border: 1px solid rgba(var(--text-primary-rgb), 0.1); border-radius: 11px; background: var(--bg-card); }
#bbSwitchCaseModal .bb-switch-timer { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 0.7rem; }
#bbSwitchCaseModal .bb-switch-timer-info { display: flex; align-items: center; gap: 0.55rem; flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; }
#bbSwitchCaseModal .bb-switch-info-title { min-width: 120px; max-width: 34%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#bbSwitchCaseModal .bb-switch-info-meta { color: var(--text-secondary); font-size: 0.72rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#bbSwitchCaseModal .bb-switch-status { font-size: 0.66rem; font-weight: 600; }
#bbSwitchCaseModal .bb-switch-timer-pending { width: 100%; min-height: 38px; padding: 0.28rem 0.4rem; gap: 0.35rem; border-left: 2px solid var(--bs-info); font-size: 0.72rem; }
#bbSwitchCaseModal .bb-switch-timer-main { display: flex; align-items: center; gap: 0.35rem; flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; }
#bbSwitchCaseModal .bb-switch-timer-title { color: var(--text-primary); font-size: 0.72rem; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#bbSwitchCaseModal .bb-switch-timer-meta { flex: 0 0 auto; color: var(--text-secondary); font-size: 0.66rem; white-space: nowrap; }
#bbSwitchCaseModal .bb-switch-timer-pending .bb-switch-case-actions { gap: 0.25rem; }
#bbSwitchCaseModal .bb-switch-timer-pending .btn { --bs-btn-padding-y: 0.18rem; --bs-btn-padding-x: 0.38rem; --bs-btn-font-size: 0.68rem; }
#bbSwitchCaseModal .bb-switch-cases { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.55rem; }
#bbSwitchCaseModal .bb-switch-case { display: flex; align-items: center; gap: 0.75rem; padding: 0.72rem; min-width: 0; }
#bbSwitchCaseModal .bb-switch-case-main { flex: 1 1 auto; min-width: 0; }
#bbSwitchCaseModal .bb-switch-case-title { font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#bbSwitchCaseModal .bb-switch-case-meta { color: var(--text-secondary); font-size: 0.75rem; margin-top: 0.15rem; }
#bbSwitchCaseModal .bb-switch-case-actions { display: flex; gap: 0.4rem; flex: 0 0 auto; }
#bbSwitchCaseModal .bb-switch-case-actions .btn { white-space: nowrap; }
#bbSwitchCaseModal .bb-switch-empty { grid-column: 1 / -1; padding: 1.4rem; text-align: center; color: var(--text-secondary); border: 1px dashed rgba(var(--text-primary-rgb), 0.14); border-radius: 11px; }
#bbSwitchCaseModal .bb-convert-panel { padding: 0.9rem; border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--accent) 6%, var(--bg-card)); }
#bbSwitchCaseModal.is-converting .bb-switch-search,
#bbSwitchCaseModal.is-converting #bbSwitchTimerActions,
#bbSwitchCaseModal.is-converting .bb-switch-section-title,
#bbSwitchCaseModal.is-converting #bbSwitchTimersList,
#bbSwitchCaseModal.is-converting #bbSwitchRecentCasesList { display: none !important; }
#bbSwitchCaseModal.is-converting .bb-switch-toolbar { grid-template-columns: 1fr; }
#bbSwitchCaseModal.is-converting .modal-dialog { max-width: 980px; }
#bbSwitchCaseModal.is-converting .modal-body { padding-bottom: 1.5rem; }
#bbSwitchCaseModal .list-group-item {
border-color: rgba(var(--text-primary-rgb), 0.08);
transition: background-color 0.2s ease;
}
#bbSwitchCaseModal .list-group-item:hover {
background-color: rgba(var(--text-primary-rgb), 0.03);
}
#bbQuickNoteInput {
border-color: rgba(var(--text-primary-rgb), 0.12);
}
#bbQuickNoteInput:focus {
border-color: var(--accent);
box-shadow: 0 0 0 0.2rem rgba(15, 76, 117, 0.12);
}
@media (max-width: 767.98px) {
:root {
--bottom-bar-expanded-height: 56vh;
}
.global-bottom-bar {
min-height: 56px;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.global-bottom-bar .bb-sheet-inner {
min-height: 240px;
flex-direction: column;
}
.global-bottom-bar.is-expanded .bb-header {
flex-wrap: wrap;
row-gap: 0.4rem;
}
.global-bottom-bar.is-expanded .bb-zone-left,
.global-bottom-bar.is-expanded .bb-zone-center,
.global-bottom-bar.is-expanded .bb-zone-right {
flex: 1 1 100%;
justify-content: flex-start;
}
.global-bottom-bar .bb-side-tabs {
width: 100%;
flex: 0 0 auto;
flex-direction: row;
border-right: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
overflow-x: auto;
}
.global-bottom-bar .bb-tab-btn { width: auto; }
.global-bottom-bar .bb-notes-layout { grid-template-columns: 1fr; overflow-y: auto; }
.global-bottom-bar .bb-boss-kpis { grid-template-columns: repeat(2, 1fr); }
.global-bottom-bar .bb-support-queue-card { align-items: stretch; flex-direction: column; }
.global-bottom-bar .bb-support-queue-actions { width: 100%; }
.global-bottom-bar .bb-support-queue-actions select { width: auto; flex: 1 1 auto; }
#bbSwitchCaseModal .bb-switch-toolbar { align-items: stretch; flex-direction: column; }
#bbSwitchCaseModal .bb-switch-search { max-width: none; }
#bbSwitchCaseModal .bb-switch-cases { grid-template-columns: 1fr; }
}
.navbar {
background: color-mix(in srgb, var(--bg-card) 94%, transparent);
-webkit-backdrop-filter: blur(18px) saturate(1.15);
backdrop-filter: blur(18px) saturate(1.15);
box-shadow: 0 8px 28px rgba(15, 47, 72, 0.07);
padding: 1rem 0;
border-bottom: 1px solid color-mix(in srgb, var(--accent) 14%, transparent);
}
/* The complete BMC menu needs more room than Bootstrap's normal lg
breakpoint. Use a dedicated wide-desktop breakpoint so 1315 inch
monitors get a clean collapsible menu instead of clipped items. */
@media (min-width: 1180px) {
.navbar-expand-wide { flex-wrap: nowrap; justify-content: flex-start; }
.navbar-expand-wide .navbar-toggler { display: none; }
.navbar-expand-wide .navbar-collapse { display: flex !important; flex-basis: auto; }
.navbar-expand-wide .navbar-nav { flex-direction: row; }
.navbar-expand-wide .navbar-nav .dropdown-menu,
.navbar-expand-wide .navbar-actions .dropdown-menu {
position: absolute;
}
}
@media (min-width: 1180px) and (max-width: 1679.98px) {
.navbar-expand-wide {
padding: .55rem 0;
}
.navbar-expand-wide > .container-fluid {
padding-left: .75rem !important;
padding-right: .75rem !important;
}
.navbar-expand-wide .navbar-brand {
width: 44px;
min-width: 44px;
overflow: visible;
margin-right: .55rem;
}
.navbar-expand-wide .navbar-brand > div {
margin-right: 0 !important;
}
.navbar-expand-wide .bmc-brand-copy {
display: none;
}
.navbar-expand-wide .navbar-nav {
margin-left: 0 !important;
margin-right: .25rem !important;
padding: 0;
border: 0;
background: transparent;
}
.navbar-expand-wide .nav-link {
min-height: 38px !important;
padding: .45rem .68rem !important;
margin: 0 .03rem;
font-size: 1rem;
white-space: nowrap;
}
.navbar-expand-wide .navbar-nav .nav-link > i,
.navbar-expand-wide .navbar-actions > .dropdown:first-child .nav-link > i {
display: none;
}
.navbar-expand-wide .navbar-actions {
flex: 1 1 auto;
gap: .12rem !important;
flex-wrap: nowrap;
}
.navbar-expand-wide .navbar-actions #globalSearchBtn {
margin-left: auto;
}
.navbar-expand-wide .navbar-actions > .btn.rounded-circle {
width: 34px;
height: 34px;
min-width: 34px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.navbar-expand-wide .navbar-utility-dock {
gap: .18rem;
padding: .18rem;
border-color: color-mix(in srgb, var(--accent) 8%, transparent);
background: color-mix(in srgb, var(--accent) 3%, var(--bg-card));
}
.navbar-expand-wide .navbar-utility-dock > .btn.rounded-circle {
width: 35px;
height: 35px;
min-width: 35px;
}
.navbar-expand-wide #currentUserDisplayName {
display: none;
}
.navbar-expand-wide #currentUserAvatar {
margin-right: 0 !important;
width: 32px;
height: 32px;
}
}
@media (min-width: 1280px) {
.navbar-expand-wide > .container-fluid {
position: relative;
}
.navbar-expand-wide .navbar-collapse {
position: static;
}
#navbarNav > .navbar-nav {
position: absolute;
left: 50%;
transform: translateX(-50%);
margin-left: 0 !important;
margin-right: 0 !important;
}
#navbarNav > .navbar-actions {
margin-left: auto;
}
}
@media (max-width: 1179.98px) {
.navbar-expand-wide {
padding: .65rem 0;
}
.navbar-expand-wide > .container-fluid {
padding-left: 1rem !important;
padding-right: 1rem !important;
}
.navbar-expand-wide .navbar-toggler {
display: inline-flex;
align-items: center;
gap: .45rem;
border: 1px solid rgba(15, 76, 117, .18);
border-radius: 10px;
padding: .45rem .7rem;
color: var(--accent);
background: var(--accent-light);
font-size: .88rem;
font-weight: 700;
}
.navbar-expand-wide .navbar-toggler:focus {
box-shadow: 0 0 0 .2rem rgba(15, 76, 117, .12);
}
.navbar-expand-wide .navbar-collapse {
max-height: calc(100vh - 76px);
overflow-y: auto;
padding: .85rem 0 1rem;
}
.navbar-expand-wide .navbar-nav {
width: 100%;
margin: 0 !important;
gap: .2rem;
}
.navbar-expand-wide .nav-link {
display: flex;
align-items: center;
min-height: 44px;
margin: 0;
}
.navbar-expand-wide .navbar-actions {
width: 100%;
flex-wrap: wrap;
justify-content: flex-start;
gap: .65rem !important;
border-top: 1px solid rgba(15, 76, 117, .12);
margin-top: .75rem;
padding-top: .85rem;
}
.navbar-expand-wide .navbar-actions > .dropdown:first-child {
flex: 1 0 100%;
}
.navbar-expand-wide .navbar-utility-dock {
width: 100%;
margin-left: 0;
border-radius: 14px;
justify-content: flex-start;
}
.navbar-expand-wide .dropdown-menu {
max-height: min(60vh, 520px);
overflow-y: auto;
}
}
.navbar-brand {
font-weight: 700;
color: var(--accent);
font-size: 1.25rem;
letter-spacing: -0.025em;
}
.navbar-brand > div {
background: linear-gradient(145deg, var(--accent), #2488bd) !important;
box-shadow: 0 6px 14px rgba(15, 76, 117, .2);
}
.bmc-brand-copy {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1.02;
}
.bmc-brand-name {
font-size: 1.08rem;
font-weight: 820;
}
.bmc-brand-subtitle {
margin-top: .16rem;
color: var(--text-secondary);
font-size: .58rem;
font-weight: 750;
letter-spacing: .12em;
text-transform: uppercase;
}
@media (min-width: 1180px) {
#navbarNav > .navbar-nav {
align-items: center;
padding: .24rem;
border: 1px solid color-mix(in srgb, var(--accent) 9%, transparent);
border-radius: 14px;
background: color-mix(in srgb, var(--accent) 4%, var(--bg-card));
}
#navbarNav > .navbar-nav > .nav-item > .nav-link {
min-height: 40px;
padding-top: .52rem !important;
padding-bottom: .52rem !important;
}
#navbarNav > .navbar-nav > .nav-item > .nav-link.active,
#navbarNav > .navbar-nav > .nav-item > .nav-link.show,
#navbarNav > .navbar-nav > .nav-item > .nav-link.section-active {
background: var(--accent);
color: #fff;
box-shadow: 0 7px 16px rgba(15, 76, 117, .2);
}
#navbarNav > .navbar-nav > .nav-item > .nav-link.active::after,
#navbarNav > .navbar-nav > .nav-item > .nav-link.show::after,
#navbarNav > .navbar-nav > .nav-item > .nav-link.section-active::after {
border-top-color: currentColor;
}
}
@media (min-width: 1180px) and (max-width: 1679.98px) {
#navbarNav > .navbar-nav {
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
#navbarNav > .navbar-nav > .nav-item > .nav-link {
min-height: 38px;
padding-top: .45rem !important;
padding-bottom: .45rem !important;
box-shadow: none;
}
#navbarNav > .navbar-nav > .nav-item > .nav-link.active,
#navbarNav > .navbar-nav > .nav-item > .nav-link.show,
#navbarNav > .navbar-nav > .nav-item > .nav-link.section-active {
box-shadow: 0 5px 13px rgba(15, 76, 117, .18);
}
.navbar-actions > .dropdown:first-child > .nav-link {
min-height: 38px;
border-radius: 10px;
}
}
.navbar-utility-dock {
display: flex;
align-items: center;
gap: .35rem;
margin-left: auto;
padding: .28rem;
border: 1px solid color-mix(in srgb, var(--accent) 10%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--accent) 4%, var(--bg-card));
}
.navbar-utility-dock > .btn.rounded-circle {
width: 38px;
height: 38px;
min-width: 38px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0 !important;
background: transparent !important;
box-shadow: none;
}
.navbar-utility-dock > .btn.rounded-circle:hover {
background: var(--accent-light) !important;
transform: none;
box-shadow: none;
}
.navbar-utility-dock > .dropdown:last-child > a {
min-height: 38px;
padding: .15rem .3rem .15rem .2rem;
border-radius: 999px;
}
.navbar-utility-dock > .dropdown:last-child > a:hover {
background: var(--accent-light);
}
@media (min-width: 1180px) and (max-width: 1679.98px) {
.navbar-expand-wide .navbar-utility-dock {
gap: .16rem;
padding: .16rem;
border-color: color-mix(in srgb, var(--accent) 8%, transparent);
background: color-mix(in srgb, var(--accent) 3%, var(--bg-card));
}
.navbar-expand-wide .navbar-utility-dock > .btn.rounded-circle {
width: 35px;
height: 35px;
min-width: 35px;
}
}
.nav-link {
color: var(--text-secondary);
padding: 0.6rem 1.2rem !important;
border-radius: 11px;
transition: background-color .16s ease, color .16s ease, box-shadow .16s ease;
font-weight: 650;
margin: 0 0.2rem;
}
.nav-link:hover, .nav-link.active, .nav-link.show, .nav-link.section-active {
background-color: var(--accent-light);
color: var(--accent);
}
#navbarNav > .navbar-nav > .nav-item > .nav-link.active,
#navbarNav > .navbar-nav > .nav-item > .nav-link.show,
#navbarNav > .navbar-nav > .nav-item > .nav-link.section-active {
box-shadow: inset 0 -2px 0 var(--accent);
}
/* Top-level labels are clearer and use materially less horizontal room
without decorative icons. Icons inside dropdown items stay visible. */
#navbarNav > .navbar-nav > .nav-item > .nav-link > i,
#navbarNav > .navbar-actions > .dropdown:first-child > .nav-link > i {
display: none;
}
.card {
border: 2px solid var(--frame-border-strong);
border-left: 4px solid var(--accent);
border-radius: var(--border-radius);
box-shadow: var(--frame-shadow);
transition: transform 0.2s, background-color 0.3s, border-color 0.2s, box-shadow 0.2s;
background: linear-gradient(165deg, color-mix(in srgb, var(--accent) 6%, var(--bg-card)) 0%, var(--bg-card) 100%);
overflow: hidden;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(15, 76, 117, 0.16);
}
.card .card-header {
border-bottom: 1px solid color-mix(in srgb, var(--accent) 22%, #d1d5db);
background: color-mix(in srgb, var(--accent) 7%, var(--bg-card));
}
.module-priority-low {
--module-accent: #64748b;
}
.module-priority-normal {
--module-accent: var(--accent);
}
.module-priority-high {
--module-accent: #d97706;
}
.module-priority-critical {
--module-accent: #dc2626;
}
.module-card,
.left-module-card,
.right-module-card {
border-color: var(--frame-border-strong) !important;
border-left-color: var(--module-accent, var(--accent)) !important;
background: linear-gradient(165deg, color-mix(in srgb, var(--module-accent, var(--accent)) 6%, var(--bg-card)) 0%, var(--bg-card) 100%);
}
[data-bs-theme="dark"] .card {
border-color: var(--frame-border-strong);
box-shadow: var(--frame-shadow);
background: linear-gradient(165deg, color-mix(in srgb, var(--accent) 12%, rgba(18, 28, 40, 0.94)) 0%, rgba(18, 28, 40, 0.94) 100%);
}
[data-bs-theme="dark"] .card .card-header {
border-bottom-color: color-mix(in srgb, var(--accent) 45%, #4b5563);
background: color-mix(in srgb, var(--accent) 18%, rgba(18, 28, 40, 0.98));
}
[data-bs-theme="dark"] .module-card,
[data-bs-theme="dark"] .left-module-card,
[data-bs-theme="dark"] .right-module-card {
border-color: var(--frame-border-strong) !important;
border-left-color: var(--module-accent, var(--accent)) !important;
background: linear-gradient(165deg, color-mix(in srgb, var(--module-accent, #69a6d5) 12%, rgba(18, 28, 40, 0.94)) 0%, rgba(18, 28, 40, 0.94) 100%);
}
.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 {
min-width: 245px;
border: 1px solid color-mix(in srgb, var(--accent) 13%, transparent);
box-shadow: 0 18px 46px rgba(15, 47, 72, .16), 0 3px 10px rgba(15, 47, 72, .07);
border-radius: 16px;
padding: .55rem;
background: color-mix(in srgb, var(--bg-card) 97%, transparent);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
animation: bmcMenuReveal .14s ease-out;
}
@keyframes bmcMenuReveal {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
#navbarNav .dropdown-item {
display: flex;
align-items: center;
min-height: 39px;
border-radius: 10px;
color: var(--text-primary);
font-size: .95rem;
font-weight: 560;
transition: background-color .14s ease, color .14s ease, padding-left .14s ease;
}
#navbarNav .dropdown-item:hover,
#navbarNav .dropdown-item:focus {
padding-left: .85rem;
background: color-mix(in srgb, var(--accent) 9%, var(--bg-card));
color: var(--accent);
}
#navbarNav .dropdown-item.active {
background: color-mix(in srgb, var(--accent) 14%, var(--bg-card));
color: var(--accent);
font-weight: 750;
}
#navbarNav .dropdown-item > i {
width: 1.35rem;
color: color-mix(in srgb, var(--accent) 82%, var(--text-secondary));
}
#navbarNav .dropdown-header {
padding: .55rem .65rem .3rem;
color: color-mix(in srgb, var(--text-secondary) 84%, var(--accent));
font-size: .66rem;
font-weight: 800;
letter-spacing: .09em;
text-transform: uppercase;
}
#navbarNav .dropdown-divider {
margin: .45rem .35rem;
border-color: color-mix(in srgb, var(--accent) 12%, transparent);
}
.navbar-actions > .btn.rounded-circle {
border: 1px solid transparent !important;
box-shadow: 0 3px 9px rgba(15, 76, 117, .07);
transition: transform .14s ease, box-shadow .14s ease, background-color .14s ease;
}
.navbar-actions > .btn.rounded-circle:hover {
transform: translateY(-1px);
border-color: color-mix(in srgb, var(--accent) 16%, transparent) !important;
box-shadow: 0 7px 16px rgba(15, 76, 117, .13);
}
/* 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);
}
/* Make section headers in nav dropdowns clearly non-clickable labels */
#navbarNav .dropdown-menu li > .dropdown-header {
margin: 0.2rem 0 0.35rem;
padding: 0.35rem 0.75rem;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent);
opacity: 0.8;
pointer-events: none;
user-select: none;
cursor: default;
}
.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 %}
<link rel="stylesheet" href="/static/messages.css?v=1">
</head>
{% set _xff = request.headers.get('x-forwarded-for') if request and request.headers else '' %}
{% set _xff_first = _xff.split(',')[0].strip() if _xff else '' %}
{% set _client_ip = (request.headers.get('cf-connecting-ip') if request and request.headers else '') or (request.headers.get('true-client-ip') if request and request.headers else '') or _xff_first or (request.headers.get('x-real-ip') if request and request.headers else '') or (request.client.host if request and request.client else '') %}
{% set _can_click_to_call = true %}
<body>
<nav class="navbar navbar-expand-wide fixed-top">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center" href="/">
<div class="bmc-brand-mark text-white rounded p-1 me-2 d-flex align-items-center justify-content-center" style="width: 36px; height: 36px;">
<i class="bi bi-hdd-network-fill" style="font-size: 16px;"></i>
</div>
<span class="bmc-brand-copy"><span class="bmc-brand-name">BMC Hub</span><span class="bmc-brand-subtitle">Operations</span></span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Åbn hovedmenu">
<i class="bi bi-list fs-5"></i><span>Menu</span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto">
<li class="nav-item dropdown" data-menu-key="menu-crm">
<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><h6 class="dropdown-header">Kunderelationer</h6></li>
<li data-menu-key="menu-crm-customers"><a class="dropdown-item py-2" href="/customers">Kunder</a></li>
<li data-menu-key="menu-crm-contacts"><a class="dropdown-item py-2" href="/contacts">Kontakter</a></li>
<li data-menu-key="menu-crm-vendors"><a class="dropdown-item py-2" href="/vendors">Leverandører</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Struktur</h6></li>
<li data-menu-key="menu-crm-links"><a class="dropdown-item py-2" href="/links">Links</a></li>
<li data-menu-key="menu-crm-locations"><a class="dropdown-item py-2" href="/app/locations">Lokaliteter</a></li>
<li data-menu-key="menu-crm-cross-fields"><a class="dropdown-item py-2" href="/app/locations/cross-fields">Krydsfelter</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Pipeline</h6></li>
<li data-menu-key="menu-crm-opportunities"><a class="dropdown-item py-2" href="/opportunities"><i class="bi bi-briefcase me-2"></i>Muligheder</a></li>
<li data-menu-key="menu-crm-pipeline"><a class="dropdown-item py-2" href="/pipeline"><i class="bi bi-diagram-3 me-2"></i>Pipeline</a></li>
</ul>
</li>
<li class="nav-item" data-menu-key="menu-sager">
<a class="nav-link" href="/sag">
<i class="bi bi-list-check me-2"></i>Sager
</a>
</li>
<li class="nav-item dropdown" data-menu-key="menu-kalender">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-calendar3 me-2"></i>Kalender
</a>
<ul class="dropdown-menu mt-2">
<li data-menu-key="menu-kalender-overview"><a class="dropdown-item py-2" href="/calendar"><i class="bi bi-calendar3 me-2"></i>Kalenderoversigt</a></li>
<li data-menu-key="menu-planner"><a class="dropdown-item py-2" href="/planner"><i class="bi bi-calendar2-week me-2"></i>Planlægger</a></li>
</ul>
</li>
<li class="nav-item dropdown" data-menu-key="menu-support">
<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><h6 class="dropdown-header">Support</h6></li>
<li data-menu-key="menu-support-conversations"><a class="dropdown-item py-2" href="/conversations/my"><i class="bi bi-mic me-2"></i>Mine Samtaler</a></li>
<li data-menu-key="menu-support-tickets"><a class="dropdown-item py-2" href="/ticket/archived"><i class="bi bi-archive me-2"></i>Arkiverede Tickets</a></li>
<li data-menu-key="menu-support-emails"><a class="dropdown-item py-2" href="/emails"><i class="bi bi-envelope me-2"></i>Email</a></li>
<li data-menu-key="menu-support-telefoni"><a class="dropdown-item py-2" href="/telefoni"><i class="bi bi-telephone me-2"></i>Telefoni</a></li>
<li data-menu-key="menu-support-internet-connections"><a class="dropdown-item py-2" href="/economy/internet-connections"><i class="bi bi-hdd-network me-2"></i>Internetforbindelser</a></li>
<li data-menu-key="menu-support-losninger"><a class="dropdown-item py-2 {{ 'active' if request and (request.url.path.startswith('/solutions') or request.url.path.startswith('/knowledge')) else '' }}" href="/solutions"><i class="bi bi-lightbulb me-2"></i>Løsninger</a></li>
<li data-menu-key="menu-support-mission"><a class="dropdown-item py-2" href="/dashboard/mission-control"><i class="bi bi-broadcast-pin me-2"></i>Mission Control</a></li>
<li data-menu-key="menu-support-anydesk"><a class="dropdown-item py-2" href="/anydesk/sessions"><i class="bi bi-display me-2"></i>AnyDesk Sessions</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Assets & Licenser</h6></li>
<li data-menu-key="menu-support-hardware"><a class="dropdown-item py-2" href="/hardware"><i class="bi bi-laptop me-2"></i>BMC Assets</a></li>
<li data-menu-key="menu-support-hardware-customers"><a class="dropdown-item py-2" href="/hardware/customers"><i class="bi bi-building me-2"></i>Kundehardware</a></li>
<li data-menu-key="menu-support-eset"><a class="dropdown-item py-2" href="/hardware/eset"><i class="bi bi-shield-check me-2"></i>ESET Oversigt</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Værktøjer</h6></li>
<li data-menu-key="menu-support-manual"><a class="dropdown-item py-2" href="/manual"><i class="bi bi-journal-richtext me-2"></i>Manualer</a></li>
<li data-menu-key="menu-support-website-content"><a class="dropdown-item py-2" href="/website-content"><i class="bi bi-window-stack me-2"></i>Website-indhold</a></li>
</ul>
</li>
<li class="nav-item dropdown" data-menu-key="menu-salg">
<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><h6 class="dropdown-header">Salg</h6></li>
<li data-menu-key="menu-salg-orders"><a class="dropdown-item py-2" href="/ordre"><i class="bi bi-receipt me-2"></i>Ordre</a></li>
<li data-menu-key="menu-salg-products"><a class="dropdown-item py-2" href="/products"><i class="bi bi-box-seam me-2"></i>Produkter</a></li>
<li data-menu-key="menu-salg-webshop"><a class="dropdown-item py-2" href="/webshop"><i class="bi bi-shop me-2"></i>Webshop Administration</a></li>
</ul>
</li>
<li class="nav-item dropdown" data-menu-key="menu-okonomi">
<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><h6 class="dropdown-header">Fakturering</h6></li>
<li data-menu-key="menu-okonomi-time-queue"><a class="dropdown-item py-2" href="/economy/time-queue"><i class="bi bi-clock-history me-2"></i>Time Queue</a></li>
<li data-menu-key="menu-okonomi-also-cloud"><a class="dropdown-item py-2" href="/economy/also-cloud"><i class="bi bi-cloud-arrow-up me-2"></i>ALSO Cloud Marketplace</a></li>
<li data-menu-key="menu-okonomi-supplier-invoices"><a class="dropdown-item py-2" href="/billing/supplier-invoices"><i class="bi bi-receipt me-2"></i>Leverandør fakturaer</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Aftaler</h6></li>
<li data-menu-key="menu-okonomi-prepaid"><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 data-menu-key="menu-okonomi-fixed-price"><a class="dropdown-item py-2" href="/fixed-price-agreements"><i class="bi bi-calendar-check me-2"></i>Fastpris Aftaler</a></li>
<li data-menu-key="menu-okonomi-subscriptions"><a class="dropdown-item py-2" href="/subscriptions"><i class="bi bi-repeat me-2"></i>Abonnementer</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Kontrol</h6></li>
<li data-menu-key="menu-okonomi-invoice-error-finder"><a class="dropdown-item py-2" href="/invoice-error-finder"><i class="bi bi-search me-2"></i>Faktura-fejl-finder</a></li>
</ul>
</li>
</ul>
<div class="navbar-actions d-flex align-items-center gap-3">
<div class="dropdown" data-menu-key="menu-datamigration">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-clock-history me-2"></i>Data migration
</a>
<ul class="dropdown-menu dropdown-menu-end mt-2">
<li data-menu-key="menu-datamigration-migration-center"><a class="dropdown-item py-2" href="/migration-center"><i class="bi bi-arrow-left-right me-2"></i>Migreringscenter</a></li>
<li><hr class="dropdown-divider"></li>
<li data-menu-key="menu-datamigration-dashboard"><a class="dropdown-item py-2" href="/timetracking"><i class="bi bi-speedometer2 me-2"></i>Dashboard</a></li>
<li data-menu-key="menu-datamigration-registrations"><a class="dropdown-item py-2" href="/timetracking/registrations"><i class="bi bi-list-columns-reverse me-2"></i>Registreringer</a></li>
<li data-menu-key="menu-datamigration-wizard"><a class="dropdown-item py-2" href="/timetracking/wizard"><i class="bi bi-magic me-2"></i>Godkend Timer</a></li>
<li data-menu-key="menu-datamigration-employee-log"><a class="dropdown-item py-2" href="/timetracking/employee-log"><i class="bi bi-bar-chart-steps me-2"></i>Medarbejder Log</a></li>
<li data-menu-key="menu-datamigration-service-contract-wizard"><a class="dropdown-item py-2" href="/timetracking/service-contract-wizard"><i class="bi bi-diagram-3 me-2"></i>Servicekontrakt Migration</a></li>
<li data-menu-key="menu-datamigration-service-contract-report"><a class="dropdown-item py-2" href="/timetracking/service-contract-report"><i class="bi bi-file-earmark-bar-graph me-2"></i>Servicekontrakt Rapport</a></li>
<li data-menu-key="menu-datamigration-internet-wizard-v2"><a class="dropdown-item py-2" href="/data-migration/internet-wizard-v2"><i class="bi bi-hdd-network me-2"></i>Internet Wizard v2</a></li>
<li data-menu-key="menu-datamigration-orders"><a class="dropdown-item py-2" href="/timetracking/orders"><i class="bi bi-receipt me-2"></i>Ordrer</a></li>
<li data-menu-key="menu-datamigration-customers"><a class="dropdown-item py-2" href="/timetracking/customers"><i class="bi bi-people me-2"></i>Kunder</a></li>
</ul>
</div>
<div class="navbar-utility-dock">
<button class="btn btn-light rounded-circle border-0" id="globalSearchBtn" style="background: var(--accent-light); color: var(--accent);" title="Global søgning (Cmd/Ctrl+K)">
<i class="bi bi-search"></i>
</button>
<button class="btn btn-light rounded-circle border-0" id="quickCreateBtn" style="background: var(--accent-light); color: var(--accent);" title="Opret ny sag (+ eller Cmd+Shift+C)">
<i class="bi bi-plus-circle-fill fs-5"></i>
</button>
<button class="btn btn-light rounded-circle border-0" id="contextManualBtn" style="background: var(--accent-light); color: var(--accent);" title="Kontekstuel hjælp">
<i class="bi bi-question-circle"></i>
</button>
<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" id="globalRemindersBtn" style="background: var(--accent-light); color: var(--accent);" title="Åbn reminders">
<i class="bi bi-bell"></i>
</button>
<button class="btn btn-light rounded-circle border-0" id="bugReportBtn" style="background: var(--accent-light); color: var(--accent);" title="Rapporter fejl (Ctrl+Shift+B)">
<i class="bi bi-bug"></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 id="currentUserAvatar" src="https://ui-avatars.com/api/?name=Bruger&background=0f4c75&color=fff" class="rounded-circle me-2" width="32">
<span id="currentUserDisplayName" class="small fw-bold" style="color: var(--text-primary)">Bruger</span>
</a>
<ul class="dropdown-menu dropdown-menu-end mt-2">
<li><a class="dropdown-item py-2" href="#" data-bs-toggle="modal" data-bs-target="#profileModal">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="/tags#search"><i class="bi bi-tags me-2"></i>Tag søgning</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 id="projectCtProfileLink" class="d-none"><a class="dropdown-item py-2" href="/admin/hub-impact"><i class="bi bi-graph-up-arrow me-2"></i>Project CT · Hub Impact</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2 text-danger" href="#" onclick="logoutUser(event)"><i class="bi bi-box-arrow-right me-2"></i>Log ud</a></li>
</ul>
</div>
</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="/devportal" 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="/devportal" 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>
<!-- Case Results -->
<div id="caseResults" 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-card-checklist me-2"></i>Sager
</h6>
<a href="/sag" class="btn btn-sm btn-outline-primary">
<i class="bi bi-list-check me-1"></i>Alle sager
</a>
</div>
<div class="result-items"></div>
</div>
<!-- Email Results -->
<div id="emailResults" 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-envelope me-2"></i>Email
</h6>
<a href="/emails" class="btn btn-sm btn-outline-primary">
<i class="bi bi-envelope-open me-1"></i>Åbn Email
</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="/devportal" 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="/devportal" 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>
{% include "shared/frontend/bug_report_modal.html" %}
{% block content_wrapper %}
<div class="container-fluid px-4 py-4">
{% block content %}{% endblock %}
</div>
{% endblock %}
<div id="globalBottomBar" class="global-bottom-bar" hidden>
<div class="bb-header">
<div class="bb-zone bb-zone-left" role="status" aria-live="polite">
<button id="bbBackBtn" class="bb-action-btn bb-back-btn" type="button" title="Tilbage">
<i class="bi bi-arrow-left"></i>
<span>Tilbage</span>
</button>
<button class="bb-chip" type="button" data-bb-key="mail"><i class="bi bi-envelope"></i> <span class="bb-chip-label">Ulæste mails</span> <span class="bb-chip-bubble" aria-hidden="true">0</span> <span class="bb-chip-text visually-hidden">Ulæste mails: 0</span></button>
<button class="bb-chip" type="button" data-bb-key="urgent"><i class="bi bi-exclamation-octagon"></i> <span class="bb-chip-label">Hastesager</span> <span class="bb-chip-bubble" aria-hidden="true">0</span> <span class="bb-chip-text visually-hidden">Hastesager: 0</span></button>
<button class="bb-chip" type="button" data-bb-key="unassigned"><i class="bi bi-person-x"></i> <span class="bb-chip-label">Uden ansvarlig</span> <span class="bb-chip-bubble" aria-hidden="true">0</span> <span class="bb-chip-text visually-hidden">Uden ansvarlig: 0</span></button>
<a class="bb-chip" href="/procurement" data-bb-key="procurement"><i class="bi bi-cart-plus"></i> <span class="bb-chip-label">Indkøb</span> <span class="bb-chip-bubble" aria-hidden="true">0</span> <span class="bb-chip-text visually-hidden">Skal bestilles: 0</span></a>
<a class="bb-chip" href="/drift" data-bb-key="drift"><i class="bi bi-broadcast"></i> <span class="bb-chip-label">Drift</span> <span class="bb-chip-bubble" aria-hidden="true">0</span> <span class="bb-chip-text visually-hidden">Drift: 0</span></a>
</div>
<div class="bb-zone bb-zone-center">
<button id="bbSearchBtn" class="bb-action-btn bb-search-btn" type="button" title="Søg (Cmd/Ctrl+K)">
<i class="bi bi-search"></i>
</button>
</div>
<div class="bb-zone bb-zone-right">
<button id="bbActiveTimerChip" class="bb-activity-chip is-hidden" type="button" title="Aktiv timer">
<i class="bi bi-stopwatch"></i>
<span id="bbActiveTimerText">Ingen aktiv timer</span>
</button>
<button id="bbNotificationsBtn" class="bb-activity-chip" type="button" title="Notifikationer">
<i class="bi bi-bell"></i>
<span id="bbNotificationsCount" class="bb-notification-count">0</span>
</button>
<button id="bbTimerPauseBtn" class="bb-action-btn" type="button" title="Pause timer"><i class="bi bi-pause-fill"></i></button>
<button id="bbTimerStopBtn" class="bb-action-btn" type="button" title="Stop timer"><i class="bi bi-stop-fill"></i></button>
<button id="bbTimerSwitchBtn" class="bb-action-btn" type="button" title="Skift sag"><i class="bi bi-arrow-left-right"></i></button>
<button id="bbSheetToggle" class="bb-sheet-toggle" type="button" aria-expanded="false" aria-controls="bbSheetPanel" aria-label="Toggle detaljer">
<span>Info</span>
<i class="bi bi-chevron-up" aria-hidden="true"></i>
</button>
</div>
</div>
<div id="bbCountDetail" class="bb-detail-line" role="status" aria-live="polite"><i class="bi bi-info-circle me-1 opacity-75"></i> Klik på en kategori for at se detaljer</div>
<div id="bbSheetPanel" class="bb-sheet-panel" aria-hidden="true">
<div class="bb-sheet-inner">
<div class="bb-side-tabs" role="tablist" aria-label="Bundbar kategorier">
<button class="bb-tab-btn is-active" type="button" data-bb-tab="overview" role="tab" aria-selected="true"><i class="bi bi-bell"></i> Overblik</button>
<button class="bb-tab-btn" type="button" data-bb-tab="timer" role="tab" aria-selected="false"><i class="bi bi-stopwatch"></i> Timer</button>
<button class="bb-tab-btn" type="button" data-bb-tab="messages" role="tab" aria-selected="false"><i class="bi bi-chat-dots"></i> Beskeder</button>
<button class="bb-tab-btn" type="button" data-bb-tab="tasks" role="tab" aria-selected="false"><i class="bi bi-calendar-check"></i> Opgaver</button>
<button class="bb-tab-btn" type="button" data-bb-tab="notes" role="tab" aria-selected="false"><i class="bi bi-journal-text"></i> Noter</button>
<button class="bb-tab-btn" type="button" data-bb-tab="locations" role="tab" aria-selected="false"><i class="bi bi-geo-alt"></i> Lokationer</button>
<!-- Vises kun for chefer, men her i markup -->
<button class="bb-tab-btn" type="button" data-bb-tab="boss" role="tab" aria-selected="false"><i class="bi bi-diagram-3"></i> Sagsfordeling</button>
</div>
<div class="bb-tab-content" role="tabpanel" aria-live="polite">
<div class="bb-tab-heading">
<div id="bbTabTitle" class="bb-tab-title"><i class="bi bi-bell me-1 text-accent"></i> <span class="bb-tab-title-text">Overblik</span></div>
<div id="bbTabDescription" class="bb-tab-description">Det vigtigste samlet ét sted.</div>
</div>
<div id="bbTabInnerContent">
<ul id="bbTabList" class="bb-tab-list">
<li>Venter på data...</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="bbSwitchCaseModal" tabindex="-1" aria-hidden="true" aria-labelledby="bbSwitchCaseModalLabel">
<div class="modal-dialog modal-dialog-scrollable modal-xl modal-dialog-bottom">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="bbSwitchCaseModalLabel"><i class="bi bi-arrow-left-right me-2"></i>Skift sag</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button>
</div>
<div class="modal-body">
<div class="bb-switch-toolbar">
<div id="bbSwitchCaseStatus" class="small text-muted">Henter data...</div>
<div class="input-group input-group-sm bb-switch-search"><span class="input-group-text"><i class="bi bi-search"></i></span><input id="bbSwitchCaseSearch" type="search" class="form-control" placeholder="Søg efter sag eller nummer" aria-label="Søg efter sag"></div>
</div>
<div id="bbSwitchTimerActions" class="mb-3 d-none">
<div class="fw-semibold mb-2">Aktiv timer fundet</div>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-warning btn-sm" data-bb-switch-action="pause-now"><i class="bi bi-pause-fill me-1"></i>Pause nu</button>
<button type="button" class="btn btn-outline-danger btn-sm" data-bb-switch-action="stop-now"><i class="bi bi-stop-fill me-1"></i>Stop nu</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-bb-switch-action="continue-unchanged">Fortsæt uændret</button>
</div>
</div>
<div id="bbConvertTimePanel" class="bb-convert-panel d-none mb-3">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div><div class="fw-bold">Konverter afsluttet tid</div><div id="bbConvertTimeName" class="bb-switch-timer-info small mt-1"></div></div>
<button type="button" class="btn-close" data-bb-cancel-convert aria-label="Luk"></button>
</div>
<div class="row g-2">
<div class="col-6 col-lg-3"><label class="form-label small">Arbejdstype</label><select id="bbConvertWorkType" class="form-select form-select-sm"><option value="support">Support</option><option value="troubleshooting">Fejlsøgning</option><option value="development">Udvikling</option><option value="maintenance">Vedligehold</option><option value="on_site">Kørsel / On-site</option><option value="meeting">Møde</option><option value="other">Andet</option></select></div>
<div class="col-6 col-lg-3"><label class="form-label small">Fakturerbare minutter</label><input id="bbConvertMinutes" type="number" min="0" step="1" class="form-control form-control-sm"></div>
<div class="col-12 col-lg-4"><label class="form-label small">Afregning</label><select id="bbConvertBillingMethod" class="form-select form-select-sm"></select><div id="bbConvertRecommendation" class="form-text"></div></div>
<div class="col-12 col-lg-2 d-flex align-items-end"><button id="bbConvertSubmit" type="button" class="btn btn-primary btn-sm w-100"><i class="bi bi-check2-circle me-1"></i>Konverter tid</button></div>
</div>
</div>
<h6 class="bb-switch-section-title mb-2">Timere på dine sager</h6>
<div id="bbSwitchTimersList" class="bb-switch-timers small"><div class="bb-switch-empty">Henter timere...</div></div>
<h6 class="bb-switch-section-title mb-2">Seneste sager</h6>
<div id="bbSwitchRecentCasesList" class="bb-switch-cases small"><div class="bb-switch-empty">Henter sager...</div></div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="bbNoteTargetModal" tabindex="-1" aria-hidden="true" aria-labelledby="bbNoteTargetModalLabel">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="bbNoteTargetModalLabel"><i class="bi bi-journal-plus me-2"></i>Indsæt note</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button>
</div>
<div class="modal-body">
<div id="bbNoteTargetStatus" class="small text-muted mb-3">Vælg mål og indsæt tekst.</div>
<div class="mb-3">
<label for="bbNoteTargetIdInput" id="bbNoteTargetIdLabel" class="form-label">Mål ID</label>
<input type="number" class="form-control" id="bbNoteTargetIdInput" min="1" step="1" placeholder="ID">
</div>
<div class="mb-3 d-none" id="bbNoteTargetFieldWrap">
<label for="bbNoteTargetFieldSelect" id="bbNoteTargetFieldLabel" class="form-label">Felt</label>
<select id="bbNoteTargetFieldSelect" class="form-select"></select>
</div>
<div class="mb-3">
<label for="bbNoteTargetTextInput" class="form-label">Tekst der indsættes</label>
<textarea id="bbNoteTargetTextInput" class="form-control" rows="6" placeholder="Tekst fra note"></textarea>
</div>
<div class="form-text">Tip: brug kun den del af noten du vil gemme i målet.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" id="bbNoteTargetSubmitBtn">Indsæt</button>
</div>
</div>
</div>
</div>
<script>
window.addEventListener('unhandledrejection', function(event) {
const reason = event && event.reason;
const msg = String((reason && reason.message) || reason || '');
const stack = String((reason && reason.stack) || '');
const combined = (msg + '\n' + stack).toLowerCase();
// Known Safari/extension autofill overlay crash; ignore noisy external script rejection.
if (combined.includes('bootstrap-autofill-overlay.js') || combined.includes('autocompletetype.includes')) {
event.preventDefault();
}
});
const bmcOriginalFetch = window.fetch ? window.fetch.bind(window) : null;
if (bmcOriginalFetch) {
window.fetch = function(resource, init) {
const options = init ? { ...init } : {};
if (!Object.prototype.hasOwnProperty.call(options, 'credentials')) {
options.credentials = 'include';
}
return bmcOriginalFetch(resource, options);
};
}
</script>
<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?v=2.3"></script>
<script src="/static/js/task-template-selector.js?v=1.1"></script>
<script src="/static/js/notifications.js?v=1.0"></script>
<script src="/static/js/telefoni.js?v=2.5"></script>
<script src="/static/js/sms.js?v=1.1"></script>
<script src="/static/js/bug-report.js?v=1.6"></script>
{% include "shared/frontend/internal_message.html" %}
<script src="/static/js/message-ui.js?v=1"></script>
<script src="/static/js/bottom-bar.js?v=2.71"></script>
<script>
// Dark Mode Toggle Logic
window.BMC_CAN_CLICK_TO_CALL = true;
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');
}
});
const MENU_PREFS_CACHE_KEY = 'bmc_menu_hidden_keys';
function normalizeHiddenMenuKeys(keys) {
if (!Array.isArray(keys)) return [];
const seen = new Set();
const normalized = [];
for (const raw of keys) {
const key = String(raw || '').trim().toLowerCase();
if (!key) continue;
if (seen.has(key)) continue;
seen.add(key);
normalized.push(key);
}
return normalized;
}
function isVisibleActionItem(li) {
if (!li) return false;
const anchor = li.querySelector('a.dropdown-item');
if (!anchor) return false;
if (li.classList.contains('bmc-menu-hidden') || anchor.classList.contains('bmc-menu-hidden')) return false;
if (li.style.display === 'none') return false;
if (anchor.style.display === 'none') return false;
return true;
}
function cleanupDropdownSections(menu) {
const items = Array.from(menu.children).filter((node) => node.tagName === 'LI');
// Reset section and divider display first, then recalculate.
items.forEach((li) => {
if (li.querySelector('h6.dropdown-header') || li.querySelector('hr.dropdown-divider')) {
li.style.display = '';
}
});
// Hide headers that no longer have visible action items below them.
items.forEach((li, idx) => {
if (!li.querySelector('h6.dropdown-header')) return;
let hasVisibleItem = false;
for (let j = idx + 1; j < items.length; j += 1) {
if (items[j].querySelector('h6.dropdown-header')) break;
if (isVisibleActionItem(items[j])) {
hasVisibleItem = true;
break;
}
}
li.style.display = hasVisibleItem ? '' : 'none';
});
// Hide separators that don't separate visible action groups.
items.forEach((li, idx) => {
if (!li.querySelector('hr.dropdown-divider')) return;
let hasVisibleBefore = false;
for (let j = idx - 1; j >= 0; j -= 1) {
if (isVisibleActionItem(items[j])) {
hasVisibleBefore = true;
break;
}
}
let hasVisibleAfter = false;
for (let j = idx + 1; j < items.length; j += 1) {
if (isVisibleActionItem(items[j])) {
hasVisibleAfter = true;
break;
}
}
li.style.display = hasVisibleBefore && hasVisibleAfter ? '' : 'none';
});
}
function cleanupNavbarDropdowns(hiddenSet) {
document.querySelectorAll('#navbarNav .dropdown-menu').forEach((menu) => {
cleanupDropdownSections(menu);
});
document.querySelectorAll('#navbarNav .nav-item.dropdown[data-menu-key], #navbarNav .d-flex > .dropdown[data-menu-key]').forEach((dropdownRoot) => {
const key = String(dropdownRoot.getAttribute('data-menu-key') || '').trim().toLowerCase();
if (!key || hiddenSet.has(key)) return;
const menu = dropdownRoot.querySelector(':scope > .dropdown-menu');
if (!menu) return;
const hasVisibleItems = Array.from(menu.children).some((li) => isVisibleActionItem(li));
dropdownRoot.style.display = hasVisibleItems ? '' : 'none';
});
}
function applyMenuVisibility(hiddenKeys) {
const hidden = new Set(normalizeHiddenMenuKeys(hiddenKeys));
document.querySelectorAll('[data-menu-key]').forEach((node) => {
const key = String(node.getAttribute('data-menu-key') || '').trim().toLowerCase();
if (!key) return;
const shouldHide = hidden.has(key);
node.classList.toggle('bmc-menu-hidden', shouldHide);
node.hidden = shouldHide;
if (shouldHide) {
node.style.setProperty('display', 'none', 'important');
} else {
node.style.removeProperty('display');
}
});
cleanupNavbarDropdowns(hidden);
window.__bmcMenuHiddenKeys = Array.from(hidden);
}
async function loadAndApplyMenuVisibility() {
try {
const res = await fetch('/api/v1/auth/me/menu-preferences', { credentials: 'include' });
if (!res.ok) throw new Error(`HTTP_${res.status}`);
const data = await res.json();
const hidden = normalizeHiddenMenuKeys(data.hidden_menu_keys || []);
localStorage.setItem(MENU_PREFS_CACHE_KEY, JSON.stringify(hidden));
applyMenuVisibility(hidden);
} catch (_err) {
try {
const cached = JSON.parse(localStorage.getItem(MENU_PREFS_CACHE_KEY) || '[]');
applyMenuVisibility(cached);
} catch (_cacheErr) {
applyMenuVisibility([]);
}
}
}
window.addEventListener('bmc:menu-preferences-updated', (event) => {
const hidden = normalizeHiddenMenuKeys(event?.detail?.hidden_menu_keys || []);
localStorage.setItem(MENU_PREFS_CACHE_KEY, JSON.stringify(hidden));
applyMenuVisibility(hidden);
});
// Global Search Modal (Cmd+K) - Initialize after DOM is ready
let selectedResultIndex = -1;
let allResults = [];
document.addEventListener('DOMContentLoaded', () => {
const primaryMenu = document.querySelector('#navbarNav > .navbar-nav');
const dataMigrationMenu = document.querySelector('#navbarNav > .navbar-actions > [data-menu-key="menu-datamigration"]');
if (primaryMenu && dataMigrationMenu) {
const menuItem = document.createElement('li');
menuItem.className = 'nav-item dropdown';
menuItem.setAttribute('data-menu-key', 'menu-datamigration');
while (dataMigrationMenu.firstChild) menuItem.appendChild(dataMigrationMenu.firstChild);
primaryMenu.appendChild(menuItem);
dataMigrationMenu.remove();
}
loadAndApplyMenuVisibility();
const currentPath = String(window.location.pathname || '/').replace(/\/$/, '') || '/';
const menuLinks = Array.from(document.querySelectorAll('#navbarNav a[href^="/"]'))
.filter((link) => {
const href = String(link.getAttribute('href') || '').replace(/\/$/, '') || '/';
return href !== '/' && (currentPath === href || currentPath.startsWith(`${href}/`));
})
.sort((a, b) => String(b.getAttribute('href')).length - String(a.getAttribute('href')).length);
const bestMenuLink = menuLinks[0];
if (bestMenuLink) bestMenuLink.classList.add('active');
document.querySelectorAll('#navbarNav .dropdown-item.active').forEach((item) => {
item.closest('.dropdown')?.querySelector(':scope > .nav-link')?.classList.add('section-active');
});
const searchModal = new bootstrap.Modal(document.getElementById('globalSearchModal'));
const searchBubbleBtn = document.getElementById('globalSearchBtn');
const contextManualBtn = document.getElementById('contextManualBtn');
const remindersBubbleBtn = document.getElementById('globalRemindersBtn');
const profileModalEl = document.getElementById('profileModal');
const profileModalInstance = profileModalEl ? new bootstrap.Modal(profileModalEl) : null;
const globalSearchInput = document.getElementById('globalSearchInput');
function getCurrentModuleContext() {
const path = (window.location.pathname || '').toLowerCase();
if (path.startsWith('/sag')) return 'sag';
if (path.startsWith('/hardware')) return 'hardware';
if (path.startsWith('/emails')) return 'mail';
if (path.startsWith('/ordre')) return 'salg';
if (path.startsWith('/customers') || path.startsWith('/contacts')) return 'crm';
return '';
}
function openGlobalSearchModal() {
searchModal.show();
setTimeout(() => {
if (globalSearchInput) {
globalSearchInput.focus();
}
loadLiveStats();
loadRecentActivity();
}, 300);
}
function openRemindersModalTab() {
if (!profileModalInstance || !profileModalEl) {
return;
}
profileModalInstance.show();
setTimeout(() => {
const remindersTabBtn = document.getElementById('profile-reminders-tab');
if (remindersTabBtn) {
bootstrap.Tab.getOrCreateInstance(remindersTabBtn).show();
}
loadReminderPreferences();
loadProfileReminders();
}, 220);
}
if (searchBubbleBtn) {
searchBubbleBtn.addEventListener('click', (e) => {
e.preventDefault();
openGlobalSearchModal();
});
}
if (remindersBubbleBtn) {
remindersBubbleBtn.addEventListener('click', (e) => {
e.preventDefault();
openRemindersModalTab();
});
}
if (contextManualBtn) {
contextManualBtn.addEventListener('click', (e) => {
e.preventDefault();
const module = getCurrentModuleContext();
if (module) {
window.location.href = `/manual?module=${encodeURIComponent(module)}`;
return;
}
window.location.href = '/manual';
});
}
// 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();
if (navigateToSagFromScan(e.target.value)) {
return;
}
selectCurrentResult();
}
});
}
// Keyboard shortcut: Cmd+K or Ctrl+K
document.addEventListener('keydown', (e) => {
// Cmd+K / Ctrl+K for global search
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
openGlobalSearchModal();
}
// '+' key for QuickCreate (not in input fields)
if (e.key === '+' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName)) return;
e.preventDefault();
openNewCasePage();
}
// Cmd+Shift+C / Ctrl+Shift+C for QuickCreate
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'c') {
e.preventDefault();
openNewCasePage();
}
// ESC to close
if (e.key === 'Escape') {
searchModal.hide();
}
});
function openNewCasePage() {
window.location.href = '/sag/new';
}
// QuickCreate button click handler
document.getElementById('quickCreateBtn')?.addEventListener('click', (e) => {
e.preventDefault();
openNewCasePage();
});
// 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('caseResults')) document.getElementById('caseResults').style.display = 'none';
if (document.getElementById('emailResults')) document.getElementById('emailResults').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/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/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();
}
}
function extractSagIdFromScanToken(value) {
const cleaned = String(value || '').toUpperCase().replace(/\s+/g, ' ').trim();
if (!cleaned) return null;
// Scanner tokens from work order and hardware labels
const workOrderMatch = cleaned.match(/\bBMCSCAN-WO-S(\d+)\b/);
if (workOrderMatch) return parseInt(workOrderMatch[1], 10);
const hardwareMatch = cleaned.match(/\bBMCSCAN-HW-(\d+)\b/);
if (hardwareMatch) return parseInt(hardwareMatch[1], 10);
return null;
}
function navigateToSagFromScan(value) {
const sagId = extractSagIdFromScanToken(value);
if (!sagId || Number.isNaN(sagId)) {
return false;
}
window.location.href = `/sag/${sagId}/v3`;
return true;
}
// Global search function
async function performGlobalSearch(query) {
if (navigateToSagFromScan(query)) {
return;
}
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('emailResults')) document.getElementById('emailResults').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 cases, including linked tags and buzzwords
try {
const casesResponse = await fetch(`/api/v1/search/sag?q=${encodeURIComponent(query)}`);
const cases = await casesResponse.json();
const caseResults = document.getElementById('caseResults');
if (Array.isArray(cases) && cases.length > 0) {
hasResults = true;
caseResults.style.display = 'block';
const caseList = caseResults.querySelector('.result-items');
caseList.innerHTML = cases.slice(0, 10).map(item => {
const buzzwords = Array.isArray(item.buzzwords)
? item.buzzwords.filter(Boolean)
: [];
const buzzwordHtml = buzzwords.length
? ` • <i class="bi bi-lightbulb ms-1"></i> ${buzzwords.map(word => escapeHtml(word)).join(', ')}`
: '';
const archivedHtml = item.is_archived
? ' • <span class="badge bg-secondary">Arkiveret</span>'
: '';
return `
<div class="result-item" onclick="window.location.href='/sag/${Number(item.id)}/v3'" style="cursor: pointer;">
<div>
<div class="fw-bold">#${Number(item.id)} ${escapeHtml(item.titel || 'Uden titel')}</div>
<div class="small text-muted">
<i class="bi bi-card-checklist me-1"></i>${escapeHtml(item.status || '-')}
${item.customer_name ? `${escapeHtml(item.customer_name)}` : ''}
${buzzwordHtml}
${archivedHtml}
</div>
</div>
<i class="bi bi-arrow-right"></i>
</div>
`;
}).join('');
} else if (caseResults) {
caseResults.style.display = 'none';
}
} catch (e) {
console.log('Case and buzzword search not available');
const caseResults = document.getElementById('caseResults');
if (caseResults) caseResults.style.display = 'none';
}
// Search emails
try {
const emailsResponse = await fetch(`/api/v1/emails?q=${encodeURIComponent(query)}&limit=5`);
const emailsData = await emailsResponse.json();
if (Array.isArray(emailsData) && emailsData.length > 0) {
hasResults = true;
const emailResults = document.getElementById('emailResults');
if (emailResults) {
emailResults.style.display = 'block';
const emailList = emailResults.querySelector('.result-items');
if (emailList) {
emailList.innerHTML = emailsData.map(mail => {
const received = mail.received_date
? new Date(mail.received_date).toLocaleString('da-DK')
: '-';
const sender = mail.sender_name || mail.sender_email || '-';
const isUnread = !Boolean(mail.is_read);
return `
<div class="result-item" onclick="window.location.href='/emails?open=${mail.id}'" style="cursor: pointer;">
<div>
<div class="fw-bold">${escapeHtml(mail.subject || '(Ingen emne)')}</div>
<div class="small text-muted">
<i class="bi bi-envelope me-1"></i>${escapeHtml(sender)}
${mail.linked_case_id ? ` • Sag #${mail.linked_case_id}` : ''}
${isUnread ? ' • <span class="text-warning">Ulæst</span>' : ''}
${escapeHtml(received)}
</div>
</div>
<i class="bi bi-arrow-right"></i>
</div>
`;
}).join('');
}
}
} else {
const emailResults = document.getElementById('emailResults');
if (emailResults) emailResults.style.display = 'none';
}
} catch (e) {
console.log('Email search not available');
const emailResults = document.getElementById('emailResults');
if (emailResults) emailResults.style.display = 'none';
}
// 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: '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` }
],
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>
<!-- Manual Help Modal -->
{% include ["manual_modal.html", "shared/frontend/manual_modal.html"] ignore missing %}
<!-- Profile Modal -->
<div class="modal fade" id="profileModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content" style="border-radius: 16px;">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-person-circle me-2"></i>Profil</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<ul class="nav nav-tabs mb-3" id="profileTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="profile-overview-tab" data-bs-toggle="tab" data-bs-target="#profile-overview" type="button" role="tab">
Overblik
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-reminders-tab" data-bs-toggle="tab" data-bs-target="#profile-reminders" type="button" role="tab">
Reminders
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-task-lists-tab" data-bs-toggle="tab" data-bs-target="#profile-task-lists" type="button" role="tab">
Opgavelister
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-menu-tab" data-bs-toggle="tab" data-bs-target="#profile-menu" type="button" role="tab" onclick="loadProfileMenuPreferences()">
Menu
</button>
</li>
</ul>
<div class="tab-content" id="profileTabsContent">
<div class="tab-pane fade show active" id="profile-overview" role="tabpanel" tabindex="0">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Fuldt navn</label>
<input type="text" class="form-control" id="prof_full_name" placeholder="Dit navn">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Titel / rolle</label>
<input type="text" class="form-control" id="prof_title" placeholder="f.eks. Teknikker">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Mobilnummer</label>
<input type="tel" class="form-control" id="prof_phone" placeholder="f.eks. +45 12 34 56 78">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Standard sagstype</label>
<select class="form-select" id="prof_default_case_type"></select>
<div class="form-text">Bruges som udgangspunkt på “Ny sag”.</div>
</div>
<div class="col-12">
<label class="form-label fw-semibold">
<i class="bi bi-display me-1" style="color:var(--accent)"></i>Mine AnyDesk IDs
</label>
<div class="form-text mb-2">Tilføj alle maskiner du bruger som teknikker — bruges til automatisk at genkende dig i remote sessions.</div>
<div id="prof-anydesk-chips" class="d-flex flex-wrap gap-2 mb-2"></div>
<div class="input-group" style="max-width:400px">
<input type="text" class="form-control font-monospace" id="prof_anydesk_new_id"
placeholder="AnyDesk ID (tal)" autocomplete="off"
onkeydown="if(event.key==='Enter'){event.preventDefault();addAnyDeskId()}">
<input type="text" class="form-control" id="prof_anydesk_new_label"
placeholder="Navn (valgfri, f.eks. Laptop)" style="max-width:160px"
onkeydown="if(event.key==='Enter'){event.preventDefault();addAnyDeskId()}">
<button class="btn btn-outline-primary" type="button" onclick="addAnyDeskId()">
<i class="bi bi-plus-lg"></i>
</button>
</div>
</div>
<div class="col-12 pt-1">
<button class="btn btn-primary btn-sm" onclick="saveUserProfile()">
<i class="bi bi-check-lg me-1"></i>Gem profil
</button>
<span id="prof-save-status" class="ms-2 small text-success" style="display:none">Gemt ✓</span>
</div>
</div>
</div>
<div class="tab-pane fade" id="profile-reminders" role="tabpanel" tabindex="0">
<div class="row g-3">
<div class="col-lg-5">
<div class="card">
<div class="card-header">
<h6 class="mb-0 text-primary"><i class="bi bi-sliders me-2"></i>Notifikationsindstillinger</h6>
</div>
<div class="card-body">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="pref_notify_frontend">
<label class="form-check-label" for="pref_notify_frontend">Popup</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="pref_notify_email">
<label class="form-check-label" for="pref_notify_email">Email</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="pref_notify_mattermost">
<label class="form-check-label" for="pref_notify_mattermost">Mattermost</label>
</div>
<div class="mb-3">
<label class="form-label">Email override</label>
<input type="email" class="form-control" id="pref_email_override" placeholder="f.eks. navn@firma.dk">
</div>
<div class="mb-3">
<label class="form-label">Mattermost-brugernavn</label>
<input type="text" class="form-control" id="pref_mattermost_username" placeholder="Tomt = BMC Hub-brugernavn">
<div class="form-text">Bruges til direkte beskeder som @brugernavn.</div>
</div>
<button class="btn btn-sm btn-primary" onclick="saveReminderPreferences()">Gem</button>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0 text-primary"><i class="bi bi-bell me-2"></i>Dine reminders</h6>
<button class="btn btn-sm btn-outline-primary" onclick="loadProfileReminders()">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush" id="profileRemindersList">
<div class="p-4 text-center text-muted">Indlæser reminders...</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="profile-task-lists" role="tabpanel" tabindex="0">
<div class="card border-0 bg-light-subtle">
<div class="card-body">
<div class="d-flex align-items-start gap-3 mb-3">
<div class="rounded-3 text-primary bg-primary-subtle d-grid" style="width:42px;height:42px;place-items:center"><i class="bi bi-list-check fs-5"></i></div>
<div><h6 class="mb-1 fw-bold">Automatiske opgavelister</h6><p class="small text-muted mb-0">Send dine egne og gruppens åbne sager på faste tidspunkter. Beskeden indeholder direkte links til sagerne.</p></div>
</div>
<div class="row g-3 align-items-end">
<div class="col-md-6"><label class="form-label small fw-semibold" for="profileTaskListTitle">Navn</label><input id="profileTaskListTitle" class="form-control" value="Min opgaveliste"></div>
<div class="col-md-6"><label class="form-label small fw-semibold" for="profileTaskListTimes">Tidspunkter</label><input id="profileTaskListTimes" class="form-control" value="09:00, 12:00, 14:00"><div class="form-text">Fx 09:00, 12:00, 14:00</div></div>
<div class="col-12 d-flex flex-wrap gap-4"><label class="form-check mb-0"><input id="profileTaskListGroups" class="form-check-input" type="checkbox" checked><span class="form-check-label">Medtag mine gruppers sager</span></label><label class="form-check mb-0"><input id="profileTaskListMattermost" class="form-check-input" type="checkbox" checked><span class="form-check-label">Send til Mattermost</span></label></div>
<div class="col-12 d-flex justify-content-end"><button id="profileTaskListSave" class="btn btn-primary btn-sm px-3" type="button" onclick="saveProfileTaskList()"><i class="bi bi-plus-lg me-1"></i>Gem opgaveliste</button></div>
</div>
</div>
</div>
<div class="mt-3" id="profileTaskLists"><div class="p-3 text-muted small">Indlæser opgavelister...</div></div>
</div>
<div class="tab-pane fade" id="profile-menu" role="tabpanel" tabindex="0">
<div class="card border-0">
<div class="card-body px-0">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0 text-primary"><i class="bi bi-layout-text-window-reverse me-2"></i>Menuvisning (min konto)</h6>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary" type="button" id="profMenuShowAllBtn" onclick="showAllProfileMenuItems()">Vis alle</button>
<button class="btn btn-sm btn-primary" type="button" id="profMenuSaveBtn" onclick="saveProfileMenuPreferences()">Gem</button>
</div>
</div>
<div id="profMenuFeedback" class="small text-muted mb-2">Vælg hvilke menupunkter der skal vises for dig.</div>
<div id="profMenuPrefsGrid" class="row g-2"><div class="col-12 text-muted small py-3"><span class="spinner-border spinner-border-sm me-2"></span>Indlæser menuindstillinger…</div></div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button>
</div>
</div>
</div>
</div>
<style>
/* Personal menu preferences must also win over responsive navbar layout rules. */
#navbarNav .bmc-menu-hidden { display: none !important; }
</style>
<script>
const PROFILE_MENU_PREF_ITEMS = [
{ key: 'menu-crm', label: 'CRM' },
{ key: 'menu-sager', label: 'Sager' },
{ key: 'menu-kalender', label: 'Kalender' },
{ key: 'menu-kalender-overview', label: 'Kalender: Kalenderoversigt' },
{ key: 'menu-planner', label: 'Planlægger' },
{ key: 'menu-support', label: 'Support' },
{ key: 'menu-salg', label: 'Salg' },
{ key: 'menu-okonomi', label: 'Økonomi' },
{ key: 'menu-datamigration', label: 'Data migration' },
{ key: 'menu-crm-customers', label: 'CRM: Kunder' },
{ key: 'menu-crm-contacts', label: 'CRM: Kontakter' },
{ key: 'menu-crm-vendors', label: 'CRM: Leverandører' },
{ key: 'menu-crm-links', label: 'CRM: Links' },
{ key: 'menu-crm-locations', label: 'CRM: Lokaliteter' },
{ key: 'menu-crm-opportunities', label: 'CRM: Muligheder' },
{ key: 'menu-crm-pipeline', label: 'CRM: Pipeline' },
{ key: 'menu-support-conversations', label: 'Support: Mine Samtaler' },
{ key: 'menu-support-tickets', label: 'Support: Arkiverede Tickets' },
{ key: 'menu-support-emails', label: 'Support: Email' },
{ key: 'menu-support-telefoni', label: 'Support: Telefoni' },
{ key: 'menu-support-internet-connections', label: 'Support: Internetforbindelser' },
{ key: 'menu-support-losninger', label: 'Support: Løsninger' },
{ key: 'menu-support-mission', label: 'Support: Mission Control' },
{ key: 'menu-support-anydesk', label: 'Support: AnyDesk Sessions' },
{ key: 'menu-support-hardware', label: 'Support: BMC Assets' },
{ key: 'menu-support-hardware-customers', label: 'Support: Kundehardware' },
{ key: 'menu-support-eset', label: 'Support: ESET Oversigt' },
{ key: 'menu-support-manual', label: 'Support: Manualer' },
{ key: 'menu-support-website-content', label: 'Support: Website-indhold' },
{ key: 'menu-salg-orders', label: 'Salg: Ordre' },
{ key: 'menu-salg-products', label: 'Salg: Produkter' },
{ key: 'menu-salg-webshop', label: 'Salg: Webshop Administration' },
{ key: 'menu-okonomi-time-queue', label: 'Økonomi: Time Queue' },
{ key: 'menu-okonomi-also-cloud', label: 'Økonomi: ALSO Cloud Marketplace' },
{ key: 'menu-okonomi-supplier-invoices', label: 'Økonomi: Leverandør fakturaer' },
{ key: 'menu-okonomi-prepaid', label: 'Økonomi: Prepaid Cards' },
{ key: 'menu-okonomi-fixed-price', label: 'Økonomi: Fastpris Aftaler' },
{ key: 'menu-okonomi-subscriptions', label: 'Økonomi: Abonnementer' },
{ key: 'menu-datamigration-migration-center', label: 'Data migration: Migreringscenter' },
{ key: 'menu-datamigration-dashboard', label: 'Data migration: Dashboard' },
{ key: 'menu-datamigration-registrations', label: 'Data migration: Registreringer' },
{ key: 'menu-datamigration-wizard', label: 'Data migration: Godkend Timer' },
{ key: 'menu-datamigration-employee-log', label: 'Data migration: Medarbejder Log' },
{ key: 'menu-datamigration-service-contract-wizard', label: 'Data migration: Servicekontrakt Migration' },
{ key: 'menu-datamigration-service-contract-report', label: 'Data migration: Servicekontrakt Rapport' },
{ key: 'menu-datamigration-internet-wizard-v2', label: 'Data migration: Internet Wizard v2' },
{ key: 'menu-datamigration-orders', label: 'Data migration: Ordrer' },
{ key: 'menu-datamigration-customers', label: 'Data migration: Kunder' },
];
function setProfileMenuFeedback(message, type = 'muted') {
const el = document.getElementById('profMenuFeedback');
if (!el) return;
const cls = type === 'error' ? 'text-danger' : type === 'success' ? 'text-success' : 'text-muted';
el.className = `small ${cls} mb-2`;
el.textContent = message || '';
}
function renderProfileMenuPrefs(hiddenKeys = []) {
const grid = document.getElementById('profMenuPrefsGrid');
if (!grid) return;
const hidden = new Set((hiddenKeys || []).map(v => String(v || '').trim().toLowerCase()));
grid.innerHTML = PROFILE_MENU_PREF_ITEMS.map(item => {
const checked = hidden.has(item.key.toLowerCase()) ? '' : 'checked';
return `
<div class="col-md-6 col-lg-4">
<div class="form-check mb-1">
<input class="form-check-input prof-menu-checkbox" type="checkbox" id="prof-menu-${item.key}" value="${item.key}" ${checked}>
<label class="form-check-label" for="prof-menu-${item.key}">${item.label}</label>
</div>
</div>
`;
}).join('');
}
async function loadProfileMenuPreferences() {
try {
setProfileMenuFeedback('Indlæser menu...');
const res = await fetch('/api/v1/auth/me/menu-preferences', { credentials: 'include' });
if (!res.ok) {
if (res.status === 401) throw new Error('Session udløbet. Log ind igen.');
if (res.status === 404) throw new Error('Menu-endpoint mangler på serveren (genstart API).');
let detail = '';
try {
const err = await res.json();
detail = err?.detail || '';
} catch (_parseErr) {
detail = '';
}
throw new Error(detail || `Kunne ikke indlæse menu (HTTP ${res.status})`);
}
const data = await res.json();
renderProfileMenuPrefs(data.hidden_menu_keys || []);
setProfileMenuFeedback('');
} catch (e) {
renderProfileMenuPrefs(window.__bmcMenuHiddenKeys || []);
setProfileMenuFeedback(e.message || 'Kunne ikke indlæse menu', 'error');
}
}
async function saveProfileMenuPreferences() {
const checks = Array.from(document.querySelectorAll('.prof-menu-checkbox'));
const hiddenKeys = checks
.filter(cb => !cb.checked)
.map(cb => String(cb.value || '').trim().toLowerCase())
.filter(Boolean);
const saveBtn = document.getElementById('profMenuSaveBtn');
if (saveBtn) saveBtn.disabled = true;
setProfileMenuFeedback('Gemmer menu...');
try {
const res = await fetch('/api/v1/auth/me/menu-preferences', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ hidden_menu_keys: hiddenKeys })
});
if (!res.ok) {
if (res.status === 401) throw new Error('Session udløbet. Log ind igen.');
if (res.status === 404) throw new Error('Menu-endpoint mangler på serveren (genstart API).');
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || 'Kunne ikke gemme menu');
}
window.dispatchEvent(new CustomEvent('bmc:menu-preferences-updated', {
detail: { hidden_menu_keys: hiddenKeys }
}));
// Apply immediately as well. This avoids relying on a custom event
// when a page has scripts from an older browser cache.
applyMenuVisibility(hiddenKeys);
setProfileMenuFeedback('Menu gemt.', 'success');
} catch (e) {
setProfileMenuFeedback(e.message || 'Kunne ikke gemme menu', 'error');
} finally {
if (saveBtn) saveBtn.disabled = false;
}
}
function showAllProfileMenuItems() {
document.querySelectorAll('.prof-menu-checkbox').forEach((cb) => {
cb.checked = true;
});
}
async function loadReminderPreferences() {
try {
const res = await fetch('/api/v1/users/me/notification-preferences', { credentials: 'include' });
if (!res.ok) return;
const prefs = await res.json();
document.getElementById('pref_notify_frontend').checked = !!prefs.notify_frontend;
document.getElementById('pref_notify_email').checked = !!prefs.notify_email;
document.getElementById('pref_notify_mattermost').checked = !!prefs.notify_mattermost;
document.getElementById('pref_email_override').value = prefs.email_override || '';
document.getElementById('pref_mattermost_username').value = prefs.mattermost_username || '';
} catch (e) {
console.error('Failed to load reminder preferences', e);
}
}
async function saveReminderPreferences() {
const payload = {
notify_frontend: document.getElementById('pref_notify_frontend').checked,
notify_email: document.getElementById('pref_notify_email').checked,
notify_mattermost: document.getElementById('pref_notify_mattermost').checked,
email_override: document.getElementById('pref_email_override').value || null,
mattermost_username: document.getElementById('pref_mattermost_username').value.trim().replace(/^@/, '') || null
};
try {
const res = await fetch('/api/v1/users/me/notification-preferences', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload)
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Kunne ikke gemme indstillinger');
}
} catch (e) {
alert('Fejl: ' + e.message);
}
}
async function loadProfileReminders() {
const list = document.getElementById('profileRemindersList');
if (!list) return;
list.innerHTML = '<div class="p-4 text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Henter reminders...</div>';
try {
const res = await fetch('/api/v1/reminders/my', { credentials: 'include' });
if (!res.ok) {
list.innerHTML = '<div class="p-4 text-center text-muted">Kunne ikke hente reminders.</div>';
return;
}
const reminders = await res.json();
renderProfileReminders(reminders || []);
} catch (e) {
console.error('Failed to load reminders', e);
list.innerHTML = '<div class="p-4 text-center text-muted">Kunne ikke hente reminders.</div>';
}
}
function renderProfileReminders(reminders) {
const list = document.getElementById('profileRemindersList');
if (!list) return;
if (!reminders.length) {
list.innerHTML = '<div class="p-4 text-center text-muted">Ingen reminders fundet.</div>';
return;
}
list.innerHTML = reminders.map(reminder => {
const statusBadge = reminder.is_active
? '<span class="badge bg-success">Aktiv</span>'
: '<span class="badge bg-secondary">Inaktiv</span>';
return `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="me-3">
<div class="fw-bold">${reminder.title}</div>
<div class="small text-muted">Sag #${reminder.sag_id} · ${reminder.case_title || '-'}</div>
<div class="small text-muted">${reminder.message || ''}</div>
</div>
<div class="d-flex flex-column align-items-end gap-2">
${statusBadge}
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary" onclick="toggleReminderActive(${reminder.id}, ${reminder.is_active ? 'false' : 'true'})">
${reminder.is_active ? 'Pause' : 'Aktivér'}
</button>
<button class="btn btn-outline-danger" onclick="deleteProfileReminder(${reminder.id})">
Slet
</button>
</div>
</div>
</div>
</div>
`;
}).join('');
}
async function toggleReminderActive(reminderId, isActive) {
try {
const res = await fetch(`/api/v1/sag/reminders/${reminderId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ is_active: isActive })
});
if (!res.ok) throw new Error('Kunne ikke opdatere reminder');
loadProfileReminders();
} catch (e) {
alert('Fejl: ' + e.message);
}
}
async function deleteProfileReminder(reminderId) {
if (!confirm('Vil du slette denne reminder?')) return;
try {
const res = await fetch(`/api/v1/sag/reminders/${reminderId}`, {
method: 'DELETE',
credentials: 'include'
});
if (!res.ok) throw new Error('Kunne ikke slette reminder');
loadProfileReminders();
} catch (e) {
alert('Fejl: ' + e.message);
}
}
async function loadProfileTaskLists() {
const target = document.getElementById('profileTaskLists');
if (!target) return;
try {
const res = await fetch('/api/v1/reminder-task-rules', { credentials: 'include' });
if (!res.ok) throw new Error('Kunne ikke hente opgavelister');
const rules = await res.json();
target.innerHTML = rules.length ? rules.map(rule => `
<div class="border rounded-3 bg-white p-3 mb-2 d-flex justify-content-between align-items-center gap-3">
<div><div class="fw-semibold">${escapeHtml(rule.title)}</div><div class="small text-muted mt-1"><i class="bi bi-clock me-1"></i>${(rule.times_json || []).map(escapeHtml).join(' · ')}${rule.include_groups ? ' · Mine grupper' : ''}</div></div>
<button class="btn btn-sm btn-outline-primary flex-shrink-0" type="button" onclick="sendProfileTaskList(${rule.id}, this)"><i class="bi bi-send me-1"></i>Send nu</button>
</div>`).join('') : '<div class="border rounded-3 bg-white p-3 text-muted small">Du har endnu ingen automatiske opgavelister.</div>';
} catch (e) {
target.innerHTML = `<div class="alert alert-danger py-2 small mb-0">${escapeHtml(e.message || 'Kunne ikke hente opgavelister.')}</div>`;
}
}
async function saveProfileTaskList() {
const button = document.getElementById('profileTaskListSave');
const times = (document.getElementById('profileTaskListTimes')?.value || '').split(',').map(value => value.trim()).filter(Boolean);
if (!times.length) return alert('Skriv mindst ét tidspunkt, fx 09:00.');
button.disabled = true;
const original = button.innerHTML;
button.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Gemmer';
try {
const res = await fetch('/api/v1/reminder-task-rules', {
method: 'POST', credentials: 'include', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: document.getElementById('profileTaskListTitle').value,
times,
include_groups: document.getElementById('profileTaskListGroups').checked,
notify_mattermost: document.getElementById('profileTaskListMattermost').checked
})
});
const result = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(result.detail || 'Kunne ikke gemme opgavelisten');
await loadProfileTaskLists();
} catch (e) { alert('Fejl: ' + e.message); }
finally { button.disabled = false; button.innerHTML = original; }
}
async function sendProfileTaskList(ruleId, button) {
const original = button.innerHTML;
button.disabled = true; button.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sender';
try {
const res = await fetch(`/api/v1/reminder-task-rules/${ruleId}/send-now`, {method: 'POST', credentials: 'include'});
const result = await res.json().catch(() => ({}));
if (!res.ok || !result.sent) throw new Error(result.detail || result.message || 'Kunne ikke sende opgavelisten');
alert(`Opgavelisten er sendt med ${result.count} sager.`);
} catch (e) { alert('Fejl: ' + e.message); }
finally { button.disabled = false; button.innerHTML = original; }
}
async function loadUserProfile() {
try {
const res = await fetch('/api/v1/auth/me/profile', { credentials: 'include' });
if (!res.ok) return;
const p = await res.json();
document.getElementById('prof_full_name').value = p.full_name || '';
document.getElementById('prof_title').value = p.title || '';
document.getElementById('prof_phone').value = p.phone || '';
await loadProfileCaseTypes(p.default_case_type || 'ticket');
} catch (e) { console.error('Failed to load profile', e); }
loadAnyDeskChips();
}
async function loadProfileCaseTypes(selectedType = 'ticket') {
const select = document.getElementById('prof_default_case_type');
if (!select) return;
const labels = { ticket: '🎫 Ticket', pipeline: '📈 Pipeline', opgave: '🧩 Opgave', ordre: '🧾 Ordre', projekt: '📁 Projekt', service: '🛠️ Service' };
try {
const res = await fetch('/api/v1/settings/case_types', { credentials: 'include' });
const setting = res.ok ? await res.json() : { value: '[]' };
const configured = JSON.parse(setting.value || '[]');
const types = Array.isArray(configured) ? configured.map(value => String(value).toLowerCase()) : [];
if (!types.includes('pipeline')) types.splice(1, 0, 'pipeline');
const finalTypes = types.length ? [...new Set(types)] : Object.keys(labels);
select.innerHTML = finalTypes.map(type => `<option value="${type}">${labels[type] || type}</option>`).join('');
select.value = finalTypes.includes(selectedType) ? selectedType : (finalTypes.includes('ticket') ? 'ticket' : finalTypes[0]);
} catch (e) { console.error('Failed to load profile case types', e); }
}
function buildInitials(name) {
const clean = String(name || '').trim();
if (!clean) return 'BR';
const parts = clean.split(/\s+/).filter(Boolean).slice(0, 2);
if (!parts.length) return 'BR';
return parts.map((part) => part.charAt(0).toUpperCase()).join('');
}
async function loadCurrentUserMenuIdentity() {
try {
const res = await fetch('/api/v1/auth/me', { credentials: 'include' });
if (!res.ok) return;
const user = await res.json();
const displayName = String(user.full_name || user.username || user.email || 'Bruger').trim() || 'Bruger';
const avatarEl = document.getElementById('currentUserAvatar');
const nameEl = document.getElementById('currentUserDisplayName');
if (nameEl) {
nameEl.textContent = displayName;
}
if (avatarEl) {
const initials = buildInitials(displayName);
avatarEl.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=0f4c75&color=fff`;
avatarEl.alt = displayName;
}
const projectCtLink = document.getElementById('projectCtProfileLink');
if (projectCtLink && user.is_superadmin === true) {
projectCtLink.classList.remove('d-none');
}
} catch (e) {
console.error('Failed to load current user identity', e);
}
}
async function loadAnyDeskChips() {
try {
const res = await fetch('/api/v1/auth/me/anydesk-ids', { credentials: 'include' });
if (!res.ok) return;
const { ids } = await res.json();
const box = document.getElementById('prof-anydesk-chips');
box.innerHTML = ids.length
? ids.map(entry => `
<span class="badge d-inline-flex align-items-center gap-1 fs-6 fw-normal"
style="background:rgba(15,76,117,0.1);color:#0f4c75;border:1px solid rgba(15,76,117,0.25);padding:.35rem .7rem;border-radius:6px">
<i class="bi bi-display" style="font-size:.8rem"></i>
<code style="font-size:.85rem;background:none;color:inherit">${entry.anydesk_id}</code>
${entry.label ? `<span style="opacity:.7;font-size:.8rem">— ${entry.label}</span>` : ''}
<button type="button" onclick="removeAnyDeskId(${entry.id})"
style="background:none;border:none;cursor:pointer;opacity:.6;padding:0 0 0 2px;line-height:1;color:inherit"
title="Fjern">&times;</button>
</span>`).join('')
: '<span class="text-secondary small">Ingen IDs tilføjet endnu</span>';
} catch(e) { /* silent */ }
}
async function addAnyDeskId() {
const id = document.getElementById('prof_anydesk_new_id').value.trim();
const label = document.getElementById('prof_anydesk_new_label').value.trim();
if (!id) return;
try {
const res = await fetch('/api/v1/auth/me/anydesk-ids', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ anydesk_id: id, label: label || null })
});
if (!res.ok) { const e = await res.json(); alert(e.detail || 'Fejl'); return; }
document.getElementById('prof_anydesk_new_id').value = '';
document.getElementById('prof_anydesk_new_label').value = '';
loadAnyDeskChips();
} catch(e) { alert('Fejl: ' + e.message); }
}
async function removeAnyDeskId(entryId) {
try {
const res = await fetch(`/api/v1/auth/me/anydesk-ids/${entryId}`, {
method: 'DELETE', credentials: 'include'
});
if (!res.ok) throw new Error('Fejl');
loadAnyDeskChips();
} catch(e) { alert('Fejl: ' + e.message); }
}
async function saveUserProfile() {
const payload = {
full_name: document.getElementById('prof_full_name').value || null,
title: document.getElementById('prof_title').value || null,
phone: document.getElementById('prof_phone').value || null,
default_case_type: document.getElementById('prof_default_case_type').value || 'ticket',
};
try {
const res = await fetch('/api/v1/auth/me/profile', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error((await res.json()).detail || 'Fejl');
loadCurrentUserMenuIdentity();
const statusEl = document.getElementById('prof-save-status');
statusEl.style.display = '';
setTimeout(() => { statusEl.style.display = 'none'; }, 3000);
} catch (e) { alert('Fejl: ' + e.message); }
}
document.addEventListener('DOMContentLoaded', () => {
loadCurrentUserMenuIdentity();
const profileModalEl = document.getElementById('profileModal');
if (profileModalEl) {
profileModalEl.addEventListener('shown.bs.modal', () => {
loadReminderPreferences();
loadProfileReminders();
loadUserProfile();
loadProfileMenuPreferences();
loadProfileTaskLists();
});
}
});
</script>
<!-- SMS Modal -->
<div class="modal fade" id="smsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="smsModalTitle">Send SMS</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Modtager</label>
<input type="text" class="form-control" id="smsRecipientInput" readonly>
</div>
<div class="mb-3">
<label class="form-label">Afsender</label>
<input type="text" class="form-control" id="smsSenderInput" placeholder="Valgfrit">
</div>
<div class="mb-2">
<label class="form-label">Besked</label>
<textarea class="form-control" id="smsMessageInput" rows="4" maxlength="1530"></textarea>
<div class="d-flex justify-content-end mt-1">
<small class="text-muted" id="smsCharCounter">0/1530</small>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" id="smsSendBtn">Send</button>
</div>
</div>
</div>
</div>
<!-- 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);
// Global Logout Function
function logoutUser(event) {
if (event) event.preventDefault();
// Clear local storage
localStorage.removeItem('access_token');
localStorage.removeItem('user');
// Clear cookies
document.cookie = "access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
// Redirect to login
window.location.href = '/login';
}
</script>
{% block scripts %}{% endblock %}
{% block extra_js %}{% endblock %}
</body>
</html>