- Removed opportunity detail page route from views.py. - Deleted opportunity_service.py as it is no longer needed. - Updated router.py to seed new setting for case_type_module_defaults. - Enhanced settings.html to include standard modules per case type with UI for selection. - Implemented JavaScript functions to manage case type module defaults. - Added RelationService for handling case relations with a tree structure. - Created migration scripts (128 and 129) for new pipeline fields and descriptions. - Added script to fix relation types in the database.
1145 lines
59 KiB
HTML
1145 lines
59 KiB
HTML
{% 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;
|
|
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);
|
|
word-break: break-all;
|
|
}
|
|
|
|
.action-card {
|
|
background: white;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
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;
|
|
gap: 0.5rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.action-card:hover {
|
|
border-color: var(--accent);
|
|
background: var(--accent-light);
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.action-card i {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
/* Timeline Styling */
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: 1.5rem;
|
|
}
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0.4rem;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: #e9ecef;
|
|
}
|
|
.timeline-item {
|
|
position: relative;
|
|
padding-bottom: 1.5rem;
|
|
}
|
|
.timeline-marker {
|
|
position: absolute;
|
|
left: -1.09rem;
|
|
top: 0.3rem;
|
|
width: 0.8rem;
|
|
height: 0.8rem;
|
|
border-radius: 50%;
|
|
background: white;
|
|
border: 2px solid var(--accent);
|
|
}
|
|
.timeline-item.active .timeline-marker {
|
|
background: #28a745;
|
|
border-color: #28a745;
|
|
}
|
|
|
|
/* Quick Info Bar */
|
|
.quick-info-label {
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
margin-right: 0.4rem;
|
|
}
|
|
.quick-info-item {
|
|
display: flex;
|
|
align-items: center;
|
|
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;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<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>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Customer (Current Owner) -->
|
|
{% set current_owner = ownership[0] if ownership else None %}
|
|
{% 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 %}
|
|
{% 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>
|
|
</div>
|
|
{% 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>
|
|
</div>
|
|
{% 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>
|
|
</div>
|
|
|
|
<!-- Link to Edit -->
|
|
<div class="ms-auto d-flex gap-2">
|
|
<button onclick="syncEsetData()" class="btn btn-sm btn-outline-secondary" title="Opdater fra ESET">
|
|
<i class="bi bi-arrow-repeat"></i>
|
|
</button>
|
|
<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>
|
|
</button>
|
|
</div>
|
|
</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>
|
|
<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>
|
|
<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>
|
|
</div>
|
|
{% 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 %}
|
|
{% 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>
|
|
</div>
|
|
{% endif %}
|
|
{% 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>
|
|
</div>
|
|
{% 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 %}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- 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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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 %}
|
|
</div>
|
|
{% else %}
|
|
<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>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<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-success mb-0"><i class="bi bi-person me-2"></i>Ejer</h6>
|
|
<button class="btn btn-sm btn-link p-0" data-bs-toggle="modal" data-bs-target="#ownerModal">Ændre</button>
|
|
</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>
|
|
{% 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 %}
|
|
<p class="text-muted small mb-0">Siden: {{ current_owner.start_date }}</p>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4 text-muted">
|
|
<p class="mb-0">Ingen aktiv ejer</p>
|
|
<button class="btn btn-sm btn-outline-success mt-2" data-bs-toggle="modal" data-bs-target="#ownerModal">Sæt ejer</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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">
|
|
{% if cases and cases|length > 0 %}
|
|
{% 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>
|
|
</div>
|
|
<div class="small text-muted ms-3">{{ case.created_at.strftime('%Y-%m-%d') if case.created_at else '' }}</div>
|
|
</a>
|
|
{% endfor %}
|
|
{% 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 %}
|
|
{% else %}
|
|
<div class="text-center py-4 text-muted small">
|
|
Ingen sager tilknyttet
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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">
|
|
{% if eset_specs and (eset_specs.os_name or eset_specs.primary_local_ip or eset_specs.cpu_models or eset_specs.deployed_components) %}
|
|
<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>
|
|
<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>
|
|
</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>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<p class="text-muted ps-4">Ingen historik.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<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 %}
|
|
{% else %}
|
|
<p class="text-muted ps-4">Ingen historik.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
<select id="ownerContactSelect" name="owner_contact_id" class="form-select" required>
|
|
<option value="">-- Vælg kontaktperson --</option>
|
|
{% for contact in owner_contacts %}
|
|
<option
|
|
value="{{ contact.id }}"
|
|
data-customer-id="{{ contact.customer_id }}"
|
|
{% if owner_contact_ns.contact and owner_contact_ns.contact.contact_id == contact.id %}selected{% endif %}
|
|
>
|
|
{{ contact.first_name }} {{ contact.last_name }}{% if contact.email %} ({{ contact.email }}){% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</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>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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>
|
|
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
const ownerCustomerSearch = document.getElementById('ownerCustomerSearch');
|
|
const ownerCustomerSelect = document.getElementById('ownerCustomerSelect');
|
|
const ownerContactSelect = document.getElementById('ownerContactSelect');
|
|
const ownerCustomerHelp = document.getElementById('ownerCustomerHelp');
|
|
const ownerContactHelp = document.getElementById('ownerContactHelp');
|
|
|
|
function filterOwnerCustomers() {
|
|
if (!ownerCustomerSearch || !ownerCustomerSelect) {
|
|
return;
|
|
}
|
|
|
|
const filter = ownerCustomerSearch.value.toLowerCase().trim();
|
|
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).`;
|
|
}
|
|
}
|
|
}
|
|
|
|
function filterOwnerContacts() {
|
|
if (!ownerCustomerSelect || !ownerContactSelect) {
|
|
return;
|
|
}
|
|
|
|
const selectedCustomerId = ownerCustomerSelect.value;
|
|
const options = Array.from(ownerContactSelect.options);
|
|
let visibleCount = 0;
|
|
|
|
options.forEach((option, index) => {
|
|
if (index === 0) {
|
|
option.hidden = false;
|
|
return;
|
|
}
|
|
|
|
const optionCustomerId = option.getAttribute('data-customer-id');
|
|
const isVisible = selectedCustomerId && optionCustomerId === selectedCustomerId;
|
|
option.hidden = !isVisible;
|
|
if (isVisible) {
|
|
visibleCount += 1;
|
|
}
|
|
});
|
|
|
|
const selectedOption = ownerContactSelect.selectedOptions[0];
|
|
if (!selectedOption || selectedOption.hidden) {
|
|
ownerContactSelect.value = '';
|
|
}
|
|
|
|
ownerContactSelect.disabled = !selectedCustomerId || visibleCount === 0;
|
|
if (ownerContactHelp) {
|
|
if (!selectedCustomerId) {
|
|
ownerContactHelp.textContent = 'Vælg først virksomhed.';
|
|
} else if (visibleCount === 0) {
|
|
ownerContactHelp.textContent = 'Ingen kontaktpersoner fundet for valgt virksomhed.';
|
|
} else {
|
|
ownerContactHelp.textContent = 'Viser kun kontakter for valgt virksomhed.';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ownerCustomerSelect && ownerContactSelect) {
|
|
ownerCustomerSelect.addEventListener('change', filterOwnerContacts);
|
|
if (ownerCustomerSearch) {
|
|
ownerCustomerSearch.addEventListener('input', function() {
|
|
filterOwnerCustomers();
|
|
filterOwnerContacts();
|
|
});
|
|
}
|
|
filterOwnerCustomers();
|
|
filterOwnerContacts();
|
|
}
|
|
|
|
if (window.renderEntityTags) {
|
|
window.renderEntityTags('hardware', {{ hardware.id }}, 'hardware-tags');
|
|
}
|
|
|
|
// Set default context for keyboard shortcuts (Option+Shift+T)
|
|
if (window.setTagPickerContext) {
|
|
window.setTagPickerContext('hardware', {{ hardware.id }}, () => window.renderEntityTags('hardware', {{ hardware.id }}, 'hardware-tags'));
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|