bmc_hub/app/modules/sag/templates/detail.html
Christian 29acdf3e01 Add tests for new SAG module endpoints and module deactivation
- Implement test script for new SAG module endpoints BE-003 (Tag State Management) and BE-004 (Bulk Operations).
- Create test cases for creating, updating, and bulk operations on cases and tags.
- Add a test for module deactivation to ensure data integrity is maintained.
- Include setup and teardown for tests to clear database state before and after each test.
2026-01-31 23:16:24 +01:00

906 lines
37 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" style="margin-top: 2rem; margin-bottom: 2rem;">
<!-- Back Link -->
<a href="/cases" class="back-link">
<i class="bi bi-chevron-left"></i> Tilbage til sager
</a>
<!-- Main Info Card -->
<div class="card">
<div class="card-header">
<h1 style="margin: 0; color: var(--accent);">{{ case.titel }}</h1>
</div>
<div class="card-body">
<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>
<!-- Tags Card (Global) -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" style="color: var(--accent);">📌 Tags</h5>
<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> Tilføj
</button>
</div>
<div class="card-body">
<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>
<!-- Related Hardware -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" style="color: var(--accent);">💻 Hardware</h5>
<button class="btn btn-sm btn-outline-primary" onclick="promptLinkHardware()">
<i class="bi bi-link-45deg"></i> Link ID
</button>
</div>
<div class="card-body 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>
<!-- Related Locations -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" style="color: var(--accent);">📍 Lokationer</h5>
<button class="btn btn-sm btn-outline-primary" onclick="promptLinkLocation()">
<i class="bi bi-plus-lg"></i> Tilføj ID
</button>
</div>
<div class="card-body 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>
<!-- Relations Section -->
<div class="card">
<div class="card-header">
<h5 style="margin: 0;">🔗 Relaterede sager</h5>
</div>
<div class="card-body">
{% if relations %}
{% for rel in relations %}
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; border-bottom: 1px solid #eee;">
<div>
<div class="relation-type" style="display: inline-block; background: var(--accent-light); color: var(--accent-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.85rem; margin-right: 0.5rem;">{{ rel.relationstype }}</div>
{% if rel.kilde_sag_id == case.id %}
<a href="/cases/{{ rel.målsag_id }}" class="relation-link" style="color: var(--accent-color); text-decoration: none;">{{ rel.mål_titel }}</a>
{% else %}
<a href="/cases/{{ rel.kilde_sag_id }}" class="relation-link" style="color: var(--accent-color); text-decoration: none;">{{ rel.kilde_titel }}</a>
{% endif %}
</div>
<button onclick="deleteRelation({{ rel.id }})" class="btn btn-sm" style="background-color: #e74c3c; color: white; padding: 0.25rem 0.5rem;"></button>
</div>
{% endfor %}
{% else %}
<p style="color: #999;">Ingen relaterede sager.</p>
{% endif %}
</div>
<div class="card-footer">
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<div style="flex: 1; min-width: 200px; position: relative;">
<input type="text" id="relationCaseSearch" placeholder="Søg efter relateret sag..." class="form-control" style="font-size: 0.9rem;">
<div id="relationSearchResults" style="position: absolute; top: 100%; left: 0; right: 0; background: var(--bg-body); border: 1px solid #ddd; max-height: 200px; overflow-y: auto; display: none; z-index: 10;"></div>
</div>
<select id="relationTypeSelect" class="form-control" style="flex: 1; min-width: 150px;">
<option value="">Vælg relationstype</option>
<option value="relateret">Relateret</option>
<option value="afhænger af">Afhænger af</option>
<option value="blokkerer">Blokkerer</option>
<option value="duplikat">Duplikat</option>
</select>
<button onclick="addRelation()" class="btn" style="background-color: var(--accent-color); color: white; padding: 0.5rem 1rem;">+</button>
</div>
</div>
</div>
<!-- Contacts Section -->
{% if contacts %}
<div class="card">
<div class="card-header">
<h5 style="margin: 0;">👥 Tilknyttede kontakter</h5>
</div>
<div class="card-body">
{% for contact in contacts %}
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; border-bottom: 1px solid #eee;">
<div>
<strong>{{ contact.contact_name }}</strong>
<div style="font-size: 0.9rem; color: #666;">{{ contact.role }}</div>
{% if contact.contact_email %}
<div style="font-size: 0.85rem; color: #999;">{{ contact.contact_email }}</div>
{% endif %}
</div>
<button onclick="removeContact({{ case.id }}, {{ contact.contact_id }})" class="btn btn-sm" style="background-color: #e74c3c; color: white; padding: 0.25rem 0.5rem;"></button>
</div>
{% endfor %}
</div>
<div class="card-footer">
<div style="position: relative;">
<input type="text" id="contactSearch" placeholder="Søg efter kontakt..." class="form-control" style="font-size: 0.9rem;">
<div id="contactSearchResults" style="position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #ddd; max-height: 200px; overflow-y: auto; display: none; z-index: 10;"></div>
</div>
</div>
</div>
{% else %}
<div class="card">
<div class="card-header">
<h5 style="margin: 0;">👥 Tilknyttede kontakter</h5>
</div>
<div class="card-body">
<p style="color: #999;">Ingen kontakter tilknyttet denne sag.</p>
</div>
<div class="card-footer">
<div style="position: relative;">
<input type="text" id="contactSearch" placeholder="Søg efter kontakt..." class="form-control" style="font-size: 0.9rem;">
<div id="contactSearchResults" style="position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #ddd; max-height: 200px; overflow-y: auto; display: none; z-index: 10;"></div>
</div>
</div>
</div>
{% endif %}
<!-- Customers Section -->
{% if customers %}
<div class="card">
<div class="card-header">
<h5 style="margin: 0;">🏢 Tilknyttede kunder</h5>
</div>
<div class="card-body">
{% for customer in customers %}
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; border-bottom: 1px solid #eee;">
<div>
<strong><a href="/customers/{{ customer.customer_id }}" style="color: var(--accent-color); text-decoration: none;">{{ customer.customer_name }}</a></strong>
<div style="font-size: 0.9rem; color: #666;">{{ customer.role }}</div>
{% if customer.customer_email %}
<div style="font-size: 0.85rem; color: #999;">{{ customer.customer_email }}</div>
{% endif %}
</div>
<button onclick="removeCustomer({{ case.id }}, {{ customer.customer_id }})" class="btn btn-sm" style="background-color: #e74c3c; color: white; padding: 0.25rem 0.5rem;"></button>
</div>
{% endfor %}
</div>
<div class="card-footer">
<div style="position: relative;">
<input type="text" id="customerSearch" placeholder="Søg efter kunde..." class="form-control" style="font-size: 0.9rem;">
<div id="customerSearchResults" style="position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #ddd; max-height: 200px; overflow-y: auto; display: none; z-index: 10;"></div>
</div>
</div>
</div>
{% else %}
<div class="card">
<div class="card-header">
<h5 style="margin: 0;">🏢 Tilknyttede kunder</h5>
</div>
<div class="card-body">
<p style="color: #999;">Ingen kunder tilknyttet denne sag.</p>
</div>
<div class="card-footer">
<div style="position: relative;">
<input type="text" id="customerSearch" placeholder="Søg efter kunde..." class="form-control" style="font-size: 0.9rem;">
<div id="customerSearchResults" style="position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #ddd; max-height: 200px; overflow-y: auto; display: none; z-index: 10;"></div>
</div>
</div>
</div>
{% endif %}
<!-- Action Buttons -->
<div style="margin-top: 2rem; display: flex; gap: 1rem; flex-wrap: wrap;">
<a href="/cases/{{ case.id }}/edit" class="btn" style="background-color: var(--accent-color); color: white;">✏️ Rediger</a>
<button class="btn" style="background-color: #e74c3c; color: white;" onclick="if(confirm('Slet denne sag?')) { fetch('/api/v1/cases/{{ case.id }}', {method: 'DELETE'}).then(() => window.location='/cases'); }">🗑️ Slet</button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const caseId = {{ case.id }};
let contactSearchTimeout;
let customerSearchTimeout;
// Initialize search functionality when DOM is ready
function initializeSearch() {
const contactSearchInput = document.getElementById('contactSearch');
const customerSearchInput = document.getElementById('customerSearch');
if (contactSearchInput) {
contactSearchInput.addEventListener('input', function(e) {
clearTimeout(contactSearchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
document.getElementById('contactSearchResults').style.display = 'none';
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 style="padding: 10px; color: #999;">Ingen kontakter fundet</div>';
} else {
resultsDiv.innerHTML = contacts.map(c => `
<div style="padding: 10px; border-bottom: 1px solid #eee; cursor: pointer;"
onmouseover="this.style.background='#f5f5f5'"
onmouseout="this.style.background='white'"
onclick="addContact(${caseId}, ${c.id}, '${(c.first_name + ' ' + c.last_name).replace(/'/g, "\\'")}')">
<strong>${c.first_name} ${c.last_name}</strong>
<div style="font-size: 0.85rem; color: #999;">${c.email || ''} ${c.user_company ? '(' + c.user_company + ')' : ''}</div>
</div>
`).join('');
}
resultsDiv.style.display = 'block';
} catch (err) {
console.error('Error searching contacts:', err);
}
}, 300);
});
}
if (customerSearchInput) {
customerSearchInput.addEventListener('input', function(e) {
clearTimeout(customerSearchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
document.getElementById('customerSearchResults').style.display = 'none';
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 style="padding: 10px; color: #999;">Ingen kunder fundet</div>';
} else {
resultsDiv.innerHTML = customers.map(c => `
<div style="padding: 10px; border-bottom: 1px solid #eee; cursor: pointer;"
onmouseover="this.style.background='#f5f5f5'"
onmouseout="this.style.background='white'"
onclick="addCustomer(${caseId}, ${c.id}, '${c.name.replace(/'/g, "\\'")}')">
<strong>${c.name}</strong>
<div style="font-size: 0.85rem; color: #999;">${c.email || ''} ${c.cvr_number ? '(CVR: ' + c.cvr_number + ')' : ''}</div>
</div>
`).join('');
}
resultsDiv.style.display = 'block';
} catch (err) {
console.error('Error searching customers:', err);
}
}, 300);
});
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeSearch);
} else {
initializeSearch();
}
async function addContact(caseId, contactId, contactName) {
try {
const response = await fetch(`/api/v1/cases/${caseId}/contacts`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({contact_id: contactId, role: 'Kontakt'})
});
if (response.ok) {
const contactSearch = document.getElementById('contactSearch');
if (contactSearch) contactSearch.value = '';
const contactResults = document.getElementById('contactSearchResults');
if (contactResults) contactResults.style.display = 'none';
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 addCustomer(caseId, customerId, customerName) {
try {
const response = await fetch(`/api/v1/cases/${caseId}/customers`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({customer_id: customerId, role: 'Kunde'})
});
if (response.ok) {
const customerSearch = document.getElementById('customerSearch');
if (customerSearch) customerSearch.value = '';
const customerResults = document.getElementById('customerSearchResults');
if (customerResults) customerResults.style.display = 'none';
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 removeContact(caseId, contactId) {
if (confirm('Fjern denne kontakt fra sagen?')) {
const response = await fetch(`/api/v1/cases/${caseId}/contacts/${contactId}`, {method: 'DELETE'});
if (response.ok) {
window.location.reload();
} else {
alert('Fejl ved fjernelse af kontakt');
}
}
}
async function removeCustomer(caseId, customerId) {
if (confirm('Fjern denne kunde fra sagen?')) {
const response = await fetch(`/api/v1/cases/${caseId}/customers/${customerId}`, {method: 'DELETE'});
if (response.ok) {
window.location.reload();
} else {
alert('Fejl ved fjernelse af kunde');
}
}
}
// Tag Management Functions
async function addTag() {
const tagInput = document.getElementById('newTagInput');
const tagName = tagInput.value.trim();
if (!tagName) {
alert('Indtast et tag navn');
return;
}
try {
const response = await fetch(`/api/v1/cases/${caseId}/tags`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({tag_navn: tagName})
});
if (response.ok) {
tagInput.value = '';
window.location.reload();
} else {
const error = await response.json();
alert(`Fejl: ${error.detail}`);
}
} catch (err) {
alert('Fejl ved tilføjelse af tag: ' + err.message);
}
}
async function deleteTag(tagId) {
if (confirm('Fjern dette tag?')) {
try {
const response = await fetch(`/api/v1/cases/${caseId}/tags/${tagId}`, {method: 'DELETE'});
if (response.ok) {
window.location.reload();
} else {
alert('Fejl ved fjernelse af tag');
}
} catch (err) {
alert('Fejl: ' + err.message);
}
}
}
async function toggleTagState(tagId, currentState) {
const newState = currentState === 'open' ? 'closed' : 'open';
try {
const response = await fetch(`/api/v1/cases/${caseId}/tags/${tagId}/state`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({state: newState})
});
if (response.ok) {
window.location.reload();
} else {
alert('Fejl ved opdatering af tag status');
}
} catch (err) {
alert('Fejl: ' + err.message);
}
}
// Relation Management Functions
let selectedRelationCaseId = null;
function initializeRelationSearch() {
const relationSearchInput = document.getElementById('relationCaseSearch');
if (!relationSearchInput) return;
relationSearchInput.addEventListener('input', function(e) {
clearTimeout(relationSearchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
document.getElementById('relationSearchResults').style.display = 'none';
return;
}
relationSearchTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/v1/search/cases?q=${encodeURIComponent(query)}`);
const cases = await response.json();
const resultsDiv = document.getElementById('relationSearchResults');
if (cases.length === 0) {
resultsDiv.innerHTML = '<div style="padding: 10px; color: #999;">Ingen sager fundet</div>';
} else {
resultsDiv.innerHTML = cases
.filter(c => c.id !== caseId) // Exclude current case
.map(c => `
<div onclick="selectRelationCase(${c.id}, '${c.titel.replace(/'/g, "\\'")}');" style="padding: 10px; cursor: pointer; border-bottom: 1px solid #eee; transition: background 0.2s;" onmouseover="this.style.background='var(--accent-light)'" onmouseout="this.style.background='transparent'">
<div style="font-weight: 600; color: var(--accent-color);">${c.titel}</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">Status: ${c.status}</div>
</div>
`).join('');
}
resultsDiv.style.display = 'block';
} catch (err) {
console.error('Error searching cases:', err);
}
}, 300);
});
}
function selectRelationCase(caseIdValue, caseTitel) {
selectedRelationCaseId = caseIdValue;
document.getElementById('relationCaseSearch').value = caseTitel;
document.getElementById('relationSearchResults').style.display = 'none';
}
async function addRelation() {
const relationType = document.getElementById('relationTypeSelect').value;
if (!selectedRelationCaseId) {
alert('Vælg en sag');
return;
}
if (!relationType) {
alert('Vælg en relationstype');
return;
}
try {
const response = await fetch(`/api/v1/cases/${caseId}/relations`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
målsag_id: selectedRelationCaseId,
relationstype: relationType
})
});
if (response.ok) {
selectedRelationCaseId = null;
document.getElementById('relationCaseSearch').value = '';
document.getElementById('relationTypeSelect').value = '';
window.location.reload();
} else {
const error = await response.json();
alert(`Fejl: ${error.detail}`);
}
} catch (err) {
alert('Fejl ved tilføjelse af relation: ' + err.message);
}
}
// Initialize Global Elements
document.addEventListener('DOMContentLoaded', () => {
// Render Global Tags
if (window.renderEntityTags) {
window.renderEntityTags('case', {{ case.id }}, 'case-tags');
}
// Load Hardware & Locations
loadCaseHardware();
loadCaseLocations();
});
// ============ Hardware Handling ============
async function loadCaseHardware() {
try {
const res = await fetch(`/api/v1/cases/{{ 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/cases/{{ 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/cases/{{ 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/cases/{{ 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/cases/{{ 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/cases/{{ case.id }}/locations/${locId}`, { method: 'DELETE' });
loadCaseLocations();
} catch (e) {
alert("Fejl ved sletning");
}
}
// Initialize relation search when DOM is ready
let relationSearchTimeout;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeRelationSearch);
} else {
initializeRelationSearch();
}
</script>
</div>
{% endblock %}