bmc_hub/app/modules/hardware/templates/index.html
Christian 85e019783b Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00

500 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "shared/frontend/base.html" %}
{% block title %}Hardware - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.page-header {
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.page-header h1 {
font-size: 2rem;
font-weight: 700;
margin: 0;
}
.btn-new-hardware {
background-color: var(--accent);
color: white;
border: none;
padding: 0.6rem 1.5rem;
border-radius: 8px;
font-weight: 500;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-new-hardware:hover {
background-color: #0056b3;
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.3);
}
.filter-section {
background: var(--bg-card);
padding: 1.5rem;
border-radius: 12px;
margin-bottom: 2rem;
border: 1px solid rgba(0,0,0,0.1);
}
.filter-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
align-items: end;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-group label {
font-weight: 500;
font-size: 0.9rem;
color: var(--text-secondary);
}
.filter-group select,
.filter-group input {
padding: 0.5rem;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 6px;
background: var(--bg-body);
color: var(--text-primary);
}
.btn-filter {
padding: 0.5rem 1rem;
background-color: var(--accent);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-filter:hover {
background-color: #0056b3;
}
.hardware-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.hardware-card {
background: var(--bg-card);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid rgba(0,0,0,0.1);
transition: all 0.3s ease;
cursor: pointer;
}
.hardware-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.hardware-header {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
align-items: flex-start;
}
.hardware-icon {
font-size: 2.5rem;
flex-shrink: 0;
}
.hardware-info {
flex: 1;
min-width: 0;
}
.hardware-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hardware-subtitle {
font-size: 0.9rem;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hardware-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.hardware-detail-row {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.hardware-detail-label {
color: var(--text-secondary);
font-weight: 500;
}
.hardware-detail-value {
color: var(--text-primary);
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hardware-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid rgba(0,0,0,0.1);
}
.status-badge {
padding: 0.3rem 0.8rem;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
display: inline-block;
}
.status-active { background-color: #28a745; color: white; }
.status-faulty_reported { background-color: #ffc107; color: #000; }
.status-in_repair { background-color: #007bff; color: white; }
.status-replaced { background-color: #6f42c1; color: white; }
.status-retired { background-color: #6c757d; color: white; }
.status-unsupported { background-color: #dc3545; color: white; }
.hardware-actions {
display: flex;
gap: 0.5rem;
}
.btn-action {
padding: 0.4rem 0.8rem;
border-radius: 6px;
font-size: 0.85rem;
text-decoration: none;
transition: all 0.3s ease;
border: 1px solid rgba(0,0,0,0.2);
background: var(--bg-body);
color: var(--text-primary);
}
.btn-action:hover {
background-color: var(--accent);
color: white;
border-color: var(--accent);
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--text-secondary);
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.3;
}
@media (max-width: 768px) {
.hardware-grid {
grid-template-columns: 1fr;
}
.filter-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<div class="page-header">
<h1>🗂️ BMC Assets Oversigt (Kun vores egne)</h1>
<div class="d-flex gap-2">
<a href="/hardware/customers" class="btn-new-hardware" style="background-color: #6c757d;">
<i class="bi bi-building"></i>
Kundehardware
</a>
<a href="/hardware/eset" class="btn-new-hardware" style="background-color: #0f4c75;">
<i class="bi bi-shield-check"></i>
ESET Oversigt
</a>
<a href="/hardware/new" class="btn-new-hardware">
➕ Nyt Hardware
</a>
</div>
</div>
<div class="filter-section">
<form method="get" action="/hardware">
<div class="filter-grid">
<div class="filter-group">
<label for="asset_type">Type</label>
<select name="asset_type" id="asset_type">
<option value="">Alle typer</option>
<option value="pc" {% if current_asset_type == 'pc' %}selected{% endif %}>🖥️ PC</option>
<option value="laptop" {% if current_asset_type == 'laptop' %}selected{% endif %}>💻 Laptop</option>
<option value="printer" {% if current_asset_type == 'printer' %}selected{% endif %}>🖨️ Printer</option>
<option value="skærm" {% if current_asset_type == 'skærm' %}selected{% endif %}>🖥️ Skærm</option>
<option value="telefon" {% if current_asset_type == 'telefon' %}selected{% endif %}>📱 Telefon</option>
<option value="server" {% if current_asset_type == 'server' %}selected{% endif %}>🗄️ Server</option>
<option value="netværk" {% if current_asset_type == 'netværk' %}selected{% endif %}>🌐 Netværk</option>
<option value="andet" {% if current_asset_type == 'andet' %}selected{% endif %}>📦 Andet</option>
</select>
</div>
<div class="filter-group">
<label for="status">Status</label>
<select name="status" id="status">
<option value="">Alle status</option>
<option value="active" {% if current_status == 'active' %}selected{% endif %}>✅ Aktiv</option>
<option value="faulty_reported" {% if current_status == 'faulty_reported' %}selected{% endif %}>⚠️ Fejl rapporteret</option>
<option value="in_repair" {% if current_status == 'in_repair' %}selected{% endif %}>🔧 Under reparation</option>
<option value="replaced" {% if current_status == 'replaced' %}selected{% endif %}>🔄 Udskiftet</option>
<option value="retired" {% if current_status == 'retired' %}selected{% endif %}>📦 Udtjent</option>
<option value="unsupported" {% if current_status == 'unsupported' %}selected{% endif %}>❌ Ikke supporteret</option>
</select>
</div>
<div class="filter-group">
<label for="rental_scope">Udlejning</label>
<select name="rental_scope" id="rental_scope">
<option value="" {% if not current_rental_scope %}selected{% endif %}>Alle assets</option>
<option value="rented" {% if current_rental_scope == 'rented' %}selected{% endif %}>Kun udlejede</option>
<option value="not_rented" {% if current_rental_scope == 'not_rented' %}selected{% endif %}>Kun ikke-udlejede</option>
</select>
</div>
<div class="filter-group">
<label for="q">Søg</label>
<input type="text" name="q" id="q" placeholder="Serial, model, mærke..." value="{{ search_query or '' }}">
</div>
<div class="filter-group">
<label>&nbsp;</label>
<button type="submit" class="btn-filter">🔍 Filtrer</button>
</div>
</div>
</form>
</div>
{% if hardware and hardware|length > 0 %}
<div class="d-flex justify-content-end mb-3">
<div class="btn-group btn-group-sm" role="group" aria-label="Visning">
<button type="button" class="btn btn-outline-secondary active" id="viewCardsBtn" onclick="setHardwareView('cards')">Kort</button>
<button type="button" class="btn btn-outline-secondary" id="viewTableBtn" onclick="setHardwareView('table')">Tabel</button>
</div>
</div>
<div id="hardwareCardsView">
<div class="hardware-grid">
{% for item in hardware %}
<div class="hardware-card" onclick="window.location.href='/hardware/{{ item.id }}'">
<div class="hardware-header">
<div class="hardware-icon">
{% if item.asset_type == 'pc' %}🖥️
{% elif item.asset_type == 'laptop' %}💻
{% elif item.asset_type == 'printer' %}🖨️
{% elif item.asset_type == 'skærm' %}🖥️
{% elif item.asset_type == 'telefon' %}📱
{% elif item.asset_type == 'server' %}🗄️
{% elif item.asset_type == 'netværk' %}🌐
{% else %}📦
{% endif %}
</div>
<div class="hardware-info">
<div class="hardware-title">{{ item.brand or 'Unknown' }} {{ item.model or '' }}</div>
<div class="hardware-subtitle">{{ item.serial_number or 'Ingen serienummer' }}</div>
</div>
</div>
<div class="hardware-details">
<div class="hardware-detail-row">
<span class="hardware-detail-label">Type:</span>
<span class="hardware-detail-value">{{ item.asset_type|title }}</span>
</div>
{% if item.anydesk_id or item.anydesk_link %}
<div class="hardware-detail-row">
<span class="hardware-detail-label">AnyDesk:</span>
<span class="hardware-detail-value">
{% if item.anydesk_link %}
<a href="{{ item.anydesk_link }}" target="_blank">{{ item.anydesk_id or 'Åbn' }}</a>
{% elif item.anydesk_id %}
<a href="anydesk://{{ item.anydesk_id }}" target="_blank">{{ item.anydesk_id }}</a>
{% endif %}
</span>
</div>
{% endif %}
{% if item.customer_name %}
<div class="hardware-detail-row">
<span class="hardware-detail-label">Ejer:</span>
<span class="hardware-detail-value">{{ item.customer_name }}</span>
</div>
{% elif item.current_owner_type %}
<div class="hardware-detail-row">
<span class="hardware-detail-label">Ejer:</span>
<span class="hardware-detail-value">{{ item.current_owner_type|title }}</span>
</div>
{% endif %}
{% if item.internal_asset_id %}
<div class="hardware-detail-row">
<span class="hardware-detail-label">Asset ID:</span>
<span class="hardware-detail-value">{{ item.internal_asset_id }}</span>
</div>
{% endif %}
<div class="hardware-detail-row">
<span class="hardware-detail-label">Udlejning:</span>
<span class="hardware-detail-value">
{% if item.is_currently_rented %}
Udlejet
{% else %}
Ledig/Intern
{% endif %}
</span>
</div>
</div>
<div class="hardware-footer">
<span class="status-badge status-{{ item.status }}">
{{ item.status|replace('_', ' ')|title }}
</span>
<div class="hardware-actions" onclick="event.stopPropagation()">
<a href="/hardware/{{ item.id }}" class="btn-action">👁️ Se</a>
<a href="/hardware/{{ item.id }}/edit" class="btn-action">✏️ Rediger</a>
<a href="/hardware/{{ item.id }}/label" class="btn-action" target="_blank" rel="noopener"><i class="bi bi-qr-code"></i> Label</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div id="hardwareTableView" class="d-none">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Hardware</th>
<th>Type</th>
<th>Serienr.</th>
<th>Ejer</th>
<th>Udlejning</th>
<th>Status</th>
<th>AnyDesk</th>
<th class="text-end">Handling</th>
</tr>
</thead>
<tbody>
{% for item in hardware %}
<tr>
<td class="fw-semibold">{{ item.brand or 'Unknown' }} {{ item.model or '' }}</td>
<td>{{ item.asset_type|title }}</td>
<td>{{ item.serial_number or 'Ingen serienummer' }}</td>
<td>{{ item.customer_name or (item.current_owner_type|title if item.current_owner_type else '—') }}</td>
<td>
{% if item.is_currently_rented %}
<span class="status-badge" style="background-color:#0f4c75;color:#fff;">Udlejet</span>
{% else %}
<span class="status-badge" style="background-color:#6c757d;color:#fff;">Ledig/Intern</span>
{% endif %}
</td>
<td>
<span class="status-badge status-{{ item.status }}">
{{ item.status|replace('_', ' ')|title }}
</span>
</td>
<td>
{% if item.anydesk_link %}
<a href="{{ item.anydesk_link }}" target="_blank">{{ item.anydesk_id or 'Åbn' }}</a>
{% elif item.anydesk_id %}
<a href="anydesk://{{ item.anydesk_id }}" target="_blank">{{ item.anydesk_id }}</a>
{% else %}
—
{% endif %}
</td>
<td class="text-end">
<a href="/hardware/{{ item.id }}" class="btn-action">👁️ Se</a>
<a href="/hardware/{{ item.id }}/edit" class="btn-action">✏️ Rediger</a>
<a href="/hardware/{{ item.id }}/label" class="btn-action" target="_blank" rel="noopener"><i class="bi bi-qr-code"></i> Label</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="empty-state">
<div class="empty-state-icon">🖥️</div>
<h3>Ingen BMC assets fundet</h3>
<p>Opret dit første interne asset for at komme i gang.</p>
<a href="/hardware/new" class="btn-new-hardware" style="margin-top: 1rem;">➕ Opret Hardware</a>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
// Auto-submit filter form on change
document.querySelectorAll('#asset_type, #status, #rental_scope').forEach(select => {
select.addEventListener('change', () => {
select.form.submit();
});
});
function setHardwareView(view) {
const cards = document.getElementById('hardwareCardsView');
const table = document.getElementById('hardwareTableView');
const cardsBtn = document.getElementById('viewCardsBtn');
const tableBtn = document.getElementById('viewTableBtn');
if (!cards || !table || !cardsBtn || !tableBtn) return;
const showTable = view === 'table';
cards.classList.toggle('d-none', showTable);
table.classList.toggle('d-none', !showTable);
cardsBtn.classList.toggle('active', !showTable);
tableBtn.classList.toggle('active', showTable);
localStorage.setItem('hardwareViewMode', showTable ? 'table' : 'cards');
}
const savedView = localStorage.getItem('hardwareViewMode');
if (savedView === 'table') {
setHardwareView('table');
}
</script>
{% endblock %}