/* All intake sources prefill the same reviewed form; no automatic hardware writes. */ function newHardwareIntakeRequestId() { if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID(); return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, token => { const value = Math.floor(Math.random() * 16); return (token === 'x' ? value : (value & 3) | 8).toString(16); }); } const hardwareIntake = {mode: 'manual', requestId: newHardwareIntakeRequestId(), preview: null, line: null, code: null, productId: null, products: [], generation: 0}; const intakeEl = id => document.getElementById(id); const intakeEscape = value => String(value ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); const intakeMessage = (message, error = false) => { intakeEl('intake-message').textContent = message; intakeEl('intake-message').className = error ? 'small text-danger' : 'small text-muted'; }; async function intakeFetch(url, options = {}) { const response = await fetch(url, options); if (!response.ok) { let message = 'Kunne ikke hente oplysninger'; try { const error = await response.json(); message = typeof error.detail === 'string' ? error.detail : message; } catch {} throw new Error(message); } return response.json(); } function setHardwareIntakeMode(mode) { hardwareIntake.inventory = null; intakeEl('intake-inventory-panel').hidden = true; hardwareIntake.mode = mode; hardwareIntake.preview = null; hardwareIntake.line = null; hardwareIntake.code = null; hardwareIntake.productId = null; hardwareIntake.generation++; for (const name of ['manual', 'invoice', 'barcode']) { intakeEl(`intake-mode-${name}`).className = name === mode ? 'btn btn-primary' : 'btn btn-outline-primary'; } intakeEl('intake-invoice-panel').hidden = mode !== 'invoice'; intakeEl('intake-barcode-panel').hidden = mode !== 'barcode'; intakeEl('intake-invoice-lines').innerHTML = ''; intakeMessage(mode === 'manual' ? 'Udfyld formularen manuelt. Intet oprettes, før du gemmer.' : 'Vælg et grundlag. Kontrollér derefter hardwaretype, ejer og asset-type i formularen.'); if (mode === 'invoice') findHardwareInvoiceSources(); if (mode === 'barcode') intakeEl('intake-code').focus(); } async function findHardwareInvoiceSources() { const version = ++hardwareIntake.generation; intakeMessage('Henter leverandørfakturaer…'); try { const data = await intakeFetch(`/api/v1/hardware-intake/sources?q=${encodeURIComponent(intakeEl('intake-invoice-search').value)}`); if (version !== hardwareIntake.generation) return; intakeEl('intake-invoice-results').innerHTML = data.sources.length ? data.sources.map(source => ``).join('') : '
Ingen fakturaer fundet. Upload en faktura ovenfor.
'; intakeMessage('Vælg en faktura eller upload en PDF/billedfil.'); } catch (error) { if (version === hardwareIntake.generation) intakeMessage(error.message, true); } } async function previewHardwareInvoice(kind, id) { const version = ++hardwareIntake.generation; hardwareIntake.preview = null; hardwareIntake.line = null; intakeMessage('Læser fakturalinjer. Tekstgenkendelse kan tage lidt tid…'); try { const data = await intakeFetch('/api/v1/hardware-intake/preview', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({kind,id})}); if (version === hardwareIntake.generation) showHardwareInvoicePreview(data); } catch (error) { if (version === hardwareIntake.generation) intakeMessage(error.message, true); } } async function uploadHardwareInvoice() { const file = intakeEl('intake-upload').files[0]; if (!file) return; const version = ++hardwareIntake.generation; hardwareIntake.preview = null; hardwareIntake.line = null; const body = new FormData(); body.append('file', file); intakeMessage('Uploader og læser fakturaen. Vent på linjerne, før du vælger hardware…'); try { const data = await intakeFetch('/api/v1/hardware-intake/upload', {method:'POST', body}); if (version === hardwareIntake.generation) showHardwareInvoicePreview(data); } catch (error) { if (version === hardwareIntake.generation) intakeMessage(error.message, true); } } function showHardwareInvoicePreview(data) { hardwareIntake.preview = data; const label = [data.header.vendor_name, data.header.invoice_number || data.header.filename].filter(Boolean).join(' · '); intakeEl('intake-invoice-lines').innerHTML = `

${intakeEscape(label || 'Fakturalinjer')}

${data.lines.map((line,index) => ``).join('')}
VareAntalEnhedspris ekskl. moms
${intakeEscape(line.description)}
${intakeEscape(line.sku || '')}
${intakeEscape(line.quantity ?? 'Ukendt')}${data.created_units?.[line.key]?.length ? `
${data.created_units[line.key].map(id => `#${Number(id)}`).join(', ')} allerede oprettet
` : ''}
${intakeEscape(line.unit_price ?? 'Ukendt')} ${intakeEscape(data.header.currency || '')}
`; intakeMessage(data.lines.length ? 'Vælg kun linjer med fysisk hardware. Kontrollér udlæste oplysninger før oprettelse.' : 'Ingen varelinjer kunne udlæses. Vælg en anden faktura eller opret manuelt.', !data.lines.length); } function chooseHardwareInvoiceLine(index) { const line = hardwareIntake.preview?.lines[index]; const count = Number(line?.quantity); const remaining = count - (hardwareIntake.preview?.created_units?.[line?.key]?.length || 0); if (!line || !Number.isInteger(count) || count < 1) { intakeMessage('Linjen mangler et positivt helt antal. Afklar fakturagrundlaget før oprettelse.', true); return; } if (remaining < 1) { intakeMessage('Alle enheder på linjen er allerede oprettet. Se de tilknyttede hardwareposter.', true); return; } hardwareIntake.line = line; intakeEl('model').value = line.description; intakeEl('brand').value = ''; intakeEl('intake-count').value = Math.min(remaining,100); intakeEl('intake-unit-cost').value = line.unit_price ?? ''; intakeEl('intake-currency').value = hardwareIntake.preview.header.currency || 'DKK'; updateHardwareIntakeQuantity(); intakeMessage(`Valgt: ${line.description}. Kontrollér antal, serienumre og ejer i formularen nedenfor. Fakturaen bogføres ikke af hardwareoprettelsen.`); intakeEl('asset_type').focus(); } async function lookupHardwareCode() { const code = intakeEl('intake-code').value.trim(); if (!code) { intakeMessage('Scan eller indtast en kode først.', true); return; } const version = ++hardwareIntake.generation; hardwareIntake.code = null; hardwareIntake.productId = null; intakeMessage('Slår koden op i varekartoteket…'); try { const local = await intakeFetch(`/api/v1/products?q=${encodeURIComponent(code)}`); let products = (Array.isArray(local) ? local : local.products || []).map(product => ({...product, intakeLocal:true})); if (!products.length) { intakeMessage('Ingen lokal vare fundet. Søger via API Gateway…'); const external = await intakeFetch(`/api/v1/products/apigateway/search?q=${encodeURIComponent(code)}&per_page=10`); products = external.products || external.items || []; } if (version !== hardwareIntake.generation) return; hardwareIntake.products = products.slice(0,15); hardwareIntake.lookupCode = code; intakeEl('intake-product-results').innerHTML = hardwareIntake.products.length ? hardwareIntake.products.map((product,index) => ``).join('') : '
Ingen vare fundet. Prøv et varenummer, eller vælg 100 % manuelt.
'; intakeMessage('Vælg den rigtige vare. Et EAN identificerer varetypen; hver maskine får sit eget serienummer.'); } catch (error) { if (version === hardwareIntake.generation) intakeMessage(error.message + ' Du kan stadig vælge 100 % manuelt.', true); } } function chooseHardwareProduct(index) { const product = hardwareIntake.products[index]; if (!product) return; hardwareIntake.code = hardwareIntake.lookupCode; hardwareIntake.productId = product.intakeLocal ? product.id : null; intakeEl('model').value = product.name || product.product_name || product.model || ''; const brand = product.manufacturer || product.brand || ''; intakeEl('brand').value = typeof brand === 'string' ? brand : brand.name || ''; intakeEl('intake-unit-cost').value = product.supplier_price ?? product.cost_price ?? product.price ?? ''; intakeEl('intake-currency').value = product.cost_currency || product.supplier_currency || product.currency || 'DKK'; intakeMessage(`Valgt vare: ${intakeEl('model').value}. Kontrollér oplysningerne og vælg hardwaretype og ejer før gemning.`); intakeEl('asset_type').focus(); } function updateHardwareIntakeQuantity() { const count = Number(intakeEl('intake-count').value); intakeEl('serial_number').closest('.form-group').hidden = count > 1; intakeEl('intake-serials-group').hidden = count <= 1; intakeEl('intake-save').textContent = count > 1 ? `Gem ${count} hardwareenheder` : 'Gem hardware'; } function hardwareIntakePayload(hardware) { if (hardwareIntake.mode === 'invoice' && !hardwareIntake.line) throw new Error('Vælg en fakturalinje først'); if (hardwareIntake.mode === 'barcode' && !hardwareIntake.code) throw new Error('Slå en kode op, og vælg en vare først'); const count = Number(intakeEl('intake-count').value); const serialText = intakeEl('intake-serials').value.trim(); if (hardwareIntake.inventory) { if (count !== 1) throw new Error('En inventory-maskine kan kun oprette ét asset'); hardware.hardware_specs = {...hardware.hardware_specs, ...hardwareIntake.inventory.hardware_specs}; } return {request_id:hardwareIntake.requestId, mode:hardwareIntake.mode, hardware, count, serial_numbers:count > 1 && serialText ? serialText.split(/\r?\n/) : [], preview_id:hardwareIntake.preview?.preview_id || null, line_key:hardwareIntake.line?.key || null, code:hardwareIntake.code, product_id:hardwareIntake.productId, approved_unit_cost:intakeEl('intake-unit-cost').value || null, currency:intakeEl('intake-currency').value}; } function openHardwareInventory() { setHardwareIntakeMode('manual'); intakeEl('intake-inventory-panel').hidden = false; findHardwareInventory(); } async function findHardwareInventory(page = 1) { const version = ++hardwareIntake.generation; intakeMessage('Henter PC / Mac inventory…'); try { const data = await intakeFetch(`/api/v1/hardware-intake/inventory?q=${encodeURIComponent(intakeEl('intake-inventory-search').value)}&by=${encodeURIComponent(intakeEl('intake-inventory-by').value || 'name')}&page=${page}`); if (version !== hardwareIntake.generation) return; hardwareIntake.inventoryItems = data.items; intakeEl('intake-inventory-results').innerHTML = data.items.map((item,index) => ``).join('') || '
Ingen maskiner fundet.
'; if (page > 1) intakeEl('intake-inventory-results').innerHTML += ``; if (data.pagination?.total_pages > page) intakeEl('intake-inventory-results').innerHTML += ``; intakeMessage(hardwareIntake.pairHardwareId ? 'Klik på en maskine for at parre den med dette asset. Eksisterende felter bevares.' : 'Vælg en maskine. Kontrollér oplysninger, hardwaretype og ejer før gemning.'); } catch (error) { if (version === hardwareIntake.generation) intakeMessage(error.message,true); } } async function chooseHardwareInventory(index) { const item = hardwareIntake.inventoryItems[index]; if (!item) return; if (hardwareIntake.pairHardwareId) { if (hardwareIntake.pairing) return; hardwareIntake.pairing = true; try { await intakeFetch(`/api/v1/hardware-intake/inventory/${hardwareIntake.pairHardwareId}/pair`, {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({inventory_id:item.id})}); window.location.reload(); } catch (error) { intakeMessage(error.message, true); } finally { hardwareIntake.pairing = false; } return; } hardwareIntake.inventory = item; for (const key of ['brand','model','serial_number']) intakeEl(key).value = item[key] || ''; intakeEl('asset_type').value = 'pc'; intakeEl('intake-count').value = 1; updateHardwareIntakeQuantity(); intakeMessage(`Valgt: ${item.name}. CPU, RAM, OS og øvrige inventory-oplysninger gemmes med asset. Kontrollér ejer og hardwaretype før gemning.`); } async function unpairHardwareInventory() { try { await intakeFetch(`/api/v1/hardware-intake/inventory/${hardwareIntake.pairHardwareId}/pair`, {method:'DELETE'}); window.location.reload(); } catch (error) { intakeMessage(error.message, true); } }