2025-12-06 02:22:01 +01:00
<!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 >
2026-04-04 02:46:37 +02:00
< 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" >
2025-12-06 02:22:01 +01:00
< 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;
2026-04-12 02:27:01 +02:00
--bg-card-rgb: 255, 255, 255;
2025-12-06 02:22:01 +01:00
--text-primary: #2c3e50;
2026-04-12 02:27:01 +02:00
--text-primary-rgb: 44, 62, 80;
2025-12-06 02:22:01 +01:00
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
2026-05-02 11:02:29 +02:00
--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);
2025-12-06 02:22:01 +01:00
--border-radius: 12px;
2026-04-12 02:27:01 +02:00
--bottom-bar-height: 50px;
2026-08-17 19:27:43 +02:00
--bottom-bar-expanded-height: 68vh;
2026-04-12 02:27:01 +02:00
--bottom-bar-zindex: 1030;
2025-12-06 02:22:01 +01:00
}
2026-08-31 13:01:35 +02:00
/* 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; }
}
2025-12-06 02:22:01 +01:00
[data-bs-theme="dark"] {
--bg-body: #212529;
--bg-card: #2c3034;
2026-04-12 02:27:01 +02:00
--bg-card-rgb: 44, 48, 52;
2025-12-06 02:22:01 +01:00
--text-primary: #f8f9fa;
2026-04-12 02:27:01 +02:00
--text-primary-rgb: 248, 249, 250;
2025-12-06 02:22:01 +01:00
--text-secondary: #adb5bd;
--accent: #3d8bfd; /* Lighter blue for dark mode */
--accent-light: #373b3e;
2026-05-02 11:02:29 +02:00
--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);
2025-12-06 02:22:01 +01:00
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
2026-08-31 13:01:35 +02:00
padding-top: 68px;
2025-12-06 02:22:01 +01:00
transition: background-color 0.3s, color 0.3s;
}
2026-04-12 02:27:01 +02:00
body.bottom-bar-visible {
padding-bottom: calc(var(--bottom-bar-height) + 10px);
}
body.bottom-bar-visible.bottom-bar-expanded {
2026-08-17 19:27:43 +02:00
padding-bottom: calc(var(--bottom-bar-height) + var(--bottom-bar-expanded-height));
2026-04-12 02:27:01 +02:00
}
.global-bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: var(--bottom-bar-zindex);
2026-08-12 07:20:04 +02:00
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;
2026-04-12 02:27:01 +02:00
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;
2026-04-24 11:28:12 +02:00
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;
2026-04-12 02:27:01 +02:00
}
.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;
2026-08-12 07:20:04 +02:00
gap: 0.6rem;
2026-04-12 02:27:01 +02:00
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;
}
2026-04-24 23:12:51 +02:00
.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;
}
2026-04-12 02:27:01 +02:00
.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 {
2026-08-12 07:20:04 +02:00
border: 1px solid rgba(15, 76, 117, 0.18);
background: var(--accent-light);
2026-04-12 02:27:01 +02:00
color: var(--text-primary);
2026-08-12 07:20:04 +02:00
border-radius: 10px;
width: 38px;
height: 34px;
2026-04-12 02:27:01 +02:00
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 {
2026-08-12 07:20:04 +02:00
background: var(--accent);
color: #fff;
2026-04-12 02:27:01 +02:00
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 {
2026-08-12 07:20:04 +02:00
border: 1px solid rgba(var(--text-primary-rgb), 0.12);
2026-04-12 02:27:01 +02:00
background: var(--bg-card);
color: var(--text-primary);
2026-08-12 07:20:04 +02:00
border-radius: 9px;
2026-04-12 02:27:01 +02:00
padding: 0.3rem 0.75rem;
font-size: 0.8rem;
font-weight: 600;
line-height: 1.2;
2026-04-24 11:28:12 +02:00
transition: all 0.2s ease;
2026-04-12 02:27:01 +02:00
}
2026-04-21 18:59:30 +02:00
.global-bottom-bar .dropdown-menu {
z-index: calc(var(--bottom-bar-zindex) + 40);
}
2026-04-12 02:27:01 +02:00
.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;
}
2026-08-30 14:34:43 +02:00
.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;
}
2026-04-12 02:27:01 +02:00
.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 {
2026-08-12 07:20:04 +02:00
border: 1px solid rgba(var(--text-primary-rgb), 0.12);
background: rgba(var(--text-primary-rgb), 0.045);
2026-04-12 02:27:01 +02:00
color: var(--text-primary);
2026-08-12 07:20:04 +02:00
border-radius: 10px;
padding: 0.34rem 0.65rem;
font-size: 0.78rem;
2026-04-12 02:27:01 +02:00
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;
}
2026-04-21 18:59:30 +02:00
.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;
}
2026-04-12 02:27:01 +02:00
.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;
}
2026-04-21 18:59:30 +02:00
.global-bottom-bar .bb-chip.sev-ok .bb-chip-bubble {
background: rgba(25, 135, 84, 0.2);
}
2026-04-12 02:27:01 +02:00
.global-bottom-bar .bb-chip.sev-warn {
background: rgba(255, 193, 7, 0.22);
border-color: rgba(255, 193, 7, 0.5);
color: #8a6d00;
}
2026-04-21 18:59:30 +02:00
.global-bottom-bar .bb-chip.sev-warn .bb-chip-bubble {
background: rgba(255, 193, 7, 0.3);
}
2026-04-12 02:27:01 +02:00
.global-bottom-bar .bb-chip.sev-critical {
background: rgba(220, 53, 69, 0.14);
border-color: rgba(220, 53, 69, 0.4);
color: #b02a37;
}
2026-04-21 18:59:30 +02:00
.global-bottom-bar .bb-chip.sev-critical .bb-chip-bubble {
background: rgba(220, 53, 69, 0.22);
}
2026-04-12 02:27:01 +02:00
[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;
}
2026-04-21 18:59:30 +02:00
[data-bs-theme="dark"] .global-bottom-bar .bb-chip .bb-chip-bubble {
background: rgba(255, 255, 255, 0.12);
}
2026-04-12 02:27:01 +02:00
[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 {
2026-04-15 09:34:26 +02:00
display: none;
2026-04-12 02:27:01 +02:00
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 {
2026-04-15 09:34:26 +02:00
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;
2026-04-12 02:27:01 +02:00
}
.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 {
2026-08-17 19:27:43 +02:00
max-height: min(var(--bottom-bar-expanded-height), 590px);
2026-04-12 02:27:01 +02:00
opacity: 1;
}
.global-bottom-bar .bb-sheet-inner {
background: var(--bg-card);
border: 1px solid rgba(var(--text-primary-rgb), 0.1);
2026-08-12 07:20:04 +02:00
border-radius: 16px;
display: flex;
2026-08-17 19:27:43 +02:00
flex-direction: row;
min-height: 320px;
max-height: min(var(--bottom-bar-expanded-height), 540px);
height: min(var(--bottom-bar-expanded-height), 540px);
2026-04-12 02:27:01 +02:00
overflow: hidden;
2026-08-12 07:20:04 +02:00
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
2026-04-12 02:27:01 +02:00
margin-top: 0.5rem;
}
2026-07-03 20:20:22 +02:00
.global-bottom-bar .bb-sheet-inner > * {
min-height: 0;
}
2026-04-12 02:27:01 +02:00
.global-bottom-bar .bb-side-tabs {
2026-08-17 19:27:43 +02:00
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;
2026-08-12 07:20:04 +02:00
display: flex;
2026-08-17 19:27:43 +02:00
flex-direction: column;
gap: 0.3rem;
align-items: stretch;
min-height: 0;
overflow-y: auto;
2026-08-12 07:20:04 +02:00
scrollbar-width: none;
2026-04-12 02:27:01 +02:00
}
2026-08-12 07:20:04 +02:00
.global-bottom-bar .bb-side-tabs::-webkit-scrollbar { display: none; }
2026-04-12 02:27:01 +02:00
.global-bottom-bar .bb-tab-btn {
border: 1px solid transparent;
background: transparent;
color: var(--text-secondary);
2026-08-12 07:20:04 +02:00
border-radius: 9px;
2026-08-17 19:27:43 +02:00
text-align: left;
font-size: 0.84rem;
2026-04-12 02:27:01 +02:00
font-weight: 600;
2026-08-17 19:27:43 +02:00
padding: 0.62rem 0.7rem;
2026-04-12 02:27:01 +02:00
line-height: 1.3;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
2026-08-12 07:20:04 +02:00
white-space: nowrap;
flex: 0 0 auto;
2026-08-17 19:27:43 +02:00
width: 100%;
2026-04-12 02:27:01 +02:00
}
.global-bottom-bar .bb-tab-btn i {
font-size: 1rem;
opacity: 0.7;
}
2026-07-03 20:20:22 +02:00
.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;
}
2026-04-12 02:27:01 +02:00
.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 {
2026-08-12 07:20:04 +02:00
background: var(--accent);
color: #fff;
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.22);
2026-04-12 02:27:01 +02:00
font-weight: 700;
}
.global-bottom-bar .bb-tab-btn.is-active i {
opacity: 1;
2026-08-12 07:20:04 +02:00
color: inherit;
2026-04-12 02:27:01 +02:00
}
2026-07-03 20:20:22 +02:00
[data-bs-theme="dark"] .global-bottom-bar .bb-tab-btn .bb-tab-badge {
background: rgba(255, 138, 148, 0.18);
color: #ffb3ba;
}
2026-04-12 02:27:01 +02:00
.global-bottom-bar .bb-tab-content {
2026-08-17 19:27:43 +02:00
padding: 1rem 1.15rem 1.1rem;
2026-07-03 20:20:22 +02:00
overflow: hidden;
display: flex;
flex-direction: column;
2026-08-17 19:27:43 +02:00
flex: 1 1 0;
width: 100%;
min-width: 0;
2026-07-03 20:20:22 +02:00
height: 100%;
min-height: 0;
2026-04-12 02:27:01 +02:00
}
.global-bottom-bar .bb-tab-title {
2026-08-17 19:27:43 +02:00
font-size: 1.05rem;
2026-04-12 02:27:01 +02:00
font-weight: 700;
color: var(--text-primary);
2026-08-17 19:27:43 +02:00
margin-bottom: 0.15rem;
2026-04-12 02:27:01 +02:00
display: flex;
align-items: center;
gap: 0.5rem;
2026-07-03 20:20:22 +02:00
line-height: 1.15;
2026-04-12 02:27:01 +02:00
}
2026-08-17 19:27:43 +02:00
.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; }
2026-04-12 02:27:01 +02:00
.global-bottom-bar .bb-tab-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
2026-08-17 19:27:43 +02:00
gap: 0.45rem;
2026-04-12 02:27:01 +02:00
}
.global-bottom-bar .bb-tab-list li {
2026-08-17 19:27:43 +02:00
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;
2026-04-12 02:27:01 +02:00
font-size: 0.88rem;
line-height: 1.4;
color: var(--text-primary);
2026-08-17 19:27:43 +02:00
box-shadow: none;
2026-04-24 11:28:12 +02:00
transition: transform 0.2s ease, box-shadow 0.2s ease;
2026-04-12 02:27:01 +02:00
}
.global-bottom-bar .bb-tab-list li:hover {
2026-08-17 19:27:43 +02:00
transform: translateY(-1px);
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
2026-04-24 11:28:12 +02:00
}
2026-08-17 19:27:43 +02:00
.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; }
2026-08-12 07:20:04 +02:00
.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;
2026-08-17 19:27:43 +02:00
gap: 0.6rem;
2026-08-12 07:20:04 +02:00
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; }
2026-07-03 20:20:22 +02:00
.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;
2026-08-17 19:27:43 +02:00
padding: 0.1rem 0 0.45rem;
2026-07-03 20:20:22 +02:00
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 {
2026-08-17 19:27:43 +02:00
max-height: none;
flex: 1 1 auto;
2026-07-03 20:20:22 +02:00
overflow-y: auto;
padding-right: 0.2rem;
}
.global-bottom-bar #bbTabInnerContent {
flex: 1 1 auto;
display: flex;
flex-direction: column;
2026-08-17 19:27:43 +02:00
width: 100%;
2026-07-03 20:20:22 +02:00
height: 100%;
min-height: 0;
overflow: hidden;
}
2026-08-17 19:27:43 +02:00
.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; }
2026-07-03 20:20:22 +02:00
.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 {
2026-08-17 19:27:43 +02:00
min-width: 0;
2026-07-03 20:20:22 +02:00
}
.global-bottom-bar .bb-notes-list {
min-height: 0;
overflow-y: auto;
padding-right: 0.2rem;
}
.global-bottom-bar .bb-messages-composer {
2026-08-17 19:27:43 +02:00
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;
2026-07-03 20:20:22 +02:00
margin-top: 0.15rem;
}
.global-bottom-bar .bb-messages-layout {
height: 100%;
overflow: hidden;
}
2026-08-17 19:27:43 +02:00
.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; }
2026-08-25 01:09:45 +02:00
.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)); }
2026-08-17 19:27:43 +02:00
.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; }
2026-07-03 20:20:22 +02:00
.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;
}
2026-04-24 11:28:12 +02:00
#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);
}
2026-08-17 19:27:43 +02:00
#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)); }
2026-08-17 20:53:08 +02:00
#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; }
2026-08-17 19:27:43 +02:00
2026-04-24 11:28:12 +02:00
#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);
2026-04-12 02:27:01 +02:00
}
@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;
2026-08-17 19:27:43 +02:00
flex-direction: column;
2026-04-12 02:27:01 +02:00
}
2026-04-15 09:34:26 +02:00
.global-bottom-bar.is-expanded .bb-header {
2026-04-12 02:27:01 +02:00
flex-wrap: wrap;
row-gap: 0.4rem;
}
2026-04-15 09:34:26 +02:00
.global-bottom-bar.is-expanded .bb-zone-left,
.global-bottom-bar.is-expanded .bb-zone-center,
.global-bottom-bar.is-expanded .bb-zone-right {
2026-04-12 02:27:01 +02:00
flex: 1 1 100%;
justify-content: flex-start;
}
.global-bottom-bar .bb-side-tabs {
2026-08-17 19:27:43 +02:00
width: 100%;
flex: 0 0 auto;
flex-direction: row;
2026-04-12 02:27:01 +02:00
border-right: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
overflow-x: auto;
}
2026-08-17 19:27:43 +02:00
.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); }
2026-08-12 07:20:04 +02:00
.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; }
2026-08-17 19:27:43 +02:00
#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; }
2026-04-12 02:27:01 +02:00
}
2025-12-06 02:22:01 +01:00
.navbar {
2026-08-31 13:01:35 +02:00
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);
2025-12-06 02:22:01 +01:00
padding: 1rem 0;
2026-08-31 13:01:35 +02:00
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 13– 15 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;
}
2025-12-06 02:22:01 +01:00
}
.navbar-brand {
font-weight: 700;
color: var(--accent);
font-size: 1.25rem;
2026-08-31 13:01:35 +02:00
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;
}
2025-12-06 02:22:01 +01:00
}
.nav-link {
color: var(--text-secondary);
padding: 0.6rem 1.2rem !important;
2026-08-31 13:01:35 +02:00
border-radius: 11px;
transition: background-color .16s ease, color .16s ease, box-shadow .16s ease;
font-weight: 650;
2025-12-06 02:22:01 +01:00
margin: 0 0.2rem;
}
2026-08-31 13:01:35 +02:00
.nav-link:hover, .nav-link.active, .nav-link.show, .nav-link.section-active {
2025-12-06 02:22:01 +01:00
background-color: var(--accent-light);
color: var(--accent);
}
2026-08-31 13:01:35 +02:00
#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;
}
2025-12-06 02:22:01 +01:00
.card {
2026-05-02 11:02:29 +02:00
border: 2px solid var(--frame-border-strong);
border-left: 4px solid var(--accent);
2025-12-06 02:22:01 +01:00
border-radius: var(--border-radius);
2026-05-02 11:02:29 +02:00
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;
2025-12-06 02:22:01 +01:00
}
.card:hover {
transform: translateY(-2px);
2026-05-02 11:02:29 +02:00
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%);
2025-12-06 02:22:01 +01:00
}
.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 {
2026-08-31 13:01:35 +02:00
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);
2025-12-06 02:22:01 +01:00
}
2025-12-13 12:06:28 +01:00
/* 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;
}
2025-12-06 02:22:01 +01:00
.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);
}
2026-06-18 22:20:39 +02:00
/* 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;
}
2025-12-13 12:06:28 +01:00
.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);
}
2025-12-06 02:22:01 +01:00
< / style >
{% block extra_css %}{% endblock %}
< / head >
2026-05-02 11:02:29 +02:00
{% 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 '') %}
2026-07-03 20:20:22 +02:00
{% set _can_click_to_call = true %}
2025-12-06 02:22:01 +01:00
< body >
2026-08-31 13:01:35 +02:00
< nav class = "navbar navbar-expand-wide fixed-top" >
2025-12-06 02:22:01 +01:00
< div class = "container-fluid px-4" >
< a class = "navbar-brand d-flex align-items-center" href = "/" >
2026-08-31 13:01:35 +02:00
< 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;" >
2025-12-06 02:22:01 +01:00
< i class = "bi bi-hdd-network-fill" style = "font-size: 16px;" > < / i >
< / div >
2026-08-31 13:01:35 +02:00
< span class = "bmc-brand-copy" > < span class = "bmc-brand-name" > BMC Hub< / span > < span class = "bmc-brand-subtitle" > Operations< / span > < / span >
2025-12-06 02:22:01 +01:00
< / a >
2026-08-31 13:01:35 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< / button >
< div class = "collapse navbar-collapse" id = "navbarNav" >
< ul class = "navbar-nav mx-auto" >
2026-06-18 22:20:39 +02:00
< li class = "nav-item dropdown" data-menu-key = "menu-crm" >
2025-12-06 02:22:01 +01:00
< 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" >
2026-05-12 08:41:13 +02:00
< li > < h6 class = "dropdown-header" > Kunderelationer< / h6 > < / li >
2026-06-18 22:20:39 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< li > < hr class = "dropdown-divider" > < / li >
2026-05-12 08:41:13 +02:00
< li > < h6 class = "dropdown-header" > Struktur< / h6 > < / li >
2026-06-18 22:20:39 +02:00
< 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 >
2026-07-23 21:32:56 +02:00
< li data-menu-key = "menu-crm-cross-fields" > < a class = "dropdown-item py-2" href = "/app/locations/cross-fields" > Krydsfelter< / a > < / li >
2026-05-12 08:41:13 +02:00
< li > < hr class = "dropdown-divider" > < / li >
< li > < h6 class = "dropdown-header" > Pipeline< / h6 > < / li >
2026-06-18 22:20:39 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< / ul >
< / li >
2026-06-18 22:20:39 +02:00
< li class = "nav-item" data-menu-key = "menu-sager" >
2026-02-01 11:58:44 +01:00
< a class = "nav-link" href = "/sag" >
2026-01-31 23:16:24 +01:00
< i class = "bi bi-list-check me-2" > < / i > Sager
< / a >
< / li >
2026-06-18 22:20:39 +02:00
< li class = "nav-item" data-menu-key = "menu-kalender" >
2026-02-14 02:26:29 +01:00
< a class = "nav-link" href = "/calendar" >
< i class = "bi bi-calendar3 me-2" > < / i > Kalender
< / a >
< / li >
2026-06-18 22:20:39 +02:00
< li class = "nav-item dropdown" data-menu-key = "menu-support" >
2025-12-06 02:22:01 +01:00
< 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" >
2026-05-12 08:41:13 +02:00
< li > < h6 class = "dropdown-header" > Support< / h6 > < / li >
2026-06-18 22:20:39 +02:00
< 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 >
2026-08-31 13:01:35 +02:00
< 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 >
2026-06-18 22:20:39 +02:00
< 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 >
2026-01-31 23:16:24 +01:00
< li > < hr class = "dropdown-divider" > < / li >
2026-05-12 08:41:13 +02:00
< li > < h6 class = "dropdown-header" > Assets & Licenser< / h6 > < / li >
2026-06-18 22:20:39 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< li > < hr class = "dropdown-divider" > < / li >
2026-05-12 08:41:13 +02:00
< li > < h6 class = "dropdown-header" > Værktøjer< / h6 > < / li >
2026-06-18 22:20:39 +02:00
< 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 >
2026-08-28 20:49:55 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< / ul >
< / li >
2026-06-18 22:20:39 +02:00
< li class = "nav-item dropdown" data-menu-key = "menu-salg" >
2025-12-06 02:22:01 +01:00
< 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" >
2026-05-12 08:41:13 +02:00
< li > < h6 class = "dropdown-header" > Salg< / h6 > < / li >
2026-06-18 22:20:39 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< / ul >
< / li >
2026-06-18 22:20:39 +02:00
< li class = "nav-item dropdown" data-menu-key = "menu-okonomi" >
2025-12-06 02:22:01 +01:00
< 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" >
2026-05-12 08:41:13 +02:00
< li > < h6 class = "dropdown-header" > Fakturering< / h6 > < / li >
2026-06-18 22:20:39 +02:00
< 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 >
2026-07-03 20:20:22 +02:00
< 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 >
2026-06-18 22:20:39 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< li > < hr class = "dropdown-divider" > < / li >
2026-05-12 08:41:13 +02:00
< li > < h6 class = "dropdown-header" > Aftaler< / h6 > < / li >
2026-06-18 22:20:39 +02:00
< 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 >
2026-07-10 07:08:49 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< / ul >
< / li >
< / ul >
2026-08-31 13:01:35 +02:00
< div class = "navbar-actions d-flex align-items-center gap-3" >
2026-06-18 22:20:39 +02:00
< div class = "dropdown" data-menu-key = "menu-datamigration" >
2026-03-20 18:43:45 +01:00
< 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" >
2026-07-28 14:18:24 +02:00
< 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 >
2026-06-18 22:20:39 +02:00
< 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 >
2026-07-09 23:44:30 +02:00
< 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 >
2026-06-18 22:20:39 +02:00
< 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 >
2026-03-20 18:43:45 +01:00
< / ul >
< / div >
2026-08-31 13:01:35 +02:00
< div class = "navbar-utility-dock" >
2026-04-01 21:34:58 +02:00
< 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 >
2026-02-20 07:10:06 +01:00
< 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 >
feat(manual): add admin interface for creating and editing manuals
- Implemented admin page for manual articles with fields for title, module, difficulty, tags, summary, content, steps, and relations.
- Added preview functionality for markdown content.
- Created list view for recent manuals with edit and view options.
- Developed detail view for individual manuals displaying content, steps, and related guides.
- Established database schema for manual articles, steps, and relations with appropriate indexing.
- Seeded initial manual articles and steps for core functionalities.
- Normalized newline characters in existing manual content.
- Added additional manuals and steps for enhanced user guidance.
2026-04-05 21:48:59 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< 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 >
2026-04-01 21:34:58 +02:00
< 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 >
2026-05-12 08:41:13 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< div class = "dropdown" >
< a href = "#" class = "d-flex align-items-center text-decoration-none text-dark dropdown-toggle" data-bs-toggle = "dropdown" >
2026-07-03 20:20:22 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< / a >
< ul class = "dropdown-menu dropdown-menu-end mt-2" >
2026-02-06 10:47:14 +01:00
< li > < a class = "dropdown-item py-2" href = "#" data-bs-toggle = "modal" data-bs-target = "#profileModal" > Profil< / a > < / li >
2025-12-06 11:04:19 +01:00
< li > < a class = "dropdown-item py-2" href = "/settings" > < i class = "bi bi-gear me-2" > < / i > Indstillinger< / a > < / li >
2026-03-20 18:43:45 +01:00
< li > < a class = "dropdown-item py-2" href = "/tags#search" > < i class = "bi bi-tags me-2" > < / i > Tag søgning< / a > < / li >
2025-12-15 12:28:12 +01:00
< li > < a class = "dropdown-item py-2" href = "/backups" > < i class = "bi bi-hdd-stack me-2" > < / i > Backup System< / a > < / li >
2025-12-06 21:27:47 +01:00
< li > < a class = "dropdown-item py-2" href = "/devportal" > < i class = "bi bi-code-square me-2" > < / i > DEV Portal< / a > < / li >
2026-08-31 13:01:35 +02:00
< 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 >
2025-12-06 02:22:01 +01:00
< li > < hr class = "dropdown-divider" > < / li >
2026-02-11 13:23:32 +01:00
< 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 >
2025-12-06 02:22:01 +01:00
< / ul >
< / div >
2026-08-31 13:01:35 +02:00
< / div >
2025-12-06 02:22:01 +01:00
< / 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;" >
2025-12-06 21:27:47 +01:00
<!-- Search Results & Workflows (3/4 width) -->
2025-12-06 02:22:01 +01:00
< div class = "col-lg-9 p-4" style = "border-right: 1px solid rgba(0,0,0,0.1);" >
2025-12-06 21:27:47 +01:00
<!-- 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 >
2025-12-06 02:22:01 +01:00
< 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 >
2025-12-06 21:27:47 +01:00
< p class = "text-muted mt-3" > Tryk < kbd > ⌘K< / kbd > eller begynd at skrive...< / p >
2025-12-06 02:22:01 +01:00
< / 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 >
2026-02-09 15:30:07 +01:00
< a href = "/devportal" class = "btn btn-sm btn-outline-primary" >
2025-12-06 02:22:01 +01:00
< 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 >
2026-02-09 15:30:07 +01:00
< a href = "/devportal" class = "btn btn-sm btn-outline-primary" >
2025-12-06 02:22:01 +01:00
< 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 >
2026-04-01 21:34:58 +02:00
2026-07-28 14:18:24 +02:00
<!-- 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 >
2026-04-01 21:34:58 +02:00
<!-- 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 >
2025-12-06 02:22:01 +01:00
<!-- 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 >
2026-02-09 15:30:07 +01:00
< a href = "/devportal" class = "btn btn-sm btn-outline-primary" >
2025-12-06 02:22:01 +01:00
< 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 >
2026-02-09 15:30:07 +01:00
< a href = "/devportal" class = "btn btn-sm btn-outline-primary" >
2025-12-06 02:22:01 +01:00
< 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 >
2025-12-06 21:27:47 +01:00
<!-- 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 -->
2025-12-06 02:22:01 +01:00
< / div >
2025-12-06 21:27:47 +01:00
< / 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 >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
2025-12-06 21:27:47 +01:00
< / 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 >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
2025-12-06 21:27:47 +01:00
< / 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 >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
2026-05-12 08:41:13 +02:00
{% include "shared/frontend/bug_report_modal.html" %}
feat(ticket-module): Implement ticket system with comprehensive database schema, permissions, and testing suite
- Added migration 025 for the Ticket System, creating tables for tickets, comments, attachments, worklogs, prepaid cards, and audit logs.
- Introduced migration 026 to add ticket-related permissions to the auth system and assign them to user groups.
- Developed a test suite for the Ticket Module, validating database schema, ticket number generation, prepaid card constraints, service logic, worklog creation, audit logging, and views.
2025-12-15 23:40:23 +01:00
{% block content_wrapper %}
2025-12-06 02:22:01 +01:00
< div class = "container-fluid px-4 py-4" >
{% block content %}{% endblock %}
< / div >
feat(ticket-module): Implement ticket system with comprehensive database schema, permissions, and testing suite
- Added migration 025 for the Ticket System, creating tables for tickets, comments, attachments, worklogs, prepaid cards, and audit logs.
- Introduced migration 026 to add ticket-related permissions to the auth system and assign them to user groups.
- Developed a test suite for the Ticket Module, validating database schema, ticket number generation, prepaid card constraints, service logic, worklog creation, audit logging, and views.
2025-12-15 23:40:23 +01:00
{% endblock %}
2025-12-06 02:22:01 +01:00
2026-04-12 02:27:01 +02:00
< div id = "globalBottomBar" class = "global-bottom-bar" hidden >
< div class = "bb-header" >
< div class = "bb-zone bb-zone-left" role = "status" aria-live = "polite" >
2026-04-24 23:12:51 +02:00
< 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 >
2026-04-21 18:59:30 +02:00
< 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 >
2026-09-07 19:05:58 +02:00
< 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 >
2026-06-26 14:15:46 +02:00
< 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 >
2026-04-12 02:27:01 +02:00
< / 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 >
2026-04-24 11:28:12 +02:00
< 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 >
2026-08-25 01:09:45 +02:00
< 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 >
2026-04-12 02:27:01 +02:00
<!-- Vises kun for chefer, men her i markup -->
2026-08-12 07:20:04 +02:00
< 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 >
2026-04-12 02:27:01 +02:00
< / div >
< div class = "bb-tab-content" role = "tabpanel" aria-live = "polite" >
2026-08-17 19:27:43 +02:00
< 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 >
2026-04-12 02:27:01 +02:00
< div id = "bbTabInnerContent" >
< ul id = "bbTabList" class = "bb-tab-list" >
< li > Venter på data...< / li >
< / ul >
< / div >
< / div >
< / div >
< / div >
< / div >
2026-04-24 11:28:12 +02:00
< div class = "modal fade" id = "bbSwitchCaseModal" tabindex = "-1" aria-hidden = "true" aria-labelledby = "bbSwitchCaseModalLabel" >
2026-08-17 19:27:43 +02:00
< div class = "modal-dialog modal-dialog-scrollable modal-xl modal-dialog-bottom" >
2026-04-24 11:28:12 +02:00
< 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" >
2026-08-17 19:27:43 +02:00
< 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 >
2026-04-24 11:28:12 +02:00
< 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 >
2026-08-17 19:27:43 +02:00
< 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" >
2026-08-17 20:53:08 +02:00
< div > < div class = "fw-bold" > Konverter afsluttet tid< / div > < div id = "bbConvertTimeName" class = "bb-switch-timer-info small mt-1" > < / div > < / div >
2026-08-17 19:27:43 +02:00
< button type = "button" class = "btn-close" data-bb-cancel-convert aria-label = "Luk" > < / button >
2026-04-24 11:28:12 +02:00
< / div >
2026-08-17 19:27:43 +02:00
< 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 >
2026-04-24 11:28:12 +02:00
< / div >
< / div >
2026-08-17 19:27:43 +02:00
< 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 >
2026-04-24 11:28:12 +02:00
< / 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 >
2026-04-04 02:46:37 +02:00
< 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();
}
});
2026-06-26 14:15:46 +02:00
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);
};
}
2026-04-04 02:46:37 +02:00
< / script >
2025-12-06 02:22:01 +01:00
< script src = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" > < / script >
2026-05-01 20:58:13 +02:00
< script src = "/static/js/tag-picker.js?v=2.2" > < / script >
< script src = "/static/js/task-template-selector.js?v=1.1" > < / script >
2026-02-06 10:47:14 +01:00
< script src = "/static/js/notifications.js?v=1.0" > < / script >
2026-08-18 20:57:49 +02:00
< script src = "/static/js/telefoni.js?v=2.5" > < / script >
2026-07-09 23:44:30 +02:00
< script src = "/static/js/sms.js?v=1.1" > < / script >
2026-06-18 22:20:39 +02:00
< script src = "/static/js/bug-report.js?v=1.4" > < / script >
2026-09-07 19:05:58 +02:00
< script src = "/static/js/bottom-bar.js?v=2.69" > < / script >
2025-12-06 02:22:01 +01:00
< script >
// Dark Mode Toggle Logic
2026-07-03 20:20:22 +02:00
window.BMC_CAN_CLICK_TO_CALL = true;
2026-05-02 11:02:29 +02:00
2025-12-06 02:22:01 +01:00
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');
}
});
2026-06-18 22:20:39 +02:00
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;
2026-09-07 19:05:58 +02:00
if (li.classList.contains('bmc-menu-hidden') || anchor.classList.contains('bmc-menu-hidden')) return false;
2026-06-18 22:20:39 +02:00
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;
2026-09-07 19:05:58 +02:00
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');
}
2026-06-18 22:20:39 +02:00
});
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);
});
2025-12-08 09:15:52 +01:00
// Global Search Modal (Cmd+K) - Initialize after DOM is ready
2025-12-13 12:06:28 +01:00
let selectedResultIndex = -1;
let allResults = [];
2025-12-08 09:15:52 +01:00
document.addEventListener('DOMContentLoaded', () => {
2026-08-31 13:01:35 +02:00
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();
}
2026-06-18 22:20:39 +02:00
loadAndApplyMenuVisibility();
2026-08-31 13:01:35 +02:00
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');
});
2025-12-08 09:15:52 +01:00
const searchModal = new bootstrap.Modal(document.getElementById('globalSearchModal'));
2026-04-01 21:34:58 +02:00
const searchBubbleBtn = document.getElementById('globalSearchBtn');
feat(manual): add admin interface for creating and editing manuals
- Implemented admin page for manual articles with fields for title, module, difficulty, tags, summary, content, steps, and relations.
- Added preview functionality for markdown content.
- Created list view for recent manuals with edit and view options.
- Developed detail view for individual manuals displaying content, steps, and related guides.
- Established database schema for manual articles, steps, and relations with appropriate indexing.
- Seeded initial manual articles and steps for core functionalities.
- Normalized newline characters in existing manual content.
- Added additional manuals and steps for enhanced user guidance.
2026-04-05 21:48:59 +02:00
const contextManualBtn = document.getElementById('contextManualBtn');
2026-04-01 21:34:58 +02:00
const remindersBubbleBtn = document.getElementById('globalRemindersBtn');
const profileModalEl = document.getElementById('profileModal');
const profileModalInstance = profileModalEl ? new bootstrap.Modal(profileModalEl) : null;
2025-12-13 12:06:28 +01:00
const globalSearchInput = document.getElementById('globalSearchInput');
2026-04-01 21:34:58 +02:00
feat(manual): add admin interface for creating and editing manuals
- Implemented admin page for manual articles with fields for title, module, difficulty, tags, summary, content, steps, and relations.
- Added preview functionality for markdown content.
- Created list view for recent manuals with edit and view options.
- Developed detail view for individual manuals displaying content, steps, and related guides.
- Established database schema for manual articles, steps, and relations with appropriate indexing.
- Seeded initial manual articles and steps for core functionalities.
- Normalized newline characters in existing manual content.
- Added additional manuals and steps for enhanced user guidance.
2026-04-05 21:48:59 +02:00
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 '';
}
2026-04-01 21:34:58 +02:00
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();
});
}
feat(manual): add admin interface for creating and editing manuals
- Implemented admin page for manual articles with fields for title, module, difficulty, tags, summary, content, steps, and relations.
- Added preview functionality for markdown content.
- Created list view for recent manuals with edit and view options.
- Developed detail view for individual manuals displaying content, steps, and related guides.
- Established database schema for manual articles, steps, and relations with appropriate indexing.
- Seeded initial manual articles and steps for core functionalities.
- Normalized newline characters in existing manual content.
- Added additional manuals and steps for enhanced user guidance.
2026-04-05 21:48:59 +02:00
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';
});
}
2025-12-13 12:06:28 +01:00
// 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();
2026-04-01 21:34:58 +02:00
if (navigateToSagFromScan(e.target.value)) {
return;
}
2025-12-13 12:06:28 +01:00
selectCurrentResult();
}
});
}
2025-12-06 02:22:01 +01:00
2025-12-08 09:15:52 +01:00
// Keyboard shortcut: Cmd+K or Ctrl+K
document.addEventListener('keydown', (e) => {
2026-02-20 07:10:06 +01:00
// Cmd+K / Ctrl+K for global search
2025-12-08 09:15:52 +01:00
if ((e.metaKey || e.ctrlKey) & & e.key === 'k') {
e.preventDefault();
2026-04-01 21:34:58 +02:00
openGlobalSearchModal();
2025-12-08 09:15:52 +01:00
}
2026-02-20 07:10:06 +01:00
// '+' 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();
2026-07-17 01:58:02 +02:00
openNewCasePage();
2026-02-20 07:10:06 +01:00
}
// Cmd+Shift+C / Ctrl+Shift+C for QuickCreate
if ((e.metaKey || e.ctrlKey) & & e.shiftKey & & e.key === 'c') {
e.preventDefault();
2026-07-17 01:58:02 +02:00
openNewCasePage();
2026-02-20 07:10:06 +01:00
}
2025-12-08 09:15:52 +01:00
// ESC to close
if (e.key === 'Escape') {
searchModal.hide();
}
});
2026-07-17 01:58:02 +02:00
function openNewCasePage() {
window.location.href = '/sag/new';
2026-02-20 07:10:06 +01:00
}
// QuickCreate button click handler
document.getElementById('quickCreateBtn')?.addEventListener('click', (e) => {
e.preventDefault();
2026-07-17 01:58:02 +02:00
openNewCasePage();
2026-02-20 07:10:06 +01:00
});
2025-12-08 09:15:52 +01:00
// Reset search when modal is closed
document.getElementById('globalSearchModal').addEventListener('hidden.bs.modal', () => {
2025-12-13 12:06:28 +01:00
if (globalSearchInput) {
globalSearchInput.value = '';
}
2025-12-08 09:15:52 +01:00
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';
2026-07-28 14:18:24 +02:00
if (document.getElementById('caseResults')) document.getElementById('caseResults').style.display = 'none';
2026-04-01 21:34:58 +02:00
if (document.getElementById('emailResults')) document.getElementById('emailResults').style.display = 'none';
2025-12-08 09:15:52 +01:00
if (document.getElementById('salesResults')) document.getElementById('salesResults').style.display = 'none';
if (document.getElementById('financeResults')) document.getElementById('financeResults').style.display = 'none';
});
2025-12-06 02:22:01 +01:00
});
2025-12-06 21:27:47 +01:00
// Load live statistics for the three boxes
async function loadLiveStats() {
try {
2026-02-09 15:30:07 +01:00
const response = await fetch('/api/v1/live-stats');
2025-12-06 21:27:47 +01:00
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 {
2026-02-09 15:30:07 +01:00
const response = await fetch('/api/v1/recent-activity');
2025-12-06 21:27:47 +01:00
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';
}
2025-12-13 12:06:28 +01:00
// Helper function to escape HTML
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
2025-12-06 21:27:47 +01:00
let selectedEntity = null;
2025-12-13 12:06:28 +01:00
// 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();
}
}
2026-04-01 21:34:58 +02:00
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;
}
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
window.location.href = `/sag/${sagId}/v3`;
2026-04-01 21:34:58 +02:00
return true;
}
2025-12-13 12:06:28 +01:00
// Global search function
async function performGlobalSearch(query) {
2026-04-01 21:34:58 +02:00
if (navigateToSagFromScan(query)) {
return;
}
2025-12-13 12:06:28 +01:00
if (!query || query.trim().length < 2 ) {
document.getElementById('emptyState').style.display = 'block';
document.getElementById('crmResults').style.display = 'none';
document.getElementById('supportResults').style.display = 'none';
2026-04-01 21:34:58 +02:00
if (document.getElementById('emailResults')) document.getElementById('emailResults').style.display = 'none';
2025-12-13 12:06:28 +01:00
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');
}
2026-04-01 21:34:58 +02:00
2026-07-28 14:18:24 +02:00
// 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(', ')}`
: '';
2026-08-17 19:27:43 +02:00
const archivedHtml = item.is_archived
? ' • < span class = "badge bg-secondary" > Arkiveret< / span > '
: '';
2026-07-28 14:18:24 +02:00
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}
2026-08-17 19:27:43 +02:00
${archivedHtml}
2026-07-28 14:18:24 +02:00
< / 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';
}
2026-04-01 21:34:58 +02:00
// 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';
}
2025-12-13 12:06:28 +01:00
// 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);
}
}
2025-12-06 21:27:47 +01:00
// 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);
}
};
2025-12-06 13:13:05 +01:00
2025-12-13 12:06:28 +01:00
// Search function already implemented in DOMContentLoaded above - duplicate removed
2025-12-06 02:22:01 +01:00
// 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);
});
2025-12-13 12:06:28 +01:00
// 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');
});
}
});
});
2025-12-06 02:22:01 +01:00
< / script >
2025-12-15 12:28:12 +01:00
2026-04-12 02:27:01 +02:00
<!-- Manual Help Modal -->
{% include ["manual_modal.html", "shared/frontend/manual_modal.html"] ignore missing %}
2026-02-06 10:47:14 +01:00
<!-- 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 >
2026-06-18 22:20:39 +02:00
< li class = "nav-item" role = "presentation" >
2026-09-07 19:05:58 +02:00
< 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()" >
2026-06-18 22:20:39 +02:00
Menu
< / button >
< / li >
2026-02-06 10:47:14 +01:00
< / ul >
< div class = "tab-content" id = "profileTabsContent" >
< div class = "tab-pane fade show active" id = "profile-overview" role = "tabpanel" tabindex = "0" >
2026-03-30 07:50:15 +02:00
< 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 >
2026-07-17 01:58:02 +02:00
< 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 >
2026-03-30 07:50:15 +02:00
< 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 >
2026-02-06 10:47:14 +01:00
< / 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 >
2026-07-28 14:18:24 +02:00
< 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 >
2026-02-06 10:47:14 +01:00
< 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 >
2026-06-18 22:20:39 +02:00
2026-09-07 19:05:58 +02:00
< 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 >
2026-06-18 22:20:39 +02:00
< 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" >
2026-09-07 19:05:58 +02:00
< 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 >
2026-06-18 22:20:39 +02:00
< / div >
< / div >
2026-09-07 19:05:58 +02:00
< 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 >
2026-06-18 22:20:39 +02:00
< / div >
< / div >
< / div >
2026-02-06 10:47:14 +01:00
< / div >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Luk< / button >
< / div >
< / div >
< / div >
< / div >
2026-09-07 19:05:58 +02:00
< style >
/* Personal menu preferences must also win over responsive navbar layout rules. */
#navbarNav .bmc-menu-hidden { display: none !important; }
< / style >
2026-02-06 10:47:14 +01:00
< script >
2026-06-18 22:20:39 +02:00
const PROFILE_MENU_PREF_ITEMS = [
{ key: 'menu-crm', label: 'CRM' },
{ key: 'menu-sager', label: 'Sager' },
{ key: 'menu-kalender', label: 'Kalender' },
{ 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' },
2026-08-31 13:01:35 +02:00
{ key: 'menu-support-internet-connections', label: 'Support: Internetforbindelser' },
{ key: 'menu-support-losninger', label: 'Support: Løsninger' },
2026-06-18 22:20:39 +02:00
{ 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' },
2026-08-28 20:49:55 +02:00
{ key: 'menu-support-website-content', label: 'Support: Website-indhold' },
2026-06-18 22:20:39 +02:00
{ 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' },
2026-07-03 20:20:22 +02:00
{ key: 'menu-okonomi-also-cloud', label: 'Økonomi: ALSO Cloud Marketplace' },
2026-06-18 22:20:39 +02:00
{ 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' },
2026-07-28 14:18:24 +02:00
{ key: 'menu-datamigration-migration-center', label: 'Data migration: Migreringscenter' },
2026-06-18 22:20:39 +02:00
{ 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' },
2026-07-09 23:44:30 +02:00
{ key: 'menu-datamigration-internet-wizard-v2', label: 'Data migration: Internet Wizard v2' },
2026-06-18 22:20:39 +02:00
{ 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 }
}));
2026-09-07 19:05:58 +02:00
// Apply immediately as well. This avoids relying on a custom event
// when a page has scripts from an older browser cache.
applyMenuVisibility(hiddenKeys);
2026-06-18 22:20:39 +02:00
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;
});
}
2026-02-06 10:47:14 +01:00
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 || '';
2026-07-28 14:18:24 +02:00
document.getElementById('pref_mattermost_username').value = prefs.mattermost_username || '';
2026-02-06 10:47:14 +01:00
} 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,
2026-07-28 14:18:24 +02:00
email_override: document.getElementById('pref_email_override').value || null,
mattermost_username: document.getElementById('pref_mattermost_username').value.trim().replace(/^@/, '') || null
2026-02-06 10:47:14 +01:00
};
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);
}
}
2026-09-07 19:05:58 +02:00
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; }
}
2026-03-30 07:50:15 +02:00
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 || '';
2026-07-17 01:58:02 +02:00
await loadProfileCaseTypes(p.default_case_type || 'ticket');
2026-03-30 07:50:15 +02:00
} catch (e) { console.error('Failed to load profile', e); }
loadAnyDeskChips();
}
2026-07-17 01:58:02 +02:00
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); }
}
2026-07-03 20:20:22 +02:00
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;
}
2026-08-31 13:01:35 +02:00
const projectCtLink = document.getElementById('projectCtProfileLink');
if (projectCtLink & & user.is_superadmin === true) {
projectCtLink.classList.remove('d-none');
}
2026-07-03 20:20:22 +02:00
} catch (e) {
console.error('Failed to load current user identity', e);
}
}
2026-03-30 07:50:15 +02:00
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">× < / 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,
2026-07-17 01:58:02 +02:00
default_case_type: document.getElementById('prof_default_case_type').value || 'ticket',
2026-03-30 07:50:15 +02:00
};
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');
2026-07-03 20:20:22 +02:00
loadCurrentUserMenuIdentity();
2026-03-30 07:50:15 +02:00
const statusEl = document.getElementById('prof-save-status');
statusEl.style.display = '';
setTimeout(() => { statusEl.style.display = 'none'; }, 3000);
} catch (e) { alert('Fejl: ' + e.message); }
}
2026-02-06 10:47:14 +01:00
document.addEventListener('DOMContentLoaded', () => {
2026-07-03 20:20:22 +02:00
loadCurrentUserMenuIdentity();
2026-02-06 10:47:14 +01:00
const profileModalEl = document.getElementById('profileModal');
if (profileModalEl) {
profileModalEl.addEventListener('shown.bs.modal', () => {
loadReminderPreferences();
loadProfileReminders();
2026-03-30 07:50:15 +02:00
loadUserProfile();
2026-06-18 22:20:39 +02:00
loadProfileMenuPreferences();
2026-09-07 19:05:58 +02:00
loadProfileTaskLists();
2026-02-06 10:47:14 +01:00
});
}
});
< / script >
2026-02-14 02:26:29 +01:00
<!-- 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 >
2025-12-15 12:28:12 +01:00
<!-- 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() {
2025-12-17 07:56:33 +01:00
fetch('/api/v1/system/maintenance')
2025-12-16 15:36:11 +01:00
.then(response => {
if (!response.ok) {
// Silently ignore 404 - maintenance endpoint not implemented yet
return null;
}
return response.json();
})
2025-12-15 12:28:12 +01:00
.then(data => {
2025-12-16 15:36:11 +01:00
if (!data) return; // Skip if endpoint doesn't exist
2025-12-15 12:28:12 +01:00
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 => {
2025-12-16 15:36:11 +01:00
// Silently ignore errors - maintenance check is not critical
2025-12-15 12:28:12 +01:00
});
}
2025-12-16 15:36:11 +01:00
// Check on page load (optional feature, don't block if not available)
2025-12-15 12:28:12 +01:00
checkMaintenanceMode();
// Check periodically (every 30 seconds when not in maintenance)
setInterval(() => {
if (!maintenanceCheckInterval) {
checkMaintenanceMode();
}
}, 30000);
2026-02-11 13:23:32 +01:00
// 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';
}
2025-12-15 12:28:12 +01:00
< / script >
2026-02-09 15:30:07 +01:00
{% block scripts %}{% endblock %}
2025-12-06 02:22:01 +01:00
{% block extra_js %}{% endblock %}
< / body >
2026-07-03 20:20:22 +02:00
< / html >