6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
722 lines
30 KiB
HTML
722 lines
30 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="sv">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>LandveX Intelligence — Rapportgenerator</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, system-ui, sans-serif; background: #f5f5f5; }
|
|
.header { background: #1a1a2e; color: #fff; padding: 20px; }
|
|
.header h1 { font-size: 20px; }
|
|
.header p { opacity: 0.7; font-size: 14px; margin-top: 4px; }
|
|
.nav { background: #16213e; padding: 0 20px; display: flex; gap: 4px; }
|
|
.nav a { color: rgba(255,255,255,0.7); text-decoration: none; padding: 12px 16px; font-size: 14px; }
|
|
.nav a.active { color: #fff; background: rgba(255,255,255,0.1); }
|
|
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
|
.card { background: #fff; padding: 24px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
.card h2 { font-size: 14px; text-transform: uppercase; color: #666; margin-bottom: 12px; }
|
|
.card .value { font-size: 28px; font-weight: 600; }
|
|
.card .sub { color: #999; font-size: 14px; margin-top: 4px; }
|
|
.product-card { cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; }
|
|
.product-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
|
.product-card.selected { border: 2px solid #1a1a2e; }
|
|
.price { font-size: 32px; font-weight: 600; color: #1a1a2e; }
|
|
.pages { color: #666; font-size: 14px; }
|
|
.btn { background: #1a1a2e; color: #fff; border: none; padding: 12px 24px; border-radius: 8px; font-size: 16px; cursor: pointer; margin-top: 16px; }
|
|
.btn:hover { background: #2a2a4e; }
|
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.btn-outline { background: transparent; color: #1a1a2e; border: 2px solid #1a1a2e; }
|
|
.btn-outline:hover { background: #1a1a2e; color: #fff; }
|
|
.btn-success { background: #28a745; }
|
|
.btn-success:hover { background: #218838; }
|
|
.btn-danger { background: #dc3545; }
|
|
.btn-danger:hover { background: #c82333; }
|
|
.input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 16px; margin-top: 8px; }
|
|
.report-preview { background: #fff; padding: 24px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 20px; }
|
|
.section { border-bottom: 1px solid #eee; padding: 16px 0; }
|
|
.section:last-child { border-bottom: none; }
|
|
.section h3 { font-size: 16px; color: #1a1a2e; margin-bottom: 8px; }
|
|
.section p { color: #666; font-size: 14px; line-height: 1.5; }
|
|
.confidence-bar { height: 8px; background: #eee; border-radius: 4px; overflow: hidden; margin-top: 8px; }
|
|
.confidence-fill { height: 100%; background: linear-gradient(90deg, #dc3545, #ffc107, #28a745); border-radius: 4px; }
|
|
.risk-badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
|
.risk-critical { background: #dc3545; color: #fff; }
|
|
.risk-high { background: #fd7e14; color: #fff; }
|
|
.risk-medium { background: #ffc107; color: #000; }
|
|
.risk-low { background: #28a745; color: #fff; }
|
|
.recommendation { display: flex; align-items: center; gap: 12px; padding: 12px; background: #f8f9fa; border-radius: 8px; margin-top: 8px; }
|
|
.rec-priority { width: 32px; height: 32px; border-radius: 50%; background: #1a1a2e; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 600; }
|
|
.hidden { display: none; }
|
|
.loading { text-align: center; padding: 40px; color: #666; }
|
|
|
|
/* === KPI-RUTOR (NYTT) === */
|
|
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin: 20px 0; }
|
|
.kpi-box {
|
|
background: #fff;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
.kpi-box:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
|
.kpi-box.highlight {
|
|
background: #1a1a2e;
|
|
color: #fff;
|
|
border-color: #1a1a2e;
|
|
}
|
|
.kpi-box.highlight .kpi-value { color: #fff; }
|
|
.kpi-box.highlight .kpi-label { color: rgba(255,255,255,0.8); }
|
|
.kpi-value { font-size: 32px; font-weight: 700; color: #1a1a2e; line-height: 1.2; }
|
|
.kpi-label { font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 8px; }
|
|
.kpi-trend { font-size: 12px; margin-top: 4px; font-weight: 600; }
|
|
.kpi-trend.up { color: #28a745; }
|
|
.kpi-trend.down { color: #dc3545; }
|
|
|
|
/* === INFOBOXAR === */
|
|
.info-box {
|
|
background: #f8f9fa;
|
|
border-left: 4px solid #0f3460;
|
|
padding: 14px 18px;
|
|
margin: 12px 0;
|
|
border-radius: 0 8px 8px 0;
|
|
}
|
|
.info-box.warning { border-left-color: #ffc107; background: #fffbeb; }
|
|
.info-box.danger { border-left-color: #dc3545; background: #fef2f2; }
|
|
.info-box.success { border-left-color: #28a745; background: #f0fdf4; }
|
|
.info-box-title {
|
|
font-weight: 700;
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 6px;
|
|
color: #1a1a2e;
|
|
}
|
|
.info-box p { margin: 0; font-size: 13px; color: #333; }
|
|
|
|
/* === DOKUMENTKONTROLL === */
|
|
.doc-control { background: #fff; padding: 20px; border-radius: 12px; margin-bottom: 20px; }
|
|
.doc-control h3 { font-size: 14px; color: #1a1a2e; margin-bottom: 12px; }
|
|
.control-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.control-table th {
|
|
background: #1a1a2e;
|
|
color: #fff;
|
|
padding: 8px 12px;
|
|
text-align: left;
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
}
|
|
.control-table td { padding: 8px 12px; border-bottom: 1px solid #eee; }
|
|
.control-table tr:nth-child(even) { background: #f8f9fa; }
|
|
|
|
/* === PDF-ACTIONS === */
|
|
.pdf-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
.pdf-actions .btn { margin-top: 0; padding: 10px 18px; font-size: 14px; }
|
|
.pdf-status {
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
margin-top: 12px;
|
|
font-size: 13px;
|
|
}
|
|
.pdf-status.success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
|
|
.pdf-status.error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
|
|
.pdf-status.info { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
|
|
|
|
/* === DIVIDER === */
|
|
.divider { height: 2px; background: linear-gradient(90deg, #1a1a2e, #e94560); margin: 20px 0; border-radius: 1px; }
|
|
|
|
/* === DATA TABLE === */
|
|
.data-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
|
|
.data-table thead { background: #1a1a2e; color: #fff; }
|
|
.data-table th { padding: 10px 12px; text-align: left; font-weight: 600; font-size: 10px; text-transform: uppercase; }
|
|
.data-table td { padding: 8px 12px; border-bottom: 1px solid #eee; }
|
|
.data-table tbody tr:nth-child(even) { background: #f8f9fa; }
|
|
|
|
/* === MODAL === */
|
|
.modal-overlay {
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,0.5); display: none;
|
|
align-items: center; justify-content: center; z-index: 1000;
|
|
}
|
|
.modal-overlay.active { display: flex; }
|
|
.modal {
|
|
background: #fff; border-radius: 12px; padding: 24px;
|
|
max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto;
|
|
}
|
|
.modal h3 { margin-bottom: 16px; }
|
|
.modal .form-group { margin-bottom: 12px; }
|
|
.modal label { display: block; font-size: 13px; color: #666; margin-bottom: 4px; }
|
|
.modal input, .modal select {
|
|
width: 100%; padding: 10px; border: 1px solid #ddd;
|
|
border-radius: 6px; font-size: 14px;
|
|
}
|
|
.modal-actions { display: flex; gap: 8px; margin-top: 16px; }
|
|
.modal-actions .btn { margin-top: 0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>LandveX Intelligence</h1>
|
|
<p>Rapportgenerator — Från data till beslut</p>
|
|
</div>
|
|
|
|
<div class="nav">
|
|
<a href="index.html">Ekonomi</a>
|
|
<a href="#" class="active">Intelligence</a>
|
|
<a href="#">CRM</a>
|
|
<a href="#">Support</a>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<!-- Produktväljare -->
|
|
<div id="product-selector">
|
|
<h2 style="margin-bottom: 16px;">Välj produkt</h2>
|
|
<div class="grid" id="products"></div>
|
|
</div>
|
|
|
|
<!-- Rapportgenerator -->
|
|
<div id="generator" class="hidden">
|
|
<div class="card">
|
|
<h2>Generera rapport</h2>
|
|
<p style="color: #666; margin-bottom: 12px;">Produkt: <strong id="selected-product-name"></strong></p>
|
|
<label style="color: #666; font-size: 14px;">Objekt / Adress</label>
|
|
<input type="text" class="input" id="object-name" placeholder="t.ex. Storgatan 1, Stockholm" value="Storgatan 1, Stockholm">
|
|
<button class="btn" onclick="generateReport()">Generera rapport</button>
|
|
<button class="btn btn-outline" style="margin-left: 8px;" onclick="backToProducts()">Tillbaka</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Laddar -->
|
|
<div id="loading" class="hidden">
|
|
<div class="loading">
|
|
<div style="font-size: 24px; margin-bottom: 12px;">⏳</div>
|
|
<p>Genererar rapport... Detta tar några sekunder.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rapportvisning -->
|
|
<div id="report-view" class="hidden">
|
|
<!-- Dokumentkontroll -->
|
|
<div class="doc-control" id="doc-control">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
|
<h3>📋 Dokumentkontroll</h3>
|
|
<button class="btn btn-outline" style="padding: 6px 12px; font-size: 12px; margin-top: 0;" onclick="openDocControlModal()">Redigera</button>
|
|
</div>
|
|
<table class="control-table" id="doc-control-table">
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- KPI-rutor -->
|
|
<div class="kpi-grid" id="kpi-grid">
|
|
<div class="kpi-box">
|
|
<div class="kpi-value" id="kpi-confidence">87%</div>
|
|
<div class="kpi-label">Confidence Score</div>
|
|
</div>
|
|
<div class="kpi-box highlight">
|
|
<div class="kpi-value" id="kpi-risks">3</div>
|
|
<div class="kpi-label">Kritiska Risker</div>
|
|
</div>
|
|
<div class="kpi-box">
|
|
<div class="kpi-value" id="kpi-observations">47</div>
|
|
<div class="kpi-label">Observationer</div>
|
|
</div>
|
|
<div class="kpi-box">
|
|
<div class="kpi-value" id="kpi-budget">$75K</div>
|
|
<div class="kpi-label">Total Budget</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
<h2 id="report-title"></h2>
|
|
<div class="pdf-actions">
|
|
<button class="btn btn-success" onclick="generatePdf()" id="btn-generate-pdf">
|
|
📄 Generera PDF
|
|
</button>
|
|
<button class="btn" onclick="previewHtml()">
|
|
👁️ HTML-preview
|
|
</button>
|
|
<button class="btn" onclick="exportReport('json')">
|
|
JSON
|
|
</button>
|
|
<button class="btn" onclick="exportReport('csv')">
|
|
CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="pdf-status" class="pdf-status hidden"></div>
|
|
|
|
<div class="report-preview" id="report-content"></div>
|
|
|
|
<div style="margin-top: 16px; display: flex; gap: 8px;">
|
|
<button class="btn" onclick="backToProducts()">Ny rapport</button>
|
|
<button class="btn btn-outline" onclick="cloneReport()">Klonera rapport</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal för dokumentkontroll -->
|
|
<div class="modal-overlay" id="doc-control-modal">
|
|
<div class="modal">
|
|
<h3>Redigera dokumentkontroll</h3>
|
|
<div class="form-group">
|
|
<label>Referens / Reference</label>
|
|
<input type="text" id="dc-reference" placeholder="t.ex. LVX-ABC123">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Version</label>
|
|
<input type="text" id="dc-version" value="1.0">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Beställare / Client</label>
|
|
<input type="text" id="dc-client" placeholder="Kundnamn">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Analytiker / Analyst</label>
|
|
<input type="text" id="dc-analyst" value="Bernt (AI) / LandveX Intelligence">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Klassificering / Classification</label>
|
|
<select id="dc-classification">
|
|
<option value="Konfidentiell / Confidential">Konfidentiell</option>
|
|
<option value="Strikt konfidentiell / Strictly Confidential">Strikt konfidentiell</option>
|
|
<option value="Intern / Internal">Intern</option>
|
|
<option value="Offentlig / Public">Offentlig</option>
|
|
</select>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn" onclick="saveDocControl()">Spara</button>
|
|
<button class="btn btn-outline" onclick="closeDocControlModal()">Avbryt</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let selectedProduct = null;
|
|
let currentReport = null;
|
|
let currentDocControl = null;
|
|
|
|
async function loadProducts() {
|
|
const res = await fetch('/api/intelligence/products');
|
|
const data = await res.json();
|
|
const container = document.getElementById('products');
|
|
container.innerHTML = data.products.map(p => `
|
|
<div class="card product-card" onclick="selectProduct('${p.id}')" data-id="${p.id}">
|
|
<h2>${p.name}</h2>
|
|
<div class="price">$${p.price.toLocaleString()}</div>
|
|
<div class="pages">${p.pages} sidor • ${p.deliveryDays} dagar leverans</div>
|
|
<p style="color: #666; margin-top: 8px; font-size: 14px;">${p.description}</p>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function selectProduct(id) {
|
|
document.querySelectorAll('.product-card').forEach(c => c.classList.remove('selected'));
|
|
document.querySelector(`[data-id="${id}"]`).classList.add('selected');
|
|
selectedProduct = id;
|
|
|
|
document.getElementById('product-selector').classList.add('hidden');
|
|
document.getElementById('generator').classList.remove('hidden');
|
|
|
|
fetch('/api/intelligence/products')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const product = data.products.find(p => p.id === id);
|
|
document.getElementById('selected-product-name').textContent = product.name;
|
|
});
|
|
}
|
|
|
|
function backToProducts() {
|
|
document.getElementById('product-selector').classList.remove('hidden');
|
|
document.getElementById('generator').classList.add('hidden');
|
|
document.getElementById('report-view').classList.add('hidden');
|
|
selectedProduct = null;
|
|
currentReport = null;
|
|
currentDocControl = null;
|
|
}
|
|
|
|
async function generateReport() {
|
|
const objectName = document.getElementById('object-name').value;
|
|
if (!objectName) return alert('Ange objektnamn');
|
|
|
|
document.getElementById('generator').classList.add('hidden');
|
|
document.getElementById('loading').classList.remove('hidden');
|
|
|
|
const res = await fetch('/api/intelligence/demo/generate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ productId: selectedProduct, objectName })
|
|
});
|
|
|
|
const data = await res.json();
|
|
currentReport = data.report;
|
|
|
|
await loadReport(data.report.id);
|
|
|
|
document.getElementById('loading').classList.add('hidden');
|
|
document.getElementById('report-view').classList.remove('hidden');
|
|
}
|
|
|
|
async function loadReport(id) {
|
|
const res = await fetch(`/api/intelligence/reports/${id}`);
|
|
const data = await res.json();
|
|
const report = data.report;
|
|
currentReport = report;
|
|
|
|
document.getElementById('report-title').textContent = `${report.productName} — ${report.objectName}`;
|
|
|
|
// Uppdatera KPI-rutor
|
|
updateKPIs(report);
|
|
|
|
// Ladda dokumentkontroll
|
|
await loadDocControl(id);
|
|
|
|
// Rendera sektioner
|
|
const content = document.getElementById('report-content');
|
|
content.innerHTML = report.sections.map(s => renderSection(s)).join('');
|
|
}
|
|
|
|
function updateKPIs(report) {
|
|
const confidence = report.confidence?.overall || 0.87;
|
|
document.getElementById('kpi-confidence').textContent = Math.round(confidence * 100) + '%';
|
|
|
|
// Räkna risker
|
|
const riskSection = report.sections.find(s => s.id === 'risk_matrix');
|
|
const criticalRisks = riskSection?.content?.risks?.filter(r => r.level === 'critical').length || 0;
|
|
document.getElementById('kpi-risks').textContent = criticalRisks;
|
|
|
|
// Observationer
|
|
const obsSection = report.sections.find(s => s.id === 'observations');
|
|
const obsCount = obsSection?.content?.length || 0;
|
|
document.getElementById('kpi-observations').textContent = obsCount;
|
|
|
|
// Budget
|
|
const recSection = report.sections.find(s => s.id === 'recommendations');
|
|
let totalBudget = 0;
|
|
if (recSection?.content) {
|
|
recSection.content.forEach(r => {
|
|
const match = r.budget.match(/\$?([\d,]+)/);
|
|
if (match) totalBudget += parseInt(match[1].replace(/,/g, ''));
|
|
});
|
|
}
|
|
document.getElementById('kpi-budget').textContent = totalBudget > 0 ? '$' + (totalBudget / 1000).toFixed(0) + 'K' : '—';
|
|
}
|
|
|
|
async function loadDocControl(reportId) {
|
|
try {
|
|
const res = await fetch(`/api/reports/${reportId}/document-control`);
|
|
const data = await res.json();
|
|
currentDocControl = data.documentControl;
|
|
renderDocControl(data.documentControl);
|
|
} catch (e) {
|
|
console.log('Doc control not available yet');
|
|
}
|
|
}
|
|
|
|
function renderDocControl(dc) {
|
|
const tbody = document.querySelector('#doc-control-table tbody');
|
|
tbody.innerHTML = `
|
|
<tr><td><strong>Referens</strong></td><td>${dc.reference || '—'}</td></tr>
|
|
<tr><td><strong>Version</strong></td><td>${dc.version || '1.0'}</td></tr>
|
|
<tr><td><strong>Datum</strong></td><td>${dc.date || new Date().toISOString().split('T')[0]}</td></tr>
|
|
<tr><td><strong>Beställare</strong></td><td>${dc.client || '—'}</td></tr>
|
|
<tr><td><strong>Analytiker</strong></td><td>${dc.analyst || '—'}</td></tr>
|
|
<tr><td><strong>Klassificering</strong></td><td><span class="risk-badge risk-critical">${dc.classification || 'Konfidentiell'}</span></td></tr>
|
|
`;
|
|
}
|
|
|
|
function openDocControlModal() {
|
|
if (!currentDocControl) return;
|
|
document.getElementById('dc-reference').value = currentDocControl.reference || '';
|
|
document.getElementById('dc-version').value = currentDocControl.version || '1.0';
|
|
document.getElementById('dc-client').value = currentDocControl.client || '';
|
|
document.getElementById('dc-analyst').value = currentDocControl.analyst || '';
|
|
document.getElementById('dc-classification').value = currentDocControl.classification || 'Konfidentiell / Confidential';
|
|
document.getElementById('doc-control-modal').classList.add('active');
|
|
}
|
|
|
|
function closeDocControlModal() {
|
|
document.getElementById('doc-control-modal').classList.remove('active');
|
|
}
|
|
|
|
async function saveDocControl() {
|
|
if (!currentReport) return;
|
|
|
|
const body = {
|
|
reference: document.getElementById('dc-reference').value,
|
|
version: document.getElementById('dc-version').value,
|
|
client: document.getElementById('dc-client').value,
|
|
analyst: document.getElementById('dc-analyst').value,
|
|
classification: document.getElementById('dc-classification').value,
|
|
changes: 'Uppdaterad via admin-gränssnitt'
|
|
};
|
|
|
|
const res = await fetch(`/api/reports/${currentReport.id}/document-control`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
currentDocControl = data.documentControl;
|
|
renderDocControl(data.documentControl);
|
|
closeDocControlModal();
|
|
showPdfStatus('Dokumentkontroll uppdaterad', 'success');
|
|
}
|
|
}
|
|
|
|
async function generatePdf() {
|
|
if (!currentReport) return;
|
|
|
|
const btn = document.getElementById('btn-generate-pdf');
|
|
btn.disabled = true;
|
|
btn.textContent = '⏳ Genererar PDF...';
|
|
showPdfStatus('Genererar professionell PDF med LandveX-branding...', 'info');
|
|
|
|
try {
|
|
const res = await fetch(`/api/reports/${currentReport.id}/pdf`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(currentDocControl || {})
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
showPdfStatus(`✅ PDF genererad! <a href="${data.downloadUrl}" target="_blank" style="color: #166534; font-weight: 600;">Ladda ner PDF</a> | <a href="${data.previewUrl}" target="_blank" style="color: #1e40af;">HTML-preview</a>`, 'success');
|
|
} else {
|
|
showPdfStatus('❌ PDF-generering misslyckades: ' + (data.error || 'Okänt fel'), 'error');
|
|
}
|
|
} catch (e) {
|
|
showPdfStatus('❌ Nätverksfel: ' + e.message, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = '📄 Generera PDF';
|
|
}
|
|
}
|
|
|
|
function previewHtml() {
|
|
if (!currentReport) return;
|
|
window.open(`/api/reports/${currentReport.id}/html`, '_blank');
|
|
}
|
|
|
|
function showPdfStatus(message, type) {
|
|
const el = document.getElementById('pdf-status');
|
|
el.className = 'pdf-status ' + type;
|
|
el.innerHTML = message;
|
|
el.classList.remove('hidden');
|
|
}
|
|
|
|
async function cloneReport() {
|
|
if (!currentReport) return;
|
|
|
|
const res = await fetch(`/api/reports/${currentReport.id}/clone`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ version: '2.0' })
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
showPdfStatus(`✅ Rapport klonad! Nytt ID: ${data.newId}`, 'success');
|
|
}
|
|
}
|
|
|
|
function renderSection(section) {
|
|
const c = section.content;
|
|
|
|
switch(section.id) {
|
|
case 'front_page':
|
|
return '';
|
|
|
|
case 'executive_summary':
|
|
return `
|
|
<div class="section">
|
|
<h3>📊 ${section.name}</h3>
|
|
<div class="info-box">
|
|
<div class="info-box-title">Vad vet vi? / What do we know?</div>
|
|
<p>${c.whatWeKnow}</p>
|
|
</div>
|
|
<div class="info-box success">
|
|
<div class="info-box-title">Hur säkert är det? / How confident are we?</div>
|
|
<p>${c.confidence}</p>
|
|
</div>
|
|
<div class="info-box warning">
|
|
<div class="info-box-title">Vad bör kunden göra? / What should the client do?</div>
|
|
<p>${c.recommendation}</p>
|
|
</div>
|
|
</div>`;
|
|
|
|
case 'scope':
|
|
return `
|
|
<div class="section">
|
|
<h3>🎯 ${section.name}</h3>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
|
<div>
|
|
<h4 style="color: #28a745; margin-bottom: 8px;">✓ Ingår</h4>
|
|
<ul style="margin-left: 20px; color: #666;">
|
|
${c.included.map(i => `<li>${i}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 style="color: #dc3545; margin-bottom: 8px;">✗ Ingår inte</h4>
|
|
<ul style="margin-left: 20px; color: #666;">
|
|
${c.excluded.map(i => `<li>${i}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
case 'confidence':
|
|
return `
|
|
<div class="section">
|
|
<h3>🎯 ${section.name} — ${Math.round(c.overall * 100)}%</h3>
|
|
<div class="confidence-bar">
|
|
<div class="confidence-fill" style="width: ${c.overall * 100}%"></div>
|
|
</div>
|
|
<div class="info-box" style="margin-top: 12px;">
|
|
<div class="info-box-title">Metodologi</div>
|
|
<p>${c.methodology}</p>
|
|
</div>
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr><th>Källa</th><th>Score</th><th>Vikt</th><th>Noteringar</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
${c.sources.map(s => `
|
|
<tr>
|
|
<td><strong>${s.name}</strong></td>
|
|
<td>${Math.round(s.score * 100)}%</td>
|
|
<td>${s.weight}</td>
|
|
<td><small style="color: #999;">${s.notes}</small></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
|
|
case 'data_sources':
|
|
return `
|
|
<div class="section">
|
|
<h3>📚 ${section.name}</h3>
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr><th>Källa</th><th>Typ</th><th>Datum</th><th>Metod</th><th>Begränsningar</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
${c.map(s => `
|
|
<tr>
|
|
<td><strong>${s.name}</strong></td>
|
|
<td>${s.type}</td>
|
|
<td>${s.date}</td>
|
|
<td>${s.method}</td>
|
|
<td><small style="color: #999;">${s.limitations}</small></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
|
|
case 'analysis':
|
|
return `
|
|
<div class="section">
|
|
<h3>🔍 ${section.name}</h3>
|
|
<div class="info-box">
|
|
<div class="info-box-title">Metodologi</div>
|
|
<p>${c.methodology}</p>
|
|
</div>
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr><th>Område</th><th>Fynd</th><th>Allvarlighet</th><th>Bevis</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
${c.findings.map(f => `
|
|
<tr>
|
|
<td><strong>${f.area}</strong></td>
|
|
<td>${f.finding}</td>
|
|
<td><span class="risk-badge risk-${f.severity}">${f.severity.toUpperCase()}</span></td>
|
|
<td>${f.evidence} observationer</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
|
|
case 'risk_matrix':
|
|
return `
|
|
<div class="section">
|
|
<h3>⚠️ ${section.name}</h3>
|
|
${c.risks.map(r => `
|
|
<div class="info-box ${r.level === 'critical' ? 'danger' : r.level === 'high' ? 'warning' : ''}" style="margin: 8px 0;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
|
<strong>${r.id}: ${r.description}</strong>
|
|
<span class="risk-badge risk-${r.level}">${r.level.toUpperCase()}</span>
|
|
</div>
|
|
<p style="font-size: 13px;">Sannolikhet: ${r.probability} • Konsekvens: ${r.consequence}</p>
|
|
<p style="margin-top: 4px; font-size: 13px;"><strong>Åtgärd:</strong> ${r.mitigation}</p>
|
|
</div>
|
|
`).join('')}
|
|
<div class="info-box" style="margin-top: 12px;">
|
|
<div class="info-box-title">Metodologi</div>
|
|
<p>${c.methodology}</p>
|
|
</div>
|
|
</div>`;
|
|
|
|
case 'recommendations':
|
|
return `
|
|
<div class="section">
|
|
<h3>💡 ${section.name}</h3>
|
|
${c.map(r => `
|
|
<div class="recommendation">
|
|
<div class="rec-priority">${r.priority}</div>
|
|
<div>
|
|
<strong>${r.action}</strong>
|
|
<p style="color: #666; font-size: 13px;">📅 ${r.timeline} | 👤 ${r.owner} | 💰 ${r.budget}</p>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>`;
|
|
|
|
case 'appendix':
|
|
return `
|
|
<div class="section">
|
|
<h3>📎 ${section.name}</h3>
|
|
${c.dataFiles.map(f => `
|
|
<div class="info-box" style="margin: 8px 0;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<strong>${f.name}</strong>
|
|
<span style="background: #1a1a2e; color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-family: monospace;">${f.type}</span>
|
|
</div>
|
|
<p style="font-size: 12px; color: #999; margin-top: 4px;">📦 ${f.size}</p>
|
|
<p style="font-size: 13px; margin-top: 4px;">${f.description}</p>
|
|
</div>
|
|
`).join('')}
|
|
<div class="info-box" style="margin-top: 12px;">
|
|
<div class="info-box-title">Reproducerbarhet</div>
|
|
<p>${c.reproducibilityNote}</p>
|
|
<p style="margin-top: 4px; font-size: 12px; color: #999;">Protokoll: ${c.methodology} • Version: ${c.version} • Genererad: ${c.generatedAt}</p>
|
|
</div>
|
|
</div>`;
|
|
|
|
default:
|
|
return `
|
|
<div class="section">
|
|
<h3>${section.name}</h3>
|
|
<pre style="background: #f8f9fa; padding: 12px; border-radius: 4px; overflow-x: auto; font-size: 12px;">${JSON.stringify(c, null, 2)}</pre>
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
function exportReport(format) {
|
|
if (!currentReport) return;
|
|
window.open(`/api/intelligence/reports/${currentReport.id}/export/${format}`, '_blank');
|
|
}
|
|
|
|
loadProducts();
|
|
</script>
|
|
</body>
|
|
</html>
|