Add monetization spine + Build Your Own Case (Min Sak)

- 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>
This commit is contained in:
2026-05-20 20:52:54 +02:00
parent ed5489d174
commit ba9cddf9a1
30 changed files with 2804 additions and 133 deletions
+113
View File
@@ -171,3 +171,116 @@
list.innerHTML = '<p class="workbench-docs__empty">' + t('error') + '</p>';
});
}());
// ── Upload zone ───────────────────────────────────────────────────────────────
(function () {
'use strict';
var zone = document.getElementById('wbUploadZone');
var input = document.getElementById('wbUploadInput');
var status = document.getElementById('wbUploadStatus');
var list = document.getElementById('myDocsList');
if (!zone || !input) return;
var lang = (window.DBN_TOOLS_LANG || 'en');
var statusTimer = 0;
var i18n = {
uploading: { en: 'Uploading…', no: 'Laster opp…', uk: 'Завантаження…', pl: 'Przesyłanie…' },
saved: { en: 'Saved.', no: 'Lagret.', uk: 'Збережено.', pl: 'Zapisano.' },
error: { en: 'Upload failed.', no: 'Opplasting mislyktes.', uk: 'Помилка завантаження.', pl: 'Błąd przesyłania.' },
remove: { en: 'Remove', no: 'Fjern', uk: 'Видалити', pl: 'Usuń' },
empty: { en: 'No documents yet.', no: 'Ingen dokumenter ennå.', uk: 'Документів ще немає.', pl: 'Brak dokumentów.' },
};
function t(key) {
return (i18n[key] && i18n[key][lang]) || (i18n[key] && i18n[key]['en']) || key;
}
function esc(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function setStatus(msg, keep) {
if (!status) return;
status.textContent = msg;
window.clearTimeout(statusTimer);
if (!keep) {
statusTimer = window.setTimeout(function () { status.textContent = ''; }, 2400);
}
}
function prependDoc(doc) {
if (!list) return;
var empty = list.querySelector('.workbench-docs__empty, .workbench-docs__loading');
if (empty) empty.remove();
var el = document.createElement('div');
el.className = 'workbench-docs__item';
el.setAttribute('role', 'listitem');
el.setAttribute('data-doc-id', doc.doc_id);
el.innerHTML = '<span class="workbench-docs__icon">📄</span>'
+ '<span class="workbench-docs__name" title="' + esc(doc.filename) + '">' + esc(doc.filename) + '</span>'
+ '<span class="workbench-docs__meta">' + esc(new Date(doc.created_at).toLocaleDateString()) + ' · workbench</span>'
+ '<button class="workbench-docs__remove" type="button" data-doc-id="' + esc(doc.doc_id) + '" aria-label="' + t('remove') + ' ' + esc(doc.filename) + '">' + t('remove') + '</button>';
el.querySelector('.workbench-docs__remove').addEventListener('click', function () {
var btn = this;
btn.disabled = true;
fetch('/api/user-docs.php?id=' + encodeURIComponent(doc.doc_id), { method: 'DELETE', credentials: 'include' })
.then(function () {
var item = list.querySelector('[data-doc-id="' + doc.doc_id + '"]');
if (item) item.remove();
if (!list.querySelector('.workbench-docs__item')) {
list.innerHTML = '<p class="workbench-docs__empty">' + t('empty') + '</p>';
}
})
.catch(function () { btn.disabled = false; });
});
list.insertBefore(el, list.firstChild);
}
function uploadFile(file) {
var fd = new FormData();
fd.append('file', file);
setStatus(t('uploading'), true);
return fetch('/api/user-docs.php', { method: 'POST', credentials: 'include', body: fd })
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.ok && data.doc) {
prependDoc(data.doc);
setStatus(t('saved'));
} else {
setStatus((data.error || t('error')));
}
})
.catch(function () { setStatus(t('error')); });
}
function handleFiles(files) {
if (!files || !files.length) return;
var chain = Promise.resolve();
Array.prototype.forEach.call(files, function (file) {
chain = chain.then(function () { return uploadFile(file); });
});
}
zone.addEventListener('dragover', function (e) {
e.preventDefault();
zone.classList.add('is-drag-over');
});
zone.addEventListener('dragleave', function () {
zone.classList.remove('is-drag-over');
});
zone.addEventListener('drop', function (e) {
e.preventDefault();
zone.classList.remove('is-drag-over');
handleFiles(e.dataTransfer.files);
});
input.addEventListener('change', function () {
handleFiles(input.files);
input.value = '';
});
}());