502 lines
21 KiB
HTML
502 lines
21 KiB
HTML
js{% extends "shared/frontend/base.html" %}
|
|
|
|
{% 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>
|
|
|
|
<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>
|
|
|
|
<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">
|
|
<label for="asset_type">Hardwaretype <span class="required">*</span></label>
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
<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>
|
|
|
|
<!-- 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>
|
|
<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>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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>
|
|
<button type="submit" id="intake-save" class="btn btn-primary">💾 Gem hardware</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="/static/js/hardware-intake.js"></script>
|
|
<script>
|
|
function toggleCustomerSelect() {
|
|
const ownerType = document.getElementById('current_owner_type').value;
|
|
const customerGroup = document.getElementById('customerSelectGroup');
|
|
document.getElementById('bmcAssetCategoryGroup').style.display = ownerType === 'bmc' ? '' : 'none';
|
|
document.getElementById('bmc_asset_category').disabled = ownerType !== 'bmc';
|
|
|
|
if (ownerType === 'customer') {
|
|
customerGroup.style.display = 'block';
|
|
} else {
|
|
customerGroup.style.display = 'none';
|
|
document.getElementById('current_owner_customer_id').value = '';
|
|
}
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
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);
|
|
} 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);
|
|
} else {
|
|
data[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
data.bmc_asset_category = data.current_owner_type === 'bmc'
|
|
? (document.getElementById('bmc_asset_category').value || null) : null;
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/hardware-intake/create', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(hardwareIntakePayload(data))
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
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');
|
|
} 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();
|
|
|
|
// AnyDesk live preview
|
|
document.getElementById('anydesk_id').addEventListener('input', updateAnyDeskPreview);
|
|
updateAnyDeskPreview();
|
|
</script>
|
|
{% endblock %}
|