2026-02-11 13:23:32 +01:00
js{% extends "shared/frontend/base.html" %}
2026-01-31 23:16:24 +01:00
{% block title %}Opret Hardware - BMC Hub{% endblock %}
{% block extra_css %}
< style >
.form-container {
max-width: 800px;
margin: 0 auto;
}
.form-header {
margin-bottom: 2rem;
}
.form-header h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.form-header p {
color: var(--text-secondary);
}
.form-card {
background: var(--bg-card);
border-radius: 12px;
padding: 2rem;
border: 1px solid rgba(0,0,0,0.1);
}
.form-section {
margin-bottom: 2rem;
}
.form-section-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--accent);
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group.full-width {
grid-column: 1 / -1;
}
.form-group label {
font-weight: 500;
color: var(--text-primary);
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 0.75rem;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 8px;
background: var(--bg-body);
color: var(--text-primary);
font-size: 1rem;
transition: all 0.3s ease;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(15, 76, 117, 0.1);
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid rgba(0,0,0,0.1);
}
.btn {
padding: 0.75rem 2rem;
border-radius: 8px;
font-weight: 500;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
border: none;
text-decoration: none;
}
.btn-primary {
background-color: var(--accent);
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.3);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.required {
color: #dc3545;
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column-reverse;
}
.btn {
width: 100%;
}
}
< / style >
{% endblock %}
{% block content %}
< div class = "form-container" >
< div class = "form-header" >
< h1 > 🖥️ Opret Nyt Hardware< / h1 >
< p > Tilføj et nyt hardware asset til systemet< / p >
< / div >
2026-10-20 07:18:57 +02:00
< div class = "form-card mb-3" >
< div class = "p-3" >
< h2 class = "h5 mb-3" > Hvordan vil du oprette hardware?< / h2 >
< div class = "d-flex flex-wrap gap-2 mb-3" >
< button type = "button" class = "btn btn-outline-primary" id = "intake-mode-invoice" onclick = "setHardwareIntakeMode('invoice')" > Fra leverandørfaktura< / button >
< button type = "button" class = "btn btn-outline-primary" id = "intake-mode-barcode" onclick = "setHardwareIntakeMode('barcode')" > Stregkode / varenummer< / button >
< button type = "button" class = "btn btn-outline-primary" onclick = "openHardwareInventory()" > Fra PC / Mac inventory< / button >
< button type = "button" class = "btn btn-primary" id = "intake-mode-manual" onclick = "setHardwareIntakeMode('manual')" > 100 % manuelt< / button >
< / div >
< div id = "intake-invoice-panel" hidden >
< label for = "intake-invoice-search" class = "form-label" > Find en eksisterende faktura eller upload en ny< / label >
< div class = "d-flex gap-2 mb-2" >
< input id = "intake-invoice-search" class = "form-control" placeholder = "Leverandør, fakturanummer eller filnavn" onkeydown = "if(event.key==='Enter'){event.preventDefault();findHardwareInvoiceSources();}" >
< button type = "button" class = "btn btn-outline-primary" onclick = "findHardwareInvoiceSources()" > Søg< / button >
< / div >
< label class = "form-label mt-2" for = "intake-upload" > Ny faktura · PDF eller billede< / label >
< input id = "intake-upload" class = "form-control mb-3" type = "file" accept = ".pdf,.png,.jpg,.jpeg" onchange = "uploadHardwareInvoice()" >
< div id = "intake-invoice-results" class = "list-group mb-3" > < / div >
< div id = "intake-invoice-lines" > < / div >
< / div >
< div id = "intake-barcode-panel" hidden >
< label for = "intake-code" class = "form-label" > Scan eller indtast EAN/stregkode eller varenummer< / label >
< div class = "d-flex gap-2 mb-2" >
< input id = "intake-code" class = "form-control" autocomplete = "off" placeholder = "Scan koden og tryk Enter" oninput = "hardwareIntake.code=null;hardwareIntake.productId=null;" onkeydown = "if(event.key==='Enter'){event.preventDefault();lookupHardwareCode();}" >
< button type = "button" class = "btn btn-outline-primary" onclick = "lookupHardwareCode()" > Slå op< / button >
< / div >
< div id = "intake-product-results" class = "list-group" > < / div >
< / div >
< div id = "intake-inventory-panel" hidden >
< label for = "intake-inventory-search" > Find PC / Mac i API Gateway< / label >
< div class = "d-flex gap-2 mb-2" >
< select id = "intake-inventory-by" aria-label = "Søg efter" > < option value = "name" > Maskinnavn< / option > < option value = "serial" > Serienummer (præcist)< / option > < / select >
< input id = "intake-inventory-search" class = "form-control" placeholder = "Maskinnavn eller serienummer" onkeydown = "if(event.key==='Enter'){event.preventDefault();findHardwareInventory();}" >
< button type = "button" class = "btn btn-outline-primary" onclick = "findHardwareInventory()" > Søg< / button >
< / div >
< div id = "intake-inventory-results" class = "list-group mb-2" > < / div >
< / div >
< div id = "intake-message" class = "small text-muted" role = "status" aria-live = "polite" > Udfyld formularen manuelt. Intet oprettes, før du gemmer.< / div >
< / div >
< / div >
2026-01-31 23:16:24 +01:00
< div class = "form-card" >
< form id = "hardwareForm" onsubmit = "submitForm(event)" >
<!-- Basic Information -->
< div class = "form-section" >
< h3 class = "form-section-title" > 📋 Grundlæggende Information< / h3 >
< div class = "form-grid" >
< div class = "form-group" >
2026-10-20 07:18:57 +02:00
< label for = "asset_type" > Hardwaretype < span class = "required" > *< / span > < / label >
2026-01-31 23:16:24 +01:00
< select id = "asset_type" name = "asset_type" required >
< option value = "" > Vælg type...< / option >
< option value = "pc" > 🖥️ PC< / option >
< option value = "laptop" > 💻 Laptop< / option >
< option value = "printer" > 🖨️ Printer< / option >
< option value = "skærm" > 🖥️ Skærm< / option >
< option value = "telefon" > 📱 Telefon< / option >
< option value = "server" > 🗄️ Server< / option >
< option value = "netværk" > 🌐 Netværk< / option >
< option value = "andet" > 📦 Andet< / option >
< / select >
< / div >
2026-10-20 07:18:57 +02:00
< div class = "form-group" id = "bmcAssetCategoryGroup" >
< label for = "bmc_asset_category" > Asset-type< / label >
< select id = "bmc_asset_category" name = "bmc_asset_category" >
< option value = "" > Ikke valgt< / option >
< option value = "rental" > Udlejningsvare< / option >
< option value = "internal" > Internt udstyr< / option >
< / select >
< small class = "text-muted" > Kun BMC-ejet udstyr: udlejning eller eget udstyr til intern brug.< / small >
< / div >
2026-01-31 23:16:24 +01:00
< div class = "form-group" >
< label for = "brand" > Mærke< / label >
< input type = "text" id = "brand" name = "brand" placeholder = "fx Dell, HP, Lenovo..." >
< / div >
< div class = "form-group" >
< label for = "model" > Model< / label >
< input type = "text" id = "model" name = "model" placeholder = "fx OptiPlex 7090" >
< / div >
< div class = "form-group" >
< label for = "serial_number" > Serienummer< / label >
< input type = "text" id = "serial_number" name = "serial_number" placeholder = "Unik serienummer" >
< / div >
2026-10-20 07:18:57 +02:00
< div class = "form-group" >
< label for = "intake-count" > Antal fysiske enheder< / label >
< input id = "intake-count" type = "number" min = "1" max = "100" step = "1" value = "1" required onchange = "updateHardwareIntakeQuantity()" >
< small > Én hardwarepost pr. enhed. Serienumre må ikke genbruges.< / small >
< / div >
< div class = "form-group full-width" id = "intake-serials-group" hidden >
< label for = "intake-serials" > Serienumre · ét pr. linje, valgfrit< / label >
< textarea id = "intake-serials" rows = "3" placeholder = "Ét serienummer pr. fysisk enhed. Lad feltet være tomt, hvis de ikke kendes." > < / textarea >
< / div >
< div class = "form-group" >
< label for = "intake-unit-cost" > Indkøbspris pr. enhed ekskl. moms · valgfri< / label >
< input id = "intake-unit-cost" type = "number" min = "0" step = "0.01" placeholder = "Ukendt" >
< / div >
< div class = "form-group" >
< label for = "intake-currency" > Valuta for indkøbspris< / label >
< input id = "intake-currency" value = "DKK" maxlength = "3" pattern = "[A-Z]{3}" required >
< / div >
2026-01-31 23:16:24 +01:00
< div class = "form-group" >
< label for = "internal_asset_id" > Internt Asset ID< / label >
< input type = "text" id = "internal_asset_id" name = "internal_asset_id" placeholder = "BMC-PC-001" >
< / div >
< div class = "form-group" >
< label for = "customer_asset_id" > Kunde Asset ID< / label >
< input type = "text" id = "customer_asset_id" name = "customer_asset_id" placeholder = "Kundens ID" >
< / div >
< / div >
< / div >
2026-02-10 14:40:38 +01:00
<!-- AnyDesk -->
< div class = "form-section" >
< h3 class = "form-section-title" > 🧷 AnyDesk< / h3 >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "anydesk_id" > AnyDesk ID< / label >
< input type = "text" id = "anydesk_id" name = "anydesk_id" placeholder = "123-456-789" >
< / div >
< div class = "form-group" >
< label for = "anydesk_link" > AnyDesk Link< / label >
< input type = "text" id = "anydesk_link" name = "anydesk_link" placeholder = "anydesk://..." >
< / div >
< / div >
2026-02-11 13:23:32 +01:00
< div class = "mt-2 small" id = "anydesk_preview" style = "display: none;" >
< a href = "#" id = "anydesk_preview_link" target = "_blank" > Test forbindelse< / a >
< / div >
< / div >
<!-- ESET -->
< div class = "form-section" >
< h3 class = "form-section-title" > 🛡️ ESET< / h3 >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "eset_uuid" > ESET UUID< / label >
< input type = "text" id = "eset_uuid" name = "eset_uuid" placeholder = "ESET Device UUID" >
< / div >
< / div >
2026-02-10 14:40:38 +01:00
< / div >
2026-01-31 23:16:24 +01:00
<!-- Ownership -->
< div class = "form-section" >
< h3 class = "form-section-title" > 👥 Ejerskab< / h3 >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "current_owner_type" > Ejer Type< / label >
< select id = "current_owner_type" name = "current_owner_type" onchange = "toggleCustomerSelect()" >
< option value = "bmc" > BMC< / option >
< option value = "customer" > Kunde< / option >
< option value = "third_party" > Tredje Part< / option >
< / select >
< / div >
< div class = "form-group" id = "customerSelectGroup" style = "display: none;" >
< label for = "current_owner_customer_id" > Kunde< / label >
< select id = "current_owner_customer_id" name = "current_owner_customer_id" >
< option value = "" > Vælg kunde...< / option >
{% for customer in customers %}
< option value = "{{ customer.id }}" > {{ customer.navn }}< / option >
{% endfor %}
< / select >
< / div >
< / div >
< / div >
<!-- Status -->
< div class = "form-section" >
< h3 class = "form-section-title" > 📊 Status & Garanti< / h3 >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "status" > Status< / label >
< select id = "status" name = "status" >
< option value = "active" > ✅ Aktiv< / option >
< option value = "faulty_reported" > ⚠️ Fejl Rapporteret< / option >
< option value = "in_repair" > 🔧 Under Reparation< / option >
< option value = "replaced" > 🔄 Udskiftet< / option >
< option value = "retired" > 📦 Udtjent< / option >
< option value = "unsupported" > ❌ Ikke Supporteret< / option >
< / select >
< / div >
< div class = "form-group" >
< label for = "warranty_until" > Garanti Udløber< / label >
< input type = "date" id = "warranty_until" name = "warranty_until" >
< / div >
< div class = "form-group" >
< label for = "end_of_life" > End of Life< / label >
< input type = "date" id = "end_of_life" name = "end_of_life" >
< / div >
< / div >
< / div >
2026-04-21 01:34:40 +02:00
<!-- Rental Defaults -->
< div class = "form-section" >
< h3 class = "form-section-title" > 💶 Standardpriser for Udlejning< / h3 >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "rental_default_start_price" > Standard startpris< / label >
< input type = "number" id = "rental_default_start_price" name = "rental_default_start_price" min = "0" step = "0.01" value = "0" >
< / div >
< div class = "form-group" >
< label for = "rental_default_freight_price" > Standard fragt< / label >
< input type = "number" id = "rental_default_freight_price" name = "rental_default_freight_price" min = "0" step = "0.01" value = "0" >
< / div >
< div class = "form-group" >
< label for = "rental_default_preparation_price" > Standard klargoring< / label >
< input type = "number" id = "rental_default_preparation_price" name = "rental_default_preparation_price" min = "0" step = "0.01" value = "0" >
< / div >
< div class = "form-group" >
< label for = "rental_default_operations_monthly_price" > Standard drift pr. maned< / label >
< input type = "number" id = "rental_default_operations_monthly_price" name = "rental_default_operations_monthly_price" min = "0" step = "0.01" value = "0" >
< / div >
< / div >
< div class = "form-text mt-2" > Bruges til at autoudfylde Udlej-modal pa asseten.< / div >
< / div >
2026-01-31 23:16:24 +01:00
<!-- Notes -->
< div class = "form-section" >
< h3 class = "form-section-title" > 📝 Noter< / h3 >
< div class = "form-group full-width" >
< label for = "notes" > Beskrivelse/Noter< / label >
< textarea id = "notes" name = "notes" placeholder = "Tilføj eventuelle noter eller beskrivelse..." > < / textarea >
< / div >
< / div >
<!-- Actions -->
< div class = "form-actions" >
< a href = "/hardware" class = "btn btn-secondary" > Annuller< / a >
2026-10-20 07:18:57 +02:00
< button type = "submit" id = "intake-save" class = "btn btn-primary" > 💾 Gem hardware< / button >
2026-01-31 23:16:24 +01:00
< / div >
< / form >
< / div >
< / div >
{% endblock %}
{% block extra_js %}
2026-10-20 07:18:57 +02:00
< script src = "/static/js/hardware-intake.js" > < / script >
2026-01-31 23:16:24 +01:00
< script >
function toggleCustomerSelect() {
const ownerType = document.getElementById('current_owner_type').value;
const customerGroup = document.getElementById('customerSelectGroup');
2026-10-20 07:18:57 +02:00
document.getElementById('bmcAssetCategoryGroup').style.display = ownerType === 'bmc' ? '' : 'none';
document.getElementById('bmc_asset_category').disabled = ownerType !== 'bmc';
2026-01-31 23:16:24 +01:00
if (ownerType === 'customer') {
customerGroup.style.display = 'block';
} else {
customerGroup.style.display = 'none';
document.getElementById('current_owner_customer_id').value = '';
}
}
2026-02-11 13:23:32 +01:00
function updateAnyDeskPreview() {
const idValue = document.getElementById('anydesk_id').value.trim();
const preview = document.getElementById('anydesk_preview');
const previewLink = document.getElementById('anydesk_preview_link');
const linkInput = document.getElementById('anydesk_link');
if (!preview || !previewLink) {
return;
}
if (!idValue) {
preview.style.display = 'none';
previewLink.href = '#';
if (linkInput) {
linkInput.value = '';
}
return;
}
const deepLink = `anydesk://${idValue}`;
previewLink.href = deepLink;
if (linkInput) {
linkInput.value = deepLink;
}
preview.style.display = 'block';
}
2026-01-31 23:16:24 +01:00
async function submitForm(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = {};
for (const [key, value] of formData.entries()) {
if (value) {
// Convert customer_id to integer
if (key === 'current_owner_customer_id') {
data[key] = parseInt(value);
2026-04-21 01:34:40 +02:00
} else if (
key === 'rental_default_start_price' ||
key === 'rental_default_freight_price' ||
key === 'rental_default_preparation_price' ||
key === 'rental_default_operations_monthly_price'
) {
data[key] = Number(value);
2026-01-31 23:16:24 +01:00
} else {
data[key] = value;
}
}
}
2026-10-20 07:18:57 +02:00
data.bmc_asset_category = data.current_owner_type === 'bmc'
? (document.getElementById('bmc_asset_category').value || null) : null;
2026-01-31 23:16:24 +01:00
try {
2026-10-20 07:18:57 +02:00
const response = await fetch('/api/v1/hardware-intake/create', {
2026-01-31 23:16:24 +01:00
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
2026-10-20 07:18:57 +02:00
body: JSON.stringify(hardwareIntakePayload(data))
2026-01-31 23:16:24 +01:00
});
if (response.ok) {
const result = await response.json();
2026-10-20 07:18:57 +02:00
const ids = result.hardware_ids || [];
alert(`${result.already_created ? 'Allerede oprettet' : 'Oprettet'}: ${ids.length} hardwareenhed${ids.length === 1 ? '' : 'er'}`);
window.location.href = ids.length === 1 ? `/hardware/${ids[0]}`
: (data.current_owner_type === 'customer' ? '/hardware/customers' : '/hardware');
2026-01-31 23:16:24 +01:00
} else {
const error = await response.json();
alert('Fejl: ' + (error.detail || 'Kunne ikke oprette hardware'));
}
} catch (error) {
alert('Fejl ved oprettelse: ' + error.message);
}
}
// Initialize customer select visibility
toggleCustomerSelect();
2026-02-11 13:23:32 +01:00
// AnyDesk live preview
document.getElementById('anydesk_id').addEventListener('input', updateAnyDeskPreview);
updateAnyDeskPreview();
2026-01-31 23:16:24 +01:00
< / script >
{% endblock %}