feat: Enhance case detail view with tab count badges and importance bubbles
This commit is contained in:
parent
fcc7192015
commit
ca6640c33c
@ -1141,6 +1141,26 @@
|
|||||||
box-shadow: 0 0 0 2px rgba(20, 28, 36, 0.95);
|
box-shadow: 0 0 0 2px rgba(20, 28, 36, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.case-tab-count-badge {
|
||||||
|
display: none;
|
||||||
|
margin-left: 0.42rem;
|
||||||
|
min-width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
padding: 0 0.35rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
background: color-mix(in srgb, var(--accent) 82%, #2f9e44);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .case-tab-count-badge {
|
||||||
|
box-shadow: 0 0 0 2px rgba(20, 28, 36, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .narrative-description {
|
[data-bs-theme="dark"] .narrative-description {
|
||||||
border-color: rgba(117, 194, 239, 0.24);
|
border-color: rgba(117, 194, 239, 0.24);
|
||||||
background: linear-gradient(180deg, rgba(117, 194, 239, 0.14), rgba(117, 194, 239, 0.06));
|
background: linear-gradient(180deg, rgba(117, 194, 239, 0.14), rgba(117, 194, 239, 0.06));
|
||||||
@ -1776,10 +1796,13 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0.55rem 0.45rem;
|
padding: 0.55rem 0.45rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.case-tabs-topbar.topbar-primary {
|
.case-tabs-topbar.topbar-primary {
|
||||||
grid-template-columns: 105px minmax(260px, 1.45fr) minmax(150px, 0.95fr) minmax(170px, 1fr) minmax(170px, 1fr) minmax(240px, 1.35fr);
|
grid-template-columns: repeat(8, minmax(130px, 1fr));
|
||||||
background: linear-gradient(140deg, rgba(15,76,117,0.08), rgba(15,76,117,0.01));
|
background: linear-gradient(140deg, rgba(15,76,117,0.08), rgba(15,76,117,0.01));
|
||||||
border: 1px solid rgba(15,76,117,0.22);
|
border: 1px solid rgba(15,76,117,0.22);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@ -1968,11 +1991,9 @@
|
|||||||
.case-tabs-topbar.topbar-secondary {
|
.case-tabs-topbar.topbar-secondary {
|
||||||
/* Vægt kolonner så dato-felter (som har indlejrede ikoner) får mere plads */
|
/* Vægt kolonner så dato-felter (som har indlejrede ikoner) får mere plads */
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
minmax(110px, 0.75fr) /* Type */
|
|
||||||
minmax(110px, 0.8fr) /* Prioritet */
|
|
||||||
minmax(105px, 0.75fr) /* Oprettet */
|
minmax(105px, 0.75fr) /* Oprettet */
|
||||||
minmax(195px, 1.3fr) /* Arbejdsstart (2 knapper) */
|
minmax(180px, 1.2fr) /* Arbejdsstart (2 knapper) */
|
||||||
minmax(195px, 1.3fr) /* Start senest (2 knapper) */
|
minmax(180px, 1.2fr) /* Start senest (2 knapper) */
|
||||||
minmax(150px, 1.1fr) /* Deadline (1 knap) */
|
minmax(150px, 1.1fr) /* Deadline (1 knap) */
|
||||||
minmax(120px, 0.85fr) /* AnyDesk */
|
minmax(120px, 0.85fr) /* AnyDesk */
|
||||||
minmax(140px, 1fr) /* Dokumenter */;
|
minmax(140px, 1fr) /* Dokumenter */;
|
||||||
@ -2003,13 +2024,51 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
opacity: 0.75;
|
opacity: 0.95;
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.case-tabs-topbar-label .field-importance-bubble {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-importance-bubble {
|
||||||
|
width: 0.78rem;
|
||||||
|
height: 0.78rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: 1px solid rgba(0,0,0,0.18);
|
||||||
|
background: rgba(148, 163, 184, 0.45);
|
||||||
|
box-shadow: 0 0 0 1px rgba(255,255,255,0.65), 0 0 0 0.5px rgba(0,0,0,0.12) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-importance-bubble.sev-neutral {
|
||||||
|
background: rgba(148, 163, 184, 0.55);
|
||||||
|
border-color: rgba(100, 116, 139, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-importance-bubble.sev-ok {
|
||||||
|
background: #22c55e;
|
||||||
|
border-color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-importance-bubble.sev-warn {
|
||||||
|
background: #f59e0b;
|
||||||
|
border-color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-importance-bubble.sev-critical {
|
||||||
|
background: #ef4444;
|
||||||
|
border-color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .field-importance-bubble {
|
||||||
|
box-shadow: 0 0 0 1px rgba(10, 17, 26, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
.case-tabs-topbar-value {
|
.case-tabs-topbar-value {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -2085,6 +2144,10 @@
|
|||||||
padding: 0.45rem 0.5rem;
|
padding: 0.45rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.case-tabs-topbar .dropdown-menu {
|
||||||
|
z-index: 1080;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar-secondary-inline {
|
.topbar-secondary-inline {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -2379,11 +2442,24 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.case-detail-page-shell {
|
||||||
|
--case-topbar-offset: 0px;
|
||||||
|
margin-top: calc(3rem + var(--case-topbar-offset));
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.case-detail-page-shell {
|
||||||
|
margin-top: calc(2.2rem + var(--case-topbar-offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid" style="margin-top: 2rem; margin-bottom: 2rem;">
|
<div class="container-fluid case-detail-page-shell">
|
||||||
|
|
||||||
<div class="case-tabs-topbar topbar-primary" role="region" aria-label="Primær sagsbar">
|
<div class="case-tabs-topbar topbar-primary" role="region" aria-label="Primær sagsbar">
|
||||||
<div class="case-tabs-topbar-item">
|
<div class="case-tabs-topbar-item">
|
||||||
@ -2404,16 +2480,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="case-tabs-topbar-item">
|
<div class="case-tabs-topbar-item field-status-item">
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-circle-half"></i>Status</div>
|
<div class="case-tabs-topbar-label"><i class="bi bi-circle-half"></i>Status <span class="field-importance-bubble {{ 'sev-ok' if (case.status or '')|lower in ['lukket', 'løst', 'closed', 'resolved'] else 'sev-warn' }}" data-field-bubble="status" aria-hidden="true"></span></div>
|
||||||
<select id="topbarStatusSelect" class="case-inline-select" onchange="saveCaseStatusFromTopbar()">
|
<select id="topbarStatusSelect" class="case-inline-select" onchange="saveCaseStatusFromTopbar()">
|
||||||
{% for st in status_options %}
|
{% for st in status_options %}
|
||||||
<option value="{{ st }}" {% if (case.status or '')|lower == st|lower %}selected{% endif %}>{{ st|capitalize }}</option>
|
<option value="{{ st }}" {% if (case.status or '')|lower == st|lower %}selected{% endif %}>{{ st|capitalize }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="case-tabs-topbar-item">
|
<div class="case-tabs-topbar-item field-type">
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-person-check"></i>Ansvarlig</div>
|
<div class="case-tabs-topbar-label"><i class="bi bi-tag"></i>Type <span class="field-importance-bubble sev-ok" data-field-bubble="type" aria-hidden="true"></span></div>
|
||||||
|
<select id="topbarTypeSelect" class="case-inline-select" onchange="saveCaseTypeFromTopbar()">
|
||||||
|
{% set topbar_type = (case.template_key or case.type or 'ticket')|lower %}
|
||||||
|
<option value="ticket" {% if topbar_type == 'ticket' %}selected{% endif %}>Ticket</option>
|
||||||
|
<option value="pipeline" {% if topbar_type == 'pipeline' %}selected{% endif %}>Pipeline</option>
|
||||||
|
<option value="opgave" {% if topbar_type == 'opgave' %}selected{% endif %}>Opgave</option>
|
||||||
|
<option value="ordre" {% if topbar_type == 'ordre' %}selected{% endif %}>Ordre</option>
|
||||||
|
<option value="projekt" {% if topbar_type == 'projekt' %}selected{% endif %}>Projekt</option>
|
||||||
|
<option value="service" {% if topbar_type == 'service' %}selected{% endif %}>Service</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="case-tabs-topbar-item field-priority">
|
||||||
|
<div class="case-tabs-topbar-label"><i class="bi bi-flag"></i>Prioritet <span class="field-importance-bubble {{ 'sev-critical' if (case.priority or 'normal')|lower == 'urgent' else ('sev-warn' if (case.priority or 'normal')|lower == 'high' else 'sev-ok') }}" data-field-bubble="priority" aria-hidden="true"></span></div>
|
||||||
|
<select id="topbarPrioritySelect" class="case-inline-select" onchange="saveCasePriorityFromTopbar()">
|
||||||
|
{% set topbar_priority = (case.priority or 'normal')|lower %}
|
||||||
|
<option value="low" {% if topbar_priority == 'low' %}selected{% endif %}>Lav</option>
|
||||||
|
<option value="normal" {% if topbar_priority == 'normal' %}selected{% endif %}>Normal</option>
|
||||||
|
<option value="high" {% if topbar_priority == 'high' %}selected{% endif %}>Hoj</option>
|
||||||
|
<option value="urgent" {% if topbar_priority == 'urgent' %}selected{% endif %}>Akut</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="case-tabs-topbar-item field-assignee-item">
|
||||||
|
<div class="case-tabs-topbar-label"><i class="bi bi-person-check"></i>Ansvarlig <span class="field-importance-bubble {{ 'sev-ok' if case.ansvarlig_bruger_id else 'sev-critical' }}" data-field-bubble="assignee" aria-hidden="true"></span></div>
|
||||||
<select id="tabsAssignmentUserSelect" class="case-inline-select" onchange="saveAssignmentFromTabsBar()">
|
<select id="tabsAssignmentUserSelect" class="case-inline-select" onchange="saveAssignmentFromTabsBar()">
|
||||||
<option value="">Ingen bruger</option>
|
<option value="">Ingen bruger</option>
|
||||||
{% for user in assignment_users or [] %}
|
{% for user in assignment_users or [] %}
|
||||||
@ -2421,8 +2519,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="case-tabs-topbar-item">
|
<div class="case-tabs-topbar-item field-group-item">
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-people"></i>Gruppe</div>
|
<div class="case-tabs-topbar-label"><i class="bi bi-people"></i>Gruppe <span class="field-importance-bubble {{ 'sev-ok' if case.assigned_group_id else 'sev-warn' }}" data-field-bubble="group" aria-hidden="true"></span></div>
|
||||||
<select id="tabsAssignmentGroupSelect" class="case-inline-select" onchange="saveAssignmentFromTabsBar()">
|
<select id="tabsAssignmentGroupSelect" class="case-inline-select" onchange="saveAssignmentFromTabsBar()">
|
||||||
<option value="">Ingen gruppe</option>
|
<option value="">Ingen gruppe</option>
|
||||||
{% for group in assignment_groups or [] %}
|
{% for group in assignment_groups or [] %}
|
||||||
@ -2430,8 +2528,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="case-tabs-topbar-item">
|
<div class="case-tabs-topbar-item field-next-item">
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-list-check"></i>Næste</div>
|
<div class="case-tabs-topbar-label"><i class="bi bi-list-check"></i>Næste <span class="field-importance-bubble sev-warn" data-field-bubble="next" aria-hidden="true"></span></div>
|
||||||
<div id="topbarNextTodoValue" class="case-tabs-topbar-value multiline">Henter næste todo...</div>
|
<div id="topbarNextTodoValue" class="case-tabs-topbar-value multiline">Henter næste todo...</div>
|
||||||
<div id="topbarNextTodoMeta" class="topbar-next-meta">-</div>
|
<div id="topbarNextTodoMeta" class="topbar-next-meta">-</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2445,34 +2543,12 @@
|
|||||||
|
|
||||||
<div class="case-top-aux">
|
<div class="case-top-aux">
|
||||||
<div class="case-tabs-topbar topbar-secondary" role="region" aria-label="Sekundær sagsbar">
|
<div class="case-tabs-topbar topbar-secondary" role="region" aria-label="Sekundær sagsbar">
|
||||||
<div class="case-tabs-topbar-item field-type">
|
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-tag"></i>Type</div>
|
|
||||||
<select id="topbarTypeSelect" class="case-inline-select" onchange="saveCaseTypeFromTopbar()">
|
|
||||||
{% set topbar_type = (case.template_key or case.type or 'ticket')|lower %}
|
|
||||||
<option value="ticket" {% if topbar_type == 'ticket' %}selected{% endif %}>Ticket</option>
|
|
||||||
<option value="pipeline" {% if topbar_type == 'pipeline' %}selected{% endif %}>Pipeline</option>
|
|
||||||
<option value="opgave" {% if topbar_type == 'opgave' %}selected{% endif %}>Opgave</option>
|
|
||||||
<option value="ordre" {% if topbar_type == 'ordre' %}selected{% endif %}>Ordre</option>
|
|
||||||
<option value="projekt" {% if topbar_type == 'projekt' %}selected{% endif %}>Projekt</option>
|
|
||||||
<option value="service" {% if topbar_type == 'service' %}selected{% endif %}>Service</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="case-tabs-topbar-item field-priority">
|
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-flag"></i>Prioritet</div>
|
|
||||||
<select id="topbarPrioritySelect" class="case-inline-select" onchange="saveCasePriorityFromTopbar()">
|
|
||||||
{% set topbar_priority = (case.priority or 'normal')|lower %}
|
|
||||||
<option value="low" {% if topbar_priority == 'low' %}selected{% endif %}>Lav</option>
|
|
||||||
<option value="normal" {% if topbar_priority == 'normal' %}selected{% endif %}>Normal</option>
|
|
||||||
<option value="high" {% if topbar_priority == 'high' %}selected{% endif %}>Hoj</option>
|
|
||||||
<option value="urgent" {% if topbar_priority == 'urgent' %}selected{% endif %}>Akut</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="case-tabs-topbar-item field-created">
|
<div class="case-tabs-topbar-item field-created">
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-plus-circle"></i>Oprettelses dato</div>
|
<div class="case-tabs-topbar-label"><i class="bi bi-plus-circle"></i>Oprettelses dato <span class="field-importance-bubble sev-ok" data-field-bubble="created" aria-hidden="true"></span></div>
|
||||||
<div class="case-tabs-topbar-value">{{ case.created_at.strftime('%d/%m/%Y') if case.created_at else '-' }}</div>
|
<div class="case-tabs-topbar-value">{{ case.created_at.strftime('%d/%m/%Y') if case.created_at else '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="case-tabs-topbar-item field-start">
|
<div class="case-tabs-topbar-item field-start">
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-play-circle"></i>Arbejdsstart</div>
|
<div class="case-tabs-topbar-label"><i class="bi bi-play-circle"></i>Arbejdsstart <span class="field-importance-bubble {{ 'sev-ok' if case.start_date else 'sev-warn' }}" data-field-bubble="start" aria-hidden="true"></span></div>
|
||||||
<div class="topbar-secondary-inline" style="gap: 0.25rem;">
|
<div class="topbar-secondary-inline" style="gap: 0.25rem;">
|
||||||
<input
|
<input
|
||||||
id="topbarStartDateInput"
|
id="topbarStartDateInput"
|
||||||
@ -2501,7 +2577,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="case-tabs-topbar-item field-start-before">
|
<div class="case-tabs-topbar-item field-start-before">
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-hourglass-split"></i>Start senest</div>
|
<div class="case-tabs-topbar-label"><i class="bi bi-hourglass-split"></i>Start senest <span class="field-importance-bubble {{ 'sev-warn' if case.deferred_until else 'sev-neutral' }}" data-field-bubble="start_before" aria-hidden="true"></span></div>
|
||||||
<div class="topbar-secondary-inline" style="gap: 0.25rem;">
|
<div class="topbar-secondary-inline" style="gap: 0.25rem;">
|
||||||
<input
|
<input
|
||||||
id="topbarDeferredInput"
|
id="topbarDeferredInput"
|
||||||
@ -2545,7 +2621,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="case-tabs-topbar-item field-deadline">
|
<div class="case-tabs-topbar-item field-deadline">
|
||||||
<div class="case-tabs-topbar-label"><i class="bi bi-clock"></i>Deadline dato</div>
|
<div class="case-tabs-topbar-label"><i class="bi bi-clock"></i>Deadline dato <span class="field-importance-bubble {{ 'sev-warn' if case.deadline else 'sev-neutral' }}" data-field-bubble="deadline" aria-hidden="true"></span></div>
|
||||||
<div class="topbar-secondary-inline" style="gap: 0.25rem;">
|
<div class="topbar-secondary-inline" style="gap: 0.25rem;">
|
||||||
<input
|
<input
|
||||||
id="topbarDeadlineInput"
|
id="topbarDeadlineInput"
|
||||||
@ -2789,11 +2865,13 @@
|
|||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" id="details-tab" data-bs-toggle="tab" data-bs-target="#details" type="button" role="tab" onclick="forceCaseTabActivation('details', this)">
|
<button class="nav-link active" id="details-tab" data-bs-toggle="tab" data-bs-target="#details" type="button" role="tab" onclick="forceCaseTabActivation('details', this)">
|
||||||
<i class="bi bi-card-text me-2"></i>Sagsdetaljer
|
<i class="bi bi-card-text me-2"></i>Sagsdetaljer
|
||||||
|
<span class="case-tab-count-badge" id="detailsTabCountBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="solution-tab" data-bs-toggle="tab" data-bs-target="#solution" type="button" role="tab" data-module-tab="solution" onclick="forceCaseTabActivation('solution', this)">
|
<button class="nav-link" id="solution-tab" data-bs-toggle="tab" data-bs-target="#solution" type="button" role="tab" data-module-tab="solution" onclick="forceCaseTabActivation('solution', this)">
|
||||||
<i class="bi bi-lightbulb me-2"></i>Løsning
|
<i class="bi bi-lightbulb me-2"></i>Løsning
|
||||||
|
<span class="case-tab-count-badge" id="solutionTabCountBadge"></span>
|
||||||
{% if solution %}
|
{% if solution %}
|
||||||
<span class="badge bg-success ms-1 rounded-pill"><i class="bi bi-check"></i></span>
|
<span class="badge bg-success ms-1 rounded-pill"><i class="bi bi-check"></i></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -2802,32 +2880,38 @@
|
|||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="emails-tab" data-bs-toggle="tab" data-bs-target="#emails" type="button" role="tab" data-module-tab="emails" onclick="forceCaseTabActivation('emails', this)">
|
<button class="nav-link" id="emails-tab" data-bs-toggle="tab" data-bs-target="#emails" type="button" role="tab" data-module-tab="emails" onclick="forceCaseTabActivation('emails', this)">
|
||||||
<i class="bi bi-envelope me-2"></i>E-mail
|
<i class="bi bi-envelope me-2"></i>E-mail
|
||||||
|
<span class="case-tab-count-badge" id="emailsTabCountBadge"></span>
|
||||||
<span id="emailTabUnreadBadge" class="email-tab-unread-badge" aria-label="Ulæste emails"></span>
|
<span id="emailTabUnreadBadge" class="email-tab-unread-badge" aria-label="Ulæste emails"></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="sales-tab" data-bs-toggle="tab" data-bs-target="#sales" type="button" role="tab" data-module-tab="sales" onclick="forceCaseTabActivation('sales', this)">
|
<button class="nav-link" id="sales-tab" data-bs-toggle="tab" data-bs-target="#sales" type="button" role="tab" data-module-tab="sales" onclick="forceCaseTabActivation('sales', this)">
|
||||||
<i class="bi bi-basket3 me-2"></i>Varekøb & Salg
|
<i class="bi bi-basket3 me-2"></i>Varekøb & Salg
|
||||||
|
<span class="case-tab-count-badge" id="salesTabCountBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="supplier-tab" data-bs-toggle="tab" data-bs-target="#supplier" type="button" role="tab" data-module-tab="supplier" onclick="forceCaseTabActivation('supplier', this)">
|
<button class="nav-link" id="supplier-tab" data-bs-toggle="tab" data-bs-target="#supplier" type="button" role="tab" data-module-tab="supplier" onclick="forceCaseTabActivation('supplier', this)">
|
||||||
<i class="bi bi-receipt-cutoff me-2"></i>Leverandør
|
<i class="bi bi-receipt-cutoff me-2"></i>Leverandør
|
||||||
|
<span class="case-tab-count-badge" id="supplierTabCountBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="timetracking-tab" data-bs-toggle="tab" data-bs-target="#timetracking" type="button" role="tab" onclick="forceCaseTabActivation('timetracking', this)">
|
<button class="nav-link" id="timetracking-tab" data-bs-toggle="tab" data-bs-target="#timetracking" type="button" role="tab" onclick="forceCaseTabActivation('timetracking', this)">
|
||||||
<i class="bi bi-clock-history me-2"></i>Tidsforbrug
|
<i class="bi bi-clock-history me-2"></i>Tidsforbrug
|
||||||
|
<span class="case-tab-count-badge" id="timetrackingTabCountBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="subscription-tab" data-bs-toggle="tab" data-bs-target="#subscription" type="button" role="tab" data-module-tab="subscription" onclick="forceCaseTabActivation('subscription', this)">
|
<button class="nav-link" id="subscription-tab" data-bs-toggle="tab" data-bs-target="#subscription" type="button" role="tab" data-module-tab="subscription" onclick="forceCaseTabActivation('subscription', this)">
|
||||||
<i class="bi bi-repeat me-2"></i>Abonnement
|
<i class="bi bi-repeat me-2"></i>Abonnement
|
||||||
|
<span class="case-tab-count-badge" id="subscriptionTabCountBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="reminders-tab" data-bs-toggle="tab" data-bs-target="#reminders" type="button" role="tab" data-module-tab="reminders" onclick="forceCaseTabActivation('reminders', this)">
|
<button class="nav-link" id="reminders-tab" data-bs-toggle="tab" data-bs-target="#reminders" type="button" role="tab" data-module-tab="reminders" onclick="forceCaseTabActivation('reminders', this)">
|
||||||
<i class="bi bi-bell me-2"></i>Påmindelser
|
<i class="bi bi-bell me-2"></i>Påmindelser
|
||||||
|
<span class="case-tab-count-badge" id="remindersTabCountBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -3757,6 +3841,7 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
hydrateTopbarStatusOptions();
|
hydrateTopbarStatusOptions();
|
||||||
loadCaseCustomerTopAlerts();
|
loadCaseCustomerTopAlerts();
|
||||||
|
applyTopbarImportanceBubbles();
|
||||||
// Initialize modals
|
// Initialize modals
|
||||||
contactSearchModal = new bootstrap.Modal(document.getElementById('contactSearchModal'));
|
contactSearchModal = new bootstrap.Modal(document.getElementById('contactSearchModal'));
|
||||||
customerSearchModal = new bootstrap.Modal(document.getElementById('customerSearchModal'));
|
customerSearchModal = new bootstrap.Modal(document.getElementById('customerSearchModal'));
|
||||||
@ -3813,6 +3898,36 @@
|
|||||||
todoForm.addEventListener('submit', createTodoStep);
|
todoForm.addEventListener('submit', createTodoStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
['topbarStatusSelect', 'tabsAssignmentUserSelect', 'tabsAssignmentGroupSelect', 'topbarTypeSelect', 'topbarPrioritySelect', 'topbarStartDateInput', 'topbarDeferredInput', 'topbarDeadlineInput'].forEach((id) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
el.addEventListener('change', applyTopbarImportanceBubbles);
|
||||||
|
el.addEventListener('input', applyTopbarImportanceBubbles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const caseTabsContent = document.getElementById('caseTabsContent');
|
||||||
|
if (caseTabsContent && typeof MutationObserver !== 'undefined') {
|
||||||
|
let tabBadgeTimer = null;
|
||||||
|
const scheduleBadgeRefresh = () => {
|
||||||
|
if (tabBadgeTimer) {
|
||||||
|
clearTimeout(tabBadgeTimer);
|
||||||
|
}
|
||||||
|
tabBadgeTimer = setTimeout(() => updateCaseTabCountBadges(), 80);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabObserver = new MutationObserver(scheduleBadgeRefresh);
|
||||||
|
tabObserver.observe(caseTabsContent, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-has-content', 'class']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCaseTabCountBadges();
|
||||||
|
|
||||||
const caseTabs = document.getElementById('caseTabs');
|
const caseTabs = document.getElementById('caseTabs');
|
||||||
if (caseTabs) {
|
if (caseTabs) {
|
||||||
caseTabs.addEventListener('shown.bs.tab', async (event) => {
|
caseTabs.addEventListener('shown.bs.tab', async (event) => {
|
||||||
@ -4917,10 +5032,42 @@
|
|||||||
if (!moduleContainer) return;
|
if (!moduleContainer) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/tags/entity/case/${caseId}`);
|
let tags = [];
|
||||||
if (!response.ok) throw new Error('Kunne ikke hente tags');
|
let usingLegacyCaseTags = false;
|
||||||
|
|
||||||
|
const normalizeLegacyTags = (legacyTags) => (
|
||||||
|
Array.isArray(legacyTags)
|
||||||
|
? legacyTags.map((row) => ({
|
||||||
|
id: row.id || row.tag_id,
|
||||||
|
name: row.tag_navn || row.name || 'Tag',
|
||||||
|
color: row.color || '#0f4c75',
|
||||||
|
icon: row.icon || 'bi-tag'
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadLegacyTags = async () => {
|
||||||
|
const legacyResponse = await fetch(`/api/v1/sag/${caseId}/tags`, { credentials: 'include' });
|
||||||
|
if (!legacyResponse.ok) {
|
||||||
|
throw new Error('Kunne ikke hente tags');
|
||||||
|
}
|
||||||
|
const legacyTags = await legacyResponse.json();
|
||||||
|
usingLegacyCaseTags = true;
|
||||||
|
return normalizeLegacyTags(legacyTags);
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/api/v1/tags/entity/case/${caseId}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const genericTags = await response.json();
|
||||||
|
tags = Array.isArray(genericTags) ? genericTags : [];
|
||||||
|
// Some hubs still store case tags via legacy sag endpoint.
|
||||||
|
if (tags.length === 0) {
|
||||||
|
tags = await loadLegacyTags();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tags = await loadLegacyTags();
|
||||||
|
}
|
||||||
|
|
||||||
const tags = await response.json();
|
|
||||||
if (!Array.isArray(tags) || tags.length === 0) {
|
if (!Array.isArray(tags) || tags.length === 0) {
|
||||||
moduleContainer.innerHTML = '<div class="p-3 text-center text-muted small">Ingen tags paaa sagen endnu</div>';
|
moduleContainer.innerHTML = '<div class="p-3 text-center text-muted small">Ingen tags paaa sagen endnu</div>';
|
||||||
setModuleContentState('tags', false);
|
setModuleContentState('tags', false);
|
||||||
@ -4936,6 +5083,13 @@
|
|||||||
</span>
|
</span>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
|
if (usingLegacyCaseTags) {
|
||||||
|
moduleContainer.insertAdjacentHTML(
|
||||||
|
'beforeend',
|
||||||
|
'<div class="small text-muted mt-2">Viser tags via sag-endpoint</div>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setModuleContentState('tags', true);
|
setModuleContentState('tags', true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading case tags module:', error);
|
console.error('Error loading case tags module:', error);
|
||||||
@ -5003,12 +5157,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeCaseTagAndSync(tagId) {
|
async function removeCaseTagAndSync(tagId) {
|
||||||
|
try {
|
||||||
|
if (window.removeEntityTag) {
|
||||||
await window.removeEntityTag('case', caseId, tagId, 'case-tags-module');
|
await window.removeEntityTag('case', caseId, tagId, 'case-tags-module');
|
||||||
|
} else {
|
||||||
|
const legacyResponse = await fetch(`/api/v1/sag/${caseId}/tags/${tagId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!legacyResponse.ok) {
|
||||||
|
throw new Error('Kunne ikke slette tag');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const legacyResponse = await fetch(`/api/v1/sag/${caseId}/tags/${tagId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!legacyResponse.ok) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
await syncCaseTagsUi();
|
await syncCaseTagsUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncCaseTagsUi() {
|
async function syncCaseTagsUi() {
|
||||||
if (window.renderEntityTags) {
|
if (window.renderEntityTags && document.getElementById('case-tags')) {
|
||||||
await window.renderEntityTags('case', caseId, 'case-tags');
|
await window.renderEntityTags('case', caseId, 'case-tags');
|
||||||
}
|
}
|
||||||
await loadCaseTagsModule();
|
await loadCaseTagsModule();
|
||||||
@ -5186,6 +5360,7 @@
|
|||||||
valueEl.textContent = 'Ingen åbne todo-opgaver';
|
valueEl.textContent = 'Ingen åbne todo-opgaver';
|
||||||
metaEl.textContent = 'Alt er færdigt';
|
metaEl.textContent = 'Alt er færdigt';
|
||||||
setNextTodoOverrideId(null);
|
setNextTodoOverrideId(null);
|
||||||
|
applyTopbarImportanceBubbles();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5201,6 +5376,104 @@
|
|||||||
metaEl.textContent = nextStep.due_date
|
metaEl.textContent = nextStep.due_date
|
||||||
? `Forfald: ${formatTodoDate(nextStep.due_date)}`
|
? `Forfald: ${formatTodoDate(nextStep.due_date)}`
|
||||||
: 'Ingen forfaldsdato';
|
: 'Ingen forfaldsdato';
|
||||||
|
|
||||||
|
applyTopbarImportanceBubbles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIsoDateToDay(value) {
|
||||||
|
const str = String(value || '').trim();
|
||||||
|
if (!str) return null;
|
||||||
|
const d = new Date(str + 'T00:00:00');
|
||||||
|
if (Number.isNaN(d.getTime())) return null;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayDiffFromToday(dateObj) {
|
||||||
|
if (!dateObj) return null;
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return Math.round((dateObj.getTime() - today.getTime()) / 86400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bubbleSeverityFromDate(rawValue, opts = {}) {
|
||||||
|
const dateObj = parseIsoDateToDay(rawValue);
|
||||||
|
if (!dateObj) return opts.empty || 'sev-neutral';
|
||||||
|
const diff = dayDiffFromToday(dateObj);
|
||||||
|
const warnDays = Number(opts.warnDays || 2);
|
||||||
|
if (diff < 0) return 'sev-critical';
|
||||||
|
if (diff <= warnDays) return 'sev-warn';
|
||||||
|
return 'sev-ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTopbarImportanceBubbles() {
|
||||||
|
const statusValue = String(document.getElementById('topbarStatusSelect')?.value || '').toLowerCase();
|
||||||
|
const assigneeValue = String(document.getElementById('tabsAssignmentUserSelect')?.value || '').trim();
|
||||||
|
const groupValue = String(document.getElementById('tabsAssignmentGroupSelect')?.value || '').trim();
|
||||||
|
const typeValue = String(document.getElementById('topbarTypeSelect')?.value || '').toLowerCase();
|
||||||
|
const priorityValue = String(document.getElementById('topbarPrioritySelect')?.value || '').toLowerCase();
|
||||||
|
const startValue = String(document.getElementById('topbarStartDateInput')?.value || '').trim();
|
||||||
|
const startBeforeValue = String(document.getElementById('topbarDeferredInput')?.value || '').trim();
|
||||||
|
const deadlineValue = String(document.getElementById('topbarDeadlineInput')?.value || '').trim();
|
||||||
|
|
||||||
|
const nextValue = String(document.getElementById('topbarNextTodoValue')?.textContent || '').trim().toLowerCase();
|
||||||
|
const nextMeta = String(document.getElementById('topbarNextTodoMeta')?.textContent || '').trim().toLowerCase();
|
||||||
|
|
||||||
|
const createdText = String(document.querySelector('.field-created .case-tabs-topbar-value')?.textContent || '').trim();
|
||||||
|
let createdSeverity = 'sev-neutral';
|
||||||
|
const createdMatch = createdText.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
||||||
|
if (createdMatch) {
|
||||||
|
const createdDate = new Date(Number(createdMatch[3]), Number(createdMatch[2]) - 1, Number(createdMatch[1]));
|
||||||
|
const age = Math.abs(dayDiffFromToday(createdDate));
|
||||||
|
if (age > 120) createdSeverity = 'sev-critical';
|
||||||
|
else if (age > 45) createdSeverity = 'sev-warn';
|
||||||
|
else createdSeverity = 'sev-ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusSeverity = 'sev-neutral';
|
||||||
|
if (!statusValue) statusSeverity = 'sev-critical';
|
||||||
|
else if (['lukket', 'løst', 'closed', 'resolved'].includes(statusValue)) statusSeverity = 'sev-ok';
|
||||||
|
else if (['afventer', 'under behandling'].includes(statusValue)) statusSeverity = 'sev-warn';
|
||||||
|
else statusSeverity = 'sev-warn';
|
||||||
|
|
||||||
|
let nextSeverity = 'sev-neutral';
|
||||||
|
if (nextValue.includes('henter')) {
|
||||||
|
nextSeverity = 'sev-neutral';
|
||||||
|
} else if (nextValue.includes('ingen åbne')) {
|
||||||
|
nextSeverity = 'sev-warn';
|
||||||
|
} else if (nextMeta.includes('forfald:')) {
|
||||||
|
const metaDateMatch = nextMeta.match(/(\d{2})[./-](\d{2})[./-](\d{4})/);
|
||||||
|
if (metaDateMatch) {
|
||||||
|
const due = new Date(Number(metaDateMatch[3]), Number(metaDateMatch[2]) - 1, Number(metaDateMatch[1]));
|
||||||
|
const diff = dayDiffFromToday(due);
|
||||||
|
if (diff < 0) nextSeverity = 'sev-critical';
|
||||||
|
else if (diff <= 2) nextSeverity = 'sev-warn';
|
||||||
|
else nextSeverity = 'sev-ok';
|
||||||
|
} else {
|
||||||
|
nextSeverity = 'sev-warn';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextSeverity = 'sev-ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldSeverity = {
|
||||||
|
status: statusSeverity,
|
||||||
|
assignee: assigneeValue ? 'sev-ok' : 'sev-critical',
|
||||||
|
group: groupValue ? 'sev-ok' : 'sev-warn',
|
||||||
|
next: nextSeverity,
|
||||||
|
type: typeValue ? 'sev-ok' : 'sev-neutral',
|
||||||
|
priority: priorityValue === 'urgent' ? 'sev-critical' : (priorityValue === 'high' ? 'sev-warn' : 'sev-ok'),
|
||||||
|
created: createdSeverity,
|
||||||
|
start: bubbleSeverityFromDate(startValue, { empty: 'sev-warn', warnDays: 0 }),
|
||||||
|
start_before: bubbleSeverityFromDate(startBeforeValue, { empty: 'sev-neutral', warnDays: 2 }),
|
||||||
|
deadline: bubbleSeverityFromDate(deadlineValue, { empty: 'sev-neutral', warnDays: 2 })
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-field-bubble]').forEach((bubble) => {
|
||||||
|
const key = String(bubble.getAttribute('data-field-bubble') || '').trim();
|
||||||
|
const sev = fieldSeverity[key] || 'sev-neutral';
|
||||||
|
bubble.classList.remove('sev-neutral', 'sev-ok', 'sev-warn', 'sev-critical');
|
||||||
|
bubble.classList.add(sev);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setNextTodoStep(stepId, isNext) {
|
async function setNextTodoStep(stepId, isNext) {
|
||||||
@ -10201,11 +10474,16 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!caseAnyDeskModal) {
|
|
||||||
const modalEl = document.getElementById('caseAnyDeskModal');
|
const modalEl = document.getElementById('caseAnyDeskModal');
|
||||||
if (!modalEl) return;
|
if (!modalEl || typeof bootstrap === 'undefined' || !bootstrap.Modal) return;
|
||||||
|
|
||||||
|
if (!caseAnyDeskModal || typeof caseAnyDeskModal.show !== 'function') {
|
||||||
|
if (typeof bootstrap.Modal.getOrCreateInstance === 'function') {
|
||||||
|
caseAnyDeskModal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||||
|
} else {
|
||||||
caseAnyDeskModal = new bootstrap.Modal(modalEl);
|
caseAnyDeskModal = new bootstrap.Modal(modalEl);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const noteInput = document.getElementById('caseAnydeskNoteInput');
|
const noteInput = document.getElementById('caseAnydeskNoteInput');
|
||||||
if (noteInput) noteInput.value = '';
|
if (noteInput) noteInput.value = '';
|
||||||
@ -10241,8 +10519,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (caseAnyDeskModal && typeof caseAnyDeskModal.show === 'function') {
|
||||||
caseAnyDeskModal.show();
|
caseAnyDeskModal.show();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function registerCaseAnyDeskSession() {
|
async function registerCaseAnyDeskSession() {
|
||||||
if (caseAnyDeskConnectInFlight) {
|
if (caseAnyDeskConnectInFlight) {
|
||||||
@ -10301,7 +10581,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
if (caseAnyDeskModal) caseAnyDeskModal.hide();
|
if (caseAnyDeskModal && typeof caseAnyDeskModal.hide === 'function') {
|
||||||
|
caseAnyDeskModal.hide();
|
||||||
|
}
|
||||||
|
|
||||||
if (result?.deep_link) {
|
if (result?.deep_link) {
|
||||||
window.location.href = result.deep_link;
|
window.location.href = result.deep_link;
|
||||||
@ -10325,6 +10607,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure inline handlers and fallback listeners can always resolve AnyDesk actions.
|
||||||
|
window.openCaseAnyDeskModal = openCaseAnyDeskModal;
|
||||||
|
window.registerCaseAnyDeskSession = registerCaseAnyDeskSession;
|
||||||
|
window.onCaseAnyDeskIdInputChange = onCaseAnyDeskIdInputChange;
|
||||||
|
window.setCaseAnyDeskInputFromSaved = setCaseAnyDeskInputFromSaved;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const adjustCaseTopbarOffset = () => {
|
||||||
|
const shell = document.querySelector('.case-detail-page-shell');
|
||||||
|
const nav = document.querySelector('.navbar.fixed-top');
|
||||||
|
if (!shell || !nav) return;
|
||||||
|
|
||||||
|
const navHeight = Math.ceil(nav.getBoundingClientRect().height || 0);
|
||||||
|
const bodyPaddingTop = parseFloat(getComputedStyle(document.body).paddingTop || '0') || 0;
|
||||||
|
const needed = Math.max(0, navHeight - bodyPaddingTop + 8);
|
||||||
|
shell.style.setProperty('--case-topbar-offset', `${needed}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
adjustCaseTopbarOffset();
|
||||||
|
window.addEventListener('resize', adjustCaseTopbarOffset);
|
||||||
|
|
||||||
|
const openBtn = document.getElementById('caseAnyDeskOpenBtn');
|
||||||
|
if (openBtn) {
|
||||||
|
openBtn.removeAttribute('onclick');
|
||||||
|
openBtn.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
openCaseAnyDeskModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectBtn = document.getElementById('caseAnyDeskConnectBtn');
|
||||||
|
if (connectBtn) {
|
||||||
|
connectBtn.removeAttribute('onclick');
|
||||||
|
connectBtn.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
registerCaseAnyDeskSession();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const anydeskInput = document.getElementById('caseAnydeskIdInput');
|
||||||
|
if (anydeskInput) {
|
||||||
|
anydeskInput.removeAttribute('oninput');
|
||||||
|
anydeskInput.addEventListener('input', onCaseAnyDeskIdInputChange);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function openCaseModuleAddPanel() {
|
async function openCaseModuleAddPanel() {
|
||||||
if (typeof loadModulePrefs === 'function') {
|
if (typeof loadModulePrefs === 'function') {
|
||||||
await loadModulePrefs();
|
await loadModulePrefs();
|
||||||
@ -11091,6 +11421,8 @@
|
|||||||
if (initialValue) {
|
if (initialValue) {
|
||||||
select.value = Array.from(known.values()).find((v) => v.toLowerCase() === initialValue.toLowerCase()) || initialValue;
|
select.value = Array.from(known.values()).find((v) => v.toLowerCase() === initialValue.toLowerCase()) || initialValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyTopbarImportanceBubbles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCaseTypeFromTopbar() {
|
function saveCaseTypeFromTopbar() {
|
||||||
@ -11368,6 +11700,7 @@
|
|||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.setAttribute('data-has-content', hasContent ? 'true' : 'false');
|
el.setAttribute('data-has-content', hasContent ? 'true' : 'false');
|
||||||
applyViewLayout(currentCaseView);
|
applyViewLayout(currentCaseView);
|
||||||
|
updateCaseTabCountBadges();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyViewLayout(viewName) {
|
function applyViewLayout(viewName) {
|
||||||
@ -11432,6 +11765,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Moduler med indhold skal altid vises.
|
||||||
|
if (hasContent) {
|
||||||
|
setVisibility(true);
|
||||||
|
el.classList.remove('module-empty-compact');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// HVIS specifik præference deaktiverer den - Skjul den! Uanset content.
|
// HVIS specifik præference deaktiverer den - Skjul den! Uanset content.
|
||||||
if (pref === false) {
|
if (pref === false) {
|
||||||
setVisibility(false);
|
setVisibility(false);
|
||||||
@ -11446,13 +11786,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default logic (ingen brugervalg) - har den content, så vis den
|
|
||||||
if (hasContent) {
|
|
||||||
setVisibility(true);
|
|
||||||
el.classList.remove('module-empty-compact');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default logic - ingen content: se på layout defaults
|
// Default logic - ingen content: se på layout defaults
|
||||||
if (standardModuleSet.has(moduleName)) {
|
if (standardModuleSet.has(moduleName)) {
|
||||||
setVisibility(true);
|
setVisibility(true);
|
||||||
@ -11510,6 +11843,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _setCaseTabCountBadge(badgeId, count) {
|
||||||
|
const badge = document.getElementById(badgeId);
|
||||||
|
if (!badge) return;
|
||||||
|
const safeCount = Number(count || 0);
|
||||||
|
if (safeCount <= 0) {
|
||||||
|
badge.style.display = 'none';
|
||||||
|
badge.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
badge.style.display = 'inline-block';
|
||||||
|
badge.textContent = safeCount > 99 ? '99+' : String(safeCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _countRows(selector) {
|
||||||
|
const container = document.querySelector(selector);
|
||||||
|
if (!container) return 0;
|
||||||
|
const rows = Array.from(container.querySelectorAll('tr'));
|
||||||
|
return rows.filter((row) => {
|
||||||
|
const cells = row.querySelectorAll('td');
|
||||||
|
if (cells.length === 0) return false;
|
||||||
|
const colspanCell = row.querySelector('td[colspan]');
|
||||||
|
if (colspanCell && cells.length === 1) return false;
|
||||||
|
return true;
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCaseTabCountBadges() {
|
||||||
|
const detailCount = document.querySelectorAll('#details .right-module-card[data-has-content="true"]').length;
|
||||||
|
_setCaseTabCountBadge('detailsTabCountBadge', detailCount);
|
||||||
|
|
||||||
|
const solutionHasContent = String(document.getElementById('solution')?.getAttribute('data-has-content') || '').toLowerCase() === 'true';
|
||||||
|
_setCaseTabCountBadge('solutionTabCountBadge', solutionHasContent ? 1 : 0);
|
||||||
|
|
||||||
|
const emailThreads = Number(document.getElementById('linkedEmailThreadsCount')?.textContent || 0);
|
||||||
|
_setCaseTabCountBadge('emailsTabCountBadge', emailThreads);
|
||||||
|
|
||||||
|
const salesCount = _countRows('#saleItemsSalesBody') + _countRows('#saleItemsPurchaseBody');
|
||||||
|
_setCaseTabCountBadge('salesTabCountBadge', salesCount);
|
||||||
|
|
||||||
|
const supplierCount = _countRows('#supplierPurchaseLinesBody');
|
||||||
|
_setCaseTabCountBadge('supplierTabCountBadge', supplierCount);
|
||||||
|
|
||||||
|
const timeEntriesStore = (typeof timeV1EntriesById !== 'undefined' && timeV1EntriesById && typeof timeV1EntriesById === 'object')
|
||||||
|
? timeV1EntriesById
|
||||||
|
: null;
|
||||||
|
const timeCount = timeEntriesStore
|
||||||
|
? Object.keys(timeEntriesStore).length
|
||||||
|
: document.querySelectorAll('#timetracking tbody tr').length;
|
||||||
|
_setCaseTabCountBadge('timetrackingTabCountBadge', timeCount);
|
||||||
|
|
||||||
|
const subscriptionCount = _countRows('#subscriptionItemsBody');
|
||||||
|
_setCaseTabCountBadge('subscriptionTabCountBadge', subscriptionCount);
|
||||||
|
|
||||||
|
const reminderCount = document.querySelectorAll('#remindersList .list-group-item').length;
|
||||||
|
_setCaseTabCountBadge('remindersTabCountBadge', reminderCount);
|
||||||
|
}
|
||||||
|
|
||||||
async function applyViewFromTags() {
|
async function applyViewFromTags() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/tags/entity/case/${caseIds}`);
|
const res = await fetch(`/api/v1/tags/entity/case/${caseIds}`);
|
||||||
@ -11517,6 +11907,7 @@
|
|||||||
const tags = await res.json();
|
const tags = await res.json();
|
||||||
const viewTag = tags.find(t => ['Pipeline', 'Kundevisning', 'Sag-detalje'].includes(t.name));
|
const viewTag = tags.find(t => ['Pipeline', 'Kundevisning', 'Sag-detalje'].includes(t.name));
|
||||||
applyViewLayout(viewTag ? viewTag.name : 'Sag-detalje');
|
applyViewLayout(viewTag ? viewTag.name : 'Sag-detalje');
|
||||||
|
updateCaseTabCountBadges();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('View tag lookup failed', e);
|
console.error('View tag lookup failed', e);
|
||||||
}
|
}
|
||||||
@ -11532,6 +11923,7 @@
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
modulePrefs.time = true;
|
modulePrefs.time = true;
|
||||||
|
updateCaseTabCountBadges();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Module prefs load failed', e);
|
console.error('Module prefs load failed', e);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user