Fix tag addition error handling and add legacy support for case tags
- Improved error handling when adding tags by parsing JSON response safely.
- Added support for legacy tag addition via the /sag/{id}/tags endpoint for case context.
- Enhanced user feedback for tag addition errors and success notifications.
This commit is contained in:
parent
5bd54a27dc
commit
6133823ade
@ -864,11 +864,11 @@
|
||||
|
||||
/* Forslag 1: Opgavebeskrivelse + kommentarspor (venstre side) */
|
||||
.narrative-description {
|
||||
border: 1px solid rgba(15, 76, 117, 0.22);
|
||||
background: linear-gradient(165deg, rgba(15, 76, 117, 0.11), rgba(15, 76, 117, 0.04));
|
||||
border-radius: 12px;
|
||||
padding: 1.1rem 1.1rem 1rem;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52), 0 3px 10px rgba(15, 76, 117, 0.06);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding: 0.25rem 0.1rem 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#beskrivelse-section {
|
||||
@ -914,10 +914,10 @@
|
||||
}
|
||||
|
||||
#beskrivelse-comments-wrap {
|
||||
border: 1px solid rgba(15, 76, 117, 0.2);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 3%, var(--bg-card)), var(--bg-card));
|
||||
padding: 0.95rem;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.narrative-lead {
|
||||
@ -1163,14 +1163,51 @@
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.case-details-shell {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#case-left-column > .row.g-4,
|
||||
#inner-center-col > .row.mb-3 {
|
||||
--bs-gutter-x: 0;
|
||||
--bs-gutter-y: 0;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
#inner-left-col,
|
||||
#inner-center-col,
|
||||
#inner-center-col > .row.mb-3 > .col-12 {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
#inner-center-col > .row.mb-3 > .col-12 {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
#inner-center-col .case-details-shell > .card-body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#beskrivelse-section {
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
border-top: 0 !important;
|
||||
}
|
||||
|
||||
[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 {
|
||||
border-color: rgba(117, 194, 239, 0.32);
|
||||
background: linear-gradient(180deg, rgba(117, 194, 239, 0.2), rgba(117, 194, 239, 0.08));
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 4px 12px rgba(5, 18, 30, 0.28);
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .case-left-section-title {
|
||||
@ -1182,8 +1219,7 @@
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .case-left-panel,
|
||||
[data-bs-theme="dark"] #beskrivelse-history-wrap,
|
||||
[data-bs-theme="dark"] #beskrivelse-comments-wrap {
|
||||
[data-bs-theme="dark"] #beskrivelse-history-wrap {
|
||||
border-color: rgba(117, 194, 239, 0.28);
|
||||
background: linear-gradient(180deg, rgba(117, 194, 239, 0.08), rgba(17, 24, 33, 0.94));
|
||||
}
|
||||
@ -1236,13 +1272,17 @@
|
||||
border-bottom: 1px solid rgba(15, 76, 117, 0.08);
|
||||
}
|
||||
|
||||
#caseTabsContent > .tab-pane {
|
||||
display: none;
|
||||
#caseTabsContent .tab-pane {
|
||||
display: none !important;
|
||||
opacity: 1 !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
#caseTabsContent > .tab-pane.active,
|
||||
#caseTabsContent > .tab-pane.show.active {
|
||||
display: block;
|
||||
#caseTabsContent .tab-pane.active,
|
||||
#caseTabsContent .tab-pane.show.active {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.todo-step-item {
|
||||
@ -1605,6 +1645,10 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
#beskrivelse-section {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.module-priority-low {
|
||||
--module-accent: #64748b;
|
||||
}
|
||||
@ -1630,9 +1674,30 @@
|
||||
box-shadow: 0 4px 14px rgba(15, 76, 117, 0.08);
|
||||
}
|
||||
|
||||
.right-module-card .card-header {
|
||||
.left-module-card {
|
||||
border: 1px solid rgba(15, 76, 117, 0.14);
|
||||
border-left: 4px solid var(--module-accent, var(--accent));
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(165deg, color-mix(in srgb, var(--module-accent, var(--accent)) 6%, var(--bg-card)) 0%, var(--bg-card) 100%);
|
||||
box-shadow: 0 4px 14px rgba(15, 76, 117, 0.08);
|
||||
}
|
||||
|
||||
.right-module-card .card-header,
|
||||
.left-module-card .card-header {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--module-accent, var(--accent)) 22%, #d1d5db);
|
||||
background: color-mix(in srgb, var(--module-accent, var(--accent)) 7%, var(--bg-card));
|
||||
padding: 0.35rem 0.6rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.right-module-card > .card-body,
|
||||
.left-module-card > .card-body {
|
||||
padding: 0.4rem !important;
|
||||
}
|
||||
|
||||
#beskrivelse-section .left-module-card + .left-module-card {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.module-title {
|
||||
@ -2032,11 +2097,26 @@
|
||||
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.10);
|
||||
}
|
||||
|
||||
.left-module-card,
|
||||
.right-module-card {
|
||||
border: 2px solid rgba(15, 76, 117, 0.28) !important;
|
||||
border-left: 2px solid rgba(15, 76, 117, 0.28) !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.10) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .card[data-module] {
|
||||
border-color: rgba(117, 167, 204, 0.45) !important;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .left-module-card,
|
||||
[data-bs-theme="dark"] .right-module-card {
|
||||
border: 2px solid rgba(117, 167, 204, 0.45) !important;
|
||||
border-left: 2px solid rgba(117, 167, 204, 0.45) !important;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .topbar-company-edit-btn {
|
||||
border-color: rgba(170,205,245,0.5);
|
||||
box-shadow: 0 1px 6px rgba(75,145,255,0.35);
|
||||
@ -2057,11 +2137,24 @@
|
||||
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%);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .right-module-card .card-header {
|
||||
[data-bs-theme="dark"] .left-module-card {
|
||||
border-color: rgba(140, 182, 219, 0.25);
|
||||
box-shadow: 0 4px 16px rgba(5, 22, 40, 0.45);
|
||||
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%);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .right-module-card .card-header,
|
||||
[data-bs-theme="dark"] .left-module-card .card-header {
|
||||
border-bottom-color: color-mix(in srgb, var(--module-accent, #69a6d5) 45%, #4b5563);
|
||||
background: color-mix(in srgb, var(--module-accent, #69a6d5) 18%, rgba(18, 28, 40, 0.98));
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .case-details-shell {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .module-title {
|
||||
color: #e5edf5;
|
||||
}
|
||||
@ -2988,37 +3081,73 @@
|
||||
window.caseTypeModuleDefaults = caseTypeModuleDefaults;
|
||||
window.caseTypeKey = window.caseTypeKey || {{ ((case.template_key or case.type or 'ticket')|lower)|tojson }};
|
||||
|
||||
window.forceCaseTabActivation = window.forceCaseTabActivation || function(tabId) {
|
||||
if (!tabId) return;
|
||||
window.syncCaseTabPaneVisibility = window.syncCaseTabPaneVisibility || function(activeTabId) {
|
||||
if (!activeTabId) return;
|
||||
const tabContent = document.getElementById('caseTabsContent');
|
||||
const targetPane = document.getElementById(tabId);
|
||||
if (!tabContent || !targetPane) return;
|
||||
if (!tabContent) return;
|
||||
const paneIds = Array.from(document.querySelectorAll('#caseTabs [data-bs-target^="#"]'))
|
||||
.map((btn) => (btn.getAttribute('data-bs-target') || '').replace('#', ''))
|
||||
.filter(Boolean);
|
||||
if (!paneIds.length) return;
|
||||
|
||||
tabContent.querySelectorAll(':scope > .tab-pane').forEach((pane) => {
|
||||
pane.classList.remove('show', 'active');
|
||||
pane.style.display = 'none';
|
||||
paneIds.forEach((paneId) => {
|
||||
const pane = document.getElementById(paneId);
|
||||
if (pane && pane.parentElement !== tabContent) {
|
||||
tabContent.appendChild(pane);
|
||||
}
|
||||
});
|
||||
|
||||
targetPane.classList.add('show', 'active');
|
||||
targetPane.style.display = 'block';
|
||||
paneIds.forEach((paneId) => {
|
||||
const pane = document.getElementById(paneId);
|
||||
if (!pane) return;
|
||||
const isActive = pane.id === activeTabId;
|
||||
pane.classList.toggle('active', isActive);
|
||||
pane.classList.toggle('show', isActive);
|
||||
pane.classList.remove('d-none');
|
||||
pane.style.display = isActive ? 'block' : 'none';
|
||||
pane.style.opacity = '1';
|
||||
pane.style.visibility = isActive ? 'visible' : 'hidden';
|
||||
pane.hidden = !isActive;
|
||||
});
|
||||
|
||||
const tabButtons = document.querySelectorAll('#caseTabs [data-bs-target]');
|
||||
tabButtons.forEach((btn) => {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-bs-target') === `#${tabId}`);
|
||||
const isActive = btn.getAttribute('data-bs-target') === `#${activeTabId}`;
|
||||
btn.classList.toggle('active', isActive);
|
||||
btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
btn.tabIndex = isActive ? 0 : -1;
|
||||
});
|
||||
};
|
||||
|
||||
window.forceCaseTabActivation = window.forceCaseTabActivation || function(tabId) {
|
||||
if (!tabId) return;
|
||||
const trigger = document.querySelector(`#caseTabs [data-bs-target="#${tabId}"]`);
|
||||
if (!trigger) return;
|
||||
window.syncCaseTabPaneVisibility(tabId);
|
||||
trigger.dispatchEvent(new Event('shown.bs.tab', { bubbles: true }));
|
||||
if (typeof window.loadCaseTabData === 'function') window.loadCaseTabData(tabId);
|
||||
};
|
||||
|
||||
window.activateCaseTabFromButton = window.activateCaseTabFromButton || function(event, tabId) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
window.forceCaseTabActivation(tabId);
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<ul class="nav nav-tabs mb-4" id="caseTabs" role="tablist">
|
||||
<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-target="#details" type="button" role="tab" onclick="return activateCaseTabFromButton(event, 'details')">
|
||||
<i class="bi bi-card-text me-2"></i>Sagsdetaljer
|
||||
<span class="case-tab-count-badge" id="detailsTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#solution" type="button" role="tab" data-module-tab="solution" onclick="return activateCaseTabFromButton(event, 'solution')">
|
||||
<i class="bi bi-lightbulb me-2"></i>Løsning
|
||||
<span class="case-tab-count-badge" id="solutionTabCountBadge"></span>
|
||||
{% if solution %}
|
||||
@ -3027,38 +3156,38 @@
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#emails" type="button" role="tab" data-module-tab="emails" onclick="return activateCaseTabFromButton(event, 'emails')">
|
||||
<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>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#sales" type="button" role="tab" data-module-tab="sales" onclick="return activateCaseTabFromButton(event, 'sales')">
|
||||
<i class="bi bi-basket3 me-2"></i>Varekøb & Salg
|
||||
<span class="case-tab-count-badge" id="salesTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#supplier" type="button" role="tab" data-module-tab="supplier" onclick="return activateCaseTabFromButton(event, 'supplier')">
|
||||
<i class="bi bi-receipt-cutoff me-2"></i>Leverandør
|
||||
<span class="case-tab-count-badge" id="supplierTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#timetracking" type="button" role="tab" onclick="return activateCaseTabFromButton(event, 'timetracking')">
|
||||
<i class="bi bi-clock-history me-2"></i>Tidsforbrug
|
||||
<span class="case-tab-count-badge" id="timetrackingTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#subscription" type="button" role="tab" data-module-tab="subscription" onclick="return activateCaseTabFromButton(event, 'subscription')">
|
||||
<i class="bi bi-repeat me-2"></i>Abonnement
|
||||
<span class="case-tab-count-badge" id="subscriptionTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#reminders" type="button" role="tab" data-module-tab="reminders" onclick="return activateCaseTabFromButton(event, 'reminders')">
|
||||
<i class="bi bi-bell me-2"></i>Påmindelser
|
||||
<span class="case-tab-count-badge" id="remindersTabCountBadge"></span>
|
||||
</button>
|
||||
@ -3083,10 +3212,10 @@
|
||||
<!-- ROW 1: Main Info -->
|
||||
<div class="row mb-3">
|
||||
<!-- MAIN HERO CARD: Titel & Beskrivelse -->
|
||||
<div class="col-12 mb-4 mt-2">
|
||||
<div class="card shadow-sm border-0 border-start border-4 border-primary" style="background-color: var(--bg-card); border-radius: 8px;">
|
||||
<div class="card-body p-4 pt-4 pb-5 position-relative">
|
||||
<div class="d-flex justify-content-between align-items-start mb-4">
|
||||
<div class="col-12 mb-3 mt-1">
|
||||
<div class="case-details-shell">
|
||||
<div class="card-body p-3 position-relative">
|
||||
<div class="d-flex justify-content-between align-items-start mb-1 d-none">
|
||||
<div class="w-100 pe-3">
|
||||
<!-- Title view -->
|
||||
<div id="sag-titel-view" class="d-flex align-items-center gap-2">
|
||||
@ -3106,106 +3235,114 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-top border-light" id="beskrivelse-section">
|
||||
<div class="case-left-panel">
|
||||
<div class="case-left-section-title"><i class="bi bi-card-text"></i>Opgavebeskrivelse</div>
|
||||
<!-- View mode -->
|
||||
<div id="beskrivelse-view" class="narrative-description" style="min-height: 120px; cursor: pointer;" ondblclick="startBeskrivelsEdit()">
|
||||
<div class="narrative-lead">{{ case.titel }}</div>
|
||||
<div id="beskrivelse-text" class="prose" style="white-space: pre-wrap;">{{ case.beskrivelse or '' }}</div>
|
||||
{% if not case.beskrivelse %}
|
||||
<div id="beskrivelse-empty" class="text-center p-3">
|
||||
<p class="text-muted fst-italic mb-2">Ingen opgavebeskrivelse tilføjet endnu.</p>
|
||||
<span class="text-muted small"><i class="bi bi-pencil me-1"></i>Dobbeltklik for at tilføje</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="pt-2" id="beskrivelse-section">
|
||||
<div class="card left-module-card module-priority-normal">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="module-title"><i class="bi bi-card-text module-icon"></i>{{ case.titel }}</h6>
|
||||
<button class="btn btn-sm btn-link text-info p-0" onclick="openManualHelp('Sag')" title="Hjælp til sagsbehandling"><i class="bi bi-question-circle"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit mode (hidden by default) -->
|
||||
<div id="beskrivelse-editor" class="d-none mt-1">
|
||||
<textarea id="beskrivelse-textarea" class="form-control"
|
||||
rows="8" style="font-size: 1rem; line-height: 1.7; resize: vertical; min-height: 150px;"></textarea>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
<span class="text-muted small"><i class="bi bi-keyboard me-1"></i>Ctrl+Enter for at gemme · Esc for at annullere</span>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cancelBeskrivelsEdit()">
|
||||
<i class="bi bi-x me-1"></i>Annuller
|
||||
</button>
|
||||
<button id="beskrivelse-rewrite-btn" type="button" class="btn btn-sm btn-outline-primary" onclick="rewriteCaseDescriptionWithApproval()">
|
||||
<i class="bi bi-magic me-1"></i>Renskriv med AI
|
||||
</button>
|
||||
<button id="beskrivelse-save-btn" class="btn btn-sm btn-primary" onclick="saveBeskrivelsEdit()">
|
||||
<i class="bi bi-check2 me-1"></i>Gem
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History accordion -->
|
||||
<div id="beskrivelse-history-wrap" class="mt-3 d-none">
|
||||
<button class="btn btn-link btn-sm p-0 text-muted text-decoration-none"
|
||||
type="button" data-bs-toggle="collapse" data-bs-target="#beskrivelse-history-collapse"
|
||||
onclick="loadBeskrivelsHistory()">
|
||||
<i class="bi bi-clock-history me-1"></i><span id="beskrivelse-history-label">Historik</span>
|
||||
</button>
|
||||
<div class="collapse mt-2" id="beskrivelse-history-collapse">
|
||||
<div id="beskrivelse-history-list" class="list-group list-group-flush small border rounded">
|
||||
<div class="list-group-item text-muted text-center py-3">
|
||||
<span class="spinner-border spinner-border-sm me-1"></span> Indlæser...
|
||||
<div class="card-body">
|
||||
<!-- View mode -->
|
||||
<div id="beskrivelse-view" class="narrative-description" style="min-height: 120px; cursor: pointer;" ondblclick="startBeskrivelsEdit()">
|
||||
<div id="beskrivelse-text" class="prose" style="white-space: pre-wrap;">{{ case.beskrivelse or '' }}</div>
|
||||
{% if not case.beskrivelse %}
|
||||
<div id="beskrivelse-empty" class="text-center p-3">
|
||||
<p class="text-muted fst-italic mb-2">Ingen opgavebeskrivelse tilføjet endnu.</p>
|
||||
<span class="text-muted small"><i class="bi bi-pencil me-1"></i>Dobbeltklik for at tilføje</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="beskrivelse-comments-wrap">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="text-muted text-uppercase small mb-0 fw-bold" style="letter-spacing: 0.05em;">
|
||||
<i class="bi bi-chat-left-text me-1"></i>Kommentarer
|
||||
</h6>
|
||||
<span class="badge bg-secondary">{{ comments|length if comments else 0 }}</span>
|
||||
</div>
|
||||
|
||||
<div class="comment-thread" id="comments-container">
|
||||
{% if comments %}
|
||||
{% for comment in comments %}
|
||||
{% if comment.er_system_besked or comment.forfatter == 'System' %}
|
||||
<div class="comment-item comment-system" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% elif comment.er_intern %}
|
||||
<div class="comment-item comment-internal" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% else %}
|
||||
<div class="comment-item comment-external" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% endif %}
|
||||
<div class="comment-meta">
|
||||
<span class="comment-avatar">{{ (comment.forfatter or 'Bruger')[:2]|upper }}</span>
|
||||
<b>{{ comment.forfatter }}</b>
|
||||
<span class="comment-time">{{ comment.created_at.strftime('%d/%m-%Y %H:%M') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Edit mode (hidden by default) -->
|
||||
<div id="beskrivelse-editor" class="d-none mt-1">
|
||||
<textarea id="beskrivelse-textarea" class="form-control"
|
||||
rows="8" style="font-size: 1rem; line-height: 1.7; resize: vertical; min-height: 150px;"></textarea>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
<span class="text-muted small"><i class="bi bi-keyboard me-1"></i>Ctrl+Enter for at gemme · Esc for at annullere</span>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cancelBeskrivelsEdit()">
|
||||
<i class="bi bi-x me-1"></i>Annuller
|
||||
</button>
|
||||
<button id="beskrivelse-rewrite-btn" type="button" class="btn btn-sm btn-outline-primary" onclick="rewriteCaseDescriptionWithApproval()">
|
||||
<i class="bi bi-magic me-1"></i>Renskriv med AI
|
||||
</button>
|
||||
<button id="beskrivelse-save-btn" class="btn btn-sm btn-primary" onclick="saveBeskrivelsEdit()">
|
||||
<i class="bi bi-check2 me-1"></i>Gem
|
||||
</button>
|
||||
</div>
|
||||
<div class="comment-body" data-comment-raw="{{ comment.indhold|e }}">{{ comment.indhold|replace('\n', '<br>')|safe }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-center text-muted my-3">Ingen kommentarer endnu.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- History accordion -->
|
||||
<div id="beskrivelse-history-wrap" class="mt-3 d-none">
|
||||
<button class="btn btn-link btn-sm p-0 text-muted text-decoration-none"
|
||||
type="button" data-bs-toggle="collapse" data-bs-target="#beskrivelse-history-collapse"
|
||||
onclick="loadBeskrivelsHistory()">
|
||||
<i class="bi bi-clock-history me-1"></i><span id="beskrivelse-history-label">Historik</span>
|
||||
</button>
|
||||
<div class="collapse mt-2" id="beskrivelse-history-collapse">
|
||||
<div id="beskrivelse-history-list" class="list-group list-group-flush small border rounded">
|
||||
<div class="list-group-item text-muted text-center py-3">
|
||||
<span class="spinner-border spinner-border-sm me-1"></span> Indlæser...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="comment-quick-reply-host"></div>
|
||||
<div class="card left-module-card module-priority-low">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="module-title"><i class="bi bi-chat-left-text module-icon"></i>Kommentarer</h6>
|
||||
<span id="commentsTotalCountBadge" class="badge bg-secondary">{{ comments|length if comments else 0 }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="beskrivelse-comments-wrap">
|
||||
|
||||
<div class="mt-3">
|
||||
<form id="comment-form" onsubmit="submitComment(event)">
|
||||
<div class="comment-composer">
|
||||
<textarea class="form-control" name="indhold" required placeholder="Skriv en kommentar..." rows="2" style="resize: none;"></textarea>
|
||||
<button type="submit" class="btn btn-primary d-flex align-items-center justify-content-center comment-send">
|
||||
<i class="bi bi-send me-1"></i>Send
|
||||
</button>
|
||||
<div class="comment-thread" id="comments-container">
|
||||
{% if comments %}
|
||||
{% for comment in comments %}
|
||||
{% if comment.er_system_besked or comment.forfatter == 'System' %}
|
||||
<div class="comment-item comment-system" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% elif comment.er_intern %}
|
||||
<div class="comment-item comment-internal" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% else %}
|
||||
<div class="comment-item comment-external" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% endif %}
|
||||
<div class="comment-meta">
|
||||
<span class="comment-avatar">{{ (comment.forfatter or 'Bruger')[:2]|upper }}</span>
|
||||
<b>{{ comment.forfatter }}</b>
|
||||
<span class="comment-time">{{ comment.created_at.strftime('%d/%m-%Y %H:%M') }}</span>
|
||||
</div>
|
||||
<div class="comment-body" data-comment-raw="{{ comment.indhold|e }}">{{ comment.indhold|replace('\n', '<br>')|safe }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-center text-muted my-3">Ingen kommentarer endnu.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="comment-quick-reply-host"></div>
|
||||
|
||||
<div class="mt-3">
|
||||
<form id="comment-form" onsubmit="submitComment(event)">
|
||||
<div class="comment-composer">
|
||||
<textarea class="form-control" name="indhold" required placeholder="Skriv en kommentar..." rows="2" style="resize: none;"></textarea>
|
||||
<button type="submit" class="btn btn-primary d-flex align-items-center justify-content-center comment-send">
|
||||
<i class="bi bi-send me-1"></i>Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ROW 1B: Pipeline -->
|
||||
@ -3876,6 +4013,7 @@
|
||||
let selectedRelationCaseId = null;
|
||||
let customerSearchMode = 'link';
|
||||
const caseTypeKey = {{ ((case.template_key or case.type or 'ticket')|lower)|tojson }};
|
||||
const initialCaseTagsSnapshot = {{ (tags or [])|tojson }};
|
||||
|
||||
async function markCaseAsRecentlyOpened() {
|
||||
try {
|
||||
@ -3954,22 +4092,11 @@
|
||||
function forceCaseTabActivation(tabId) {
|
||||
if (!tabId) return;
|
||||
|
||||
const tabContent = document.getElementById('caseTabsContent');
|
||||
const targetPane = document.getElementById(tabId);
|
||||
if (!tabContent || !targetPane) return;
|
||||
|
||||
tabContent.querySelectorAll(':scope > .tab-pane').forEach((pane) => {
|
||||
pane.classList.remove('show', 'active');
|
||||
pane.style.display = 'none';
|
||||
});
|
||||
|
||||
targetPane.classList.add('show', 'active');
|
||||
targetPane.style.display = 'block';
|
||||
|
||||
const tabButtons = document.querySelectorAll('#caseTabs [data-bs-target]');
|
||||
tabButtons.forEach((btn) => {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-bs-target') === `#${tabId}`);
|
||||
});
|
||||
const trigger = document.querySelector(`#caseTabs [data-bs-target="#${tabId}"]`);
|
||||
if (!trigger) return;
|
||||
window.syncCaseTabPaneVisibility(tabId);
|
||||
trigger.dispatchEvent(new Event('shown.bs.tab', { bubbles: true }));
|
||||
if (typeof window.loadCaseTabData === 'function') window.loadCaseTabData(tabId);
|
||||
}
|
||||
|
||||
window.moduleDisplayNames = {
|
||||
@ -4093,11 +4220,8 @@
|
||||
|
||||
const caseTabs = document.getElementById('caseTabs');
|
||||
if (caseTabs) {
|
||||
caseTabs.addEventListener('shown.bs.tab', async (event) => {
|
||||
const targetSelector = event?.target?.getAttribute('data-bs-target') || '';
|
||||
const tabId = targetSelector.startsWith('#') ? targetSelector.slice(1) : targetSelector;
|
||||
|
||||
forceCaseTabActivation(tabId);
|
||||
const loadCaseTabData = async (tabId) => {
|
||||
if (!tabId) return;
|
||||
|
||||
try {
|
||||
if (tabId === 'sales' && typeof loadVarekobSalg === 'function') {
|
||||
@ -4113,20 +4237,27 @@
|
||||
} catch (tabLoadError) {
|
||||
console.error('Tab data reload failed:', tabLoadError);
|
||||
}
|
||||
});
|
||||
};
|
||||
window.loadCaseTabData = loadCaseTabData;
|
||||
|
||||
caseTabs.addEventListener('click', (event) => {
|
||||
const btn = event.target.closest('[data-bs-target]');
|
||||
if (!btn) return;
|
||||
const targetSelector = btn.getAttribute('data-bs-target') || '';
|
||||
caseTabs.addEventListener('click', async (event) => {
|
||||
const trigger = event.target.closest('[data-bs-target]');
|
||||
if (!trigger || !caseTabs.contains(trigger)) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const targetSelector = trigger.getAttribute('data-bs-target') || '';
|
||||
const tabId = targetSelector.startsWith('#') ? targetSelector.slice(1) : targetSelector;
|
||||
if (tabId) {
|
||||
setTimeout(() => forceCaseTabActivation(tabId), 0);
|
||||
}
|
||||
});
|
||||
if (!tabId) return;
|
||||
|
||||
window.syncCaseTabPaneVisibility(tabId);
|
||||
trigger.dispatchEvent(new Event('shown.bs.tab', { bubbles: true }));
|
||||
await loadCaseTabData(tabId);
|
||||
}, true);
|
||||
}
|
||||
|
||||
forceCaseTabActivation('details');
|
||||
window.syncCaseTabPaneVisibility('details');
|
||||
|
||||
// Focus on title when create modal opens
|
||||
const createModalEl = document.getElementById('createRelatedCaseModal');
|
||||
@ -5231,6 +5362,12 @@
|
||||
tags = await loadLegacyTags();
|
||||
}
|
||||
|
||||
// Final fallback: use server-rendered tags from page context when API calls return empty.
|
||||
if ((!Array.isArray(tags) || tags.length === 0) && Array.isArray(initialCaseTagsSnapshot) && initialCaseTagsSnapshot.length > 0) {
|
||||
tags = normalizeLegacyTags(initialCaseTagsSnapshot);
|
||||
usingLegacyCaseTags = true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
moduleContainer.innerHTML = '<div class="p-3 text-center text-muted small">Ingen tags paaa sagen endnu</div>';
|
||||
setModuleContentState('tags', false);
|
||||
@ -5240,9 +5377,9 @@
|
||||
moduleContainer.innerHTML = tags.map((tag) => `
|
||||
<span class="badge me-1 mb-1" style="background-color: ${tag.color};">
|
||||
${tag.icon ? `<i class="bi ${tag.icon}"></i> ` : ''}${escapeHtml(tag.name)}
|
||||
<button type="button" class="btn-close btn-close-white btn-sm ms-1"
|
||||
${tag.id ? `<button type="button" class="btn-close btn-close-white btn-sm ms-1"
|
||||
onclick="removeCaseTagAndSync(${tag.id})"
|
||||
style="font-size: 0.6rem; vertical-align: middle;"></button>
|
||||
style="font-size: 0.6rem; vertical-align: middle;"></button>` : ''}
|
||||
</span>
|
||||
`).join('');
|
||||
|
||||
@ -5961,8 +6098,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div></div><!-- slut inner cols -->
|
||||
</div> <!-- /#inner-center-col -->
|
||||
</div> <!-- /.row.g-4 (inner) -->
|
||||
</div> <!-- /#case-left-column -->
|
||||
<div class="col-xl-4 col-lg-4" id="case-right-column">
|
||||
<div class="right-modules-grid">
|
||||
<div class="card h-100 d-flex flex-column right-module-card module-priority-normal" data-module="locations" data-has-content="unknown">
|
||||
@ -5982,8 +6120,8 @@
|
||||
<div class="card h-100 d-flex flex-column right-module-card module-priority-low" data-module="tags" data-has-content="unknown">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="module-title"><i class="bi bi-tags-fill module-icon"></i>TAGS</h6>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
onclick="window.showTagPicker('case', {{ case.id }}, () => syncCaseTagsUi())"
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="event.stopPropagation(); window.showTagPicker('case', {{ case.id }}, () => syncCaseTagsUi()); return false;"
|
||||
title="Tilføj tag">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
@ -6293,6 +6431,42 @@
|
||||
</div>
|
||||
</div> <!-- End Details Tab -->
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
function normalizeDetailsColumns() {
|
||||
const detailsPane = document.getElementById('details');
|
||||
if (!detailsPane) return;
|
||||
|
||||
let detailsRow = null;
|
||||
try {
|
||||
detailsRow = detailsPane.querySelector(':scope > .row.g-4');
|
||||
} catch (e) {
|
||||
detailsRow = detailsPane.querySelector('.row.g-4');
|
||||
}
|
||||
const leftCol = detailsPane.querySelector('#case-left-column');
|
||||
const rightCol = detailsPane.querySelector('#case-right-column');
|
||||
|
||||
if (!detailsRow || !leftCol || !rightCol) return;
|
||||
|
||||
// Defensive: ensure right column is a direct sibling inside the outer details row.
|
||||
if (rightCol.parentElement !== detailsRow) {
|
||||
detailsRow.appendChild(rightCol);
|
||||
}
|
||||
|
||||
if (!leftCol.classList.contains('col-xl-8')) leftCol.classList.add('col-xl-8');
|
||||
if (!leftCol.classList.contains('col-lg-8')) leftCol.classList.add('col-lg-8');
|
||||
if (!rightCol.classList.contains('col-xl-4')) rightCol.classList.add('col-xl-4');
|
||||
if (!rightCol.classList.contains('col-lg-4')) rightCol.classList.add('col-lg-4');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', normalizeDetailsColumns);
|
||||
} else {
|
||||
normalizeDetailsColumns();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- E-mail Tab -->
|
||||
<div class="tab-pane fade" id="emails" role="tabpanel" tabindex="0" data-module="emails" data-has-content="unknown" style="display:none;">
|
||||
<div class="card mb-3">
|
||||
@ -7610,7 +7784,8 @@
|
||||
|
||||
function _refreshCommentCountBadge() {
|
||||
const container = document.getElementById('comments-container');
|
||||
const badge = document.querySelector('#beskrivelse-comments-wrap .badge.bg-secondary');
|
||||
const badge = document.getElementById('commentsTotalCountBadge')
|
||||
|| document.querySelector('#beskrivelse-comments-wrap .badge.bg-secondary');
|
||||
if (!container || !badge) return;
|
||||
badge.textContent = String(container.querySelectorAll('.comment-item').length);
|
||||
}
|
||||
@ -11964,10 +12139,23 @@
|
||||
}
|
||||
|
||||
function updateRightColumnVisibility() {
|
||||
const detailsPane = document.getElementById('details');
|
||||
const rightColumn = document.getElementById('case-right-column');
|
||||
const leftColumn = document.getElementById('case-left-column');
|
||||
if (!rightColumn || !leftColumn) return;
|
||||
|
||||
if (detailsPane) {
|
||||
let detailsRow = null;
|
||||
try {
|
||||
detailsRow = detailsPane.querySelector(':scope > .row.g-4');
|
||||
} catch (e) {
|
||||
detailsRow = detailsPane.querySelector('.row.g-4');
|
||||
}
|
||||
if (detailsRow && rightColumn.parentElement !== detailsRow) {
|
||||
detailsRow.appendChild(rightColumn);
|
||||
}
|
||||
}
|
||||
|
||||
const visibleRightModules = rightColumn.querySelectorAll('.right-module-card:not(.d-none)');
|
||||
if (visibleRightModules.length === 0) {
|
||||
rightColumn.classList.add('d-none');
|
||||
@ -12883,8 +13071,7 @@
|
||||
function openCaseEmailTab() {
|
||||
const trigger = document.getElementById('emails-tab');
|
||||
if (!trigger) return;
|
||||
const instance = bootstrap.Tab.getOrCreateInstance(trigger);
|
||||
instance.show();
|
||||
forceCaseTabActivation('emails');
|
||||
}
|
||||
|
||||
window.quickReplyToEmailFromComment = async function(emailId) {
|
||||
@ -14161,8 +14348,9 @@
|
||||
|| document.querySelector(`[data-module-tab="${tabParam}"]`);
|
||||
if (tabBtn) {
|
||||
setTimeout(() => {
|
||||
bootstrap.Tab.getOrCreateInstance(tabBtn).show();
|
||||
forceCaseTabActivation(tabParam);
|
||||
const targetSelector = tabBtn.getAttribute('data-bs-target') || '';
|
||||
const targetTabId = targetSelector.startsWith('#') ? targetSelector.slice(1) : tabParam;
|
||||
forceCaseTabActivation(targetTabId);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@ -864,11 +864,11 @@
|
||||
|
||||
/* Forslag 1: Opgavebeskrivelse + kommentarspor (venstre side) */
|
||||
.narrative-description {
|
||||
border: 1px solid rgba(15, 76, 117, 0.22);
|
||||
background: linear-gradient(165deg, rgba(15, 76, 117, 0.11), rgba(15, 76, 117, 0.04));
|
||||
border-radius: 12px;
|
||||
padding: 1.1rem 1.1rem 1rem;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52), 0 3px 10px rgba(15, 76, 117, 0.06);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding: 0.25rem 0.1rem 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#beskrivelse-section {
|
||||
@ -927,10 +927,10 @@
|
||||
}
|
||||
|
||||
#beskrivelse-comments-wrap {
|
||||
border: 1px solid rgba(15, 76, 117, 0.2);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 3%, var(--bg-card)), var(--bg-card));
|
||||
padding: 0.95rem;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.narrative-lead {
|
||||
@ -1393,14 +1393,51 @@
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.case-details-shell {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#case-left-column > .row.g-4,
|
||||
#inner-center-col > .row.mb-3 {
|
||||
--bs-gutter-x: 0;
|
||||
--bs-gutter-y: 0;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
#inner-left-col,
|
||||
#inner-center-col,
|
||||
#inner-center-col > .row.mb-3 > .col-12 {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
#inner-center-col > .row.mb-3 > .col-12 {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
#inner-center-col .case-details-shell > .card-body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#beskrivelse-section {
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
border-top: 0 !important;
|
||||
}
|
||||
|
||||
[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 {
|
||||
border-color: rgba(117, 194, 239, 0.32);
|
||||
background: linear-gradient(180deg, rgba(117, 194, 239, 0.2), rgba(117, 194, 239, 0.08));
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 4px 12px rgba(5, 18, 30, 0.28);
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .case-left-section-title {
|
||||
@ -1412,8 +1449,7 @@
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .case-left-panel,
|
||||
[data-bs-theme="dark"] #beskrivelse-history-wrap,
|
||||
[data-bs-theme="dark"] #beskrivelse-comments-wrap {
|
||||
[data-bs-theme="dark"] #beskrivelse-history-wrap {
|
||||
border-color: rgba(117, 194, 239, 0.28);
|
||||
background: linear-gradient(180deg, rgba(117, 194, 239, 0.08), rgba(17, 24, 33, 0.94));
|
||||
}
|
||||
@ -1488,13 +1524,22 @@
|
||||
border-bottom: 1px solid rgba(15, 76, 117, 0.08);
|
||||
}
|
||||
|
||||
#caseTabsContent > .tab-pane {
|
||||
display: none;
|
||||
#caseTabsContent .tab-pane {
|
||||
display: none !important;
|
||||
opacity: 1 !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
#caseTabsContent > .tab-pane.active,
|
||||
#caseTabsContent > .tab-pane.show.active {
|
||||
display: block;
|
||||
#caseTabsContent {
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
#caseTabsContent .tab-pane.active,
|
||||
#caseTabsContent .tab-pane.show.active {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.todo-step-item {
|
||||
@ -1857,6 +1902,10 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
#beskrivelse-section {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.module-priority-low {
|
||||
--module-accent: #64748b;
|
||||
}
|
||||
@ -1882,9 +1931,30 @@
|
||||
box-shadow: 0 4px 14px rgba(15, 76, 117, 0.08);
|
||||
}
|
||||
|
||||
.right-module-card .card-header {
|
||||
.left-module-card {
|
||||
border: 1px solid rgba(15, 76, 117, 0.14);
|
||||
border-left: 4px solid var(--module-accent, var(--accent));
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(165deg, color-mix(in srgb, var(--module-accent, var(--accent)) 6%, var(--bg-card)) 0%, var(--bg-card) 100%);
|
||||
box-shadow: 0 4px 14px rgba(15, 76, 117, 0.08);
|
||||
}
|
||||
|
||||
.right-module-card .card-header,
|
||||
.left-module-card .card-header {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--module-accent, var(--accent)) 22%, #d1d5db);
|
||||
background: color-mix(in srgb, var(--module-accent, var(--accent)) 7%, var(--bg-card));
|
||||
padding: 0.35rem 0.6rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.right-module-card > .card-body,
|
||||
.left-module-card > .card-body {
|
||||
padding: 0.4rem !important;
|
||||
}
|
||||
|
||||
#beskrivelse-section .left-module-card + .left-module-card {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.module-title {
|
||||
@ -2284,11 +2354,26 @@
|
||||
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.10);
|
||||
}
|
||||
|
||||
.left-module-card,
|
||||
.right-module-card {
|
||||
border: 2px solid rgba(15, 76, 117, 0.28) !important;
|
||||
border-left: 2px solid rgba(15, 76, 117, 0.28) !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.10) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .card[data-module] {
|
||||
border-color: rgba(117, 167, 204, 0.45) !important;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .left-module-card,
|
||||
[data-bs-theme="dark"] .right-module-card {
|
||||
border: 2px solid rgba(117, 167, 204, 0.45) !important;
|
||||
border-left: 2px solid rgba(117, 167, 204, 0.45) !important;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .topbar-company-edit-btn {
|
||||
border-color: rgba(170,205,245,0.5);
|
||||
box-shadow: 0 1px 6px rgba(75,145,255,0.35);
|
||||
@ -2309,11 +2394,24 @@
|
||||
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%);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .right-module-card .card-header {
|
||||
[data-bs-theme="dark"] .left-module-card {
|
||||
border-color: rgba(140, 182, 219, 0.25);
|
||||
box-shadow: 0 4px 16px rgba(5, 22, 40, 0.45);
|
||||
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%);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .right-module-card .card-header,
|
||||
[data-bs-theme="dark"] .left-module-card .card-header {
|
||||
border-bottom-color: color-mix(in srgb, var(--module-accent, #69a6d5) 45%, #4b5563);
|
||||
background: color-mix(in srgb, var(--module-accent, #69a6d5) 18%, rgba(18, 28, 40, 0.98));
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .case-details-shell {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .module-title {
|
||||
color: #e5edf5;
|
||||
}
|
||||
@ -3351,37 +3449,73 @@
|
||||
window.caseTypeModuleDefaults = caseTypeModuleDefaults;
|
||||
window.caseTypeKey = window.caseTypeKey || {{ ((case.template_key or case.type or 'ticket')|lower)|tojson }};
|
||||
|
||||
window.forceCaseTabActivation = window.forceCaseTabActivation || function(tabId) {
|
||||
if (!tabId) return;
|
||||
window.syncCaseTabPaneVisibility = window.syncCaseTabPaneVisibility || function(activeTabId) {
|
||||
if (!activeTabId) return;
|
||||
const tabContent = document.getElementById('caseTabsContent');
|
||||
const targetPane = document.getElementById(tabId);
|
||||
if (!tabContent || !targetPane) return;
|
||||
if (!tabContent) return;
|
||||
const paneIds = Array.from(document.querySelectorAll('#caseTabs [data-bs-target^="#"]'))
|
||||
.map((btn) => (btn.getAttribute('data-bs-target') || '').replace('#', ''))
|
||||
.filter(Boolean);
|
||||
if (!paneIds.length) return;
|
||||
|
||||
tabContent.querySelectorAll(':scope > .tab-pane').forEach((pane) => {
|
||||
pane.classList.remove('show', 'active');
|
||||
pane.style.display = 'none';
|
||||
paneIds.forEach((paneId) => {
|
||||
const pane = document.getElementById(paneId);
|
||||
if (pane && pane.parentElement !== tabContent) {
|
||||
tabContent.appendChild(pane);
|
||||
}
|
||||
});
|
||||
|
||||
targetPane.classList.add('show', 'active');
|
||||
targetPane.style.display = 'block';
|
||||
paneIds.forEach((paneId) => {
|
||||
const pane = document.getElementById(paneId);
|
||||
if (!pane) return;
|
||||
const isActive = pane.id === activeTabId;
|
||||
pane.classList.toggle('active', isActive);
|
||||
pane.classList.toggle('show', isActive);
|
||||
pane.classList.remove('d-none');
|
||||
pane.style.display = isActive ? 'block' : 'none';
|
||||
pane.style.opacity = '1';
|
||||
pane.style.visibility = isActive ? 'visible' : 'hidden';
|
||||
pane.hidden = !isActive;
|
||||
});
|
||||
|
||||
const tabButtons = document.querySelectorAll('#caseTabs [data-bs-target]');
|
||||
tabButtons.forEach((btn) => {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-bs-target') === `#${tabId}`);
|
||||
const isActive = btn.getAttribute('data-bs-target') === `#${activeTabId}`;
|
||||
btn.classList.toggle('active', isActive);
|
||||
btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
btn.tabIndex = isActive ? 0 : -1;
|
||||
});
|
||||
};
|
||||
|
||||
window.forceCaseTabActivation = window.forceCaseTabActivation || function(tabId) {
|
||||
if (!tabId) return;
|
||||
const trigger = document.querySelector(`#caseTabs [data-bs-target="#${tabId}"]`);
|
||||
if (!trigger) return;
|
||||
window.syncCaseTabPaneVisibility(tabId);
|
||||
trigger.dispatchEvent(new Event('shown.bs.tab', { bubbles: true }));
|
||||
if (typeof window.loadCaseTabData === 'function') window.loadCaseTabData(tabId);
|
||||
};
|
||||
|
||||
window.activateCaseTabFromButton = window.activateCaseTabFromButton || function(event, tabId) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
window.forceCaseTabActivation(tabId);
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<ul class="nav nav-tabs mb-4" id="caseTabs" role="tablist">
|
||||
<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-target="#details" type="button" role="tab" onclick="return activateCaseTabFromButton(event, 'details')">
|
||||
<i class="bi bi-card-text me-2"></i>Sagsdetaljer
|
||||
<span class="case-tab-count-badge" id="detailsTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#solution" type="button" role="tab" data-module-tab="solution" onclick="return activateCaseTabFromButton(event, 'solution')">
|
||||
<i class="bi bi-lightbulb me-2"></i>Løsning
|
||||
<span class="case-tab-count-badge" id="solutionTabCountBadge"></span>
|
||||
{% if solution %}
|
||||
@ -3390,44 +3524,44 @@
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#emails" type="button" role="tab" data-module-tab="emails" onclick="return activateCaseTabFromButton(event, 'emails')">
|
||||
<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>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#sales" type="button" role="tab" data-module-tab="sales" onclick="return activateCaseTabFromButton(event, 'sales')">
|
||||
<i class="bi bi-basket3 me-2"></i>Varekøb & Salg
|
||||
<span class="case-tab-count-badge" id="salesTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#supplier" type="button" role="tab" data-module-tab="supplier" onclick="return activateCaseTabFromButton(event, 'supplier')">
|
||||
<i class="bi bi-receipt-cutoff me-2"></i>Leverandør
|
||||
<span class="case-tab-count-badge" id="supplierTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#timetracking" type="button" role="tab" onclick="return activateCaseTabFromButton(event, 'timetracking')">
|
||||
<i class="bi bi-clock-history me-2"></i>Tidsforbrug
|
||||
<span class="case-tab-count-badge" id="timetrackingTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#subscription" type="button" role="tab" data-module-tab="subscription" onclick="return activateCaseTabFromButton(event, 'subscription')">
|
||||
<i class="bi bi-repeat me-2"></i>Abonnement
|
||||
<span class="case-tab-count-badge" id="subscriptionTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<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-target="#reminders" type="button" role="tab" data-module-tab="reminders" onclick="return activateCaseTabFromButton(event, 'reminders')">
|
||||
<i class="bi bi-bell me-2"></i>Påmindelser
|
||||
<span class="case-tab-count-badge" id="remindersTabCountBadge"></span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="history-tab" data-bs-toggle="tab" data-bs-target="#history" type="button" role="tab" data-module-tab="history" onclick="forceCaseTabActivation('history', this)">
|
||||
<button class="nav-link" id="history-tab" data-bs-target="#history" type="button" role="tab" data-module-tab="history" onclick="return activateCaseTabFromButton(event, 'history')">
|
||||
<i class="bi bi-clock-history me-2"></i>Historik
|
||||
<span class="case-tab-count-badge" id="historyTabCountBadge"></span>
|
||||
</button>
|
||||
@ -3436,7 +3570,7 @@
|
||||
|
||||
<div class="tab-content" id="caseTabsContent">
|
||||
<!-- Tab: Sagsdetaljer (Existing Content) -->
|
||||
<div class="tab-pane fade show active" id="details" role="tabpanel" tabindex="0" style="display:block;">
|
||||
<div class="tab-pane fade show active" id="details" role="tabpanel" tabindex="0">
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-8 col-lg-8" id="case-left-column">
|
||||
<div class="row g-4">
|
||||
@ -3453,129 +3587,131 @@
|
||||
<div class="row mb-3">
|
||||
<!-- MAIN HERO CARD: Titel & Beskrivelse -->
|
||||
<div class="col-12 mb-3 mt-1">
|
||||
<div class="card shadow-sm border-0 border-start border-4 border-primary" style="background-color: var(--bg-card); border-radius: 8px;">
|
||||
<div class="case-details-shell">
|
||||
<div class="card-body p-3 position-relative">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div class="w-100 pe-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h2 class="mb-2 fw-bolder" style="color: var(--accent); font-size: 1.8rem; letter-spacing: -0.5px;">Sagsbeskrivelse</h2>
|
||||
<button class="btn btn-sm btn-link text-info p-0 mb-1 ms-1" onclick="openManualHelp('Sag')" title="Hjælp til sagsbehandling"><i class="bi bi-question-circle fs-5"></i></button>
|
||||
<div class="pt-2" id="beskrivelse-section">
|
||||
<div class="card left-module-card module-priority-normal">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="module-title"><i class="bi bi-card-text module-icon"></i>{{ case.titel }}</h6>
|
||||
<button class="btn btn-sm btn-link text-info p-0" onclick="openManualHelp('Sag')" title="Hjælp til sagsbehandling"><i class="bi bi-question-circle"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-top border-light" id="beskrivelse-section">
|
||||
<div class="case-left-panel">
|
||||
<div class="case-left-section-title"><i class="bi bi-card-text"></i>Opgavebeskrivelse</div>
|
||||
<!-- View mode -->
|
||||
<div id="beskrivelse-view" class="narrative-description" style="min-height: 120px; cursor: pointer;" ondblclick="startBeskrivelsEdit()">
|
||||
<div class="narrative-lead">{{ case.titel }}</div>
|
||||
<div id="beskrivelse-text" class="prose" style="white-space: pre-wrap;">{{ case.beskrivelse or '' }}</div>
|
||||
{% if not case.beskrivelse %}
|
||||
<div id="beskrivelse-empty" class="text-center p-3">
|
||||
<p class="text-muted fst-italic mb-2">Ingen opgavebeskrivelse tilføjet endnu.</p>
|
||||
<span class="text-muted small"><i class="bi bi-pencil me-1"></i>Dobbeltklik for at tilføje</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit mode (hidden by default) -->
|
||||
<div id="beskrivelse-editor" class="d-none mt-1">
|
||||
<textarea id="beskrivelse-textarea" class="form-control"
|
||||
rows="8" style="font-size: 1rem; line-height: 1.7; resize: vertical; min-height: 150px;"></textarea>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
<span class="text-muted small"><i class="bi bi-keyboard me-1"></i>Ctrl+Enter for at gemme · Esc for at annullere</span>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cancelBeskrivelsEdit()">
|
||||
<i class="bi bi-x me-1"></i>Annuller
|
||||
</button>
|
||||
<button id="beskrivelse-rewrite-btn" type="button" class="btn btn-sm btn-outline-primary" onclick="rewriteCaseDescriptionWithApproval()">
|
||||
<i class="bi bi-magic me-1"></i>Renskriv med AI
|
||||
</button>
|
||||
<button id="beskrivelse-save-btn" class="btn btn-sm btn-primary" onclick="saveBeskrivelsEdit()">
|
||||
<i class="bi bi-check2 me-1"></i>Gem
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="beskrivelse-comments-wrap">
|
||||
<div class="comment-toolbar">
|
||||
<h6 class="comment-title-inline"><i class="bi bi-chat-left-text me-1"></i>Kommentarer</h6>
|
||||
<div class="d-flex align-items-center gap-2 flex-shrink-0">
|
||||
<span id="commentsVisibleCountBadge" class="badge text-bg-light">0/0 vises</span>
|
||||
<span id="commentsTotalCountBadge" class="badge bg-secondary">{{ comments|length if comments else 0 }}</span>
|
||||
</div>
|
||||
<input id="commentFilterText" type="text" class="form-control form-control-sm comment-toolbar-search" placeholder="Søg...">
|
||||
<span class="comment-filter-group-label">Kilde</span>
|
||||
<label class="comment-category-check">
|
||||
<input class="form-check-input" type="checkbox" id="commentSourceFilterEmail" checked>
|
||||
<span>Email (<span id="commentSourceCountEmail" class="count">0</span>)</span>
|
||||
</label>
|
||||
<label class="comment-category-check">
|
||||
<input class="form-check-input" type="checkbox" id="commentSourceFilterSms" checked>
|
||||
<span>SMS (<span id="commentSourceCountSms" class="count">0</span>)</span>
|
||||
</label>
|
||||
<label class="comment-category-check">
|
||||
<input class="form-check-input" type="checkbox" id="commentSourceFilterCall" checked>
|
||||
<span>Kald (<span id="commentSourceCountCall" class="count">0</span>)</span>
|
||||
</label>
|
||||
<label class="comment-category-check">
|
||||
<input class="form-check-input" type="checkbox" id="commentSourceFilterModule" checked>
|
||||
<span>Andet/modul (<span id="commentSourceCountModule" class="count">0</span>)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="comment-thread" id="comments-container">
|
||||
{% if comments %}
|
||||
{% for comment in comments %}
|
||||
{% if comment.er_system_besked or comment.forfatter == 'System' %}
|
||||
<div class="comment-item comment-system" data-category="system" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% elif comment.er_intern %}
|
||||
<div class="comment-item comment-internal" data-category="internal" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% else %}
|
||||
<div class="comment-item comment-external" data-category="external" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
<div class="card-body">
|
||||
<!-- View mode -->
|
||||
<div id="beskrivelse-view" class="narrative-description" style="min-height: 120px; cursor: pointer;" ondblclick="startBeskrivelsEdit()">
|
||||
<div id="beskrivelse-text" class="prose" style="white-space: pre-wrap;">{{ case.beskrivelse or '' }}</div>
|
||||
{% if not case.beskrivelse %}
|
||||
<div id="beskrivelse-empty" class="text-center p-3">
|
||||
<p class="text-muted fst-italic mb-2">Ingen opgavebeskrivelse tilføjet endnu.</p>
|
||||
<span class="text-muted small"><i class="bi bi-pencil me-1"></i>Dobbeltklik for at tilføje</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="comment-meta">
|
||||
<span class="comment-avatar">{{ (comment.forfatter or 'Bruger')[:2]|upper }}</span>
|
||||
<b>{{ comment.forfatter }}</b>
|
||||
{% if comment.er_system_besked or comment.forfatter == 'System' %}
|
||||
<span class="comment-category-badge category-system">System</span>
|
||||
{% elif comment.er_intern %}
|
||||
<span class="comment-category-badge category-internal">Intern</span>
|
||||
{% else %}
|
||||
<span class="comment-category-badge category-external">Ekstern</span>
|
||||
{% endif %}
|
||||
<span class="comment-time">{{ comment.created_at.strftime('%d/%m-%Y %H:%M') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Edit mode (hidden by default) -->
|
||||
<div id="beskrivelse-editor" class="d-none mt-1">
|
||||
<textarea id="beskrivelse-textarea" class="form-control"
|
||||
rows="8" style="font-size: 1rem; line-height: 1.7; resize: vertical; min-height: 150px;"></textarea>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
<span class="text-muted small"><i class="bi bi-keyboard me-1"></i>Ctrl+Enter for at gemme · Esc for at annullere</span>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cancelBeskrivelsEdit()">
|
||||
<i class="bi bi-x me-1"></i>Annuller
|
||||
</button>
|
||||
<button id="beskrivelse-rewrite-btn" type="button" class="btn btn-sm btn-outline-primary" onclick="rewriteCaseDescriptionWithApproval()">
|
||||
<i class="bi bi-magic me-1"></i>Renskriv med AI
|
||||
</button>
|
||||
<button id="beskrivelse-save-btn" class="btn btn-sm btn-primary" onclick="saveBeskrivelsEdit()">
|
||||
<i class="bi bi-check2 me-1"></i>Gem
|
||||
</button>
|
||||
</div>
|
||||
<div class="comment-body" data-comment-raw="{{ comment.indhold|e }}">{{ comment.indhold|replace('\n', '<br>')|safe }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-center text-muted my-3">Ingen kommentarer endnu.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="commentFilterEmptyState" class="comment-empty-filter d-none">Ingen kommentarer matcher de valgte filtre.</div>
|
||||
</div>
|
||||
|
||||
<div id="comment-quick-reply-host"></div>
|
||||
|
||||
<div class="mt-3">
|
||||
<form id="comment-form" onsubmit="submitComment(event)">
|
||||
<div class="comment-composer">
|
||||
<textarea class="form-control" name="indhold" required placeholder="Skriv en kommentar..." rows="2" style="resize: none;"></textarea>
|
||||
<button type="submit" class="btn btn-primary d-flex align-items-center justify-content-center comment-send">
|
||||
<i class="bi bi-send me-1"></i>Send
|
||||
</button>
|
||||
<div class="card left-module-card module-priority-low">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="module-title"><i class="bi bi-chat-left-text module-icon"></i>Kommentarer</h6>
|
||||
<span id="commentsTotalCountBadge" class="badge bg-secondary">{{ comments|length if comments else 0 }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="beskrivelse-comments-wrap">
|
||||
<div class="comment-toolbar">
|
||||
<div class="d-flex align-items-center gap-2 flex-shrink-0">
|
||||
<span id="commentsVisibleCountBadge" class="badge text-bg-light">0/0 vises</span>
|
||||
</div>
|
||||
<input id="commentFilterText" type="text" class="form-control form-control-sm comment-toolbar-search" placeholder="Søg...">
|
||||
<span class="comment-filter-group-label">Kilde</span>
|
||||
<label class="comment-category-check">
|
||||
<input class="form-check-input" type="checkbox" id="commentSourceFilterEmail" checked>
|
||||
<span>Email (<span id="commentSourceCountEmail" class="count">0</span>)</span>
|
||||
</label>
|
||||
<label class="comment-category-check">
|
||||
<input class="form-check-input" type="checkbox" id="commentSourceFilterSms" checked>
|
||||
<span>SMS (<span id="commentSourceCountSms" class="count">0</span>)</span>
|
||||
</label>
|
||||
<label class="comment-category-check">
|
||||
<input class="form-check-input" type="checkbox" id="commentSourceFilterCall" checked>
|
||||
<span>Kald (<span id="commentSourceCountCall" class="count">0</span>)</span>
|
||||
</label>
|
||||
<label class="comment-category-check">
|
||||
<input class="form-check-input" type="checkbox" id="commentSourceFilterModule" checked>
|
||||
<span>Andet/modul (<span id="commentSourceCountModule" class="count">0</span>)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="comment-thread" id="comments-container">
|
||||
{% if comments %}
|
||||
{% for comment in comments %}
|
||||
{% if comment.er_system_besked or comment.forfatter == 'System' %}
|
||||
<div class="comment-item comment-system" data-category="system" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% elif comment.er_intern %}
|
||||
<div class="comment-item comment-internal" data-category="internal" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% else %}
|
||||
<div class="comment-item comment-external" data-category="external" data-created-at="{{ comment.created_at.timestamp() if comment.created_at else 0 }}">
|
||||
{% endif %}
|
||||
<div class="comment-meta">
|
||||
<span class="comment-avatar">{{ (comment.forfatter or 'Bruger')[:2]|upper }}</span>
|
||||
<b>{{ comment.forfatter }}</b>
|
||||
{% if comment.er_system_besked or comment.forfatter == 'System' %}
|
||||
<span class="comment-category-badge category-system">System</span>
|
||||
{% elif comment.er_intern %}
|
||||
<span class="comment-category-badge category-internal">Intern</span>
|
||||
{% else %}
|
||||
<span class="comment-category-badge category-external">Ekstern</span>
|
||||
{% endif %}
|
||||
<span class="comment-time">{{ comment.created_at.strftime('%d/%m-%Y %H:%M') }}</span>
|
||||
</div>
|
||||
<div class="comment-body" data-comment-raw="{{ comment.indhold|e }}">{{ comment.indhold|replace('\n', '<br>')|safe }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-center text-muted my-3">Ingen kommentarer endnu.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="commentFilterEmptyState" class="comment-empty-filter d-none">Ingen kommentarer matcher de valgte filtre.</div>
|
||||
|
||||
<div id="comment-quick-reply-host"></div>
|
||||
|
||||
<div class="mt-3">
|
||||
<form id="comment-form" onsubmit="submitComment(event)">
|
||||
<div class="comment-composer">
|
||||
<textarea class="form-control" name="indhold" required placeholder="Skriv en kommentar..." rows="2" style="resize: none;"></textarea>
|
||||
<button type="submit" class="btn btn-primary d-flex align-items-center justify-content-center comment-send">
|
||||
<i class="bi bi-send me-1"></i>Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ROW 1B: Pipeline -->
|
||||
@ -4246,6 +4382,7 @@
|
||||
let selectedRelationCaseId = null;
|
||||
let customerSearchMode = 'link';
|
||||
const caseTypeKey = {{ ((case.template_key or case.type or 'ticket')|lower)|tojson }};
|
||||
const initialCaseTagsSnapshot = {{ (tags or [])|tojson }};
|
||||
|
||||
async function markCaseAsRecentlyOpened() {
|
||||
try {
|
||||
@ -4324,22 +4461,11 @@
|
||||
function forceCaseTabActivation(tabId) {
|
||||
if (!tabId) return;
|
||||
|
||||
const tabContent = document.getElementById('caseTabsContent');
|
||||
const targetPane = document.getElementById(tabId);
|
||||
if (!tabContent || !targetPane) return;
|
||||
|
||||
tabContent.querySelectorAll(':scope > .tab-pane').forEach((pane) => {
|
||||
pane.classList.remove('show', 'active');
|
||||
pane.style.display = 'none';
|
||||
});
|
||||
|
||||
targetPane.classList.add('show', 'active');
|
||||
targetPane.style.display = 'block';
|
||||
|
||||
const tabButtons = document.querySelectorAll('#caseTabs [data-bs-target]');
|
||||
tabButtons.forEach((btn) => {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-bs-target') === `#${tabId}`);
|
||||
});
|
||||
const trigger = document.querySelector(`#caseTabs [data-bs-target="#${tabId}"]`);
|
||||
if (!trigger) return;
|
||||
window.syncCaseTabPaneVisibility(tabId);
|
||||
trigger.dispatchEvent(new Event('shown.bs.tab', { bubbles: true }));
|
||||
if (typeof window.loadCaseTabData === 'function') window.loadCaseTabData(tabId);
|
||||
}
|
||||
|
||||
window.moduleDisplayNames = {
|
||||
@ -4463,11 +4589,8 @@
|
||||
|
||||
const caseTabs = document.getElementById('caseTabs');
|
||||
if (caseTabs) {
|
||||
caseTabs.addEventListener('shown.bs.tab', async (event) => {
|
||||
const targetSelector = event?.target?.getAttribute('data-bs-target') || '';
|
||||
const tabId = targetSelector.startsWith('#') ? targetSelector.slice(1) : targetSelector;
|
||||
|
||||
forceCaseTabActivation(tabId);
|
||||
const loadCaseTabData = async (tabId) => {
|
||||
if (!tabId) return;
|
||||
|
||||
try {
|
||||
if (tabId === 'sales' && typeof loadVarekobSalg === 'function') {
|
||||
@ -4483,20 +4606,27 @@
|
||||
} catch (tabLoadError) {
|
||||
console.error('Tab data reload failed:', tabLoadError);
|
||||
}
|
||||
});
|
||||
};
|
||||
window.loadCaseTabData = loadCaseTabData;
|
||||
|
||||
caseTabs.addEventListener('click', (event) => {
|
||||
const btn = event.target.closest('[data-bs-target]');
|
||||
if (!btn) return;
|
||||
const targetSelector = btn.getAttribute('data-bs-target') || '';
|
||||
caseTabs.addEventListener('click', async (event) => {
|
||||
const trigger = event.target.closest('[data-bs-target]');
|
||||
if (!trigger || !caseTabs.contains(trigger)) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const targetSelector = trigger.getAttribute('data-bs-target') || '';
|
||||
const tabId = targetSelector.startsWith('#') ? targetSelector.slice(1) : targetSelector;
|
||||
if (tabId) {
|
||||
setTimeout(() => forceCaseTabActivation(tabId), 0);
|
||||
}
|
||||
});
|
||||
if (!tabId) return;
|
||||
|
||||
window.syncCaseTabPaneVisibility(tabId);
|
||||
trigger.dispatchEvent(new Event('shown.bs.tab', { bubbles: true }));
|
||||
await loadCaseTabData(tabId);
|
||||
}, true);
|
||||
}
|
||||
|
||||
forceCaseTabActivation('details');
|
||||
window.syncCaseTabPaneVisibility('details');
|
||||
|
||||
// Focus on title when create modal opens
|
||||
const createModalEl = document.getElementById('createRelatedCaseModal');
|
||||
@ -5601,6 +5731,12 @@
|
||||
tags = await loadLegacyTags();
|
||||
}
|
||||
|
||||
// Final fallback: use server-rendered tags from page context when API calls return empty.
|
||||
if ((!Array.isArray(tags) || tags.length === 0) && Array.isArray(initialCaseTagsSnapshot) && initialCaseTagsSnapshot.length > 0) {
|
||||
tags = normalizeLegacyTags(initialCaseTagsSnapshot);
|
||||
usingLegacyCaseTags = true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
moduleContainer.innerHTML = '<div class="p-3 text-center text-muted small">Ingen tags paaa sagen endnu</div>';
|
||||
setModuleContentState('tags', false);
|
||||
@ -5610,9 +5746,9 @@
|
||||
moduleContainer.innerHTML = tags.map((tag) => `
|
||||
<span class="badge me-1 mb-1" style="background-color: ${tag.color};">
|
||||
${tag.icon ? `<i class="bi ${tag.icon}"></i> ` : ''}${escapeHtml(tag.name)}
|
||||
<button type="button" class="btn-close btn-close-white btn-sm ms-1"
|
||||
${tag.id ? `<button type="button" class="btn-close btn-close-white btn-sm ms-1"
|
||||
onclick="removeCaseTagAndSync(${tag.id})"
|
||||
style="font-size: 0.6rem; vertical-align: middle;"></button>
|
||||
style="font-size: 0.6rem; vertical-align: middle;"></button>` : ''}
|
||||
</span>
|
||||
`).join('');
|
||||
|
||||
@ -6331,8 +6467,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div></div><!-- slut inner cols -->
|
||||
</div> <!-- /#inner-center-col -->
|
||||
</div> <!-- /.row.g-4 (inner) -->
|
||||
</div> <!-- /#case-left-column -->
|
||||
<div class="col-xl-4 col-lg-4" id="case-right-column">
|
||||
<div class="right-modules-grid">
|
||||
<div class="card h-100 d-flex flex-column right-module-card module-priority-normal" data-module="locations" data-has-content="unknown">
|
||||
@ -6352,8 +6489,8 @@
|
||||
<div class="card h-100 d-flex flex-column right-module-card module-priority-low" data-module="tags" data-has-content="unknown">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="module-title"><i class="bi bi-tags-fill module-icon"></i>TAGS</h6>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
onclick="window.showTagPicker('case', {{ case.id }}, () => syncCaseTagsUi())"
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="event.stopPropagation(); window.showTagPicker('case', {{ case.id }}, () => syncCaseTagsUi()); return false;"
|
||||
title="Tilføj tag">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
@ -6663,8 +6800,44 @@
|
||||
</div>
|
||||
</div> <!-- End Details Tab -->
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
function normalizeDetailsColumns() {
|
||||
const detailsPane = document.getElementById('details');
|
||||
if (!detailsPane) return;
|
||||
|
||||
let detailsRow = null;
|
||||
try {
|
||||
detailsRow = detailsPane.querySelector(':scope > .row.g-4');
|
||||
} catch (e) {
|
||||
detailsRow = detailsPane.querySelector('.row.g-4');
|
||||
}
|
||||
const leftCol = detailsPane.querySelector('#case-left-column');
|
||||
const rightCol = detailsPane.querySelector('#case-right-column');
|
||||
|
||||
if (!detailsRow || !leftCol || !rightCol) return;
|
||||
|
||||
// Defensive: ensure right column is a direct sibling inside the outer details row.
|
||||
if (rightCol.parentElement !== detailsRow) {
|
||||
detailsRow.appendChild(rightCol);
|
||||
}
|
||||
|
||||
if (!leftCol.classList.contains('col-xl-8')) leftCol.classList.add('col-xl-8');
|
||||
if (!leftCol.classList.contains('col-lg-8')) leftCol.classList.add('col-lg-8');
|
||||
if (!rightCol.classList.contains('col-xl-4')) rightCol.classList.add('col-xl-4');
|
||||
if (!rightCol.classList.contains('col-lg-4')) rightCol.classList.add('col-lg-4');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', normalizeDetailsColumns);
|
||||
} else {
|
||||
normalizeDetailsColumns();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- E-mail Tab -->
|
||||
<div class="tab-pane fade" id="emails" role="tabpanel" tabindex="0" data-module="emails" data-has-content="unknown" style="display:none;">
|
||||
<div class="tab-pane fade" id="emails" role="tabpanel" tabindex="0" data-module="emails" data-has-content="unknown">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0 text-primary"><i class="bi bi-envelope me-2"></i>E-mail på sagen</h6>
|
||||
@ -6825,7 +6998,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Solution Tab -->
|
||||
<div class="tab-pane fade" id="solution" role="tabpanel" tabindex="0" data-module="solution" data-has-content="{{ 'true' if solution or is_nextcloud else 'false' }}" style="display:none;">
|
||||
<div class="tab-pane fade" id="solution" role="tabpanel" tabindex="0" data-module="solution" data-has-content="{{ 'true' if solution or is_nextcloud else 'false' }}">
|
||||
<!-- Nextcloud Integration Box -->
|
||||
{% if is_nextcloud %}
|
||||
<div class="card mb-3">
|
||||
@ -6921,7 +7094,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Varekøb & Salg Tab -->
|
||||
<div class="tab-pane fade" id="sales" role="tabpanel" tabindex="0" data-module="sales" data-has-content="unknown" style="display:none;">
|
||||
<div class="tab-pane fade" id="sales" role="tabpanel" tabindex="0" data-module="sales" data-has-content="unknown">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-3">
|
||||
@ -7065,7 +7238,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Leverandør Tab -->
|
||||
<div class="tab-pane fade" id="supplier" role="tabpanel" tabindex="0" data-module="supplier" data-has-content="unknown" style="display:none;">
|
||||
<div class="tab-pane fade" id="supplier" role="tabpanel" tabindex="0" data-module="supplier" data-has-content="unknown">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-xl-8 col-12">
|
||||
<div class="card mb-3">
|
||||
@ -7195,7 +7368,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Tidsforbrug Tab -->
|
||||
<div class="tab-pane fade" id="timetracking" role="tabpanel" tabindex="0" data-has-content="unknown" style="display:none;">
|
||||
<div class="tab-pane fade" id="timetracking" role="tabpanel" tabindex="0" data-has-content="unknown">
|
||||
<div id="timeActiveBanner" class="alert alert-warning d-none d-flex justify-content-between align-items-center" role="alert">
|
||||
<div>
|
||||
<strong>Aktiv timer:</strong>
|
||||
@ -7381,7 +7554,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Subscription Tab -->
|
||||
<div class="tab-pane fade" id="subscription" role="tabpanel" tabindex="0" data-module="subscription" data-has-content="unknown" style="display:none;">
|
||||
<div class="tab-pane fade" id="subscription" role="tabpanel" tabindex="0" data-module="subscription" data-has-content="unknown">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-3">
|
||||
@ -7591,7 +7764,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Reminders Tab -->
|
||||
<div class="tab-pane fade" id="reminders" role="tabpanel" tabindex="0" data-module="reminders" data-has-content="unknown" style="display:none;">
|
||||
<div class="tab-pane fade" id="reminders" role="tabpanel" tabindex="0" data-module="reminders" data-has-content="unknown">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-3">
|
||||
@ -7646,7 +7819,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="history" role="tabpanel" tabindex="0" data-module="history" data-has-content="true" style="display:none;">
|
||||
<div class="tab-pane fade" id="history" role="tabpanel" tabindex="0" data-module="history" data-has-content="true">
|
||||
<div class="history-timeline-shell">
|
||||
<div class="history-timeline-toolbar">
|
||||
<label class="form-check-label small d-inline-flex align-items-center gap-2 me-2">
|
||||
@ -12653,10 +12826,23 @@
|
||||
}
|
||||
|
||||
function updateRightColumnVisibility() {
|
||||
const detailsPane = document.getElementById('details');
|
||||
const rightColumn = document.getElementById('case-right-column');
|
||||
const leftColumn = document.getElementById('case-left-column');
|
||||
if (!rightColumn || !leftColumn) return;
|
||||
|
||||
if (detailsPane) {
|
||||
let detailsRow = null;
|
||||
try {
|
||||
detailsRow = detailsPane.querySelector(':scope > .row.g-4');
|
||||
} catch (e) {
|
||||
detailsRow = detailsPane.querySelector('.row.g-4');
|
||||
}
|
||||
if (detailsRow && rightColumn.parentElement !== detailsRow) {
|
||||
detailsRow.appendChild(rightColumn);
|
||||
}
|
||||
}
|
||||
|
||||
const visibleRightModules = rightColumn.querySelectorAll('.right-module-card:not(.d-none)');
|
||||
if (visibleRightModules.length === 0) {
|
||||
rightColumn.classList.add('d-none');
|
||||
@ -13572,8 +13758,7 @@
|
||||
function openCaseEmailTab() {
|
||||
const trigger = document.getElementById('emails-tab');
|
||||
if (!trigger) return;
|
||||
const instance = bootstrap.Tab.getOrCreateInstance(trigger);
|
||||
instance.show();
|
||||
forceCaseTabActivation('emails');
|
||||
}
|
||||
|
||||
window.quickReplyToEmailFromComment = async function(emailId) {
|
||||
@ -14850,8 +15035,9 @@
|
||||
|| document.querySelector(`[data-module-tab="${tabParam}"]`);
|
||||
if (tabBtn) {
|
||||
setTimeout(() => {
|
||||
bootstrap.Tab.getOrCreateInstance(tabBtn).show();
|
||||
forceCaseTabActivation(tabParam);
|
||||
const targetSelector = tabBtn.getAttribute('data-bs-target') || '';
|
||||
const targetTabId = targetSelector.startsWith('#') ? targetSelector.slice(1) : tabParam;
|
||||
forceCaseTabActivation(targetTabId);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,16 +273,47 @@ class TagPicker {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
if (error.detail.includes('already exists')) {
|
||||
let errorDetail = '';
|
||||
try {
|
||||
const error = await response.json();
|
||||
errorDetail = String(error?.detail || '');
|
||||
} catch (parseError) {
|
||||
errorDetail = '';
|
||||
}
|
||||
|
||||
if (response.status === 409 || errorDetail.toLowerCase().includes('already exists')) {
|
||||
alert('Dette tag er allerede tilføjet');
|
||||
return;
|
||||
}
|
||||
throw new Error(error.detail);
|
||||
}
|
||||
|
||||
// Show success feedback
|
||||
this.showSuccess(tag.name);
|
||||
// Backward compatibility: some case pages still persist tags via /sag/{id}/tags.
|
||||
if (String(this.contextType).toLowerCase() === 'case') {
|
||||
const legacyResponse = await fetch(`/api/v1/sag/${this.contextId}/tags`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ tag_navn: tag.name })
|
||||
});
|
||||
|
||||
if (legacyResponse.ok) {
|
||||
this.showSuccess(tag.name);
|
||||
} else {
|
||||
let legacyErrorDetail = '';
|
||||
try {
|
||||
const legacyError = await legacyResponse.json();
|
||||
legacyErrorDetail = String(legacyError?.detail || '');
|
||||
} catch (parseError) {
|
||||
legacyErrorDetail = '';
|
||||
}
|
||||
throw new Error(legacyErrorDetail || errorDetail || 'Kunne ikke tilføje tag');
|
||||
}
|
||||
} else {
|
||||
throw new Error(errorDetail || 'Kunne ikke tilføje tag');
|
||||
}
|
||||
} else {
|
||||
// Show success feedback
|
||||
this.showSuccess(tag.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding tag:', error);
|
||||
alert('Fejl ved tilføjelse af tag: ' + error.message);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user