2026-01-31 23:16:24 +01:00
{% extends "shared/frontend/base.html" %}
{% block title %}{{ hardware.brand }} {{ hardware.model }} - Hardware - BMC Hub{% endblock %}
{% block extra_css %}
< style >
.info-row {
display: flex;
justify-content: space-between;
2026-02-11 13:23:32 +01:00
padding: 0.8rem 0;
border-bottom: 1px solid rgba(0,0,0,0.05);
2026-01-31 23:16:24 +01:00
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
2026-02-11 13:23:32 +01:00
font-weight: 600;
2026-01-31 23:16:24 +01:00
color: var(--text-secondary);
2026-02-11 13:23:32 +01:00
min-width: 150px;
2026-01-31 23:16:24 +01:00
}
.info-value {
color: var(--text-primary);
2026-02-11 13:23:32 +01:00
word-break: break-all;
2026-01-31 23:16:24 +01:00
}
.action-card {
background: white;
2026-02-11 13:23:32 +01:00
padding: 1rem;
border-radius: 8px;
2026-01-31 23:16:24 +01:00
text-align: center;
border: 1px dashed rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.2s;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
2026-02-11 13:23:32 +01:00
gap: 0.5rem;
2026-01-31 23:16:24 +01:00
color: var(--text-secondary);
}
2026-02-11 13:23:32 +01:00
2026-01-31 23:16:24 +01:00
.action-card:hover {
border-color: var(--accent);
background: var(--accent-light);
color: var(--accent);
2026-02-11 13:23:32 +01:00
text-decoration: none;
2026-01-31 23:16:24 +01:00
}
.action-card i {
2026-02-11 13:23:32 +01:00
font-size: 1.5rem;
2026-01-31 23:16:24 +01:00
}
2026-02-11 13:23:32 +01:00
/* Timeline Styling */
2026-01-31 23:16:24 +01:00
.timeline {
position: relative;
2026-02-11 13:23:32 +01:00
padding-left: 1.5rem;
2026-01-31 23:16:24 +01:00
}
.timeline::before {
content: '';
position: absolute;
2026-02-11 13:23:32 +01:00
left: 0.4rem;
2026-01-31 23:16:24 +01:00
top: 0;
bottom: 0;
width: 2px;
background: #e9ecef;
}
.timeline-item {
position: relative;
padding-bottom: 1.5rem;
}
.timeline-marker {
position: absolute;
2026-02-11 13:23:32 +01:00
left: -1.09rem;
top: 0.3rem;
width: 0.8rem;
height: 0.8rem;
2026-01-31 23:16:24 +01:00
border-radius: 50%;
background: white;
border: 2px solid var(--accent);
}
.timeline-item.active .timeline-marker {
background: #28a745;
border-color: #28a745;
}
2026-02-11 13:23:32 +01:00
/* Quick Info Bar */
.quick-info-label {
color: var(--accent);
font-weight: 700;
margin-right: 0.4rem;
}
.quick-info-item {
2026-01-31 23:16:24 +01:00
display: flex;
align-items: center;
2026-02-11 13:23:32 +01:00
border-right: 1px solid rgba(0,0,0,0.1);
padding-right: 0.75rem;
margin-right: 0.75rem;
}
.quick-info-item:last-child {
border-right: none;
margin-right: 0;
padding-right: 0;
2026-01-31 23:16:24 +01:00
}
< / style >
{% endblock %}
{% block content %}
2026-02-11 13:23:32 +01:00
< div class = "container-fluid" style = "margin-top: 2rem; margin-bottom: 2rem; max-width: 1400px;" >
<!-- Top Bar: Back Link + Global Tags -->
< div class = "d-flex justify-content-between align-items-start mb-2" >
< a href = "/hardware" class = "back-link text-decoration-none" >
< i class = "bi bi-chevron-left" > < / i > Tilbage til hardware
< / a >
<!-- Global Tags Area -->
< div class = "d-flex align-items-center p-2 rounded" style = "background: rgba(0,0,0,0.02);" >
< i class = "bi bi-tags text-muted me-2 small" > < / i >
< div id = "hardware-tags" class = "d-flex flex-wrap justify-content-end gap-1 align-items-center" >
< span class = "spinner-border spinner-border-sm text-muted" > < / span >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 13:23:32 +01:00
< button class = "btn btn-sm btn-link text-decoration-none ms-1 px-1 py-0 text-muted hover-primary"
onclick="window.showTagPicker('hardware', {{ hardware.id }}, () => window.renderEntityTags('hardware', {{ hardware.id }}, 'hardware-tags'))"
title="Tilføj tag">
< i class = "bi bi-plus-lg" > < / i >
< / button >
2026-01-31 23:16:24 +01:00
< / div >
< / div >
2026-02-11 13:23:32 +01:00
<!-- 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" style = "font-size: 0.85rem;" >
<!-- ID -->
< div class = "quick-info-item" >
< span class = "quick-info-label" > ID:< / span >
< span > {{ hardware.id }}< / span >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 13:23:32 +01:00
<!-- Brand / Model -->
< div class = "quick-info-item" >
< span class = "quick-info-label" > Hardware:< / span >
< span class = "fw-bold" > {{ hardware.brand or 'Unknown' }} {{ hardware.model or '' }}< / span >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 13:23:32 +01:00
<!-- Serial -->
{% if hardware.serial_number %}
< div class = "quick-info-item" >
< span class = "quick-info-label" > S/N:< / span >
< span class = "font-monospace text-muted" > {{ hardware.serial_number }}< / span >
2026-02-10 14:40:38 +01:00
< / div >
2026-02-11 13:23:32 +01:00
{% endif %}
<!-- Customer (Current Owner) -->
{% set current_owner = ownership[0] if ownership else None %}
2026-02-15 11:12:58 +01:00
{% set owner_contact_ns = namespace(contact=None) %}
{% if contacts %}
{% for c in contacts %}
{% if c.role == 'primary' and owner_contact_ns.contact is none %}
{% set owner_contact_ns.contact = c %}
{% endif %}
{% endfor %}
{% endif %}
2026-02-11 13:23:32 +01:00
{% if current_owner and not current_owner.end_date %}
< div class = "quick-info-item" >
< span class = "quick-info-label" > Ejer:< / span >
< span > {{ current_owner.customer_name or current_owner.owner_type|title }}< / span >
2026-02-10 14:40:38 +01:00
< / div >
2026-02-11 13:23:32 +01:00
{% endif %}
<!-- Location (Current) -->
{% set current_loc = locations[0] if locations else None %}
{% if current_loc and not current_loc.end_date %}
< div class = "quick-info-item" >
< span class = "quick-info-label" > Lokation:< / span >
< span > {{ current_loc.location_name }}< / span >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 13:23:32 +01:00
{% endif %}
<!-- Status -->
< div class = "quick-info-item" >
< span class = "quick-info-label" > Status:< / span >
< span class = "badge {% if hardware.status == 'active' %}bg-success{% elif hardware.status == 'retired' %}bg-secondary{% elif hardware.status == 'in_repair' %}bg-primary{% else %}bg-warning{% endif %}" style = "font-size: 0.7rem;" >
{{ hardware.status|replace('_', ' ')|title }}
< / span >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 13:23:32 +01:00
<!-- Link to Edit -->
< div class = "ms-auto d-flex gap-2" >
2026-02-11 23:51:21 +01:00
< button onclick = "syncEsetData()" class = "btn btn-sm btn-outline-secondary" title = "Opdater fra ESET" >
< i class = "bi bi-arrow-repeat" > < / i >
< / button >
2026-02-11 13:23:32 +01:00
< a href = "/hardware/{{ hardware.id }}/edit" class = "btn btn-sm btn-outline-primary" title = "Rediger" >
< i class = "bi bi-pencil" > < / i >
< / a >
< button onclick = "deleteHardware()" class = "btn btn-sm btn-outline-danger" title = "Slet" >
< i class = "bi bi-trash" > < / i >
2026-01-31 23:16:24 +01:00
< / button >
< / div >
2026-02-11 13:23:32 +01:00
< / div >
< / div >
< / div >
<!-- Tabs Navigation -->
< ul class = "nav nav-tabs mb-4 px-2" id = "hwTabs" role = "tablist" >
< li class = "nav-item" role = "presentation" >
< button class = "nav-link active" id = "details-tab" data-bs-toggle = "tab" data-bs-target = "#details" type = "button" role = "tab" >
< i class = "bi bi-card-text me-2" > < / i > Hardware Detaljer
< / button >
< / li >
2026-02-11 23:51:21 +01:00
< li class = "nav-item" role = "presentation" >
< button class = "nav-link" id = "eset-tab" data-bs-toggle = "tab" data-bs-target = "#eset-specs" type = "button" role = "tab" >
< i class = "bi bi-cpu me-2" > < / i > Specifikationer
< / button >
< / li >
2026-02-11 13:23:32 +01:00
< li class = "nav-item" role = "presentation" >
< button class = "nav-link" id = "history-tab" data-bs-toggle = "tab" data-bs-target = "#history" type = "button" role = "tab" >
< i class = "bi bi-clock-history me-2" > < / i > Historik
< / button >
< / li >
< li class = "nav-item" role = "presentation" >
< button class = "nav-link" id = "files-tab" data-bs-toggle = "tab" data-bs-target = "#files" type = "button" role = "tab" >
< i class = "bi bi-paperclip me-2" > < / i > Filer ({{ attachments|length }})
< / button >
< / li >
< li class = "nav-item" role = "presentation" >
< button class = "nav-link" id = "notes-tab" data-bs-toggle = "tab" data-bs-target = "#notes" type = "button" role = "tab" >
< i class = "bi bi-sticky me-2" > < / i > Noter
< / button >
< / li >
< / ul >
< div class = "tab-content" id = "hwTabsContent" >
<!-- Tab: Details -->
< div class = "tab-pane fade show active" id = "details" role = "tabpanel" >
< div class = "row g-4" >
<!-- Left Column -->
< div class = "col-lg-8" >
<!-- Basic Info Card -->
< div class = "card mb-4 shadow-sm border-0" >
< div class = "card-header bg-white border-bottom-0 pt-3 ps-3" >
< h6 class = "text-primary mb-0" > < i class = "bi bi-info-circle me-2" > < / i > Stamdata< / h6 >
< / div >
< div class = "card-body" >
< div class = "info-row" >
< span class = "info-label" > Type< / span >
< span class = "info-value" >
{% if hardware.asset_type == 'pc' %}🖥️ PC
{% elif hardware.asset_type == 'laptop' %}💻 Laptop
{% elif hardware.asset_type == 'printer' %}🖨️ Printer
{% elif hardware.asset_type == 'skærm' %}🖥️ Skærm
{% elif hardware.asset_type == 'telefon' %}📱 Telefon
{% elif hardware.asset_type == 'server' %}🗄️ Server
{% elif hardware.asset_type == 'netværk' %}🌐 Netværk
{% else %}📦 {{ hardware.asset_type|title }}
{% endif %}
< / span >
< / div >
< div class = "info-row" >
< span class = "info-label" > Mærke/Model< / span >
< span class = "info-value" > {{ hardware.brand or '-' }} / {{ hardware.model or '-' }}< / span >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 23:51:21 +01:00
{% if hardware.eset_uuid %}
< div class = "info-row" >
< span class = "info-label" > ESET UUID< / span >
< span class = "info-value" style = "word-break: break-all;" > {{ hardware.eset_uuid }}< / span >
< / div >
{% endif %}
{% if hardware.eset_group %}
< div class = "info-row" >
< span class = "info-label" > ESET Gruppe< / span >
< span class = "info-value" style = "word-break: break-all;" > {{ hardware.eset_group }}< / span >
< / div >
{% endif %}
2026-02-11 13:23:32 +01:00
{% if hardware.internal_asset_id %}
< div class = "info-row" >
< span class = "info-label" > Internt Asset ID< / span >
< span class = "info-value" > {{ hardware.internal_asset_id }}< / span >
2026-01-31 23:16:24 +01:00
< / div >
{% endif %}
2026-02-11 13:23:32 +01:00
{% if hardware.customer_asset_id %}
< div class = "info-row" >
< span class = "info-label" > Kunde Asset ID< / span >
< span class = "info-value" > {{ hardware.customer_asset_id }}< / span >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 13:23:32 +01:00
{% endif %}
{% if hardware.warranty_until %}
< div class = "info-row" >
< span class = "info-label" > Garanti til< / span >
< span class = "info-value" > {{ hardware.warranty_until }}< / span >
< / div >
{% endif %}
{% if hardware.end_of_life %}
< div class = "info-row" >
< span class = "info-label" > End of Life< / span >
< span class = "info-value" > {{ hardware.end_of_life }}< / span >
< / div >
{% endif %}
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 13:23:32 +01:00
< / div >
2026-01-31 23:16:24 +01:00
2026-02-11 23:51:21 +01:00
2026-02-11 13:23:32 +01:00
<!-- AnyDesk Card -->
{% set anydesk_url = hardware.anydesk_id and ('anydesk://' ~ hardware.anydesk_id) %}
< div class = "card mb-4 shadow-sm border-0" >
< div class = "card-header bg-white border-bottom-0 pt-3 ps-3 d-flex justify-content-between align-items-center" >
< h6 class = "text-danger mb-0" > < i class = "bi bi-display me-2" > < / i > Remote Access< / h6 >
< a class = "btn btn-sm btn-link p-0" href = "/hardware/{{ hardware.id }}/edit#anydesk" title = "Rediger AnyDesk" >
Ændre
< / a >
< / div >
< div class = "card-body" >
< div class = "info-row" >
< span class = "info-label" > AnyDesk ID< / span >
< span class = "info-value fw-bold font-monospace" >
{% if anydesk_url %}
< a href = "{{ anydesk_url }}" target = "_blank" rel = "noreferrer noopener" > {{ hardware.anydesk_id }}< / a >
{% else %}
-
{% endif %}
< / span >
< / div >
< div class = "info-row" >
< span class = "info-label" > Handling< / span >
< span class = "info-value" >
{% if anydesk_url %}
< a href = "{{ anydesk_url }}" target = "_blank" rel = "noreferrer noopener" class = "btn btn-sm btn-outline-danger" >
< i class = "bi bi-lightning-charge me-1" > < / i > Connect AnyDesk
< / a >
{% else %}
< span class = "text-muted small" > Ingen link< / span >
{% endif %}
< / span >
2026-01-31 23:16:24 +01:00
< / div >
< / div >
< / div >
2026-02-11 13:23:32 +01:00
<!-- Location & Owner Grid -->
< div class = "row" >
< div class = "col-md-6" >
< div class = "card h-100 shadow-sm border-0" >
< div class = "card-header bg-white border-bottom-0 pt-3 ps-3 d-flex justify-content-between align-items-center" >
< h6 class = "text-primary mb-0" > < i class = "bi bi-geo-alt me-2" > < / i > Lokation< / h6 >
< button class = "btn btn-sm btn-link p-0" data-bs-toggle = "modal" data-bs-target = "#locationModal" > Ændre< / button >
< / div >
< div class = "card-body" >
{% if current_loc and not current_loc.end_date %}
< div class = "text-center py-3" >
< div class = "fs-4 mb-2" > < i class = "bi bi-building" > < / i > < / div >
< h5 class = "fw-bold" > {{ current_loc.location_name }}< / h5 >
< p class = "text-muted small mb-0" > Siden: {{ current_loc.start_date }}< / p >
{% if current_loc.notes %}
< div class = "mt-2 text-muted fst-italic small" > "{{ current_loc.notes }}"< / div >
{% endif %}
2026-01-31 23:16:24 +01:00
< / div >
{% else %}
2026-02-11 13:23:32 +01:00
< div class = "text-center py-4 text-muted" >
< p class = "mb-2" > Ingen aktiv lokation< / p >
< button class = "btn btn-sm btn-outline-primary" data-bs-toggle = "modal" data-bs-target = "#locationModal" > Tildel nu< / button >
< / div >
2026-01-31 23:16:24 +01:00
{% endif %}
< / div >
2026-02-11 13:23:32 +01:00
< / div >
< / div >
< div class = "col-md-6" >
< div class = "card h-100 shadow-sm border-0" >
2026-02-15 11:12:58 +01:00
< div class = "card-header bg-white border-bottom-0 pt-3 ps-3 d-flex justify-content-between align-items-center" >
2026-02-11 13:23:32 +01:00
< h6 class = "text-success mb-0" > < i class = "bi bi-person me-2" > < / i > Ejer< / h6 >
2026-02-15 11:12:58 +01:00
< button class = "btn btn-sm btn-link p-0" data-bs-toggle = "modal" data-bs-target = "#ownerModal" > Ændre< / button >
2026-02-11 13:23:32 +01:00
< / div >
< div class = "card-body" >
{% if current_owner and not current_owner.end_date %}
< div class = "text-center py-3" >
< div class = "fs-4 mb-2 text-success" > < i class = "bi bi-person-badge" > < / i > < / div >
< h5 class = "fw-bold" > {{ current_owner.customer_name or current_owner.owner_type|title }}< / h5 >
2026-02-15 11:12:58 +01:00
{% if owner_contact_ns.contact %}
< p class = "mb-1" >
< span class = "badge bg-light text-dark border" > {{ owner_contact_ns.contact.first_name }} {{ owner_contact_ns.contact.last_name }}< / span >
< / p >
{% endif %}
2026-02-11 13:23:32 +01:00
< p class = "text-muted small mb-0" > Siden: {{ current_owner.start_date }}< / p >
2026-01-31 23:16:24 +01:00
< / div >
{% else %}
2026-02-11 13:23:32 +01:00
< div class = "text-center py-4 text-muted" >
< p class = "mb-0" > Ingen aktiv ejer< / p >
2026-02-15 11:12:58 +01:00
< button class = "btn btn-sm btn-outline-success mt-2" data-bs-toggle = "modal" data-bs-target = "#ownerModal" > Sæt ejer< / button >
2026-02-11 13:23:32 +01:00
< / div >
2026-01-31 23:16:24 +01:00
{% endif %}
< / div >
< / div >
< / div >
2026-02-11 13:23:32 +01:00
< / div >
< / div >
<!-- Right Column: Quick Actions & Related -->
< div class = "col-lg-4" >
<!-- Quick Actions -->
< div class = "row g-2 mb-4" >
< div class = "col-6" >
< a href = "#" class = "action-card text-decoration-none" onclick = "alert('Funktion: Opret Sag til dette hardware (kommer snart)')" >
< i class = "bi bi-ticket-perforated text-primary" > < / i >
< span class = "small fw-bold" > Opret Sag< / span >
< / a >
< / div >
< div class = "col-6" >
< div class = "action-card" data-bs-toggle = "modal" data-bs-target = "#locationModal" >
< i class = "bi bi-geo-alt text-primary" > < / i >
< span class = "small fw-bold" > Skift Lokation< / span >
< / div >
< / div >
< div class = "col-6" >
< a href = "/app/locations" class = "action-card text-decoration-none" >
< i class = "bi bi-building-add text-secondary" > < / i >
< span class = "small fw-bold" > Ny Lokation< / span >
< / a >
< / div >
< div class = "col-6" >
< div class = "action-card" onclick = "alert('Funktion: Upload bilag (kommer snart)')" >
< i class = "bi bi-paperclip text-secondary" > < / i >
< span class = "small fw-bold" > Tilføj Bilag< / span >
< / div >
< / div >
< / div >
2026-01-31 23:16:24 +01:00
2026-02-15 11:12:58 +01:00
<!-- Contacts -->
< div class = "card shadow-sm border-0 mb-4" >
< div class = "card-header bg-white border-bottom-0 pt-3 ps-3 d-flex justify-content-between align-items-center" >
< h6 class = "text-secondary mb-0 fw-bold small" > < i class = "bi bi-person-lines-fill me-2" > < / i > Kontaktpersoner< / h6 >
< button type = "button" class = "btn btn-sm btn-link text-decoration-none p-0" data-bs-toggle = "modal" data-bs-target = "#addContactModal" >
< i class = "bi bi-plus-lg" > < / i >
< / button >
< / div >
< div class = "list-group list-group-flush" >
{% if contacts %}
{% for contact in contacts %}
< div class = "list-group-item border-0 px-3 py-2" >
< div class = "d-flex w-100 justify-content-between align-items-center" >
< div class = "text-truncate" style = "max-width: 85%;" >
< div class = "fw-bold text-dark small" >
{{ contact.first_name }} {{ contact.last_name }}
{% if contact.role == 'primary' %}< span class = "badge bg-info text-dark ms-1" style = "font-size: 0.6rem;" > Primær< / span > {% endif %}
{% if contact.source == 'eset' %}< span class = "badge bg-light text-muted border ms-1" style = "font-size: 0.6rem;" > ESET< / span > {% endif %}
< / div >
< div class = "text-muted small" style = "font-size: 0.75rem;" >
{% if contact.email %}< a href = "mailto:{{ contact.email }}" class = "text-muted text-decoration-none" > < i class = "bi bi-envelope me-1" > < / i > < / a > {% endif %}
{% if contact.phone %}< a href = "tel:{{ contact.phone }}" class = "text-muted text-decoration-none" > < i class = "bi bi-phone me-1" > < / i > {{ contact.phone }}< / a > {% endif %}
< / div >
< / div >
< form action = "/hardware/{{ hardware.id }}/contacts/{{ contact.contact_id }}/delete" method = "POST" onsubmit = "return confirm('Er du sikker på at du vil fjerne denne kontakt?');" >
< button type = "submit" class = "btn btn-sm btn-link text-danger p-0" title = "Fjern kontakt" >
< i class = "bi bi-trash" > < / i >
< / button >
< / form >
< / div >
< / div >
{% endfor %}
{% else %}
< div class = "text-center py-3 text-muted small" >
Ingen kontakter tilknyttet
< / div >
{% endif %}
< / div >
< / div >
2026-02-11 13:23:32 +01:00
<!-- Linked Cases -->
< div class = "card shadow-sm border-0 mb-4" >
< div class = "card-header bg-white border-bottom-0 pt-3 ps-3" >
< h6 class = "text-secondary mb-0" > < i class = "bi bi-briefcase me-2" > < / i > Seneste Sager< / h6 >
< / div >
< div class = "list-group list-group-flush" >
2026-01-31 23:16:24 +01:00
{% if cases and cases|length > 0 %}
2026-02-11 13:23:32 +01:00
{% for case in cases[:5] %}
< a href = "/sag/{{ case.case_id }}" class = "list-group-item list-group-item-action border-0 px-3 py-2" >
< div class = "d-flex w-100 justify-content-between align-items-center" >
< div class = "text-truncate" style = "max-width: 70%;" >
< i class = "bi bi-ticket me-1 text-muted small" > < / i >
< span class = "small fw-bold text-dark" > {{ case.titel }}< / span >
< / div >
< span class = "badge bg-light text-dark border" > {{ case.status }}< / span >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 13:23:32 +01:00
< div class = "small text-muted ms-3" > {{ case.created_at.strftime('%Y-%m-%d') if case.created_at else '' }}< / div >
2026-01-31 23:16:24 +01:00
< / a >
{% endfor %}
2026-02-11 13:23:32 +01:00
{% if cases|length > 5 %}
< div class = "card-footer bg-white text-center p-2" >
< a href = "#cases-tab" class = "small text-decoration-none" onclick = "document.getElementById('cases-tab').click()" > Se alle {{ cases|length }} sager< / a >
< / div >
{% endif %}
2026-01-31 23:16:24 +01:00
{% else %}
2026-02-11 13:23:32 +01:00
< div class = "text-center py-4 text-muted small" >
Ingen sager tilknyttet
2026-01-31 23:16:24 +01:00
< / div >
{% endif %}
< / div >
2026-02-11 13:23:32 +01:00
< / div >
< / div >
< / div >
< / div >
2026-01-31 23:16:24 +01:00
2026-02-11 23:51:21 +01:00
<!-- Tab: Specs -->
< div class = "tab-pane fade" id = "eset-specs" role = "tabpanel" >
< div class = "card shadow-sm border-0" >
< div class = "card-header bg-white border-bottom-0 pt-3 ps-3" >
< h6 class = "text-primary mb-0" > < i class = "bi bi-cpu me-2" > < / i > Specifikationer< / h6 >
< / div >
< div class = "card-body" >
2026-02-14 02:26:29 +01:00
{% if eset_specs and (eset_specs.os_name or eset_specs.primary_local_ip or eset_specs.cpu_models or eset_specs.deployed_components) %}
2026-02-11 23:51:21 +01:00
< div class = "table-responsive" >
< table class = "table table-sm align-middle" >
< tbody >
< tr >
< th style = "width: 200px;" > Enhedsnavn< / th >
< td > {{ eset_specs.device_name or '-' }}< / td >
< / tr >
< tr >
< th > OS< / th >
< td > {{ eset_specs.os_name or '-' }}{% if eset_specs.os_version %} ({{ eset_specs.os_version }}){% endif %}< / td >
< / tr >
< tr >
< th > Sidst sync< / th >
< td > {{ eset_specs.last_sync_time or '-' }}< / td >
< / tr >
< tr >
< th > Status< / th >
< td > {{ eset_specs.functionality_status or '-' }}< / td >
< / tr >
< tr >
< th > Device type< / th >
< td > {{ eset_specs.device_type or '-' }}< / td >
< / tr >
< tr >
< th > Local IP< / th >
< td > {{ eset_specs.primary_local_ip or '-' }}< / td >
< / tr >
< tr >
< th > Public IP< / th >
< td > {{ eset_specs.public_ip or '-' }}< / td >
< / tr >
< tr >
< th > Chassis< / th >
< td > {{ eset_specs.manufacturer or '-' }} / {{ eset_specs.model or '-' }}< / td >
< / tr >
< tr >
< th > BIOS Serial< / th >
< td > {{ eset_specs.bios_serial or '-' }}< / td >
< / tr >
< tr >
< th > CPU< / th >
< td > {{ eset_specs.cpu_models | join(', ') if eset_specs.cpu_models else '-' }}< / td >
< / tr >
< tr >
< th > Disk< / th >
< td > {{ eset_specs.disk_summaries | join(', ') if eset_specs.disk_summaries else (eset_specs.disk_models | join(', ') if eset_specs.disk_models else '-') }}< / td >
< / tr >
< tr >
< th > Netkort< / th >
< td > {{ eset_specs.adapter_names | join(', ') if eset_specs.adapter_names else '-' }}< / td >
< / tr >
< tr >
< th > MAC< / th >
< td > {{ eset_specs.macs | join(', ') if eset_specs.macs else '-' }}< / td >
< / tr >
< tr >
< th > Installeret< / th >
2026-02-14 02:26:29 +01:00
< td >
{% if eset_specs.installed_software_details %}
< div style = "max-height: 240px; overflow: auto; border: 1px solid rgba(0,0,0,0.08); border-radius: 6px; padding: 0.5rem 0.75rem; background: rgba(0,0,0,0.02);" >
< table class = "table table-sm mb-0 align-middle" >
< thead >
< tr >
< th style = "width: 70%;" > App< / th >
< th > Version< / th >
< / tr >
< / thead >
< tbody >
{% for app in eset_specs.installed_software_details %}
< tr >
< td class = "text-break" > {{ app.name }}< / td >
< td > {{ app.version or '-' }}< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
< / div >
{% elif eset_specs.deployed_components %}
< div style = "max-height: 240px; overflow: auto; border: 1px solid rgba(0,0,0,0.08); border-radius: 6px; padding: 0.5rem 0.75rem; background: rgba(0,0,0,0.02);" >
< ul class = "mb-0 ps-3" >
{% for app in eset_specs.deployed_components %}
< li > {{ app }}< / li >
{% endfor %}
< / ul >
< / div >
{% else %}
-
{% endif %}
< / td >
2026-02-11 23:51:21 +01:00
< / tr >
< / tbody >
< / table >
< / div >
{% else %}
< div class = "text-muted" > Ingen ESET specifikationer fundet endnu.< / div >
{% endif %}
< / div >
< / div >
{% if hardware.hardware_specs %}
< div class = "card mt-4 shadow-sm border-0" >
< div class = "card-header bg-white border-bottom-0 pt-3 ps-3" >
< h6 class = "text-primary mb-0" > < i class = "bi bi-shield-check me-2" > < / i > ESET Data< / h6 >
< / div >
< div class = "card-body" >
< div class = "mb-2 text-muted" style = "font-size: 0.85rem;" >
Rå data fra ESET (til match/diagnose)
< / div >
< pre class = "p-3 rounded" style = "background: var(--bg-body); border: 1px solid rgba(0,0,0,0.1); max-height: 420px; overflow: auto; font-size: 0.85rem;" > {{ hardware.hardware_specs | tojson(indent=2) }}< / pre >
< / div >
< / div >
{% endif %}
< / div >
2026-02-11 13:23:32 +01:00
<!-- Tab: History -->
< div class = "tab-pane fade" id = "history" role = "tabpanel" >
< div class = "card shadow-sm border-0" >
< div class = "card-body" >
< div class = "row" >
< div class = "col-md-6" >
< h6 class = "text-primary mb-3 ps-3 border-start border-3 border-primary" > Lokations Historik< / h6 >
< div class = "timeline" >
{% if locations %}
{% for loc in locations %}
< div class = "timeline-item {% if not loc.end_date %}active{% endif %}" >
< div class = "timeline-marker" > < / div >
< div class = "ps-2" >
< div class = "fw-bold" > {{ loc.location_name or 'Ukendt' }}< / div >
< div class = "text-muted small" >
{{ loc.start_date }}
{% if loc.end_date %} - {{ loc.end_date }}{% else %} < span class = "badge bg-success py-0" > Nuværende< / span > {% endif %}
< / div >
{% if loc.notes %}< div class = "text-muted small fst-italic mt-1" > "{{ loc.notes }}"< / div > {% endif %}
< / div >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-11 13:23:32 +01:00
{% endfor %}
{% else %}
< p class = "text-muted ps-4" > Ingen historik.< / p >
{% endif %}
2026-01-31 23:16:24 +01:00
< / div >
< / div >
2026-02-11 13:23:32 +01:00
< div class = "col-md-6" >
< h6 class = "text-success mb-3 ps-3 border-start border-3 border-success" > Ejerskabs Historik< / h6 >
< div class = "timeline" >
{% if ownership %}
{% for own in ownership %}
< div class = "timeline-item {% if not own.end_date %}active{% endif %}" >
< div class = "timeline-marker" style = "border-color: var(--bs-success) !important; {% if not own.end_date %}background: var(--bs-success);{% endif %}" > < / div >
< div class = "ps-2" >
< div class = "fw-bold" > {{ own.customer_name or own.owner_type }}< / div >
< div class = "text-muted small" >
{{ own.start_date }}
{% if own.end_date %} - {{ own.end_date }}{% else %} < span class = "badge bg-success py-0" > Nuværende< / span > {% endif %}
< / div >
{% if own.notes %}< div class = "text-muted small fst-italic mt-1" > "{{ own.notes }}"< / div > {% endif %}
< / div >
< / div >
{% endfor %}
2026-01-31 23:16:24 +01:00
{% else %}
2026-02-11 13:23:32 +01:00
< p class = "text-muted ps-4" > Ingen historik.< / p >
2026-01-31 23:16:24 +01:00
{% endif %}
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
2026-02-11 13:23:32 +01:00
<!-- Tab: Files -->
< div class = "tab-pane fade" id = "files" role = "tabpanel" >
< div class = "card shadow-sm border-0" >
< div class = "card-body" >
< div class = "row g-3" >
{% if attachments %}
{% for att in attachments %}
< div class = "col-md-3 col-sm-6" >
< div class = "p-3 border rounded text-center bg-light h-100 position-relative" >
< div class = "display-6 mb-2" > 📎< / div >
< div class = "text-truncate fw-bold mb-1" title = "{{ att.file_name }}" > {{ att.file_name }}< / div >
< div class = "small text-muted" > {{ att.uploaded_at }}< / div >
< a href = "#" class = "stretched-link" onclick = "alert('Download not implemented yet')" > < / a >
< / div >
< / div >
{% endfor %}
{% else %}
< div class = "col-12 text-center py-5 text-muted" >
< i class = "bi bi-files display-4 opacity-25" > < / i >
< p class = "mt-3" > Ingen filer vedhæftet< / p >
< button class = "btn btn-sm btn-outline-primary" onclick = "alert('Upload funktion kommer snart')" > Upload fil< / button >
< / div >
{% endif %}
< / div >
< / div >
< / div >
< / div >
<!-- Tab: Notes -->
< div class = "tab-pane fade" id = "notes" role = "tabpanel" >
< div class = "card shadow-sm border-0" >
< div class = "card-body" >
< h6 class = "mb-3" > Noter< / h6 >
< div class = "p-3 bg-light rounded border" >
{% if hardware.notes %}
< div style = "white-space: pre-wrap;" > {{ hardware.notes }}< / div >
{% else %}
< span class = "text-muted fst-italic" > Ingen noter tilføjet...< / span >
{% endif %}
< / div >
< div class = "mt-3 text-end" >
< a href = "/hardware/{{ hardware.id }}/edit" class = "btn btn-sm btn-outline-primary" > Rediger Noter< / a >
< / div >
< / div >
< / div >
< / div >
2026-01-31 23:16:24 +01:00
< / div >
< / div >
2026-02-15 11:12:58 +01:00
<!-- Modal for Owner -->
< div class = "modal fade" id = "ownerModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Skift Ejer< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< form action = "/hardware/{{ hardware.id }}/owner" method = "post" >
< div class = "modal-body" >
< div class = "mb-3" >
< label class = "form-label fw-bold" > Vælg kunde< / label >
< input
type="text"
id="ownerCustomerSearch"
class="form-control mb-2"
placeholder="🔍 Søg virksomhed..."
autocomplete="off"
>
< select id = "ownerCustomerSelect" name = "owner_customer_id" class = "form-select" required >
< option value = "" > -- Vælg kunde --< / option >
{% for customer in owner_customers %}
< option value = "{{ customer.id }}" { % if hardware . current_owner_customer_id = = customer . id % } selected { % endif % } >
{{ customer.navn }}
< / option >
{% endfor %}
< / select >
< div id = "ownerCustomerHelp" class = "form-text" > Søg og vælg virksomhed.< / div >
< / div >
< div class = "mb-3" >
< label class = "form-label fw-bold" > Vælg kontaktperson< / label >
2026-02-17 08:29:05 +01:00
< input
type="text"
id="ownerContactSearch"
class="form-control mb-2"
placeholder="🔍 Søg kontaktperson..."
autocomplete="off"
>
< select
id="ownerContactSelect"
name="owner_contact_id"
class="form-select"
data-current-owner-contact-id="{{ owner_contact_ns.contact.contact_id if owner_contact_ns.contact else '' }}"
required
>
2026-02-15 11:12:58 +01:00
< option value = "" > -- Vælg kontaktperson --< / option >
< / select >
< div id = "ownerContactHelp" class = "form-text" > Viser kun kontakter for valgt virksomhed.< / div >
< / div >
< div class = "mb-3" >
< label class = "form-label" > Note (valgfri)< / label >
< textarea class = "form-control" name = "notes" rows = "3" placeholder = "F.eks. Overdraget til ny kunde" > < / textarea >
< / div >
< / div >
< div class = "modal-footer bg-light" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "submit" class = "btn btn-success" > Gem Ejer< / button >
< / div >
< / form >
< / div >
< / div >
< / div >
2026-01-31 23:16:24 +01:00
<!-- Modal for Location -->
< div class = "modal fade" id = "locationModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Skift Lokation< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< form action = "/hardware/{{ hardware.id }}/location" method = "post" >
< div class = "modal-body" >
< div class = "mb-3" >
< label class = "form-label fw-bold" > Vælg ny lokation< / label >
< input type = "text" class = "form-control mb-2" id = "locationSearchInput" placeholder = "🔍 Søg efter lokation..." autocomplete = "off" >
< div class = "list-group border rounded" id = "locationList" style = "max-height: 350px; overflow-y: auto; background: #fff;" >
{% macro render_location_option(node, depth) %}
< div class = "location-item-container" data-location-name = "{{ node.name | lower }}" >
< label class = "list-group-item list-group-item-action cursor-pointer border-0 py-1 px-2" style = "padding-left: {{ depth * 20 + 10 }}px !important;" >
< div class = "d-flex align-items-center" >
{% if node.children %}
< i class = "bi bi-caret-right-fill me-1 text-muted toggle-children" style = "cursor: pointer; font-size: 0.8rem;" onclick = "toggleLocationChildren(event, '{{ node.id }}')" > < / i >
{% else %}
< i class = "bi bi-dot me-1 text-muted" style = "width: 12px;" > < / i >
{% endif %}
< input class = "form-check-input me-2 mt-0" type = "radio" name = "location_id" value = "{{ node.id }}" required >
< div >
< span class = "location-name fw-normal" > {{ node.name }}< / span >
< small class = "text-muted ms-1" style = "font-size: 0.75rem;" > ({{ node.location_type }})< / small >
< / div >
< / div >
< / label >
{% if node.children %}
< div class = "children-container collapsed" id = "children-{{ node.id }}" style = "display: none;" >
{% for child in node.children %}
{{ render_location_option(child, depth + 1) }}
{% endfor %}
< / div >
{% endif %}
< / div >
{% endmacro %}
{% for node in location_tree %}
{{ render_location_option(node, 0) }}
{% endfor %}
< div id = "noResults" class = "p-3 text-center text-muted" style = "display: none;" >
Ingen lokationer fundet matching din søgning
< / div >
< / div >
< div class = "form-text mt-2" >
Kan du ikke finde lokationen? < a href = "/locations" target = "_blank" class = "text-decoration-none" > < i class = "bi bi-plus-circle" > < / i > Opret ny lokation< / a >
< / div >
< / div >
< div class = "mb-3" >
< label class = "form-label" > Noter til flytning< / label >
< textarea class = "form-control" name = "notes" rows = "3" placeholder = "F.eks. Flyttet ifm. nyansættelse" > < / textarea >
< / div >
< / div >
< div class = "modal-footer bg-light" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "submit" class = "btn btn-primary" > Gem Ændringer< / button >
< / div >
< / form >
< / div >
< / div >
< / div >
2026-02-15 11:12:58 +01:00
<!-- Add Contact Modal -->
< div class = "modal fade" id = "addContactModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Tilføj Kontaktperson< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< form method = "POST" action = "/hardware/{{ hardware.id }}/contacts/add" >
< div class = "modal-body" >
{% if available_contacts %}
< div class = "mb-3" >
< label class = "form-label" > Vælg Kontakt< / label >
< select name = "contact_id" class = "form-select" required >
< option value = "" > -- Vælg --< / option >
{% for contact in available_contacts %}
< option value = "{{ contact.id }}" >
{{ contact.first_name }} {{ contact.last_name }}
{% if contact.email %}({{ contact.email }}){% endif %}
< / option >
{% endfor %}
< / select >
< div class = "form-text" > Viser kun personer fra {{ hardware.customer_name if hardware.customer_name else 'kunden' }} som ikke allerede er tilknyttet.< / div >
< / div >
< input type = "hidden" name = "role" value = "user" >
< input type = "hidden" name = "source" value = "manual" >
{% else %}
< div class = "alert alert-warning" >
Ingen tilgængelige kontakter fundet for denne kunde.
< br > < small > Opret først kontakter under kunden.< / small >
< / div >
{% endif %}
< / div >
< div class = "modal-footer bg-light" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Annuller< / button >
{% if available_contacts %}
< button type = "submit" class = "btn btn-primary" > Tilføj< / button >
{% endif %}
< / div >
< / form >
< / div >
< / div >
< / div >
2026-01-31 23:16:24 +01:00
{% endblock %}
{% block extra_js %}
< script >
2026-02-11 23:51:21 +01:00
async function syncEsetData() {
try {
const response = await fetch(`/api/v1/hardware/{{ hardware.id }}/sync-eset`, { method: 'POST' });
if (!response.ok) {
const err = await response.text();
throw new Error(err || 'Request failed');
}
location.reload();
} catch (err) {
alert(`ESET opdatering fejlede: ${err.message}`);
}
}
2026-01-31 23:16:24 +01:00
// Tree Toggle Function
function toggleLocationChildren(event, nodeId) {
event.preventDefault();
event.stopPropagation(); // Prevent triggering the radio selection
const container = document.getElementById('children-' + nodeId);
const icon = event.target;
if (container.style.display === 'none') {
container.style.display = 'block';
icon.classList.remove('bi-caret-right-fill');
icon.classList.add('bi-caret-down-fill');
} else {
container.style.display = 'none';
icon.classList.remove('bi-caret-down-fill');
icon.classList.add('bi-caret-right-fill');
}
}
// Location Search Filter with Tree Support
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('locationSearchInput');
if (searchInput) {
searchInput.addEventListener('keyup', function() {
const filter = this.value.toLowerCase().trim();
const containers = document.querySelectorAll('.location-item-container');
let matchFound = false;
// Reset visualization if cleared
if (filter === "") {
// Show top-level only, collapse others check
document.querySelectorAll('.children-container').forEach(el => el.style.display = 'none');
document.querySelectorAll('.toggle-children').forEach(el => {
el.classList.remove('bi-caret-down-fill');
el.classList.add('bi-caret-right-fill');
});
containers.forEach(c => c.style.display = "");
document.getElementById('noResults').style.display = 'none';
return;
}
// First pass: Find direct matches
containers.forEach(container => {
const name = container.getAttribute('data-location-name');
if (name.includes(filter)) {
container.classList.add('match');
matchFound = true;
} else {
container.classList.remove('match');
}
});
// Second pass: Show/Hide based on matches and hierarchy
containers.forEach(container => {
// If this container is a match, or contains a match inside it
const isMatch = container.classList.contains('match');
const hasChildMatch = container.querySelectorAll('.match').length > 0;
if (isMatch || hasChildMatch) {
container.style.display = 'block';
// If it has children that matched, expand it to show them
if (hasChildMatch) {
const childContainer = container.querySelector('.children-container');
if (childContainer) {
childContainer.style.display = 'block';
const toggle = container.querySelector('.toggle-children');
if (toggle) {
toggle.classList.remove('bi-caret-right-fill');
toggle.classList.add('bi-caret-down-fill');
}
}
}
} else {
if (isMatch || hasChildMatch) {
container.style.display = 'block';
} else {
container.style.display = 'none';
}
}
});
document.getElementById('noResults').style.display = matchFound ? 'none' : 'block';
});
// Focus search field when modal opens
const locationModal = document.getElementById('locationModal');
locationModal.addEventListener('shown.bs.modal', function () {
searchInput.focus();
});
}
});
async function deleteHardware() {
if (!confirm('Er du sikker på at du vil slette dette hardware?')) {
return;
}
try {
const response = await fetch('/api/v1/hardware/{{ hardware.id }}', {
method: 'DELETE'
});
if (response.ok) {
alert('Hardware slettet!');
window.location.href = '/hardware';
} else {
alert('Fejl ved sletning af hardware');
}
} catch (error) {
alert('Fejl ved sletning: ' + error.message);
}
}
// Initialize Tags
document.addEventListener('DOMContentLoaded', function() {
2026-02-15 11:12:58 +01:00
const ownerCustomerSearch = document.getElementById('ownerCustomerSearch');
const ownerCustomerSelect = document.getElementById('ownerCustomerSelect');
2026-02-17 08:29:05 +01:00
const ownerContactSearch = document.getElementById('ownerContactSearch');
2026-02-15 11:12:58 +01:00
const ownerContactSelect = document.getElementById('ownerContactSelect');
const ownerCustomerHelp = document.getElementById('ownerCustomerHelp');
const ownerContactHelp = document.getElementById('ownerContactHelp');
2026-02-17 08:29:05 +01:00
let ownerContactsCache = [];
const initialOwnerCustomerOptions = ownerCustomerSelect
? Array.from(ownerCustomerSelect.options).map(option => ({
value: option.value,
label: option.textContent,
selected: option.selected
}))
: [];
let ownerCustomerSearchTimeout;
function renderOwnerCustomerOptions(items, keepValue = null) {
if (!ownerCustomerSelect) {
return;
}
ownerCustomerSelect.innerHTML = '';
const placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = '-- Vælg kunde --';
ownerCustomerSelect.appendChild(placeholder);
(items || []).forEach(item => {
const option = document.createElement('option');
option.value = String(item.id);
option.textContent = item.name || item.navn || '';
ownerCustomerSelect.appendChild(option);
});
if (keepValue) {
ownerCustomerSelect.value = String(keepValue);
}
}
function restoreInitialOwnerCustomers() {
if (!ownerCustomerSelect) {
return;
}
ownerCustomerSelect.innerHTML = '';
initialOwnerCustomerOptions.forEach(item => {
const option = document.createElement('option');
option.value = item.value;
option.textContent = item.label;
if (item.selected) {
option.selected = true;
}
ownerCustomerSelect.appendChild(option);
});
}
async function searchOwnerCustomersRemote(query) {
if (!ownerCustomerSelect) {
return;
}
try {
const response = await fetch(`/api/v1/search/customers?q=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error('Search request failed');
}
const rows = await response.json();
const currentValue = ownerCustomerSelect.value;
renderOwnerCustomerOptions(rows || [], currentValue);
if (ownerCustomerHelp) {
ownerCustomerHelp.textContent = rows & & rows.length
? `Viser ${rows.length} virksomhed(er).`
: 'Ingen virksomheder matcher søgningen.';
}
} catch (error) {
if (ownerCustomerHelp) {
ownerCustomerHelp.textContent = 'Søgning fejlede. Prøv igen.';
}
}
}
2026-02-15 11:12:58 +01:00
function filterOwnerCustomers() {
if (!ownerCustomerSearch || !ownerCustomerSelect) {
return;
}
const filter = ownerCustomerSearch.value.toLowerCase().trim();
2026-02-17 08:29:05 +01:00
if (filter.length >= 2) {
clearTimeout(ownerCustomerSearchTimeout);
ownerCustomerSearchTimeout = setTimeout(() => {
searchOwnerCustomersRemote(filter);
}, 250);
return;
}
restoreInitialOwnerCustomers();
2026-02-15 11:12:58 +01:00
const options = Array.from(ownerCustomerSelect.options);
let visibleCount = 0;
options.forEach((option, index) => {
if (index === 0) {
option.hidden = false;
return;
}
const label = (option.textContent || '').toLowerCase();
const isVisible = !filter || label.includes(filter);
option.hidden = !isVisible;
if (isVisible) {
visibleCount += 1;
}
});
const selectedOption = ownerCustomerSelect.selectedOptions[0];
if (selectedOption & & selectedOption.hidden) {
ownerCustomerSelect.value = '';
}
if (ownerCustomerHelp) {
if (visibleCount === 0) {
ownerCustomerHelp.textContent = 'Ingen virksomheder matcher søgningen.';
} else {
ownerCustomerHelp.textContent = `Viser ${visibleCount} virksomhed(er).`;
}
}
}
2026-02-17 08:29:05 +01:00
async function loadOwnerContactsForCustomer(customerId) {
if (!ownerContactSelect) {
2026-02-15 11:12:58 +01:00
return;
}
2026-02-17 08:29:05 +01:00
ownerContactsCache = [];
ownerContactSelect.innerHTML = '< option value = "" > -- Vælg kontaktperson --< / option > ';
2026-02-15 11:12:58 +01:00
2026-02-17 08:29:05 +01:00
if (!customerId) {
ownerContactSelect.disabled = true;
if (ownerContactHelp) {
ownerContactHelp.textContent = 'Vælg først virksomhed.';
2026-02-15 11:12:58 +01:00
}
2026-02-17 08:29:05 +01:00
return;
}
2026-02-15 11:12:58 +01:00
2026-02-17 08:29:05 +01:00
try {
const response = await fetch(`/api/v1/customers/${encodeURIComponent(customerId)}/contacts`);
if (!response.ok) {
throw new Error('Failed to load contacts');
2026-02-15 11:12:58 +01:00
}
2026-02-17 08:29:05 +01:00
const rows = await response.json();
ownerContactsCache = rows || [];
ownerContactSelect.disabled = !ownerContactsCache.length;
filterOwnerContactsSearch();
if (ownerContactHelp) {
ownerContactHelp.textContent = ownerContactsCache.length
? `Viser ${ownerContactsCache.length} kontaktperson(er) for valgt virksomhed.`
: 'Ingen kontaktpersoner fundet for valgt virksomhed.';
}
} catch (error) {
ownerContactSelect.disabled = true;
ownerContactsCache = [];
if (ownerContactHelp) {
ownerContactHelp.textContent = 'Kunne ikke hente kontaktpersoner. Prøv igen.';
}
}
}
function filterOwnerContactsSearch() {
if (!ownerContactSelect || !ownerContactSearch) {
return;
2026-02-15 11:12:58 +01:00
}
2026-02-17 08:29:05 +01:00
const filter = ownerContactSearch.value.toLowerCase().trim();
const preferredContactId = ownerContactSelect.getAttribute('data-current-owner-contact-id');
const currentValue = ownerContactSelect.value;
const filteredContacts = ownerContactsCache.filter(contact => {
const fullName = `${contact.first_name || ''} ${contact.last_name || ''}`.trim().toLowerCase();
const email = (contact.email || '').toLowerCase();
const phone = (contact.phone || '').toLowerCase();
return !filter || fullName.includes(filter) || email.includes(filter) || phone.includes(filter);
});
ownerContactSelect.innerHTML = '< option value = "" > -- Vælg kontaktperson --< / option > ';
filteredContacts.forEach(contact => {
const option = document.createElement('option');
option.value = String(contact.id);
const fullName = `${contact.first_name || ''} ${contact.last_name || ''}`.trim();
option.textContent = contact.email ? `${fullName} (${contact.email})` : fullName;
if (
(currentValue & & String(contact.id) === String(currentValue)) ||
(!currentValue & & preferredContactId & & String(contact.id) === String(preferredContactId))
) {
option.selected = true;
}
ownerContactSelect.appendChild(option);
});
2026-02-15 11:12:58 +01:00
if (ownerContactHelp) {
2026-02-17 08:29:05 +01:00
if (ownerContactSelect.disabled) {
2026-02-15 11:12:58 +01:00
ownerContactHelp.textContent = 'Vælg først virksomhed.';
2026-02-17 08:29:05 +01:00
} else if (filteredContacts.length === 0) {
ownerContactHelp.textContent = 'Ingen kontaktpersoner matcher søgningen.';
2026-02-15 11:12:58 +01:00
} else {
2026-02-17 08:29:05 +01:00
ownerContactHelp.textContent = `Viser ${filteredContacts.length} kontaktperson(er) for valgt virksomhed.`;
2026-02-15 11:12:58 +01:00
}
}
}
if (ownerCustomerSelect & & ownerContactSelect) {
2026-02-17 08:29:05 +01:00
ownerCustomerSelect.addEventListener('change', function() {
ownerContactSelect.setAttribute('data-current-owner-contact-id', '');
loadOwnerContactsForCustomer(ownerCustomerSelect.value);
});
2026-02-15 11:12:58 +01:00
if (ownerCustomerSearch) {
ownerCustomerSearch.addEventListener('input', function() {
filterOwnerCustomers();
});
}
2026-02-17 08:29:05 +01:00
if (ownerContactSearch) {
ownerContactSearch.addEventListener('input', filterOwnerContactsSearch);
}
2026-02-15 11:12:58 +01:00
filterOwnerCustomers();
2026-02-17 08:29:05 +01:00
loadOwnerContactsForCustomer(ownerCustomerSelect.value);
2026-02-15 11:12:58 +01:00
}
2026-01-31 23:16:24 +01:00
if (window.renderEntityTags) {
window.renderEntityTags('hardware', {{ hardware.id }}, 'hardware-tags');
}
2026-02-01 00:25:02 +01:00
// Set default context for keyboard shortcuts (Option+Shift+T)
if (window.setTagPickerContext) {
window.setTagPickerContext('hardware', {{ hardware.id }}, () => window.renderEntityTags('hardware', {{ hardware.id }}, 'hardware-tags'));
}
2026-01-31 23:16:24 +01:00
});
< / script >
2026-02-11 13:23:32 +01:00
{% endblock %}