- Updated index.html to extend base template and improve structure. - Added new styles and search/filter functionality in the Sager list view. - Created a backup of the old index.html as index_old.html. - Updated navigation links in base.html for consistency. - Included new dashboard API router in main.py. - Added test scripts for customer and sag queries to validate database interactions.
1371 lines
60 KiB
HTML
1371 lines
60 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}{{ case.titel }} - BMC Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
margin-bottom: 1.5rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.back-link:hover {
|
|
gap: 1rem;
|
|
}
|
|
|
|
.card {
|
|
border: none;
|
|
border-radius: 12px;
|
|
margin-bottom: 1.5rem;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.card-header {
|
|
background: var(--accent-light);
|
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
|
padding: 1.2rem;
|
|
border-radius: 12px 12px 0 0;
|
|
}
|
|
|
|
.card-body {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.card-footer {
|
|
background: transparent;
|
|
border-top: 1px solid rgba(0,0,0,0.1);
|
|
padding: 1rem;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 0.4rem 0.8rem;
|
|
font-size: 0.85rem;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.relation-card {
|
|
padding: 1rem 0;
|
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.relation-card:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.relation-type {
|
|
display: inline-block;
|
|
background: var(--accent-light);
|
|
color: var(--accent);
|
|
padding: 0.3rem 0.8rem;
|
|
border-radius: 20px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
margin-right: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.relation-link {
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.relation-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 0.8rem 0;
|
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.info-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.info-label {
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
min-width: 150px;
|
|
}
|
|
|
|
.info-value {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.tag {
|
|
display: inline-block;
|
|
background: var(--accent-light);
|
|
color: var(--accent);
|
|
padding: 0.4rem 0.8rem;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
margin-right: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.person-card {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem;
|
|
background: var(--bg-body);
|
|
border-radius: 8px;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.person-info strong {
|
|
display: block;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.person-info small {
|
|
color: var(--text-secondary);
|
|
display: block;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.btn-delete {
|
|
background-color: #e74c3c;
|
|
color: white;
|
|
}
|
|
|
|
.btn-delete:hover {
|
|
background-color: #c0392b;
|
|
}
|
|
|
|
.form-control, .form-select {
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.tag-closed {
|
|
background-color: #e0e0e0;
|
|
color: #666;
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
[data-bs-theme="dark"] .tag-closed {
|
|
background-color: #3a3a3a;
|
|
color: #999;
|
|
}
|
|
|
|
.tag-state-badge {
|
|
font-size: 0.75rem;
|
|
padding: 0.2rem 0.4rem;
|
|
border-radius: 4px;
|
|
margin-left: 0.5rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.tag-state-open {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.tag-state-closed {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
[data-bs-theme="dark"] .tag-state-open {
|
|
background: #1e4620;
|
|
color: #7fd98d;
|
|
}
|
|
|
|
[data-bs-theme="dark"] .tag-state-closed {
|
|
background: #5c2b2f;
|
|
color: #f8a5ac;
|
|
}
|
|
|
|
.tag-toggle-btn {
|
|
background: none;
|
|
border: 1px solid rgba(0,0,0,0.2);
|
|
padding: 0.2rem 0.5rem;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.75rem;
|
|
margin-left: 0.5rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.tag-toggle-btn:hover {
|
|
background: rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.tag-toggle-open {
|
|
color: #28a745;
|
|
border-color: #28a745;
|
|
}
|
|
|
|
.tag-toggle-open:hover {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.tag-toggle-closed {
|
|
color: #6c757d;
|
|
border-color: #6c757d;
|
|
}
|
|
|
|
.tag-toggle-closed:hover {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid" style="margin-top: 2rem; margin-bottom: 2rem; max-width: 1400px;">
|
|
<!-- Back Link -->
|
|
<a href="/sag" class="back-link">
|
|
<i class="bi bi-chevron-left"></i> Tilbage til sager
|
|
</a>
|
|
|
|
<!-- Quick Info Bar -->
|
|
<div class="card mb-3" style="background: var(--bg-card); border-left: 4px solid var(--accent); box-shadow: 0 1px 3px rgba(0,0,0,0.08);">
|
|
<div class="card-body py-2 px-3">
|
|
<div class="d-flex flex-wrap align-items-center gap-3" style="font-size: 0.85rem;">
|
|
<div class="d-flex align-items-center">
|
|
<strong style="color: var(--accent); margin-right: 0.4rem;">ID:</strong>
|
|
<span>{{ case.id }}</span>
|
|
</div>
|
|
<div class="d-flex align-items-center" style="border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;">
|
|
<strong style="color: var(--accent); margin-right: 0.4rem;">Kunde:</strong>
|
|
<span>{{ customer.name if customer else 'Ingen kunde' }}</span>
|
|
</div>
|
|
<div class="d-flex align-items-center" style="border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;">
|
|
<strong style="color: var(--accent); margin-right: 0.4rem;">Hovedkontakt:</strong>
|
|
{% if hovedkontakt %}
|
|
<span style="cursor: pointer; text-decoration: underline; color: var(--accent);"
|
|
onclick="showKontaktModal()"
|
|
title="Klik for at se kontaktinfo">
|
|
{{ hovedkontakt.first_name ~ ' ' ~ hovedkontakt.last_name }}
|
|
</span>
|
|
{% else %}
|
|
<span>Ingen kontakt</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="d-flex align-items-center" style="border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;">
|
|
<strong style="color: var(--accent); margin-right: 0.4rem;">Afdeling:</strong>
|
|
<span style="cursor: pointer; text-decoration: underline; color: var(--accent);"
|
|
onclick="showAfdelingModal()"
|
|
title="Klik for at ændre afdeling">
|
|
{{ customer.department if customer and customer.department else 'N/A' }}
|
|
</span>
|
|
</div>
|
|
<div class="d-flex align-items-center" style="border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;">
|
|
<strong style="color: var(--accent); margin-right: 0.4rem;">Status:</strong>
|
|
<span class="badge" style="background: var(--accent); font-size: 0.75rem;">{{ case.status }}</span>
|
|
</div>
|
|
<div class="d-flex align-items-center" style="border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;">
|
|
<strong style="color: var(--accent); margin-right: 0.4rem;">Opdateret:</strong>
|
|
<span>{{ case.updated_at.strftime('%d/%m-%Y') if case.updated_at else 'N/A' }}</span>
|
|
</div>
|
|
<div class="d-flex align-items-center" style="border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;">
|
|
<strong style="color: var(--accent); margin-right: 0.4rem;">Deadline:</strong>
|
|
<span>{{ case.deadline.strftime('%d/%m-%Y') if case.deadline else 'Ikke sat' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ROW 1: Main Info + Tags + Hardware (40% height) -->
|
|
<div class="row mb-3" style="height: 40vh;">
|
|
<!-- Main Case Info (50% width) -->
|
|
<div class="col-md-6 h-100">
|
|
<div class="card h-100 d-flex flex-column">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h3 class="mb-0" style="color: var(--accent);">{{ case.titel }}</h3>
|
|
<div>
|
|
<a href="/sag/{{ case.id }}/edit" class="btn btn-sm btn-outline-primary me-2">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="confirmDeleteCase()">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body flex-grow-1 overflow-auto">
|
|
<div class="info-row">
|
|
<span class="info-label">Status</span>
|
|
<span class="tag">{{ case.status }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Beskrivelse</span>
|
|
<span class="info-value">{{ case.beskrivelse or 'Ingen beskrivelse' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Oprettet</span>
|
|
<span class="info-value">{{ case.created_at|string|truncate(19, True, '') if case.created_at else 'Ikke sat' }}</span>
|
|
</div>
|
|
{% if case.deadline %}
|
|
<div class="info-row">
|
|
<span class="info-label">Deadline</span>
|
|
<span class="info-value">{{ case.deadline|string|truncate(19, True, '') if case.deadline else 'Ikke sat' }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags + Hardware (50% width) -->
|
|
<div class="col-md-6 h-100">
|
|
<div class="row h-50 mb-2">
|
|
<div class="col-12">
|
|
<div class="card h-100 d-flex flex-column">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0" style="color: var(--accent);">📌 Tags</h6>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="window.showTagPicker('case', {{ case.id }}, () => window.renderEntityTags('case', {{ case.id }}, 'case-tags'))">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
</div>
|
|
<div class="card-body flex-grow-1 overflow-auto">
|
|
<div id="case-tags" class="d-flex flex-wrap">
|
|
<div class="text-center w-100 py-2">
|
|
<span class="spinner-border spinner-border-sm text-muted"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row h-50">
|
|
<div class="col-12">
|
|
<div class="card h-100 d-flex flex-column">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0" style="color: var(--accent);">💻 Hardware</h6>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="promptLinkHardware()">
|
|
<i class="bi bi-link-45deg"></i>
|
|
</button>
|
|
</div>
|
|
<div class="card-body flex-grow-1 overflow-auto p-0">
|
|
<div class="list-group list-group-flush" id="hardware-list">
|
|
<div class="p-3 text-center text-muted">Henter hardware...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ROW 2: Locations + Contacts + Customers (35% height) -->
|
|
<div class="row mb-3" style="height: 35vh;">
|
|
<div class="col-md-4 h-100">
|
|
<div class="card h-100 d-flex flex-column">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0" style="color: var(--accent);">📍 Lokationer</h6>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="promptLinkLocation()">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
</div>
|
|
<div class="card-body flex-grow-1 overflow-auto p-0">
|
|
<div class="list-group list-group-flush" id="locations-list">
|
|
<div class="p-3 text-center text-muted">Henter lokationer...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 h-100">
|
|
<div class="card h-100 d-flex flex-column">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0" style="color: var(--accent);">👥 Kontakter</h6>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="showContactSearch()">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
</div>
|
|
<div class="card-body flex-grow-1 overflow-auto" id="contacts-container">
|
|
{% if contacts %}
|
|
{% for contact in contacts %}
|
|
<div class="person-card">
|
|
<div class="person-info">
|
|
<strong>{{ contact.contact_name }}</strong>
|
|
<small>{{ contact.role }}</small>
|
|
{% if contact.contact_email %}
|
|
<small>{{ contact.contact_email }}</small>
|
|
{% endif %}
|
|
</div>
|
|
<button onclick="removeContact({{ case.id }}, {{ contact.contact_id }})" class="btn btn-sm btn-delete">✕</button>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<p class="text-muted text-center">Ingen kontakter</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 h-100">
|
|
<div class="card h-100 d-flex flex-column">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0" style="color: var(--accent);">🏢 Kunder</h6>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="showCustomerSearch()">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
</div>
|
|
<div class="card-body flex-grow-1 overflow-auto" id="customers-container">
|
|
{% if customers %}
|
|
{% for customer in customers %}
|
|
<div class="person-card">
|
|
<div class="person-info">
|
|
<strong><a href="/customers/{{ customer.customer_id }}">{{ customer.customer_name }}</a></strong>
|
|
<small>{{ customer.role }}</small>
|
|
{% if customer.customer_email %}
|
|
<small>{{ customer.customer_email }}</small>
|
|
{% endif %}
|
|
</div>
|
|
<button onclick="removeCustomer({{ case.id }}, {{ customer.customer_id }})" class="btn btn-sm btn-delete">✕</button>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<p class="text-muted text-center">Ingen kunder</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ROW 3: Relations + SAG Compatibility (25% height) -->
|
|
<div class="row" style="height: 25vh;">
|
|
<div class="col-md-8 h-100">
|
|
<div class="card h-100 d-flex flex-column">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0" style="color: var(--accent);">🔗 Relaterede sager</h6>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="showRelationModal()">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
</div>
|
|
<div class="card-body flex-grow-1 overflow-auto">
|
|
{% if relationer %}
|
|
{% for rel in relationer %}
|
|
<div class="relation-card">
|
|
<span class="relation-type">{{ rel.relationstype }}</span>
|
|
{% if rel.kilde_sag_id == case.id %}
|
|
→ <a href="/sag/{{ rel.målsag_id }}" class="relation-link">{{ rel.mål_titel }}</a>
|
|
{% else %}
|
|
← <a href="/sag/{{ rel.kilde_sag_id }}" class="relation-link">{{ rel.kilde_titel }}</a>
|
|
{% endif %}
|
|
<button onclick="deleteRelation({{ rel.id }})" class="btn btn-sm btn-delete float-end">✕</button>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<p class="text-muted text-center">Ingen relaterede sager</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 h-100">
|
|
<div class="card h-100 d-flex flex-column">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0" style="color: var(--accent);">🔧 SAG Kompatibilitet</h6>
|
|
<button class="btn btn-sm btn-primary" onclick="showCompatibilityModal()">
|
|
Vis
|
|
</button>
|
|
</div>
|
|
<div class="card-body flex-grow-1 overflow-auto text-center d-flex align-items-center justify-content-center">
|
|
<div>
|
|
<i class="bi bi-diagram-3 text-muted" style="font-size: 3rem;"></i>
|
|
<p class="text-muted mt-2 mb-0">Klik "Vis" for at se alle</p>
|
|
<p class="text-muted small">SAG kompatible moduler</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Modals -->
|
|
<div class="modal fade" id="contactSearchModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Søg kontakt</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" id="contactSearch" placeholder="Søg efter kontakt..." class="form-control mb-3">
|
|
<div id="contactSearchResults" style="max-height: 300px; overflow-y: auto;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="customerSearchModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Søg kunde</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" id="customerSearch" placeholder="Søg efter kunde..." class="form-control mb-3">
|
|
<div id="customerSearchResults" style="max-height: 300px; overflow-y: auto;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="relationModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">🔗 Tilføj relation til sag</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">1. Søg og vælg sag</label>
|
|
<input type="text"
|
|
id="relationCaseSearch"
|
|
placeholder="Søg efter sag ID, titel, kunde eller beskrivelse..."
|
|
class="form-control form-control-lg"
|
|
autocomplete="off">
|
|
<div id="relationSearchResults"
|
|
style="max-height: 400px; overflow-y: auto; margin-top: 0.5rem;"
|
|
class="border rounded"></div>
|
|
</div>
|
|
|
|
<div id="selectedCasePreview" style="display: none;" class="alert alert-info mb-3">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<strong>Valgt sag:</strong>
|
|
<div id="selectedCaseTitle" class="mt-1"></div>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSelectedRelationCase()">
|
|
Ryd valg
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">2. Vælg relationstype</label>
|
|
<select id="relationTypeSelect" class="form-control form-control-lg" onchange="updateAddRelationButton()">
|
|
<option value="">Vælg hvordan sagerne er relateret...</option>
|
|
<option value="relateret">🔗 Relateret - Generel relation</option>
|
|
<option value="afhænger af">⏳ Afhænger af - Denne sag venter på den anden</option>
|
|
<option value="blokkerer">🚫 Blokkerer - Denne sag blokerer den anden</option>
|
|
<option value="duplikat">📋 Duplikat - Sagerne er den samme</option>
|
|
<option value="forårsaget af">🔄 Forårsaget af - Denne sag er konsekvens af den anden</option>
|
|
<option value="følger op på">📌 Følger op på - Fortsættelse af tidligere sag</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="alert alert-light d-flex align-items-center" style="font-size: 0.9rem;">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
<div>
|
|
<strong>Tip:</strong> Brug pile (↑↓) til at navigere i søgeresultater, Enter til at vælge.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button>
|
|
<button type="button"
|
|
class="btn btn-primary btn-lg"
|
|
onclick="addRelation()"
|
|
id="addRelationBtn"
|
|
disabled>
|
|
<i class="bi bi-plus-circle me-1"></i> Tilføj relation
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="compatibilityModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">🔧 SAG Kompatible Moduler</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6 class="text-muted mb-3">📦 Aktive Moduler</h6>
|
|
<div class="list-group">
|
|
<div class="list-group-item">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-check-circle-fill text-success me-2"></i>
|
|
<strong>Sager (SAG)</strong>
|
|
</div>
|
|
<small class="text-muted">Aktiv modul</small>
|
|
</div>
|
|
<div class="list-group-item">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-check-circle-fill text-success me-2"></i>
|
|
<strong>Hardware</strong>
|
|
</div>
|
|
<small class="text-muted">Kan linkes til sager</small>
|
|
</div>
|
|
<div class="list-group-item">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-check-circle-fill text-success me-2"></i>
|
|
<strong>Lokationer</strong>
|
|
</div>
|
|
<small class="text-muted">Kan linkes til sager</small>
|
|
</div>
|
|
<div class="list-group-item">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-check-circle-fill text-success me-2"></i>
|
|
<strong>Tags</strong>
|
|
</div>
|
|
<small class="text-muted">Global tag system</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6 class="text-muted mb-3">🔄 Integration</h6>
|
|
<div class="list-group">
|
|
<div class="list-group-item">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-people me-2 text-primary"></i>
|
|
<strong>CRM Integration</strong>
|
|
</div>
|
|
<small class="text-muted">Kunder & Kontakter</small>
|
|
</div>
|
|
<div class="list-group-item">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-link-45deg me-2 text-primary"></i>
|
|
<strong>Relationer</strong>
|
|
</div>
|
|
<small class="text-muted">Link mellem sager</small>
|
|
</div>
|
|
<div class="list-group-item">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-diagram-3 me-2 text-primary"></i>
|
|
<strong>Workflows</strong>
|
|
</div>
|
|
<small class="text-muted">Tag-baseret automation</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const caseId = {{ case.id }};
|
|
let contactSearchTimeout;
|
|
let customerSearchTimeout;
|
|
let relationSearchTimeout;
|
|
let selectedRelationCaseId = null;
|
|
|
|
// Modal instances
|
|
let contactSearchModal, customerSearchModal, relationModal, compatibilityModal;
|
|
|
|
// Initialize everything when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Initialize modals
|
|
contactSearchModal = new bootstrap.Modal(document.getElementById('contactSearchModal'));
|
|
customerSearchModal = new bootstrap.Modal(document.getElementById('customerSearchModal'));
|
|
relationModal = new bootstrap.Modal(document.getElementById('relationModal'));
|
|
compatibilityModal = new bootstrap.Modal(document.getElementById('compatibilityModal'));
|
|
|
|
// Setup search handlers
|
|
setupContactSearch();
|
|
setupCustomerSearch();
|
|
setupRelationSearch();
|
|
|
|
// Render Global Tags
|
|
if (window.renderEntityTags) {
|
|
window.renderEntityTags('case', {{ case.id }}, 'case-tags');
|
|
}
|
|
|
|
// Set default context for keyboard shortcuts (Option+Shift+T)
|
|
if (window.setTagPickerContext) {
|
|
window.setTagPickerContext('case', {{ case.id }}, () => window.renderEntityTags('case', {{ case.id }}, 'case-tags'));
|
|
}
|
|
|
|
// Load Hardware & Locations
|
|
loadCaseHardware();
|
|
loadCaseLocations();
|
|
});
|
|
|
|
// Show modal functions
|
|
function showContactSearch() {
|
|
contactSearchModal.show();
|
|
setTimeout(() => document.getElementById('contactSearch').focus(), 300);
|
|
}
|
|
|
|
function showCustomerSearch() {
|
|
customerSearchModal.show();
|
|
setTimeout(() => document.getElementById('customerSearch').focus(), 300);
|
|
}
|
|
|
|
function showRelationModal() {
|
|
relationModal.show();
|
|
setTimeout(() => document.getElementById('relationCaseSearch').focus(), 300);
|
|
}
|
|
|
|
function showCompatibilityModal() {
|
|
compatibilityModal.show();
|
|
}
|
|
|
|
function confirmDeleteCase() {
|
|
if(confirm('Slet denne sag?')) {
|
|
fetch('/api/v1/sag/{{ case.id }}', {method: 'DELETE'})
|
|
.then(() => window.location='/sag');
|
|
}
|
|
}
|
|
|
|
// Contact Search
|
|
function setupContactSearch() {
|
|
const contactSearchInput = document.getElementById('contactSearch');
|
|
contactSearchInput.addEventListener('input', function(e) {
|
|
clearTimeout(contactSearchTimeout);
|
|
const query = e.target.value.trim();
|
|
|
|
if (query.length < 2) {
|
|
document.getElementById('contactSearchResults').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
contactSearchTimeout = setTimeout(async () => {
|
|
try {
|
|
const response = await fetch(`/api/v1/search/contacts?q=${encodeURIComponent(query)}`);
|
|
const contacts = await response.json();
|
|
|
|
const resultsDiv = document.getElementById('contactSearchResults');
|
|
if (contacts.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="p-3 text-muted">Ingen kontakter fundet</div>';
|
|
} else {
|
|
resultsDiv.innerHTML = contacts.map(c => `
|
|
<div class="list-group-item list-group-item-action" style="cursor: pointer;"
|
|
onclick="addContact(${caseId}, ${c.id}, '${(c.first_name + ' ' + c.last_name).replace(/'/g, "\\'")}')">
|
|
<strong>${c.first_name} ${c.last_name}</strong>
|
|
<div class="small text-muted">${c.email || ''} ${c.user_company ? '(' + c.user_company + ')' : ''}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error searching contacts:', err);
|
|
}
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
async function addContact(caseId, contactId, contactName) {
|
|
try {
|
|
const response = await fetch(`/api/v1/sag/${caseId}/contacts`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({contact_id: contactId, role: 'Kontakt'})
|
|
});
|
|
|
|
if (response.ok) {
|
|
contactSearchModal.hide();
|
|
window.location.reload();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Fejl: ${error.detail}`);
|
|
}
|
|
} catch (err) {
|
|
alert('Fejl ved tilføjelse af kontakt: ' + err.message);
|
|
}
|
|
}
|
|
|
|
async function removeContact(caseId, contactId) {
|
|
if (confirm('Fjern denne kontakt fra sagen?')) {
|
|
const response = await fetch(`/api/v1/sag/${caseId}/contacts/${contactId}`, {method: 'DELETE'});
|
|
if (response.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
alert('Fejl ved fjernelse af kontakt');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Customer Search
|
|
function setupCustomerSearch() {
|
|
const customerSearchInput = document.getElementById('customerSearch');
|
|
customerSearchInput.addEventListener('input', function(e) {
|
|
clearTimeout(customerSearchTimeout);
|
|
const query = e.target.value.trim();
|
|
|
|
if (query.length < 2) {
|
|
document.getElementById('customerSearchResults').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
customerSearchTimeout = setTimeout(async () => {
|
|
try {
|
|
const response = await fetch(`/api/v1/search/customers?q=${encodeURIComponent(query)}`);
|
|
const customers = await response.json();
|
|
|
|
const resultsDiv = document.getElementById('customerSearchResults');
|
|
if (customers.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="p-3 text-muted">Ingen kunder fundet</div>';
|
|
} else {
|
|
resultsDiv.innerHTML = customers.map(c => `
|
|
<div class="list-group-item list-group-item-action" style="cursor: pointer;"
|
|
onclick="addCustomer(${caseId}, ${c.id}, '${c.name.replace(/'/g, "\\'")}')">
|
|
<strong>${c.name}</strong>
|
|
<div class="small text-muted">${c.email || ''} ${c.cvr_number ? '(CVR: ' + c.cvr_number + ')' : ''}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error searching customers:', err);
|
|
}
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
async function addCustomer(caseId, customerId, customerName) {
|
|
try {
|
|
const response = await fetch(`/api/v1/sag/${caseId}/customers`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({customer_id: customerId, role: 'Kunde'})
|
|
});
|
|
|
|
if (response.ok) {
|
|
customerSearchModal.hide();
|
|
window.location.reload();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Fejl: ${error.detail}`);
|
|
}
|
|
} catch (err) {
|
|
alert('Fejl ved tilføjelse af kunde: ' + err.message);
|
|
}
|
|
}
|
|
|
|
async function removeCustomer(caseId, customerId) {
|
|
if (confirm('Fjern denne kunde fra sagen?')) {
|
|
const response = await fetch(`/api/v1/sag/${caseId}/customers/${customerId}`, {method: 'DELETE'});
|
|
if (response.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
alert('Fejl ved fjernelse af kunde');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Relation Search - Enhanced version
|
|
let currentFocusIndex = -1;
|
|
let searchResults = [];
|
|
|
|
function setupRelationSearch() {
|
|
const relationSearchInput = document.getElementById('relationCaseSearch');
|
|
|
|
// Input handler
|
|
relationSearchInput.addEventListener('input', function(e) {
|
|
clearTimeout(relationSearchTimeout);
|
|
const query = e.target.value.trim();
|
|
currentFocusIndex = -1;
|
|
|
|
if (query.length < 2) {
|
|
document.getElementById('relationSearchResults').innerHTML = '';
|
|
document.getElementById('relationSearchResults').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
relationSearchTimeout = setTimeout(async () => {
|
|
try {
|
|
const response = await fetch(`/api/v1/search/sag?q=${encodeURIComponent(query)}`);
|
|
const cases = await response.json();
|
|
searchResults = cases.filter(c => c.id !== caseId);
|
|
|
|
renderRelationSearchResults(searchResults);
|
|
} catch (err) {
|
|
console.error('Error searching cases:', err);
|
|
}
|
|
}, 200);
|
|
});
|
|
|
|
// Keyboard navigation
|
|
relationSearchInput.addEventListener('keydown', function(e) {
|
|
const resultsDiv = document.getElementById('relationSearchResults');
|
|
const items = resultsDiv.querySelectorAll('.relation-search-item');
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
currentFocusIndex = (currentFocusIndex + 1) % items.length;
|
|
updateFocusedItem(items);
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
currentFocusIndex = currentFocusIndex <= 0 ? items.length - 1 : currentFocusIndex - 1;
|
|
updateFocusedItem(items);
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (currentFocusIndex >= 0 && currentFocusIndex < items.length) {
|
|
items[currentFocusIndex].click();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateFocusedItem(items) {
|
|
items.forEach((item, index) => {
|
|
if (index === currentFocusIndex) {
|
|
item.classList.add('active');
|
|
item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
} else {
|
|
item.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderRelationSearchResults(cases) {
|
|
const resultsDiv = document.getElementById('relationSearchResults');
|
|
|
|
if (cases.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="p-3 text-muted text-center"><i class="bi bi-search me-2"></i>Ingen sager fundet</div>';
|
|
resultsDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
// Group by status
|
|
const grouped = {};
|
|
cases.forEach(c => {
|
|
const status = c.status || 'ukendt';
|
|
if (!grouped[status]) grouped[status] = [];
|
|
grouped[status].push(c);
|
|
});
|
|
|
|
let html = '<div class="list-group list-group-flush">';
|
|
|
|
// Sort status groups: åben first, then others
|
|
const statusOrder = ['åben', 'under behandling', 'afventer', 'løst', 'lukket'];
|
|
const sortedStatuses = Object.keys(grouped).sort((a, b) => {
|
|
const aIndex = statusOrder.indexOf(a);
|
|
const bIndex = statusOrder.indexOf(b);
|
|
if (aIndex === -1 && bIndex === -1) return a.localeCompare(b);
|
|
if (aIndex === -1) return 1;
|
|
if (bIndex === -1) return -1;
|
|
return aIndex - bIndex;
|
|
});
|
|
|
|
sortedStatuses.forEach(status => {
|
|
const statusCases = grouped[status];
|
|
|
|
// Status group header
|
|
html += `
|
|
<div class="list-group-item bg-light" style="padding: 0.5rem 1rem; font-weight: 600; font-size: 0.85rem; color: var(--text-secondary);">
|
|
<span class="status-badge status-${status}">${status}</span>
|
|
<span class="badge bg-secondary float-end">${statusCases.length}</span>
|
|
</div>
|
|
`;
|
|
|
|
statusCases.forEach(c => {
|
|
const createdDate = c.created_at ? new Date(c.created_at).toLocaleDateString('da-DK') : 'N/A';
|
|
const beskrivelse = c.beskrivelse ? c.beskrivelse.substring(0, 80) + '...' : '';
|
|
const customerName = c.customer_name || '';
|
|
const safeTitle = (c.titel || '').replace(/"/g, '"').replace(/'/g, ''');
|
|
const safeCustomer = customerName.replace(/"/g, '"').replace(/'/g, ''');
|
|
|
|
html += `
|
|
<div class="list-group-item list-group-item-action relation-search-item"
|
|
style="cursor: pointer; padding: 0.75rem 1rem;"
|
|
onclick="selectRelationCase(${c.id}, '${safeTitle}', '${safeCustomer}', '${status}');"
|
|
data-case-id="${c.id}">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div style="flex: 1;">
|
|
<div class="d-flex align-items-center gap-2 mb-1">
|
|
<span class="badge bg-primary" style="font-size: 0.75rem;">#${c.id}</span>
|
|
<strong style="font-size: 0.95rem;">${escapeHtml(c.titel)}</strong>
|
|
</div>
|
|
${c.customer_name ? `
|
|
<div class="small text-muted mb-1">
|
|
<i class="bi bi-building me-1"></i>${escapeHtml(c.customer_name)}
|
|
</div>
|
|
` : ''}
|
|
${beskrivelse ? `
|
|
<div class="small text-muted" style="font-size: 0.8rem;">${escapeHtml(beskrivelse)}</div>
|
|
` : ''}
|
|
</div>
|
|
<div class="text-end" style="min-width: 100px;">
|
|
<div class="small text-muted">${createdDate}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
});
|
|
|
|
html += '</div>';
|
|
resultsDiv.innerHTML = html;
|
|
resultsDiv.style.display = 'block';
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function selectRelationCase(caseIdValue, caseTitel, customerName, status) {
|
|
selectedRelationCaseId = caseIdValue;
|
|
|
|
// Update preview
|
|
const previewDiv = document.getElementById('selectedCasePreview');
|
|
const titleDiv = document.getElementById('selectedCaseTitle');
|
|
|
|
titleDiv.innerHTML = `
|
|
<div class="d-flex align-items-center gap-2 mb-1">
|
|
<span class="badge bg-primary">#${caseIdValue}</span>
|
|
<strong>${escapeHtml(caseTitel)}</strong>
|
|
<span class="status-badge status-${status}">${status}</span>
|
|
</div>
|
|
${customerName ? `<div class="small"><i class="bi bi-building me-1"></i>${escapeHtml(customerName)}</div>` : ''}
|
|
`;
|
|
|
|
previewDiv.style.display = 'block';
|
|
document.getElementById('relationSearchResults').innerHTML = '';
|
|
document.getElementById('relationSearchResults').style.display = 'none';
|
|
document.getElementById('relationCaseSearch').value = '';
|
|
|
|
// Enable add button
|
|
updateAddRelationButton();
|
|
}
|
|
|
|
function clearSelectedRelationCase() {
|
|
selectedRelationCaseId = null;
|
|
document.getElementById('selectedCasePreview').style.display = 'none';
|
|
document.getElementById('relationCaseSearch').value = '';
|
|
document.getElementById('relationCaseSearch').focus();
|
|
updateAddRelationButton();
|
|
}
|
|
|
|
function updateAddRelationButton() {
|
|
const btn = document.getElementById('addRelationBtn');
|
|
const relationType = document.getElementById('relationTypeSelect').value;
|
|
btn.disabled = !selectedRelationCaseId || !relationType;
|
|
}
|
|
|
|
async function addRelation() {
|
|
const relationType = document.getElementById('relationTypeSelect').value;
|
|
const btn = document.getElementById('addRelationBtn');
|
|
|
|
if (!selectedRelationCaseId) {
|
|
alert('Vælg en sag først');
|
|
return;
|
|
}
|
|
|
|
if (!relationType) {
|
|
alert('Vælg en relationstype');
|
|
return;
|
|
}
|
|
|
|
// Disable button during request
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Tilføjer...';
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/sag/${caseId}/relationer`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
målsag_id: selectedRelationCaseId,
|
|
relationstype: relationType
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
selectedRelationCaseId = null;
|
|
relationModal.hide();
|
|
window.location.reload();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Fejl: ${error.detail}`);
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-plus-circle me-1"></i> Tilføj relation';
|
|
}
|
|
} catch (err) {
|
|
alert('Fejl ved tilføjelse af relation: ' + err.message);
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-plus-circle me-1"></i> Tilføj relation';
|
|
}
|
|
}
|
|
|
|
async function deleteRelation(relationId) {
|
|
if (confirm('Fjern denne relation?')) {
|
|
const response = await fetch(`/api/v1/sag/${caseId}/relationer/${relationId}`, {method: 'DELETE'});
|
|
if (response.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
alert('Fejl ved fjernelse af relation');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============ Hardware Handling ============
|
|
async function loadCaseHardware() {
|
|
try {
|
|
const res = await fetch(`/api/v1/sag/{{ case.id }}/hardware`);
|
|
const hardware = await res.json();
|
|
const container = document.getElementById('hardware-list');
|
|
|
|
if (hardware.length === 0) {
|
|
container.innerHTML = '<div class="p-3 text-center text-muted small">Ingen hardware tilknyttet</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = hardware.map(h => `
|
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="fw-bold text-primary">
|
|
<a href="/hardware/${h.id}" class="text-decoration-none">
|
|
${h.brand} ${h.model}
|
|
</a>
|
|
</div>
|
|
<small class="text-muted">SN: ${h.serial_number || 'N/A'}</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-danger border-0" onclick="unlinkHardware(${h.id})" title="Fjern link">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error("Error loading hardware:", e);
|
|
document.getElementById('hardware-list').innerHTML = '<div class="p-3 text-danger text-center">Fejl ved hentning</div>';
|
|
}
|
|
}
|
|
|
|
async function promptLinkHardware() {
|
|
const id = prompt("Indtast Hardware ID:");
|
|
if (!id) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/sag/{{ case.id }}/hardware`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ hardware_id: parseInt(id) })
|
|
});
|
|
|
|
if (!res.ok) throw await res.json();
|
|
loadCaseHardware();
|
|
} catch (e) {
|
|
alert("Fejl: " + (e.detail || e.message));
|
|
}
|
|
}
|
|
|
|
async function unlinkHardware(hwId) {
|
|
if(!confirm("Fjern link til dette hardware?")) return;
|
|
try {
|
|
await fetch(`/api/v1/sag/{{ case.id }}/hardware/${hwId}`, { method: 'DELETE' });
|
|
loadCaseHardware();
|
|
} catch (e) {
|
|
alert("Fejl ved sletning");
|
|
}
|
|
}
|
|
|
|
// ============ Location Handling ============
|
|
async function loadCaseLocations() {
|
|
try {
|
|
const res = await fetch(`/api/v1/sag/{{ case.id }}/locations`);
|
|
const locations = await res.json();
|
|
const container = document.getElementById('locations-list');
|
|
|
|
if (locations.length === 0) {
|
|
container.innerHTML = '<div class="p-3 text-center text-muted small">Ingen lokationer tilknyttet</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = locations.map(l => `
|
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="fw-bold">
|
|
<i class="bi bi-geo-alt me-1 text-secondary"></i>
|
|
${l.name}
|
|
</div>
|
|
<small class="text-muted">${l.location_type || ''}</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-danger border-0" onclick="unlinkLocation(${l.id})" title="Fjern link">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error("Error loading locations:", e);
|
|
document.getElementById('locations-list').innerHTML = '<div class="p-3 text-danger text-center">Fejl ved hentning</div>';
|
|
}
|
|
}
|
|
|
|
async function promptLinkLocation() {
|
|
const id = prompt("Indtast Lokations ID:");
|
|
if (!id) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/sag/{{ case.id }}/locations`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ location_id: parseInt(id) })
|
|
});
|
|
|
|
if (!res.ok) throw await res.json();
|
|
loadCaseLocations();
|
|
} catch (e) {
|
|
alert("Fejl: " + (e.detail || e.message));
|
|
}
|
|
}
|
|
|
|
async function unlinkLocation(locId) {
|
|
if(!confirm("Fjern link til denne lokation?")) return;
|
|
try {
|
|
await fetch(`/api/v1/sag/{{ case.id }}/locations/${locId}`, { method: 'DELETE' });
|
|
loadCaseLocations();
|
|
} catch (e) {
|
|
alert("Fejl ved sletning");
|
|
}
|
|
}
|
|
|
|
|
|
// Initialize relation search when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', setupRelationSearch);
|
|
} else {
|
|
setupRelationSearch();
|
|
}
|
|
|
|
// Kontakt Modal functions
|
|
function showKontaktModal() {
|
|
const modal = new bootstrap.Modal(document.getElementById('kontaktModal'));
|
|
modal.show();
|
|
}
|
|
|
|
// Afdeling Modal functions
|
|
function showAfdelingModal() {
|
|
const modal = new bootstrap.Modal(document.getElementById('afdelingModal'));
|
|
modal.show();
|
|
}
|
|
|
|
async function updateAfdeling() {
|
|
const newAfdeling = document.getElementById('afdelingInput').value.trim();
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/customers/{{ customer.id if customer else 0 }}', {
|
|
method: 'PATCH',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ department: newAfdeling })
|
|
});
|
|
|
|
if (!response.ok) throw await response.json();
|
|
|
|
// Reload page to show updated data
|
|
window.location.reload();
|
|
} catch (e) {
|
|
alert("Fejl ved opdatering: " + (e.detail || e.message));
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- Kontakt Info Modal -->
|
|
<div class="modal fade" id="kontaktModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header" style="background: var(--accent); color: white;">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-person-circle me-2"></i>Kontakt Information
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
{% if hovedkontakt %}
|
|
<div class="mb-3">
|
|
<label class="small text-muted mb-1">Navn</label>
|
|
<div class="fw-bold">{{ hovedkontakt.first_name }} {{ hovedkontakt.last_name }}</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="small text-muted mb-1">Email</label>
|
|
<div>
|
|
{% if hovedkontakt.email %}
|
|
<a href="mailto:{{ hovedkontakt.email }}" style="color: var(--accent);">
|
|
<i class="bi bi-envelope me-1"></i>{{ hovedkontakt.email }}
|
|
</a>
|
|
{% else %}
|
|
<span class="text-muted">Ingen email</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="small text-muted mb-1">Telefon</label>
|
|
<div>
|
|
{% if hovedkontakt.phone %}
|
|
<a href="tel:{{ hovedkontakt.phone }}" style="color: var(--accent);">
|
|
<i class="bi bi-telephone me-1"></i>{{ hovedkontakt.phone }}
|
|
</a>
|
|
{% else %}
|
|
<span class="text-muted">Ingen telefon</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="small text-muted mb-1">Mobil</label>
|
|
<div>
|
|
{% if hovedkontakt.mobile %}
|
|
<a href="tel:{{ hovedkontakt.mobile }}" style="color: var(--accent);">
|
|
<i class="bi bi-phone me-1"></i>{{ hovedkontakt.mobile }}
|
|
</a>
|
|
{% else %}
|
|
<span class="text-muted">Ingen mobil</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% if hovedkontakt.title %}
|
|
<div class="mb-3">
|
|
<label class="small text-muted mb-1">Titel</label>
|
|
<div>{{ hovedkontakt.title }}</div>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="text-center text-muted mb-0">Ingen kontakt tilknyttet</p>
|
|
{% endif %}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Afdeling Modal -->
|
|
<div class="modal fade" id="afdelingModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header" style="background: var(--accent); color: white;">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-building me-2"></i>Rediger Afdeling
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<label for="afdelingInput" class="form-label">Afdeling</label>
|
|
<input type="text"
|
|
class="form-control"
|
|
id="afdelingInput"
|
|
value="{{ customer.department if customer and customer.department else '' }}"
|
|
placeholder="Indtast afdeling">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
|
|
<button type="button" class="btn btn-primary" style="background: var(--accent); border: none;" onclick="updateAfdeling()">
|
|
Gem
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|