ba9cddf9a1
- Stripe: StripeClient.php, checkout/portal/webhook endpoints, idempotent event handling - FreeTier: tier-aware credits (free/light/pro/pro_plus), bonus_balance, hourly caps per tier - pricing.php + billing.php: 4-tier cards, 3 topups, Customer Portal, balance breakdown - Min Sak: CaseStore.php, AzureDocIntelligence.php, AzureSearchAdmin.php — per-user hybrid RAG - api/case/: upload, list, delete, ingest-callback (HMAC-auth'd from n8n) - award-survey-credits: inter-site HMAC endpoint for dobetternorge.no survey bonus - dashboard.php: tier badge, balance breakdown card, Min Sak CTA, survey CTA - KorrespondAgent + all 3 other agents: use_my_case toggle wired to dbnToolsCaseContext() - bootstrap.php: dbnToolsCaseContext(), dbnToolsIntersiteSecret(), dbnToolsCurrentTier() Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
229 lines
12 KiB
PHP
229 lines
12 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/includes/bootstrap.php';
|
|
require_once __DIR__ . '/includes/FreeTier.php';
|
|
require_once __DIR__ . '/includes/CaseStore.php';
|
|
|
|
if (!dbnToolsIsAuthenticated()) {
|
|
header('Location: /?return=' . urlencode('/min-sak.php'));
|
|
exit;
|
|
}
|
|
|
|
$uiLang = dbnToolsCurrentLanguage();
|
|
$userId = (int)($_SESSION['dbn_tools_sso_uid'] ?? 0);
|
|
if ($userId <= 0) {
|
|
header('Location: /dashboard.php');
|
|
exit;
|
|
}
|
|
|
|
$detail = FreeTier::balanceDetail($userId);
|
|
$tier = (string)$detail['tier'];
|
|
|
|
// Free tier: show upgrade gate
|
|
if (!in_array($tier, ['light', 'pro', 'pro_plus'], true)) {
|
|
require_once __DIR__ . '/includes/footer.php';
|
|
$upgradeUrl = '/pricing.php';
|
|
?><!doctype html>
|
|
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
|
<head><meta charset="utf-8"><title>Min Sak — Do Better Norge</title>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
|
|
<link rel="stylesheet" href="assets/css/tools.css">
|
|
</head><body><main style="max-width:720px;margin:4rem auto;padding:0 1.5rem;text-align:center;font-family:'IBM Plex Sans',sans-serif;">
|
|
<h1 style="font-family:'Crimson Pro',serif;font-size:2.4rem;margin:0 0 0.5rem;color:#00205B;">Min Sak — bygg din egen sak</h1>
|
|
<p style="color:#4b5563;font-size:1.1rem;margin-bottom:2rem;">Last opp dokumentene fra saken din én gang, og la <strong>alle</strong> verktøyene jobbe på din private korpus.</p>
|
|
<ul style="text-align:left;display:inline-block;line-height:1.9;color:#1f2937;">
|
|
<li>📄 Privat dokumentbank med OCR</li>
|
|
<li>🔍 Hybrid søk (BM25 + vektor) i din sak</li>
|
|
<li>🧠 Alle verktøy kan referere til din egen sak</li>
|
|
<li>🇪🇺 Alt lagres i EU (Tyskland/Finland/Norge)</li>
|
|
</ul>
|
|
<p style="margin:2rem 0 0;"><a href="<?= htmlspecialchars($upgradeUrl) ?>" style="background:#00205B;color:#fff;padding:1rem 2rem;border-radius:8px;font-weight:700;text-decoration:none;display:inline-block;">Se planer fra €9/mo</a></p>
|
|
</main></body></html><?php
|
|
exit;
|
|
}
|
|
|
|
$docs = CaseStore::listDocs($userId);
|
|
$used = (int)$detail['storage_used_bytes'];
|
|
$quota = (int)$detail['storage_quota_bytes'];
|
|
$usedMb = round($used / 1048576, 1);
|
|
$quotaMb = round($quota / 1048576, 0);
|
|
$pct = $quota > 0 ? min(100, round(($used / $quota) * 100)) : 0;
|
|
?>
|
|
<!doctype html>
|
|
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Min Sak — tools.dobetternorge.no</title>
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
|
|
<link rel="stylesheet" href="assets/css/tools.css">
|
|
<style>
|
|
.ms-shell { max-width: 980px; margin: 0 auto; padding: 2rem 1.5rem 4rem; font-family: 'IBM Plex Sans', sans-serif; }
|
|
.ms-shell h1 { font-family: 'Crimson Pro', serif; margin: 0; }
|
|
.ms-shell .lede { color: #6b7280; margin: 0.25rem 0 2rem; }
|
|
.ms-status { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem; }
|
|
@media (max-width: 640px) { .ms-status { grid-template-columns: 1fr; } }
|
|
.ms-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 1.25rem 1.5rem; }
|
|
.ms-card h2 { margin: 0 0 0.4rem; font-size: 1rem; color: #374151; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
.ms-card .big { font-size: 1.6rem; font-weight: 700; color: #00205B; }
|
|
.ms-storage-bar { background: #f3f4f6; height: 10px; border-radius: 999px; overflow: hidden; margin: 0.6rem 0; }
|
|
.ms-storage-bar > div { background: linear-gradient(90deg, #00205B, #003478); height: 100%; }
|
|
.ms-upload { background: #fff; border: 2px dashed #cbd5e1; border-radius: 12px; padding: 2.5rem 1.5rem; text-align: center; cursor: pointer; transition: all 0.15s; margin-bottom: 2rem; }
|
|
.ms-upload:hover, .ms-upload.is-drag { border-color: #00205B; background: #f8fafc; }
|
|
.ms-upload input { display: none; }
|
|
.ms-upload-icon { font-size: 2.4rem; line-height: 1; margin-bottom: 0.5rem; }
|
|
.ms-upload p { margin: 0.25rem 0; color: #374151; }
|
|
.ms-upload .hint { color: #6b7280; font-size: 0.85rem; }
|
|
.ms-docs { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; }
|
|
.ms-doc { padding: 1rem 1.25rem; border-bottom: 1px solid #f3f4f6; display: flex; align-items: center; gap: 1rem; }
|
|
.ms-doc:last-child { border-bottom: none; }
|
|
.ms-doc-info { flex: 1; min-width: 0; }
|
|
.ms-doc-name { font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.ms-doc-meta { font-size: 0.85rem; color: #6b7280; }
|
|
.ms-status-pill { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 0.78rem; font-weight: 600; }
|
|
.ms-status-pending { background: #fef3c7; color: #92400e; }
|
|
.ms-status-running { background: #ddd6fe; color: #5b21b6; }
|
|
.ms-status-ready { background: #d1fae5; color: #065f46; }
|
|
.ms-status-failed { background: #fee2e2; color: #991b1b; }
|
|
.ms-doc-actions button { background: #f3f4f6; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; }
|
|
.ms-doc-actions button:hover { background: #fee2e2; color: #991b1b; }
|
|
.ms-empty { padding: 2rem 1.5rem; text-align: center; color: #6b7280; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="ms-shell">
|
|
<p style="margin:0 0 0.25rem;color:#6b7280;font-size:0.85rem;text-transform:uppercase;letter-spacing:0.06em;">
|
|
<a href="/dashboard.php" style="color:inherit;">← Dashbord</a> · <a href="/billing.php" style="color:inherit;">Fakturering</a>
|
|
</p>
|
|
<h1>Min Sak</h1>
|
|
<p class="lede">Last opp dokumentene dine én gang. Alle verktøyene kan deretter referere til din private sak når du krysser av "Bruk min sak som kontekst".</p>
|
|
|
|
<section class="ms-status">
|
|
<div class="ms-card">
|
|
<h2>Lagring</h2>
|
|
<div class="big"><?= $usedMb ?> MB <span style="color:#6b7280;font-size:1rem;">/ <?= $quotaMb ?> MB</span></div>
|
|
<div class="ms-storage-bar"><div style="width: <?= $pct ?>%;"></div></div>
|
|
<p style="margin:0;color:#6b7280;font-size:0.85rem;"><?= count($docs) ?> dokumenter</p>
|
|
</div>
|
|
<div class="ms-card">
|
|
<h2>Plan</h2>
|
|
<div class="big" style="font-size:1.3rem;">
|
|
<?= htmlspecialchars(['light'=>'Light','pro'=>'Pro','pro_plus'=>'Pro+ Familie'][$tier] ?? $tier) ?>
|
|
</div>
|
|
<p style="margin:0;color:#6b7280;font-size:0.85rem;">
|
|
<a href="/pricing.php">Oppgrader</a> · <a href="/billing.php">Administrer</a>
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<label for="msFileInput" class="ms-upload" id="msUploadZone">
|
|
<input id="msFileInput" type="file" accept="application/pdf" multiple>
|
|
<div class="ms-upload-icon">📤</div>
|
|
<p><strong>Slipp PDF-filer her, eller klikk for å bla</strong></p>
|
|
<p class="hint">Maks 25 MB per fil · OCR kjøres automatisk · alt lagres kryptert i EU</p>
|
|
</label>
|
|
|
|
<div id="msFlash" role="status" aria-live="polite" style="margin-bottom:1rem;"></div>
|
|
|
|
<section class="ms-docs" aria-label="Dine dokumenter">
|
|
<?php if (empty($docs)): ?>
|
|
<p class="ms-empty">Ingen dokumenter ennå. Last opp din første PDF over.</p>
|
|
<?php else: foreach ($docs as $d): ?>
|
|
<div class="ms-doc" data-doc-id="<?= (int)$d['id'] ?>">
|
|
<div style="font-size:1.6rem;line-height:1;">📄</div>
|
|
<div class="ms-doc-info">
|
|
<div class="ms-doc-name"><?= htmlspecialchars($d['filename']) ?></div>
|
|
<div class="ms-doc-meta">
|
|
<?= round((int)$d['size_bytes'] / 1024, 0) ?> KB
|
|
<?php if (!empty($d['page_count'])): ?> · <?= (int)$d['page_count'] ?> sider<?php endif; ?>
|
|
<?php if (!empty($d['doc_type'])): ?> · <?= htmlspecialchars($d['doc_type']) ?><?php endif; ?>
|
|
· lastet opp <?= htmlspecialchars(date('j. F Y', strtotime((string)$d['uploaded_at']))) ?>
|
|
</div>
|
|
</div>
|
|
<span class="ms-status-pill ms-status-<?= htmlspecialchars($d['ocr_status']) ?>">
|
|
<?php
|
|
echo htmlspecialchars(['pending'=>'I kø','running'=>'OCR pågår','ready'=>'Klar','failed'=>'Feilet'][$d['ocr_status']] ?? $d['ocr_status']);
|
|
?>
|
|
</span>
|
|
<div class="ms-doc-actions">
|
|
<button type="button" class="ms-delete" data-id="<?= (int)$d['id'] ?>">Slett</button>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; endif; ?>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
(function() {
|
|
const fileInput = document.getElementById('msFileInput');
|
|
const dropZone = document.getElementById('msUploadZone');
|
|
const flash = document.getElementById('msFlash');
|
|
|
|
function showFlash(msg, isError) {
|
|
flash.style.cssText = 'padding:0.75rem 1rem;border-radius:6px;font-size:0.9rem;'
|
|
+ (isError ? 'background:#fee2e2;color:#991b1b;' : 'background:#d1fae5;color:#065f46;');
|
|
flash.textContent = msg;
|
|
}
|
|
|
|
async function uploadFile(file) {
|
|
if (file.size > 25 * 1024 * 1024) {
|
|
showFlash(file.name + ' er over 25 MB — del opp filen først.', true);
|
|
return;
|
|
}
|
|
const data = new FormData();
|
|
data.append('file', file);
|
|
try {
|
|
const res = await fetch('/api/case/upload.php', { method: 'POST', body: data });
|
|
const json = await res.json();
|
|
if (json.ok) {
|
|
showFlash('Lastet opp: ' + file.name + '. OCR starter automatisk.', false);
|
|
setTimeout(() => window.location.reload(), 1200);
|
|
} else {
|
|
showFlash(file.name + ': ' + (json.error?.message || 'Ukjent feil'), true);
|
|
}
|
|
} catch (e) {
|
|
showFlash('Nettverksfeil: ' + e.message, true);
|
|
}
|
|
}
|
|
|
|
fileInput.addEventListener('change', () => {
|
|
for (const f of fileInput.files) uploadFile(f);
|
|
});
|
|
|
|
['dragenter','dragover'].forEach(ev => dropZone.addEventListener(ev, e => {
|
|
e.preventDefault(); dropZone.classList.add('is-drag');
|
|
}));
|
|
['dragleave','drop'].forEach(ev => dropZone.addEventListener(ev, e => {
|
|
e.preventDefault(); dropZone.classList.remove('is-drag');
|
|
}));
|
|
dropZone.addEventListener('drop', e => {
|
|
for (const f of e.dataTransfer.files) {
|
|
if (f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')) uploadFile(f);
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('.ms-delete').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
if (!confirm('Slette dette dokumentet for godt?')) return;
|
|
const id = btn.getAttribute('data-id');
|
|
try {
|
|
const res = await fetch('/api/case/delete.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ doc_id: parseInt(id, 10) }),
|
|
});
|
|
const json = await res.json();
|
|
if (json.ok) window.location.reload();
|
|
else alert(json.error?.message || 'Sletting feilet.');
|
|
} catch (e) { alert('Nettverksfeil: ' + e.message); }
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|