feat: quick opret leverandør split-view panel v2.2.26
This commit is contained in:
parent
2ed3118c83
commit
bdf76a2a80
@ -867,6 +867,133 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =============================================
|
||||
QUICK OPRET LEVERANDØR — Split-view modal
|
||||
Venstre: PDF iframe | Højre: Vendor form
|
||||
============================================== -->
|
||||
<div class="modal fade" id="quickVendorSplitModal" tabindex="-1" style="--bs-modal-width: 100%;">
|
||||
<div class="modal-dialog modal-fullscreen">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2 border-bottom">
|
||||
<h5 class="modal-title"><i class="bi bi-person-plus me-2"></i>Opret / Link Leverandør</h5>
|
||||
<div class="ms-3 d-flex align-items-center gap-2">
|
||||
<span class="badge bg-secondary" id="qvSplitFilename" style="font-size:.85rem"></span>
|
||||
</div>
|
||||
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0 d-flex" style="height: calc(100vh - 120px); overflow:hidden;">
|
||||
|
||||
<!-- LEFT: PDF viewer -->
|
||||
<div class="d-flex flex-column border-end" style="width:58%; min-width:400px;">
|
||||
<div class="px-3 py-2 bg-body-tertiary border-bottom small text-muted">
|
||||
<i class="bi bi-file-pdf text-danger me-1"></i>Faktura PDF
|
||||
</div>
|
||||
<iframe id="qvPdfFrame" src="" style="flex:1; border:none; width:100%; height:100%;" title="PDF Preview"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: Vendor form -->
|
||||
<div class="d-flex flex-column" style="width:42%; overflow-y:auto;">
|
||||
<div class="px-4 py-3 bg-body-tertiary border-bottom">
|
||||
<span class="small text-muted">Udfyld leverandøroplysninger — felter er preudfyldt fra faktura-PDF</span>
|
||||
</div>
|
||||
<div class="px-4 py-3">
|
||||
<input type="hidden" id="qvFileId">
|
||||
<input type="hidden" id="qvExistingVendorId">
|
||||
|
||||
<!-- Search existing -->
|
||||
<div class="card mb-3 border-primary">
|
||||
<div class="card-header py-2 bg-primary text-white small"><i class="bi bi-search me-1"></i>Link eksisterende leverandør</div>
|
||||
<div class="card-body py-2">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" id="qvSearchInput" placeholder="Søg navn eller CVR..." oninput="qvSearchVendors(this.value)">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="qvSearchVendors(document.getElementById('qvSearchInput').value)"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
<div id="qvSearchResults" class="list-group mt-2" style="max-height:160px; overflow-y:auto;">
|
||||
<div class="list-group-item text-muted small py-1">Søg for at finde eksisterende leverandør</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-muted my-2 small">— eller opret ny leverandør nedenfor —</div>
|
||||
|
||||
<form id="qvVendorForm" autocomplete="off">
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-8">
|
||||
<label class="form-label small mb-1">Navn *</label>
|
||||
<input type="text" class="form-control form-control-sm" id="qvName" required placeholder="Firma navn">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small mb-1">CVR</label>
|
||||
<input type="text" class="form-control form-control-sm" id="qvCVR" maxlength="8" placeholder="12345678">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label small mb-1">Email</label>
|
||||
<input type="email" class="form-control form-control-sm" id="qvEmail" placeholder="kontakt@firma.dk">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label small mb-1">Telefon</label>
|
||||
<input type="tel" class="form-control form-control-sm" id="qvPhone" placeholder="+45 12 34 56 78">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small mb-1">Adresse</label>
|
||||
<input type="text" class="form-control form-control-sm" id="qvAddress" placeholder="Vejnavn nr.">
|
||||
</div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-4">
|
||||
<label class="form-label small mb-1">Postnr.</label>
|
||||
<input type="text" class="form-control form-control-sm" id="qvPostal" maxlength="10">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<label class="form-label small mb-1">By</label>
|
||||
<input type="text" class="form-control form-control-sm" id="qvCity">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small mb-1">Website / domæne</label>
|
||||
<input type="text" class="form-control form-control-sm" id="qvDomain" placeholder="firma.dk">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small mb-1">Kategori</label>
|
||||
<select class="form-select form-select-sm" id="qvCategory">
|
||||
<option value="general">Generel</option>
|
||||
<option value="telecom">Telecom</option>
|
||||
<option value="hardware">Hardware</option>
|
||||
<option value="software">Software</option>
|
||||
<option value="services">Services</option>
|
||||
<option value="payroll">Løn / HR</option>
|
||||
<option value="utilities">Forsyning</option>
|
||||
<option value="insurance">Forsikring</option>
|
||||
<option value="rent">Husleje / Lokaler</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small mb-1">Noter (inkl. bank/IBAN info)</label>
|
||||
<textarea class="form-control form-control-sm" id="qvNotes" rows="3" placeholder="IBAN, kontonummer, BIC/SWIFT, betalingsbetingelser..."></textarea>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Status alert -->
|
||||
<div id="qvStatusAlert" class="alert d-none py-2 small"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.modal-body -->
|
||||
|
||||
<div class="modal-footer py-2 border-top justify-content-between">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Luk</button>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="saveQuickVendor()">
|
||||
<i class="bi bi-person-plus me-1"></i>Opret og link leverandør
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link/Create Vendor Modal -->
|
||||
<div class="modal fade" id="linkVendorModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
@ -1843,12 +1970,12 @@ function renderUnhandledFiles(files) {
|
||||
<td>${statusBadge}</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-success" onclick="openQuickVendorCreate(${file.file_id}, '${escapeHtml(file.filename)}')" title="Opret / Link leverandør">
|
||||
<i class="bi bi-person-plus"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-warning" onclick="rerunSingleFile(${file.file_id})" title="Kør analyse igen">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="viewFilePDF(${file.file_id})" title="Vis PDF">
|
||||
<i class="bi bi-file-pdf"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="deleteFile(${file.file_id})" title="Slet">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
@ -1961,6 +2088,178 @@ async function retryExtraction(fileId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Quick Vendor Split-View ─────────────────────────────────────────────
|
||||
async function openQuickVendorCreate(fileId, filename) {
|
||||
// Reset
|
||||
document.getElementById('qvFileId').value = fileId;
|
||||
document.getElementById('qvExistingVendorId').value = '';
|
||||
document.getElementById('qvSplitFilename').textContent = filename;
|
||||
document.getElementById('qvName').value = '';
|
||||
document.getElementById('qvCVR').value = '';
|
||||
document.getElementById('qvEmail').value = '';
|
||||
document.getElementById('qvPhone').value = '';
|
||||
document.getElementById('qvAddress').value = '';
|
||||
document.getElementById('qvPostal').value = '';
|
||||
document.getElementById('qvCity').value = '';
|
||||
document.getElementById('qvDomain').value = '';
|
||||
document.getElementById('qvNotes').value = '';
|
||||
document.getElementById('qvSearchInput').value = '';
|
||||
document.getElementById('qvSearchResults').innerHTML = '<div class="list-group-item text-muted small py-1">Søg for at finde eksisterende leverandør</div>';
|
||||
document.getElementById('qvStatusAlert').className = 'alert d-none py-2 small';
|
||||
|
||||
// Load PDF in iframe
|
||||
document.getElementById('qvPdfFrame').src = `/api/v1/supplier-invoices/files/${fileId}/download`;
|
||||
|
||||
// Open modal immediately
|
||||
const modal = new bootstrap.Modal(document.getElementById('quickVendorSplitModal'), {backdrop: 'static'});
|
||||
modal.show();
|
||||
|
||||
// Async: load extracted data and pre-fill form
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/supplier-invoices/files/${fileId}/extracted-data`);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
const ext = data.extraction || {};
|
||||
let ai = {};
|
||||
try { ai = JSON.parse(ext.llm_response_json || '{}'); } catch(e) {}
|
||||
|
||||
const name = ext.vendor_name || ai.vendor_name || '';
|
||||
const cvr = (ext.vendor_cvr || ai.vendor_cvr || '').replace(/^DK/i, '').trim();
|
||||
const addr = ai.vendor_address || '';
|
||||
|
||||
if (name) document.getElementById('qvName').value = name;
|
||||
if (cvr) document.getElementById('qvCVR').value = cvr;
|
||||
if (addr) {
|
||||
// Try to split address into street / postal / city
|
||||
const parts = addr.split(/,|\n/).map(s => s.trim()).filter(Boolean);
|
||||
if (parts.length >= 1) document.getElementById('qvAddress').value = parts[0];
|
||||
if (parts.length >= 2) {
|
||||
const postalCity = parts[parts.length - 1];
|
||||
const m = postalCity.match(/^(\d{4})\s+(.+)$/);
|
||||
if (m) {
|
||||
document.getElementById('qvPostal').value = m[1];
|
||||
document.getElementById('qvCity').value = m[2];
|
||||
} else {
|
||||
document.getElementById('qvCity').value = postalCity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('Could not pre-fill vendor form:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function qvSearchVendors(query) {
|
||||
const results = document.getElementById('qvSearchResults');
|
||||
if (!query || query.length < 2) {
|
||||
results.innerHTML = '<div class="list-group-item text-muted small py-1">Søg for at finde eksisterende leverandør</div>';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/vendors?search=${encodeURIComponent(query)}&active_only=true`);
|
||||
const vendors = await resp.json();
|
||||
if (!vendors || vendors.length === 0) {
|
||||
results.innerHTML = '<div class="list-group-item text-muted small py-1">Ingen leverandører fundet</div>';
|
||||
return;
|
||||
}
|
||||
results.innerHTML = vendors.slice(0, 10).map(v => `
|
||||
<button type="button" class="list-group-item list-group-item-action py-1 small"
|
||||
onclick="qvSelectVendor(${v.id}, '${escapeHtml(v.name)}', '${v.cvr_number || ''}')">
|
||||
<strong>${escapeHtml(v.name)}</strong>
|
||||
${v.cvr_number ? `<span class="text-muted ms-2">${v.cvr_number}</span>` : ''}
|
||||
</button>
|
||||
`).join('');
|
||||
} catch(e) {
|
||||
results.innerHTML = '<div class="list-group-item text-danger small py-1">Fejl ved søgning</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function qvSelectVendor(vendorId, vendorName, vendorCVR) {
|
||||
document.getElementById('qvExistingVendorId').value = vendorId;
|
||||
document.getElementById('qvName').value = vendorName;
|
||||
document.getElementById('qvCVR').value = vendorCVR;
|
||||
const alert = document.getElementById('qvStatusAlert');
|
||||
alert.className = 'alert alert-success py-2 small';
|
||||
alert.textContent = `✅ Valgt: ${vendorName} — klik "Opret og link" for at linke`;
|
||||
}
|
||||
|
||||
async function saveQuickVendor() {
|
||||
const fileId = document.getElementById('qvFileId').value;
|
||||
const existingId = document.getElementById('qvExistingVendorId').value;
|
||||
const name = document.getElementById('qvName').value.trim();
|
||||
const cvr = document.getElementById('qvCVR').value.trim();
|
||||
const email = document.getElementById('qvEmail').value.trim();
|
||||
const phone = document.getElementById('qvPhone').value.trim();
|
||||
const address = document.getElementById('qvAddress').value.trim();
|
||||
const postal = document.getElementById('qvPostal').value.trim();
|
||||
const city = document.getElementById('qvCity').value.trim();
|
||||
const domain = document.getElementById('qvDomain').value.trim();
|
||||
const category = document.getElementById('qvCategory').value;
|
||||
const notes = document.getElementById('qvNotes').value.trim();
|
||||
|
||||
const statusEl = document.getElementById('qvStatusAlert');
|
||||
|
||||
if (!name) {
|
||||
statusEl.className = 'alert alert-danger py-2 small';
|
||||
statusEl.textContent = 'Navn er påkrævet.';
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = 'alert alert-info py-2 small';
|
||||
statusEl.textContent = 'Gemmer…';
|
||||
|
||||
try {
|
||||
let vendorId = existingId ? parseInt(existingId) : null;
|
||||
|
||||
if (!vendorId) {
|
||||
// Create new vendor
|
||||
const payload = {
|
||||
name, cvr_number: cvr || null,
|
||||
email: email || null, phone: phone || null,
|
||||
address: [address, postal && city ? `${postal} ${city}` : city].filter(Boolean).join('\n') || null,
|
||||
postal_code: postal || null, city: city || null,
|
||||
domain: domain || null, category,
|
||||
notes: notes || null
|
||||
};
|
||||
const resp = await fetch('/api/v1/vendors', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Oprettelse fejlede');
|
||||
}
|
||||
const created = await resp.json();
|
||||
vendorId = created.id;
|
||||
}
|
||||
|
||||
// Link vendor to file
|
||||
const linkResp = await fetch(`/api/v1/supplier-invoices/files/${fileId}/link-vendor`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({vendor_id: vendorId})
|
||||
});
|
||||
if (!linkResp.ok) {
|
||||
const err = await linkResp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Link fejlede');
|
||||
}
|
||||
|
||||
statusEl.className = 'alert alert-success py-2 small';
|
||||
statusEl.textContent = `✅ Leverandør ${existingId ? 'linket' : 'oprettet og linket'}!`;
|
||||
|
||||
setTimeout(() => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('quickVendorSplitModal')).hide();
|
||||
loadUnhandledFiles();
|
||||
}, 900);
|
||||
|
||||
} catch(e) {
|
||||
statusEl.className = 'alert alert-danger py-2 small';
|
||||
statusEl.textContent = '❌ ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Rerun full extraction for a file in the unhandled tab
|
||||
async function rerunSingleFile(fileId) {
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user