feat: Implement legacy case details redirection and enhance contact info UI
This commit is contained in:
parent
6133823ade
commit
ec2c8fe784
@ -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
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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">
|
||||||
</div>
|
<p class="contact-info-title" id="contactInfoNameInline">Kontakt</p>
|
||||||
<div class="mb-2">
|
<div class="contact-info-subtitle" id="contactInfoSubtitle">Kontaktdetaljer</div>
|
||||||
<div class="text-muted small">Kunde</div>
|
|
||||||
<div id="contactInfoCompany">-</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<div class="text-muted small">E-mail</div>
|
|
||||||
<div id="contactInfoEmail">-</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>
|
||||||
<div id="contactInfoPrimary" class="badge bg-primary d-none">Hovedkontakt</div>
|
<div id="contactInfoPrimary" class="badge bg-primary d-none">Hovedkontakt</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="me-auto d-flex gap-2 flex-wrap">
|
<div class="contact-quick-actions">
|
||||||
<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-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 me-1"></i>SMS</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 me-1"></i>Email</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="contact-info-grid">
|
||||||
|
<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="modal-footer">
|
||||||
<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,14 +6732,26 @@
|
|||||||
<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>
|
||||||
|
<div class="contact-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
onclick="event.stopPropagation(); showContactInfoModal(this.closest('.contact-row'))"
|
||||||
|
title="Åbn kontakt"
|
||||||
|
aria-label="Åbn kontakt"
|
||||||
|
>
|
||||||
|
<i class="bi bi-person-lines-fill"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-delete"
|
class="btn btn-sm btn-delete"
|
||||||
onclick="event.stopPropagation(); removeContact({{ case.id }}, {{ contact.contact_id }})"
|
onclick="event.stopPropagation(); removeContact({{ case.id }}, {{ contact.contact_id }})"
|
||||||
title="Slet"
|
title="Slet"
|
||||||
|
aria-label="Slet kontakt"
|
||||||
>
|
>
|
||||||
✕
|
<i class="bi bi-x-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted text-center">Ingen kontakter</p>
|
<p class="text-muted text-center">Ingen kontakter</p>
|
||||||
@ -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() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user