feat: Implement legacy case details redirection and enhance contact info UI

This commit is contained in:
Christian 2026-04-30 22:20:44 +02:00
parent 6133823ade
commit ec2c8fe784
3 changed files with 267 additions and 46 deletions

View File

@ -3,7 +3,7 @@ import json
from datetime import date, datetime from datetime import date, datetime
from typing import Optional from typing import Optional
from fastapi import APIRouter, HTTPException, Query, Request from fastapi import APIRouter, HTTPException, Query, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pathlib import Path from pathlib import Path
from app.core.database import execute_query from app.core.database import execute_query
@ -455,6 +455,9 @@ async def sag_varekob_salg(request: Request):
@router.get("/sag/{sag_id}", response_class=HTMLResponse) @router.get("/sag/{sag_id}", response_class=HTMLResponse)
async def sag_detaljer(request: Request, sag_id: int): async def sag_detaljer(request: Request, sag_id: int):
"""Redirect legacy case details URL to v3."""
return RedirectResponse(url=f"/sag/{sag_id}/v3", status_code=307)
"""Display case details.""" """Display case details."""
try: try:
# Fetch main case # Fetch main case

View File

@ -2097,6 +2097,23 @@
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.10); box-shadow: 0 4px 12px rgba(15, 76, 117, 0.10);
} }
#caseTabsContent .tab-pane:not(#details) .card,
#caseTabsContent .tab-pane:not(#details) .history-timeline-shell {
--module-accent: var(--accent);
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;
background: linear-gradient(165deg, color-mix(in srgb, var(--module-accent, var(--accent)) 6%, var(--bg-card)) 0%, var(--bg-card) 100%);
overflow: hidden;
}
#caseTabsContent .tab-pane:not(#details) .card .card-header,
#caseTabsContent .tab-pane:not(#details) .history-timeline-toolbar {
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));
}
.left-module-card, .left-module-card,
.right-module-card { .right-module-card {
border: 2px solid rgba(15, 76, 117, 0.28) !important; border: 2px solid rgba(15, 76, 117, 0.28) !important;
@ -2110,6 +2127,20 @@
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
} }
[data-bs-theme="dark"] #caseTabsContent .tab-pane:not(#details) .card,
[data-bs-theme="dark"] #caseTabsContent .tab-pane:not(#details) .history-timeline-shell {
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;
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"] #caseTabsContent .tab-pane:not(#details) .card .card-header,
[data-bs-theme="dark"] #caseTabsContent .tab-pane:not(#details) .history-timeline-toolbar {
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"] .left-module-card, [data-bs-theme="dark"] .left-module-card,
[data-bs-theme="dark"] .right-module-card { [data-bs-theme="dark"] .right-module-card {
border: 2px solid rgba(117, 167, 204, 0.45) !important; border: 2px solid rgba(117, 167, 204, 0.45) !important;
@ -9336,9 +9367,11 @@
if (!res.ok) throw new Error('Kunne ikke hente tidsforbrug'); if (!res.ok) throw new Error('Kunne ikke hente tidsforbrug');
const entries = await res.json(); const entries = await res.json();
timeV1EntriesById = Object.fromEntries((entries || []).map((entry) => [Number(entry.id), entry])); timeV1EntriesById = Object.fromEntries((entries || []).map((entry) => [Number(entry.id), entry]));
window.initialCaseTabCounts = Object.assign({}, window.initialCaseTabCounts || {}, { timetracking: (entries || []).length });
renderTimeV1Timeline(entries || []); renderTimeV1Timeline(entries || []);
renderTimeV1Summary(entries || []); renderTimeV1Summary(entries || []);
setModuleContentState('timetracking', (entries || []).length > 0); setModuleContentState('timetracking', (entries || []).length > 0);
if (typeof updateCaseTabCountBadges === 'function') updateCaseTabCountBadges();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
const timeline = document.getElementById('timeTimelineColumns'); const timeline = document.getElementById('timeTimelineColumns');
@ -10700,6 +10733,9 @@
let caseAddPanelInitialized = false; let caseAddPanelInitialized = false;
let caseAddActiveAction = null; let caseAddActiveAction = null;
let caseAddOriginalShowRelModal = null; let caseAddOriginalShowRelModal = null;
window.initialCaseTabCounts = Object.assign({}, window.initialCaseTabCounts || {}, {
timetracking: {{ (time_entries or [])|length }}
});
const CASE_ADD_ACTIONS = [ const CASE_ADD_ACTIONS = [
{ action: 'time', label: 'Tidregistrering', icon: 'bi-clock', moduleKey: 'time', relFn: 'openRelTimeModal' }, { action: 'time', label: 'Tidregistrering', icon: 'bi-clock', moduleKey: 'time', relFn: 'openRelTimeModal' },
{ action: 'note', label: 'Kommentar', icon: 'bi-chat-left-text', moduleKey: 'solution', relFn: 'openRelNoteModal' }, { action: 'note', label: 'Kommentar', icon: 'bi-chat-left-text', moduleKey: 'solution', relFn: 'openRelNoteModal' },
@ -12239,9 +12275,10 @@
const timeEntriesStore = (typeof timeV1EntriesById !== 'undefined' && timeV1EntriesById && typeof timeV1EntriesById === 'object') const timeEntriesStore = (typeof timeV1EntriesById !== 'undefined' && timeV1EntriesById && typeof timeV1EntriesById === 'object')
? timeV1EntriesById ? timeV1EntriesById
: null; : null;
const timeCount = timeEntriesStore const initialTimeCount = Number(window.initialCaseTabCounts?.timetracking || 0);
? Object.keys(timeEntriesStore).length const loadedTimeCount = timeEntriesStore ? Object.keys(timeEntriesStore).length : 0;
: document.querySelectorAll('#timetracking tbody tr').length; const timeDomCount = document.querySelectorAll('#timetracking tbody tr').length;
const timeCount = Math.max(initialTimeCount, loadedTimeCount, timeDomCount);
_setCaseTabCountBadge('timetrackingTabCountBadge', timeCount); _setCaseTabCountBadge('timetrackingTabCountBadge', timeCount);
const subscriptionCount = _countRows('#subscriptionItemsBody'); const subscriptionCount = _countRows('#subscriptionItemsBody');

View File

@ -2021,6 +2021,108 @@
color: #6c757d; color: #6c757d;
} }
.contact-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 0.25rem;
width: 100%;
}
.contact-actions .btn {
width: 1.65rem;
height: 1.65rem;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.contact-info-hero {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border: 1px solid rgba(15, 76, 117, 0.18);
border-radius: 0.75rem;
background: linear-gradient(145deg, rgba(15, 76, 117, 0.08), rgba(15, 76, 117, 0.02));
margin-bottom: 0.9rem;
}
.contact-info-avatar {
width: 2.4rem;
height: 2.4rem;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-weight: 700;
color: #fff;
background: var(--accent);
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.24);
flex-shrink: 0;
}
.contact-info-title {
font-weight: 700;
margin: 0;
line-height: 1.2;
}
.contact-info-subtitle {
color: #6c757d;
font-size: 0.82rem;
margin-top: 0.1rem;
}
.contact-quick-actions {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5rem;
margin-bottom: 0.9rem;
}
.contact-quick-actions .btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
font-size: 0.8rem;
padding: 0.45rem 0.4rem;
}
.contact-info-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.45rem;
}
.contact-info-row {
display: flex;
justify-content: space-between;
gap: 0.8rem;
padding: 0.42rem 0.1rem;
border-bottom: 1px dashed rgba(0, 0, 0, 0.08);
}
.contact-info-row:last-child {
border-bottom: none;
}
.contact-info-label {
color: #6c757d;
font-size: 0.78rem;
min-width: 90px;
}
.contact-info-value {
text-align: right;
font-size: 0.86rem;
word-break: break-word;
}
.hardware-list-header, .hardware-list-header,
.hardware-row, .hardware-row,
.location-list-header, .location-list-header,
@ -2354,6 +2456,23 @@
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.10); box-shadow: 0 4px 12px rgba(15, 76, 117, 0.10);
} }
#caseTabsContent .tab-pane:not(#details) .card,
#caseTabsContent .tab-pane:not(#details) .history-timeline-shell {
--module-accent: var(--accent);
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;
background: linear-gradient(165deg, color-mix(in srgb, var(--module-accent, var(--accent)) 6%, var(--bg-card)) 0%, var(--bg-card) 100%);
overflow: hidden;
}
#caseTabsContent .tab-pane:not(#details) .card .card-header,
#caseTabsContent .tab-pane:not(#details) .history-timeline-toolbar {
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));
}
.left-module-card, .left-module-card,
.right-module-card { .right-module-card {
border: 2px solid rgba(15, 76, 117, 0.28) !important; border: 2px solid rgba(15, 76, 117, 0.28) !important;
@ -2367,6 +2486,20 @@
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
} }
[data-bs-theme="dark"] #caseTabsContent .tab-pane:not(#details) .card,
[data-bs-theme="dark"] #caseTabsContent .tab-pane:not(#details) .history-timeline-shell {
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;
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"] #caseTabsContent .tab-pane:not(#details) .card .card-header,
[data-bs-theme="dark"] #caseTabsContent .tab-pane:not(#details) .history-timeline-toolbar {
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"] .left-module-card, [data-bs-theme="dark"] .left-module-card,
[data-bs-theme="dark"] .right-module-card { [data-bs-theme="dark"] .right-module-card {
border: 2px solid rgba(117, 167, 204, 0.45) !important; border: 2px solid rgba(117, 167, 204, 0.45) !important;
@ -3288,7 +3421,7 @@
<div class="d-flex justify-content-between align-items-center mb-1"> <div class="d-flex justify-content-between align-items-center mb-1">
<label class="mb-0 text-secondary" style="font-size:0.8rem;">Status</label> <label class="mb-0 text-secondary" style="font-size:0.8rem;">Status</label>
<select id="topbarStatusSelect" class="form-select form-select-sm border-start border-warning border-3 bg-light" onchange="saveCaseStatusFromTopbar()" style="width: 62%;"> <select id="topbarStatusSelect" class="form-select form-select-sm bg-light" onchange="saveCaseStatusFromTopbar()" style="width: 62%;">
{% 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 %}
@ -3297,7 +3430,7 @@
<div class="d-flex justify-content-between align-items-center mb-1"> <div class="d-flex justify-content-between align-items-center mb-1">
<label class="mb-0 text-secondary" style="font-size:0.8rem;">Type</label> <label class="mb-0 text-secondary" style="font-size:0.8rem;">Type</label>
<select id="topbarTypeSelect" class="form-select form-select-sm border-start border-success border-3 bg-light" onchange="saveCaseTypeFromTopbar()" style="width: 62%;"> <select id="topbarTypeSelect" class="form-select form-select-sm bg-light" onchange="saveCaseTypeFromTopbar()" style="width: 62%;">
{% set topbar_type = (case.template_key or case.type or 'ticket')|lower %} {% 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="ticket" {% if topbar_type == 'ticket' %}selected{% endif %}>Ticket</option>
<option value="pipeline" {% if topbar_type == 'pipeline' %}selected{% endif %}>Pipeline</option> <option value="pipeline" {% if topbar_type == 'pipeline' %}selected{% endif %}>Pipeline</option>
@ -3310,7 +3443,7 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<label class="mb-0 text-secondary" style="font-size:0.8rem;">Prioritet</label> <label class="mb-0 text-secondary" style="font-size:0.8rem;">Prioritet</label>
<select id="topbarPrioritySelect" class="form-select form-select-sm border-start border-danger border-3 bg-light" onchange="saveCasePriorityFromTopbar()" style="width: 62%;"> <select id="topbarPrioritySelect" class="form-select form-select-sm bg-light" onchange="saveCasePriorityFromTopbar()" style="width: 62%;">
{% set topbar_priority = (case.priority or 'normal')|lower %} {% set topbar_priority = (case.priority or 'normal')|lower %}
<option value="low" {% if topbar_priority == 'low' %}selected{% endif %}>Lav</option> <option value="low" {% if topbar_priority == 'low' %}selected{% endif %}>Lav</option>
<option value="normal" {% if topbar_priority == 'normal' %}selected{% endif %}>Normal</option> <option value="normal" {% if topbar_priority == 'normal' %}selected{% endif %}>Normal</option>
@ -4087,45 +4220,56 @@
<!-- Contact Info Modal --> <!-- Contact Info Modal -->
<div class="modal fade" id="contactInfoModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="contactInfoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="contactInfoName">Kontakt</h5> <h5 class="modal-title" id="contactInfoName">Kontakt</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-2"> <div class="contact-info-hero">
<div class="text-muted small">Titel</div> <div class="contact-info-avatar" id="contactInfoAvatar">KT</div>
<div id="contactInfoTitle">-</div> <div class="flex-grow-1">
<p class="contact-info-title" id="contactInfoNameInline">Kontakt</p>
<div class="contact-info-subtitle" id="contactInfoSubtitle">Kontaktdetaljer</div>
</div>
<div id="contactInfoPrimary" class="badge bg-primary d-none">Hovedkontakt</div>
</div> </div>
<div class="mb-2">
<div class="text-muted small">Kunde</div> <div class="contact-quick-actions">
<div id="contactInfoCompany">-</div> <button type="button" class="btn btn-sm btn-outline-success" onclick="callCurrentContactFromInfo()"><i class="bi bi-telephone"></i>Ring</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="smsCurrentContactFromInfo()"><i class="bi bi-chat-left-text"></i>SMS</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="emailCurrentContactFromInfo()"><i class="bi bi-envelope"></i>Email</button>
</div> </div>
<div class="mb-2">
<div class="text-muted small">E-mail</div> <div class="contact-info-grid">
<div id="contactInfoEmail">-</div> <div class="contact-info-row">
<div class="contact-info-label">Titel</div>
<div class="contact-info-value" id="contactInfoTitle">-</div>
</div>
<div class="contact-info-row">
<div class="contact-info-label">Kunde</div>
<div class="contact-info-value" id="contactInfoCompany">-</div>
</div>
<div class="contact-info-row">
<div class="contact-info-label">E-mail</div>
<div class="contact-info-value" id="contactInfoEmail">-</div>
</div>
<div class="contact-info-row">
<div class="contact-info-label">Telefon</div>
<div class="contact-info-value" id="contactInfoPhone">-</div>
</div>
<div class="contact-info-row">
<div class="contact-info-label">Mobil</div>
<div class="contact-info-value" id="contactInfoMobile">-</div>
</div>
<div class="contact-info-row">
<div class="contact-info-label">Rolle</div>
<div class="contact-info-value" id="contactInfoRole">-</div>
</div>
</div> </div>
<div class="mb-2">
<div class="text-muted small">Telefon</div>
<div id="contactInfoPhone">-</div>
</div>
<div class="mb-2">
<div class="text-muted small">Mobil</div>
<div id="contactInfoMobile">-</div>
</div>
<div class="mb-2">
<div class="text-muted small">Rolle</div>
<div id="contactInfoRole">-</div>
</div>
<div id="contactInfoPrimary" class="badge bg-primary d-none">Hovedkontakt</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div class="me-auto d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-sm btn-outline-success" onclick="callCurrentContactFromInfo()"><i class="bi bi-telephone me-1"></i>Ring</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="smsCurrentContactFromInfo()"><i class="bi bi-chat-left-text me-1"></i>SMS</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="emailCurrentContactFromInfo()"><i class="bi bi-envelope me-1"></i>Email</button>
</div>
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Luk</button> <button type="button" class="btn btn-light" data-bs-dismiss="modal">Luk</button>
<button type="button" class="btn btn-primary" onclick="openContactRoleFromInfo()">Rediger rolle</button> <button type="button" class="btn btn-primary" onclick="openContactRoleFromInfo()">Rediger rolle</button>
</div> </div>
@ -4790,12 +4934,25 @@
}; };
document.getElementById('contactInfoName').textContent = currentContactInfo.name; document.getElementById('contactInfoName').textContent = currentContactInfo.name;
document.getElementById('contactInfoNameInline').textContent = currentContactInfo.name;
document.getElementById('contactInfoTitle').textContent = currentContactInfo.title || '-'; document.getElementById('contactInfoTitle').textContent = currentContactInfo.title || '-';
document.getElementById('contactInfoCompany').textContent = currentContactInfo.company || '-'; document.getElementById('contactInfoCompany').textContent = currentContactInfo.company || '-';
document.getElementById('contactInfoEmail').textContent = currentContactInfo.email || '-'; document.getElementById('contactInfoEmail').textContent = currentContactInfo.email || '-';
document.getElementById('contactInfoPhone').innerHTML = renderCasePhone(currentContactInfo.phone); document.getElementById('contactInfoPhone').innerHTML = renderCasePhone(currentContactInfo.phone);
document.getElementById('contactInfoMobile').innerHTML = renderCaseMobile(currentContactInfo.mobile, currentContactInfo.name); document.getElementById('contactInfoMobile').innerHTML = renderCaseMobile(currentContactInfo.mobile, currentContactInfo.name);
document.getElementById('contactInfoRole').textContent = currentContactInfo.role || '-'; document.getElementById('contactInfoRole').textContent = currentContactInfo.role || '-';
document.getElementById('contactInfoSubtitle').textContent = [currentContactInfo.title, currentContactInfo.company]
.filter((v) => String(v || '').trim() && v !== '-')
.join(' • ') || 'Kontaktdetaljer';
const initials = String(currentContactInfo.name || 'KT')
.split(' ')
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0])
.join('')
.toUpperCase() || 'KT';
document.getElementById('contactInfoAvatar').textContent = initials;
const primaryBadge = document.getElementById('contactInfoPrimary'); const primaryBadge = document.getElementById('contactInfoPrimary');
if (currentContactInfo.isPrimary) { if (currentContactInfo.isPrimary) {
@ -6554,7 +6711,7 @@
<span>Navn</span> <span>Navn</span>
<span>Titel</span> <span>Titel</span>
<span>Kunde</span> <span>Kunde</span>
<span>Slet</span> <span class="text-end">Handlinger</span>
</div> </div>
{% for contact in contacts %} {% for contact in contacts %}
<div <div
@ -6575,13 +6732,25 @@
<div class="contact-name">{{ contact.contact_name }}</div> <div class="contact-name">{{ contact.contact_name }}</div>
<small>{{ contact.title or '-' }}</small> <small>{{ contact.title or '-' }}</small>
<small>{{ contact.customer_name or '-' }}</small> <small>{{ contact.customer_name or '-' }}</small>
<button <div class="contact-actions">
class="btn btn-sm btn-delete" <button
onclick="event.stopPropagation(); removeContact({{ case.id }}, {{ contact.contact_id }})" type="button"
title="Slet" class="btn btn-sm btn-outline-primary"
> onclick="event.stopPropagation(); showContactInfoModal(this.closest('.contact-row'))"
title="Åbn kontakt"
</button> aria-label="Åbn kontakt"
>
<i class="bi bi-person-lines-fill"></i>
</button>
<button
class="btn btn-sm btn-delete"
onclick="event.stopPropagation(); removeContact({{ case.id }}, {{ contact.contact_id }})"
title="Slet"
aria-label="Slet kontakt"
>
<i class="bi bi-x-lg"></i>
</button>
</div>
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
@ -8807,6 +8976,7 @@
const payload = await res.json(); const payload = await res.json();
caseHistoryTimelineCache = Array.isArray(payload?.events) ? payload.events : []; caseHistoryTimelineCache = Array.isArray(payload?.events) ? payload.events : [];
caseHistoryTimelineLoadedKey = loadKey; caseHistoryTimelineLoadedKey = loadKey;
window.initialCaseTabCounts = Object.assign({}, window.initialCaseTabCounts || {}, { history: caseHistoryTimelineCache.length });
if (typeof _setCaseTabCountBadge === 'function') { if (typeof _setCaseTabCountBadge === 'function') {
_setCaseTabCountBadge('historyTabCountBadge', caseHistoryTimelineCache.length); _setCaseTabCountBadge('historyTabCountBadge', caseHistoryTimelineCache.length);
@ -8825,6 +8995,8 @@
historyTab.addEventListener('shown.bs.tab', () => loadCaseHistoryTimeline(false)); historyTab.addEventListener('shown.bs.tab', () => loadCaseHistoryTimeline(false));
} }
loadCaseHistoryTimeline(false);
const includeToggle = document.getElementById('historyIncludeSubcasesToggle'); const includeToggle = document.getElementById('historyIncludeSubcasesToggle');
if (includeToggle) { if (includeToggle) {
includeToggle.addEventListener('change', () => loadCaseHistoryTimeline(true)); includeToggle.addEventListener('change', () => loadCaseHistoryTimeline(true));
@ -10023,9 +10195,11 @@
if (!res.ok) throw new Error('Kunne ikke hente tidsforbrug'); if (!res.ok) throw new Error('Kunne ikke hente tidsforbrug');
const entries = await res.json(); const entries = await res.json();
timeV1EntriesById = Object.fromEntries((entries || []).map((entry) => [Number(entry.id), entry])); timeV1EntriesById = Object.fromEntries((entries || []).map((entry) => [Number(entry.id), entry]));
window.initialCaseTabCounts = Object.assign({}, window.initialCaseTabCounts || {}, { timetracking: (entries || []).length });
renderTimeV1Timeline(entries || []); renderTimeV1Timeline(entries || []);
renderTimeV1Summary(entries || []); renderTimeV1Summary(entries || []);
setModuleContentState('timetracking', (entries || []).length > 0); setModuleContentState('timetracking', (entries || []).length > 0);
if (typeof updateCaseTabCountBadges === 'function') updateCaseTabCountBadges();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
const timeline = document.getElementById('timeTimelineColumns'); const timeline = document.getElementById('timeTimelineColumns');
@ -11387,6 +11561,9 @@
let caseAddPanelInitialized = false; let caseAddPanelInitialized = false;
let caseAddActiveAction = null; let caseAddActiveAction = null;
let caseAddOriginalShowRelModal = null; let caseAddOriginalShowRelModal = null;
window.initialCaseTabCounts = Object.assign({}, window.initialCaseTabCounts || {}, {
timetracking: {{ (time_entries or [])|length }}
});
const CASE_ADD_ACTIONS = [ const CASE_ADD_ACTIONS = [
{ action: 'time', label: 'Tidregistrering', icon: 'bi-clock', moduleKey: 'time', relFn: 'openRelTimeModal' }, { action: 'time', label: 'Tidregistrering', icon: 'bi-clock', moduleKey: 'time', relFn: 'openRelTimeModal' },
{ action: 'note', label: 'Kommentar', icon: 'bi-chat-left-text', moduleKey: 'solution', relFn: 'openRelNoteModal' }, { action: 'note', label: 'Kommentar', icon: 'bi-chat-left-text', moduleKey: 'solution', relFn: 'openRelNoteModal' },
@ -12926,9 +13103,10 @@
const timeEntriesStore = (typeof timeV1EntriesById !== 'undefined' && timeV1EntriesById && typeof timeV1EntriesById === 'object') const timeEntriesStore = (typeof timeV1EntriesById !== 'undefined' && timeV1EntriesById && typeof timeV1EntriesById === 'object')
? timeV1EntriesById ? timeV1EntriesById
: null; : null;
const timeCount = timeEntriesStore const initialTimeCount = Number(window.initialCaseTabCounts?.timetracking || 0);
? Object.keys(timeEntriesStore).length const loadedTimeCount = timeEntriesStore ? Object.keys(timeEntriesStore).length : 0;
: document.querySelectorAll('#timetracking tbody tr').length; const timeDomCount = document.querySelectorAll('#timetracking tbody tr').length;
const timeCount = Math.max(initialTimeCount, loadedTimeCount, timeDomCount);
_setCaseTabCountBadge('timetrackingTabCountBadge', timeCount); _setCaseTabCountBadge('timetrackingTabCountBadge', timeCount);
const subscriptionCount = _countRows('#subscriptionItemsBody'); const subscriptionCount = _countRows('#subscriptionItemsBody');
@ -12936,6 +13114,9 @@
const reminderCount = document.querySelectorAll('#remindersList .list-group-item').length; const reminderCount = document.querySelectorAll('#remindersList .list-group-item').length;
_setCaseTabCountBadge('remindersTabCountBadge', reminderCount); _setCaseTabCountBadge('remindersTabCountBadge', reminderCount);
const historyCount = Number(window.initialCaseTabCounts?.history || 0);
_setCaseTabCountBadge('historyTabCountBadge', historyCount);
} }
async function applyViewFromTags() { async function applyViewFromTags() {