b2e1bf268d
- KorrespondAgent: add resolveDeployment() helper; fix classify/translate to use
Haiku via Bedrock, draft to use Haiku (quick) or Sonnet (thorough) — fixes broken
withDeployment('gpt-4o-mini') calls when DBN_BEDROCK_ENABLED=true
- korrespond.php: add Quick/Thorough engine picker (case_toggle already present)
- korrespond.js: pass engine in request payload
- api/korrespond.php: accept user-selected engine, auto-save to case_tool_results
for paid users after each successful run, update deployment log label
- CaseResults: add korr_status to listForUser SELECT, add updateStatus() method
- result-action.php: add set_status action for correspondence journal
- account.php: show status dropdown (Draft/Sent/Reply received/Resolved) for
korrespond entries in #analyses, wire JS change handler to result-action.php
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1274 lines
74 KiB
PHP
1274 lines
74 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
require_once __DIR__ . '/includes/bootstrap.php';
|
||
require_once __DIR__ . '/includes/FreeTier.php';
|
||
require_once __DIR__ . '/includes/PricingCatalog.php';
|
||
require_once __DIR__ . '/includes/CaseStore.php';
|
||
require_once __DIR__ . '/includes/CaseResults.php';
|
||
|
||
dbnToolsRequirePageAuth('/account.php');
|
||
|
||
$uiLang = dbnToolsCurrentLanguage();
|
||
$langSuffix = $uiLang !== 'en' ? '?lang=' . urlencode($uiLang) : '';
|
||
$isSso = dbnToolsIsFreeTier();
|
||
$userId = $isSso ? (int)($_SESSION['dbn_tools_sso_uid'] ?? 0) : 0;
|
||
|
||
$authUser = dbnToolsAuthenticatedUser();
|
||
$email = (string)($authUser['email'] ?? '');
|
||
|
||
$detail = array_merge([
|
||
'balance' => 0, 'bonus_balance' => 0, 'tier' => $isSso ? 'free' : 'caveau',
|
||
'storage_used_bytes' => 0, 'storage_quota_bytes' => 0,
|
||
'survey_completed_at' => null, 'subscription_period_end' => null,
|
||
'trial_active' => false, 'trial_days_remaining' => 0, 'trial_expires_at' => null,
|
||
], $userId > 0 ? (FreeTier::balanceDetail($userId) ?: []) : []);
|
||
|
||
$tier = (string)$detail['tier'];
|
||
$isPaidTier = in_array($tier, ['plus', 'pro'], true);
|
||
$effective = (int)$detail['balance'] + (int)$detail['bonus_balance'];
|
||
|
||
$storageBytes = (int)$detail['storage_used_bytes'];
|
||
$quotaBytes = (int)$detail['storage_quota_bytes'];
|
||
$storageMb = $storageBytes > 0 ? round($storageBytes / 1048576, 1) : 0;
|
||
$quotaMb = $quotaBytes > 0 ? (int)round($quotaBytes / 1048576) : 0;
|
||
$storagePct = $quotaBytes > 0 ? min(100, (int)round(($storageBytes / $quotaBytes) * 100)) : 0;
|
||
|
||
$tierLabels = [
|
||
'free' => ['Free', '#f3f4f6', '#374151'],
|
||
'plus' => ['Plus', '#ddd6fe', '#5b21b6'],
|
||
'pro' => ['Pro Familie', '#bfdbfe', '#1e40af'],
|
||
'caveau' => ['CaveauAI', '#d1fae5', '#065f46'],
|
||
];
|
||
$tierLabel = $tierLabels[$tier] ?? $tierLabels['free'];
|
||
|
||
$profile = $isSso ? dbnToolsMainUserProfile($userId) : null;
|
||
$profileVal = static function (string $key) use ($profile): string {
|
||
return (string)($profile[$key] ?? '');
|
||
};
|
||
|
||
$seatLimit = $tier === 'pro' ? 3 : 1;
|
||
|
||
$docs = $isPaidTier ? CaseStore::listDocs($userId) : [];
|
||
$results = $isPaidTier ? CaseResults::listForUser($userId, 50) : [];
|
||
|
||
$recent = [];
|
||
if ($userId > 0) {
|
||
try {
|
||
$stmt = dbnmDb()->prepare(
|
||
'SELECT tool, credits_used, created_at FROM user_tool_usage_log
|
||
WHERE user_id = ? ORDER BY created_at DESC LIMIT 25'
|
||
);
|
||
$stmt->execute([$userId]);
|
||
$recent = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||
} catch (Throwable $e) {
|
||
$recent = [];
|
||
}
|
||
}
|
||
|
||
$paidToast = isset($_GET['paid']) && $_GET['paid'] === '1';
|
||
|
||
// ── Localized strings (en / no / uk / pl) ──────────────────────────────────
|
||
$L = [
|
||
'en' => [
|
||
'page_title' => 'My account',
|
||
'page_lede' => 'Plan, profile, case storage, analyses, usage and MCP access — all in one place.',
|
||
'signed_in_as' => 'Signed in as',
|
||
'jump_plan' => 'Plan',
|
||
'jump_profile' => 'Profile',
|
||
'jump_workspace' => 'Workspace',
|
||
'jump_case' => 'My Case',
|
||
'jump_analyses' => 'Saved analyses',
|
||
'jump_usage' => 'Usage',
|
||
'jump_mcp' => 'MCP',
|
||
'trial_badge' => 'Trial — %d days left',
|
||
|
||
'sec_plan' => 'Plan & credits',
|
||
'current_plan' => 'Current plan',
|
||
'renews_on' => 'Renews on',
|
||
'no_active_sub' => 'No active subscription.',
|
||
'see_plans' => 'See all plans',
|
||
'available_credits' => 'Available credits',
|
||
'monthly' => 'monthly',
|
||
'prepaid' => 'prepaid',
|
||
'manage_subscription'=> 'Manage subscription',
|
||
'upgrade_plan' => 'Upgrade plan',
|
||
'top_up_credits' => 'Top up credits',
|
||
'payment_confirmed' => 'Payment confirmed. If you just subscribed, it may take a few seconds for your account to update.',
|
||
'bonus_survey_title' => 'Bonus credits (survey)',
|
||
'bonus_survey_done' => '✓ You have already received 25 bonus credits for completing the survey.',
|
||
'bonus_survey_pitch' => 'Answer 5 short questions about your needs and receive 25 bonus credits — never expire.',
|
||
'take_survey_btn' => 'Take the survey',
|
||
'portal_loading' => 'Connecting…',
|
||
'portal_error' => 'Could not open the portal.',
|
||
'network_error' => 'Network error: ',
|
||
|
||
'sec_profile' => 'Profile details',
|
||
'profile_optional' => 'Optional',
|
||
'f_display_name' => 'Display name',
|
||
'f_phone' => 'Phone',
|
||
'f_address_1' => 'Address line 1',
|
||
'f_address_2' => 'Address line 2',
|
||
'f_postal' => 'Postal code',
|
||
'f_city' => 'City',
|
||
'f_region' => 'Region',
|
||
'f_country' => 'Country',
|
||
'save_profile' => 'Save profile',
|
||
'profile_saving' => 'Saving…',
|
||
'profile_saved' => 'Saved',
|
||
'profile_error_default' => 'Could not save profile',
|
||
|
||
'sec_workspace' => 'Workspace access',
|
||
'w_email' => 'Email',
|
||
'w_seats' => 'Seats',
|
||
'w_role' => 'Role',
|
||
'w_owner' => 'Owner',
|
||
'w_plan_storage' => 'Plan storage',
|
||
'w_seats_single' => 'Single-user workspace',
|
||
'w_upgrade_for_storage' => 'Upgrade for My Case storage',
|
||
|
||
'sec_case' => 'My Case',
|
||
'case_lede' => 'Upload your case documents once. All tools can reference your private case when you tick "Use my case as context".',
|
||
'case_storage' => 'Storage',
|
||
'case_documents_count' => '%d documents',
|
||
'case_drop_pdf' => 'Drop PDF files here, or click to browse',
|
||
'case_hint' => 'Max 25 MB per file · OCR runs automatically · all stored encrypted in the EU',
|
||
'case_no_docs' => 'No documents yet. Upload your first PDF above.',
|
||
'case_uploaded_ok' => 'Uploaded: %s. OCR starts automatically.',
|
||
'case_over_25mb' => '%s is over 25 MB — split the file first.',
|
||
'case_unknown_err' => 'Unknown error',
|
||
'case_delete_failed' => 'Delete failed.',
|
||
'case_pages' => 'pages',
|
||
'case_uploaded_label' => 'uploaded',
|
||
'case_delete' => 'Delete',
|
||
'case_confirm_delete' => 'Delete this document for good?',
|
||
'case_ocr_pending' => 'Queued',
|
||
'case_ocr_running' => 'OCR in progress',
|
||
'case_ocr_ready' => 'Ready',
|
||
'case_ocr_failed' => 'Failed',
|
||
'case_free_gate_title' => 'My Case — build your own case',
|
||
'case_free_gate_pitch' => 'Upload your case documents once, and let <strong>all</strong> the tools work on your private corpus.',
|
||
'case_free_gate_b1' => '📄 Private document bank with OCR',
|
||
'case_free_gate_b2' => '🔍 Hybrid search (BM25 + vector) on your case',
|
||
'case_free_gate_b3' => '🧠 All tools can reference your own case',
|
||
'case_free_gate_b4' => '🇪🇺 Everything stored in the EU (Germany/Finland/Norway)',
|
||
'case_free_gate_cta' => 'See plans from NOK 129/mo',
|
||
|
||
'sec_analyses' => 'Saved analyses',
|
||
'analyses_lede' => 'All results from Correspondence, Advocate, CWS, Deep analysis, Discrepancy and Timeline gather here — ready to reopen, rerun or export.',
|
||
'no_analyses' => 'No saved analyses yet. Run a tool to save your first.',
|
||
'used_case_badge' => 'Use my case',
|
||
'pinned_title' => 'Pinned',
|
||
'pin' => 'Pin',
|
||
'unpin' => 'Unpin',
|
||
'open_analysis' => 'Open',
|
||
'delete_analysis' => 'Delete',
|
||
'confirm_delete_analysis' => 'Delete this analysis for good?',
|
||
'pin_failed' => 'Pin failed.',
|
||
|
||
'sec_usage' => 'Recent usage (last 25)',
|
||
'usage_col_tool' => 'Tool / event',
|
||
'usage_col_credits' => 'Credits',
|
||
'usage_col_when' => 'When',
|
||
'no_usage' => 'No usage recorded yet.',
|
||
|
||
'sec_mcp' => 'Developers & MCP',
|
||
'mcp_desc' => 'Connect Claude Desktop, Claude Code, Cursor, or any MCP-compatible client to the full tool suite.',
|
||
'mcp_token_lbl' => 'API token',
|
||
'mcp_no_token' => 'No active token',
|
||
'mcp_stdio_lbl' => 'Claude Desktop / Claude Code (stdio)',
|
||
'mcp_remote_lbl' => 'Remote HTTP — Cursor, Zed, Windsurf',
|
||
'mcp_copy' => 'Copy',
|
||
'mcp_manage' => 'Manage tokens →',
|
||
'mcp_full_setup' => 'Full setup guide & token management →',
|
||
'mcp_locked' => 'MCP tokens require Plus or Pro — upgrade to connect AI clients.',
|
||
'mcp_loading' => 'Loading…',
|
||
],
|
||
'no' => [
|
||
'page_title' => 'Min konto',
|
||
'page_lede' => 'Plan, profil, sak-lagring, analyser, bruk og MCP-tilgang — alt på ett sted.',
|
||
'signed_in_as' => 'Innlogget som',
|
||
'jump_plan' => 'Plan',
|
||
'jump_profile' => 'Profil',
|
||
'jump_workspace' => 'Arbeidsområde',
|
||
'jump_case' => 'Min Sak',
|
||
'jump_analyses' => 'Lagrede analyser',
|
||
'jump_usage' => 'Bruk',
|
||
'jump_mcp' => 'MCP',
|
||
'trial_badge' => 'Prøveperiode — %d dager igjen',
|
||
|
||
'sec_plan' => 'Plan & kreditter',
|
||
'current_plan' => 'Nåværende plan',
|
||
'renews_on' => 'Fornyes',
|
||
'no_active_sub' => 'Ingen aktiv abonnement.',
|
||
'see_plans' => 'Se alle planer',
|
||
'available_credits' => 'Tilgjengelige kreditter',
|
||
'monthly' => 'månedlige',
|
||
'prepaid' => 'forhåndsbetalte',
|
||
'manage_subscription'=> 'Administrer abonnement',
|
||
'upgrade_plan' => 'Oppgrader plan',
|
||
'top_up_credits' => 'Kjøp kreditter',
|
||
'payment_confirmed' => 'Betalingen er bekreftet. Hvis du nettopp abonnerte, kan det ta noen sekunder før kontoen oppdateres.',
|
||
'bonus_survey_title' => 'Bonus-kreditter (undersøkelse)',
|
||
'bonus_survey_done' => '✓ Du har allerede mottatt 25 bonuskreditter for å fylle ut undersøkelsen.',
|
||
'bonus_survey_pitch' => 'Svar på 5 korte spørsmål om dine behov og motta 25 bonus-kreditter — utløper aldri.',
|
||
'take_survey_btn' => 'Ta undersøkelsen',
|
||
'portal_loading' => 'Kobler til…',
|
||
'portal_error' => 'Kunne ikke åpne portalen.',
|
||
'network_error' => 'Nettverksfeil: ',
|
||
|
||
'sec_profile' => 'Profildetaljer',
|
||
'profile_optional' => 'Valgfritt',
|
||
'f_display_name' => 'Visningsnavn',
|
||
'f_phone' => 'Telefon',
|
||
'f_address_1' => 'Adresselinje 1',
|
||
'f_address_2' => 'Adresselinje 2',
|
||
'f_postal' => 'Postnummer',
|
||
'f_city' => 'Sted',
|
||
'f_region' => 'Fylke',
|
||
'f_country' => 'Land',
|
||
'save_profile' => 'Lagre profil',
|
||
'profile_saving' => 'Lagrer…',
|
||
'profile_saved' => 'Lagret',
|
||
'profile_error_default' => 'Kunne ikke lagre profilen',
|
||
|
||
'sec_workspace' => 'Arbeidsområde-tilgang',
|
||
'w_email' => 'E-post',
|
||
'w_seats' => 'Plasser',
|
||
'w_role' => 'Rolle',
|
||
'w_owner' => 'Eier',
|
||
'w_plan_storage' => 'Plan-lagring',
|
||
'w_seats_single' => 'Enkeltbruker-arbeidsområde',
|
||
'w_upgrade_for_storage' => 'Oppgrader for Min Sak-lagring',
|
||
|
||
'sec_case' => 'Min Sak',
|
||
'case_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".',
|
||
'case_storage' => 'Lagring',
|
||
'case_documents_count' => '%d dokumenter',
|
||
'case_drop_pdf' => 'Slipp PDF-filer her, eller klikk for å bla',
|
||
'case_hint' => 'Maks 25 MB per fil · OCR kjøres automatisk · alt lagres kryptert i EU',
|
||
'case_no_docs' => 'Ingen dokumenter ennå. Last opp din første PDF over.',
|
||
'case_uploaded_ok' => 'Lastet opp: %s. OCR starter automatisk.',
|
||
'case_over_25mb' => '%s er over 25 MB — del opp filen først.',
|
||
'case_unknown_err' => 'Ukjent feil',
|
||
'case_delete_failed' => 'Sletting feilet.',
|
||
'case_pages' => 'sider',
|
||
'case_uploaded_label' => 'lastet opp',
|
||
'case_delete' => 'Slett',
|
||
'case_confirm_delete' => 'Slette dette dokumentet for godt?',
|
||
'case_ocr_pending' => 'I kø',
|
||
'case_ocr_running' => 'OCR pågår',
|
||
'case_ocr_ready' => 'Klar',
|
||
'case_ocr_failed' => 'Feilet',
|
||
'case_free_gate_title' => 'Min Sak — bygg din egen sak',
|
||
'case_free_gate_pitch' => 'Last opp dokumentene fra saken din én gang, og la <strong>alle</strong> verktøyene jobbe på din private korpus.',
|
||
'case_free_gate_b1' => '📄 Privat dokumentbank med OCR',
|
||
'case_free_gate_b2' => '🔍 Hybrid søk (BM25 + vektor) i din sak',
|
||
'case_free_gate_b3' => '🧠 Alle verktøy kan referere til din egen sak',
|
||
'case_free_gate_b4' => '🇪🇺 Alt lagres i EU (Tyskland/Finland/Norge)',
|
||
'case_free_gate_cta' => 'Se planer fra NOK 129/mo',
|
||
|
||
'sec_analyses' => 'Lagrede analyser',
|
||
'analyses_lede' => 'Alle resultater fra Korrespondanse, Advokat, BVJ, Dyp analyse, Motstrid og Tidslinje samles her — klar til å gjenåpnes, kjøres på nytt eller eksporteres.',
|
||
'no_analyses' => 'Ingen lagrede analyser ennå. Kjør et verktøy for å lagre din første.',
|
||
'used_case_badge' => 'Bruk min sak',
|
||
'pinned_title' => 'Festet',
|
||
'pin' => 'Fest',
|
||
'unpin' => 'Løsne',
|
||
'open_analysis' => 'Åpne',
|
||
'delete_analysis' => 'Slett',
|
||
'confirm_delete_analysis' => 'Slette denne analysen for godt?',
|
||
'pin_failed' => 'Festing feilet.',
|
||
|
||
'sec_usage' => 'Nylig bruk (siste 25)',
|
||
'usage_col_tool' => 'Verktøy / hendelse',
|
||
'usage_col_credits' => 'Kreditter',
|
||
'usage_col_when' => 'Tidspunkt',
|
||
'no_usage' => 'Ingen bruk registrert ennå.',
|
||
|
||
'sec_mcp' => 'Utviklere & MCP',
|
||
'mcp_desc' => 'Koble Claude Desktop, Claude Code, Cursor eller annen MCP-klient til hele verktøysuiten.',
|
||
'mcp_token_lbl' => 'API-token',
|
||
'mcp_no_token' => 'Ingen aktiv token',
|
||
'mcp_stdio_lbl' => 'Claude Desktop / Claude Code (stdio)',
|
||
'mcp_remote_lbl' => 'Ekstern HTTP — Cursor, Zed, Windsurf',
|
||
'mcp_copy' => 'Kopier',
|
||
'mcp_manage' => 'Administrer tokens →',
|
||
'mcp_full_setup' => 'Full oppsettsguide & token-administrasjon →',
|
||
'mcp_locked' => 'MCP-tokens krever Plus eller Pro — oppgrader for å koble til AI-klienter.',
|
||
'mcp_loading' => 'Laster…',
|
||
],
|
||
'uk' => [
|
||
'page_title' => 'Мій обліковий запис',
|
||
'page_lede' => 'План, профіль, сховище справ, аналізи, використання та доступ MCP — все в одному місці.',
|
||
'signed_in_as' => 'Ввійшли як',
|
||
'jump_plan' => 'План',
|
||
'jump_profile' => 'Профіль',
|
||
'jump_workspace' => 'Робочий простір',
|
||
'jump_case' => 'Моя справа',
|
||
'jump_analyses' => 'Збережені аналізи',
|
||
'jump_usage' => 'Використання',
|
||
'jump_mcp' => 'MCP',
|
||
'trial_badge' => 'Пробний — %d дн. залишилось',
|
||
|
||
'sec_plan' => 'План і кредити',
|
||
'current_plan' => 'Поточний план',
|
||
'renews_on' => 'Поновлюється',
|
||
'no_active_sub' => 'Немає активної підписки.',
|
||
'see_plans' => 'Переглянути всі плани',
|
||
'available_credits' => 'Доступні кредити',
|
||
'monthly' => 'місячних',
|
||
'prepaid' => 'передоплачених',
|
||
'manage_subscription'=> 'Керувати підпискою',
|
||
'upgrade_plan' => 'Покращити план',
|
||
'top_up_credits' => 'Поповнити кредити',
|
||
'payment_confirmed' => 'Платіж підтверджено. Якщо ви щойно підписалися, оновлення облікового запису може зайняти кілька секунд.',
|
||
'bonus_survey_title' => 'Бонусні кредити (опитування)',
|
||
'bonus_survey_done' => '✓ Ви вже отримали 25 бонусних кредитів за заповнення опитування.',
|
||
'bonus_survey_pitch' => 'Дайте відповідь на 5 коротких запитань про ваші потреби та отримайте 25 бонусних кредитів — без терміну дії.',
|
||
'take_survey_btn' => 'Пройти опитування',
|
||
'portal_loading' => 'З\'єднання…',
|
||
'portal_error' => 'Не вдалося відкрити портал.',
|
||
'network_error' => 'Помилка мережі: ',
|
||
|
||
'sec_profile' => 'Дані профілю',
|
||
'profile_optional' => 'Необов\'язково',
|
||
'f_display_name' => 'Ім\'я для показу',
|
||
'f_phone' => 'Телефон',
|
||
'f_address_1' => 'Адреса, рядок 1',
|
||
'f_address_2' => 'Адреса, рядок 2',
|
||
'f_postal' => 'Поштовий індекс',
|
||
'f_city' => 'Місто',
|
||
'f_region' => 'Регіон',
|
||
'f_country' => 'Країна',
|
||
'save_profile' => 'Зберегти профіль',
|
||
'profile_saving' => 'Збереження…',
|
||
'profile_saved' => 'Збережено',
|
||
'profile_error_default' => 'Не вдалося зберегти профіль',
|
||
|
||
'sec_workspace' => 'Доступ до робочого простору',
|
||
'w_email' => 'Email',
|
||
'w_seats' => 'Місця',
|
||
'w_role' => 'Роль',
|
||
'w_owner' => 'Власник',
|
||
'w_plan_storage' => 'Сховище плану',
|
||
'w_seats_single' => 'Робочий простір на одного користувача',
|
||
'w_upgrade_for_storage' => 'Оновіть для сховища «Моя справа»',
|
||
|
||
'sec_case' => 'Моя справа',
|
||
'case_lede' => 'Завантажте документи вашої справи один раз. Усі інструменти зможуть посилатися на вашу приватну справу, коли ви поставите галочку «Використати мою справу як контекст».',
|
||
'case_storage' => 'Сховище',
|
||
'case_documents_count' => '%d документів',
|
||
'case_drop_pdf' => 'Перетягніть PDF-файли сюди або клацніть, щоб вибрати',
|
||
'case_hint' => 'Макс. 25 МБ на файл · OCR запускається автоматично · все зберігається зашифрованим у ЄС',
|
||
'case_no_docs' => 'Поки що немає документів. Завантажте перший PDF вище.',
|
||
'case_uploaded_ok' => 'Завантажено: %s. OCR починається автоматично.',
|
||
'case_over_25mb' => '%s перевищує 25 МБ — розділіть файл спочатку.',
|
||
'case_unknown_err' => 'Невідома помилка',
|
||
'case_delete_failed' => 'Видалення не вдалося.',
|
||
'case_pages' => 'стор.',
|
||
'case_uploaded_label' => 'завантажено',
|
||
'case_delete' => 'Видалити',
|
||
'case_confirm_delete' => 'Видалити цей документ назавжди?',
|
||
'case_ocr_pending' => 'У черзі',
|
||
'case_ocr_running' => 'OCR виконується',
|
||
'case_ocr_ready' => 'Готово',
|
||
'case_ocr_failed' => 'Не вдалося',
|
||
'case_free_gate_title' => 'Моя справа — побудуйте власну справу',
|
||
'case_free_gate_pitch' => 'Завантажте документи з вашої справи один раз, і нехай <strong>усі</strong> інструменти працюють з вашим приватним корпусом.',
|
||
'case_free_gate_b1' => '📄 Приватний банк документів з OCR',
|
||
'case_free_gate_b2' => '🔍 Гібридний пошук (BM25 + вектор) у вашій справі',
|
||
'case_free_gate_b3' => '🧠 Усі інструменти можуть посилатися на вашу справу',
|
||
'case_free_gate_b4' => '🇪🇺 Усе зберігається в ЄС (Німеччина/Фінляндія/Норвегія)',
|
||
'case_free_gate_cta' => 'Переглянути плани від NOK 129/міс',
|
||
|
||
'sec_analyses' => 'Збережені аналізи',
|
||
'analyses_lede' => 'Усі результати з Кореспонденції, Адвоката, BVJ, Глибокого аналізу, Розбіжностей і Хронології збираються тут — готові до повторного відкриття, перезапуску чи експорту.',
|
||
'no_analyses' => 'Поки що немає збережених аналізів. Запустіть інструмент, щоб зберегти перший.',
|
||
'used_case_badge' => 'Моя справа',
|
||
'pinned_title' => 'Закріплено',
|
||
'pin' => 'Закріпити',
|
||
'unpin' => 'Відкріпити',
|
||
'open_analysis' => 'Відкрити',
|
||
'delete_analysis' => 'Видалити',
|
||
'confirm_delete_analysis' => 'Видалити цей аналіз назавжди?',
|
||
'pin_failed' => 'Закріпити не вдалося.',
|
||
|
||
'sec_usage' => 'Нещодавнє використання (останні 25)',
|
||
'usage_col_tool' => 'Інструмент / подія',
|
||
'usage_col_credits' => 'Кредити',
|
||
'usage_col_when' => 'Коли',
|
||
'no_usage' => 'Використання ще не зареєстровано.',
|
||
|
||
'sec_mcp' => 'Розробники та MCP',
|
||
'mcp_desc' => 'Підключайте Claude Desktop, Claude Code, Cursor або будь-який MCP-клієнт до повного набору інструментів.',
|
||
'mcp_token_lbl' => 'API-токен',
|
||
'mcp_no_token' => 'Немає активного токена',
|
||
'mcp_stdio_lbl' => 'Claude Desktop / Claude Code (stdio)',
|
||
'mcp_remote_lbl' => 'Віддалений HTTP — Cursor, Zed, Windsurf',
|
||
'mcp_copy' => 'Копіювати',
|
||
'mcp_manage' => 'Керувати токенами →',
|
||
'mcp_full_setup' => 'Повна документація та управління токенами →',
|
||
'mcp_locked' => 'MCP-токени потребують Plus або Pro — оновіться для підключення AI-клієнтів.',
|
||
'mcp_loading' => 'Завантаження…',
|
||
],
|
||
'pl' => [
|
||
'page_title' => 'Moje konto',
|
||
'page_lede' => 'Plan, profil, magazyn sprawy, analizy, użycie i dostęp MCP — wszystko w jednym miejscu.',
|
||
'signed_in_as' => 'Zalogowany jako',
|
||
'jump_plan' => 'Plan',
|
||
'jump_profile' => 'Profil',
|
||
'jump_workspace' => 'Obszar roboczy',
|
||
'jump_case' => 'Moja sprawa',
|
||
'jump_analyses' => 'Zapisane analizy',
|
||
'jump_usage' => 'Użycie',
|
||
'jump_mcp' => 'MCP',
|
||
'trial_badge' => 'Próba — %d dni pozostało',
|
||
|
||
'sec_plan' => 'Plan i kredyty',
|
||
'current_plan' => 'Bieżący plan',
|
||
'renews_on' => 'Odnawia się',
|
||
'no_active_sub' => 'Brak aktywnej subskrypcji.',
|
||
'see_plans' => 'Zobacz wszystkie plany',
|
||
'available_credits' => 'Dostępne kredyty',
|
||
'monthly' => 'miesięcznych',
|
||
'prepaid' => 'przedpłaconych',
|
||
'manage_subscription'=> 'Zarządzaj subskrypcją',
|
||
'upgrade_plan' => 'Ulepsz plan',
|
||
'top_up_credits' => 'Doładuj kredyty',
|
||
'payment_confirmed' => 'Płatność potwierdzona. Jeśli właśnie wykupiono subskrypcję, aktualizacja konta może potrwać kilka sekund.',
|
||
'bonus_survey_title' => 'Kredyty bonusowe (ankieta)',
|
||
'bonus_survey_done' => '✓ Otrzymałeś już 25 kredytów bonusowych za wypełnienie ankiety.',
|
||
'bonus_survey_pitch' => 'Odpowiedz na 5 krótkich pytań o swoje potrzeby i otrzymaj 25 kredytów bonusowych — nigdy nie wygasają.',
|
||
'take_survey_btn' => 'Wypełnij ankietę',
|
||
'portal_loading' => 'Łączenie…',
|
||
'portal_error' => 'Nie można otworzyć portalu.',
|
||
'network_error' => 'Błąd sieci: ',
|
||
|
||
'sec_profile' => 'Dane profilu',
|
||
'profile_optional' => 'Opcjonalne',
|
||
'f_display_name' => 'Nazwa wyświetlana',
|
||
'f_phone' => 'Telefon',
|
||
'f_address_1' => 'Adres linia 1',
|
||
'f_address_2' => 'Adres linia 2',
|
||
'f_postal' => 'Kod pocztowy',
|
||
'f_city' => 'Miasto',
|
||
'f_region' => 'Region',
|
||
'f_country' => 'Kraj',
|
||
'save_profile' => 'Zapisz profil',
|
||
'profile_saving' => 'Zapisywanie…',
|
||
'profile_saved' => 'Zapisano',
|
||
'profile_error_default' => 'Nie udało się zapisać profilu',
|
||
|
||
'sec_workspace' => 'Dostęp do obszaru roboczego',
|
||
'w_email' => 'Email',
|
||
'w_seats' => 'Miejsca',
|
||
'w_role' => 'Rola',
|
||
'w_owner' => 'Właściciel',
|
||
'w_plan_storage' => 'Magazyn planu',
|
||
'w_seats_single' => 'Obszar roboczy jednoosobowy',
|
||
'w_upgrade_for_storage' => 'Ulepsz, aby uzyskać magazyn Mojej sprawy',
|
||
|
||
'sec_case' => 'Moja sprawa',
|
||
'case_lede' => 'Prześlij dokumenty swojej sprawy raz. Wszystkie narzędzia mogą się do nich odwoływać, gdy zaznaczysz „Użyj mojej sprawy jako kontekstu".',
|
||
'case_storage' => 'Magazyn',
|
||
'case_documents_count' => '%d dokumentów',
|
||
'case_drop_pdf' => 'Upuść pliki PDF tutaj lub kliknij, aby przeglądać',
|
||
'case_hint' => 'Maks. 25 MB na plik · OCR działa automatycznie · wszystko przechowywane zaszyfrowane w UE',
|
||
'case_no_docs' => 'Brak dokumentów. Prześlij swój pierwszy PDF powyżej.',
|
||
'case_uploaded_ok' => 'Przesłano: %s. OCR rozpoczyna się automatycznie.',
|
||
'case_over_25mb' => '%s przekracza 25 MB — najpierw podziel plik.',
|
||
'case_unknown_err' => 'Nieznany błąd',
|
||
'case_delete_failed' => 'Usuwanie nie powiodło się.',
|
||
'case_pages' => 'stron',
|
||
'case_uploaded_label' => 'przesłano',
|
||
'case_delete' => 'Usuń',
|
||
'case_confirm_delete' => 'Usunąć ten dokument na zawsze?',
|
||
'case_ocr_pending' => 'W kolejce',
|
||
'case_ocr_running' => 'OCR w toku',
|
||
'case_ocr_ready' => 'Gotowe',
|
||
'case_ocr_failed' => 'Nieudane',
|
||
'case_free_gate_title' => 'Moja sprawa — zbuduj własną sprawę',
|
||
'case_free_gate_pitch' => 'Prześlij dokumenty swojej sprawy raz i pozwól <strong>wszystkim</strong> narzędziom pracować na Twoim prywatnym korpusie.',
|
||
'case_free_gate_b1' => '📄 Prywatny bank dokumentów z OCR',
|
||
'case_free_gate_b2' => '🔍 Wyszukiwanie hybrydowe (BM25 + wektor) w Twojej sprawie',
|
||
'case_free_gate_b3' => '🧠 Wszystkie narzędzia mogą odwoływać się do Twojej sprawy',
|
||
'case_free_gate_b4' => '🇪🇺 Wszystko przechowywane w UE (Niemcy/Finlandia/Norwegia)',
|
||
'case_free_gate_cta' => 'Zobacz plany od NOK 129/mies',
|
||
|
||
'sec_analyses' => 'Zapisane analizy',
|
||
'analyses_lede' => 'Wszystkie wyniki z Korespondencji, Adwokata, BVJ, Głębokiej analizy, Rozbieżności i Osi czasu zbierają się tutaj — gotowe do ponownego otwarcia, uruchomienia lub eksportu.',
|
||
'no_analyses' => 'Brak zapisanych analiz. Uruchom narzędzie, aby zapisać pierwszą.',
|
||
'used_case_badge' => 'Moja sprawa',
|
||
'pinned_title' => 'Przypięte',
|
||
'pin' => 'Przypnij',
|
||
'unpin' => 'Odepnij',
|
||
'open_analysis' => 'Otwórz',
|
||
'delete_analysis' => 'Usuń',
|
||
'confirm_delete_analysis' => 'Usunąć tę analizę na zawsze?',
|
||
'pin_failed' => 'Przypinanie nie powiodło się.',
|
||
|
||
'sec_usage' => 'Ostatnie użycie (ostatnie 25)',
|
||
'usage_col_tool' => 'Narzędzie / zdarzenie',
|
||
'usage_col_credits' => 'Kredyty',
|
||
'usage_col_when' => 'Kiedy',
|
||
'no_usage' => 'Nie zarejestrowano jeszcze użycia.',
|
||
|
||
'sec_mcp' => 'Deweloperzy i MCP',
|
||
'mcp_desc' => 'Podłącz Claude Desktop, Claude Code, Cursor lub dowolnego klienta MCP do pełnego zestawu narzędzi.',
|
||
'mcp_token_lbl' => 'Token API',
|
||
'mcp_no_token' => 'Brak aktywnego tokenu',
|
||
'mcp_stdio_lbl' => 'Claude Desktop / Claude Code (stdio)',
|
||
'mcp_remote_lbl' => 'Zdalny HTTP — Cursor, Zed, Windsurf',
|
||
'mcp_copy' => 'Kopiuj',
|
||
'mcp_manage' => 'Zarządzaj tokenami →',
|
||
'mcp_full_setup' => 'Pełna dokumentacja i zarządzanie tokenami →',
|
||
'mcp_locked' => 'Tokeny MCP wymagają Plus lub Pro — zaktualizuj, aby połączyć klientów AI.',
|
||
'mcp_loading' => 'Ładowanie…',
|
||
],
|
||
];
|
||
$l = $L[$uiLang] ?? $L['en'];
|
||
|
||
$seatsLabel = $seatLimit > 1 ? '1 / ' . $seatLimit : $l['w_seats_single'];
|
||
|
||
$dateLocale = match ($uiLang) {
|
||
'no' => 'j. F Y',
|
||
default => 'j M Y',
|
||
};
|
||
?>
|
||
<!doctype html>
|
||
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title><?= htmlspecialchars($l['page_title']) ?> — Do Better Norge</title>
|
||
<meta name="robots" content="noindex, nofollow">
|
||
<meta name="theme-color" content="#00205B">
|
||
<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;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
|
||
<link rel="stylesheet" href="assets/css/tools.css">
|
||
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.css">
|
||
<link rel="stylesheet" href="assets/css/dashboard.css">
|
||
<style>
|
||
.acct-shell { max-width: 1100px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }
|
||
.acct-header { margin: 0 0 1.5rem; }
|
||
.acct-header h1 { font-family: 'Crimson Pro', serif; margin: 0 0 .25rem; color: #00205B; font-size: 2.1rem; }
|
||
.acct-header .lede { color: #6b7280; margin: 0 0 .5rem; font-size: 1rem; }
|
||
.acct-header .meta { color: #6b7280; font-size: .9rem; }
|
||
.acct-header .meta strong { color: #1f2937; font-weight: 600; }
|
||
|
||
.acct-subnav { position: sticky; top: 0; z-index: 20; background: #f5f3f0; padding: .75rem 1.25rem; margin: 0 -1.25rem 1.5rem; border-bottom: 1px solid #e5e7eb; overflow-x: auto; white-space: nowrap; }
|
||
.acct-subnav a { display: inline-block; padding: .4rem .9rem; margin-right: .4rem; border-radius: 999px; background: #fff; border: 1px solid #e5e7eb; color: #374151; font-size: .88rem; font-weight: 600; text-decoration: none; transition: background .12s, border-color .12s; }
|
||
.acct-subnav a:hover { background: #f8fafc; border-color: #cbd5e1; }
|
||
.acct-subnav a.is-current { background: #00205B; color: #fff; border-color: #00205B; }
|
||
|
||
.acct-section { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 1.5rem; margin: 0 0 1.5rem; scroll-margin-top: 4.5rem; }
|
||
.acct-section h2 { font-family: 'Crimson Pro', serif; margin: 0 0 1rem; font-size: 1.4rem; color: #00205B; display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
|
||
.acct-section h2 .chip { font-family: 'IBM Plex Sans', sans-serif; font-size: .72rem; font-weight: 600; padding: 3px 10px; border-radius: 999px; background: #f3f4f6; color: #374151; text-transform: uppercase; letter-spacing: .04em; }
|
||
.acct-section .acct-lede { color: #6b7280; margin: -.5rem 0 1rem; font-size: .95rem; }
|
||
|
||
.acct-plan-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
|
||
@media (max-width: 720px) { .acct-plan-grid { grid-template-columns: 1fr; } }
|
||
.acct-plan-cell { padding: 1.25rem; background: #f8fafc; border-radius: 10px; }
|
||
.acct-plan-cell h3 { margin: 0 0 .5rem; font-size: .85rem; color: #6b7280; text-transform: uppercase; letter-spacing: .05em; }
|
||
.acct-tier-pill { display: inline-block; padding: 4px 12px; border-radius: 999px; font-size: .85rem; font-weight: 700; }
|
||
.acct-trial-pill { display: inline-block; padding: 4px 12px; border-radius: 999px; font-size: .82rem; font-weight: 600; background: #fef3c7; color: #92400e; margin-left: .35rem; }
|
||
.acct-credits-big { font-size: 2.4rem; font-weight: 700; color: #00205B; margin: .25rem 0; line-height: 1; }
|
||
.acct-credits-break { color: #6b7280; font-size: .9rem; margin: 0; }
|
||
|
||
.acct-actions { display: flex; flex-wrap: wrap; gap: .65rem; margin: 1rem 0 0; }
|
||
.btn { padding: .65rem 1.2rem; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-block; font-size: .92rem; font-family: inherit; }
|
||
.btn-primary { background: #00205B; color: #fff; }
|
||
.btn-primary:hover { background: #001740; }
|
||
.btn-secondary { background: #f3f4f6; color: #1f2937; }
|
||
.btn-secondary:hover { background: #e5e7eb; }
|
||
.btn-accent { background: #c9a84c; color: #1f1300; }
|
||
.btn-accent:hover { background: #b89540; }
|
||
|
||
.acct-toast { padding: .75rem 1rem; border-radius: 8px; font-size: .92rem; margin: 0 0 1rem; background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
|
||
|
||
.acct-bonus { margin-top: 1.25rem; padding: 1.25rem; background: #fefce8; border: 1px solid #fef08a; border-radius: 10px; }
|
||
.acct-bonus h3 { margin: 0 0 .35rem; font-size: 1rem; color: #713f12; }
|
||
.acct-bonus p { margin: 0; color: #713f12; font-size: .9rem; }
|
||
.acct-bonus .acct-actions { margin-top: .75rem; }
|
||
|
||
.acct-profile-form { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||
@media (max-width: 640px) { .acct-profile-form { grid-template-columns: 1fr; } }
|
||
.acct-profile-form .span-2 { grid-column: span 2; }
|
||
@media (max-width: 640px) { .acct-profile-form .span-2 { grid-column: span 1; } }
|
||
.acct-profile-form label { display: flex; flex-direction: column; font-size: .85rem; color: #374151; font-weight: 500; }
|
||
.acct-profile-form label > span { margin-bottom: .25rem; }
|
||
.acct-profile-form input { padding: .55rem .75rem; border: 1px solid #d1d5db; border-radius: 6px; font: inherit; font-size: .95rem; background: #fff; }
|
||
.acct-profile-form input:focus { outline: none; border-color: #00205B; box-shadow: 0 0 0 3px rgba(0,32,91,.12); }
|
||
.acct-profile-actions { display: flex; align-items: center; gap: 1rem; }
|
||
.acct-profile-status { color: #059669; font-size: .9rem; margin: 0; min-height: 1.2em; }
|
||
|
||
.acct-list { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem 2rem; }
|
||
@media (max-width: 640px) { .acct-list { grid-template-columns: 1fr; } }
|
||
.acct-list > div { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; padding-bottom: .6rem; border-bottom: 1px solid #f3f4f6; }
|
||
.acct-list span { color: #6b7280; font-size: .9rem; }
|
||
.acct-list strong { color: #1f2937; font-weight: 600; font-size: .95rem; text-align: right; }
|
||
|
||
.acct-case-status { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.25rem; }
|
||
@media (max-width: 640px) { .acct-case-status { grid-template-columns: 1fr; } }
|
||
.acct-storage-bar { background: #f3f4f6; height: 10px; border-radius: 999px; overflow: hidden; margin: .6rem 0 .35rem; }
|
||
.acct-storage-bar > div { background: linear-gradient(90deg, #00205B, #003478); height: 100%; }
|
||
|
||
.acct-upload { background: #f8fafc; border: 2px dashed #cbd5e1; border-radius: 12px; padding: 2rem 1.5rem; text-align: center; cursor: pointer; transition: all .15s; margin-bottom: 1.25rem; display: block; }
|
||
.acct-upload:hover, .acct-upload.is-drag { border-color: #00205B; background: #eff6ff; }
|
||
.acct-upload input { display: none; }
|
||
.acct-upload-icon { font-size: 2.2rem; line-height: 1; margin-bottom: .5rem; }
|
||
.acct-upload p { margin: .25rem 0; color: #374151; }
|
||
.acct-upload .hint { color: #6b7280; font-size: .85rem; }
|
||
|
||
.acct-flash { margin: 0 0 1rem; min-height: 1.2em; }
|
||
.acct-flash.is-ok { padding: .7rem 1rem; border-radius: 6px; background: #d1fae5; color: #065f46; font-size: .9rem; }
|
||
.acct-flash.is-err { padding: .7rem 1rem; border-radius: 6px; background: #fee2e2; color: #991b1b; font-size: .9rem; }
|
||
|
||
.acct-docs { border: 1px solid #e5e7eb; border-radius: 10px; overflow: hidden; }
|
||
.acct-doc { padding: .9rem 1.1rem; border-bottom: 1px solid #f3f4f6; display: flex; align-items: center; gap: 1rem; background: #fff; }
|
||
.acct-doc:last-child { border-bottom: none; }
|
||
.acct-doc-info { flex: 1; min-width: 0; }
|
||
.acct-doc-name { font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.acct-doc-meta { font-size: .85rem; color: #6b7280; }
|
||
.acct-status-pill { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: .78rem; font-weight: 600; }
|
||
.acct-status-pending { background: #fef3c7; color: #92400e; }
|
||
.acct-status-running { background: #ddd6fe; color: #5b21b6; }
|
||
.acct-status-ready { background: #d1fae5; color: #065f46; }
|
||
.acct-status-failed { background: #fee2e2; color: #991b1b; }
|
||
.acct-empty { padding: 1.5rem; text-align: center; color: #6b7280; background: #fff; }
|
||
|
||
.acct-result { display: flex; align-items: center; gap: 1rem; padding: .9rem 1.1rem; border-bottom: 1px solid #f3f4f6; background: #fff; }
|
||
.acct-result:last-child { border-bottom: none; }
|
||
.acct-result-title { font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.acct-result-title a { color: inherit; text-decoration: none; }
|
||
.acct-result-meta { font-size: .85rem; color: #6b7280; }
|
||
.acct-result-actions { display: flex; gap: .4rem; flex-shrink: 0; }
|
||
.btn-mini { background: #f3f4f6; border: none; padding: 5px 10px; border-radius: 6px; cursor: pointer; font: inherit; font-size: .82rem; color: #1f2937; text-decoration: none; display: inline-block; }
|
||
.btn-mini:hover { background: #e5e7eb; }
|
||
.btn-mini-primary { background: #00205B; color: #fff; }
|
||
.btn-mini-primary:hover { background: #001740; }
|
||
|
||
.acct-usage-table { width: 100%; border-collapse: collapse; }
|
||
.acct-usage-table th, .acct-usage-table td { padding: 8px 12px; border-bottom: 1px solid #f3f4f6; text-align: left; font-size: .9rem; }
|
||
.acct-usage-table th { color: #6b7280; font-weight: 500; font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; }
|
||
.acct-usage-table .credit-neg { color: #059669; font-weight: 600; }
|
||
.acct-usage-table .credit-pos { color: #b91c1c; }
|
||
|
||
.acct-mcp-row { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin: 0 0 1rem; padding: .7rem 1rem; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; }
|
||
.acct-mcp-row code { font-size: .85rem; color: #111827; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.acct-mcp-locked { padding: .75rem 1rem; background: #fefce8; border: 1px solid #fef08a; border-radius: 8px; color: #713f12; font-size: .92rem; display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
||
.acct-mcp-pre { background: #101828; color: #f9fafb; padding: .85rem 3.5rem .85rem 1rem; border-radius: 8px; font-size: .78rem; overflow-x: auto; margin: 0; line-height: 1.7; white-space: pre; }
|
||
.acct-mcp-pre-wrap { position: relative; margin: 0 0 1rem; }
|
||
.acct-mcp-pre-wrap button { position: absolute; top: .5rem; right: .5rem; background: rgba(249,250,251,.12); border: 1px solid rgba(249,250,251,.2); color: #f9fafb; font-size: .72rem; padding: 3px 9px; border-radius: 5px; cursor: pointer; }
|
||
.acct-mcp-pre-wrap button:hover { background: rgba(249,250,251,.2); }
|
||
</style>
|
||
</head>
|
||
<body data-authenticated="true" class="lt-app">
|
||
<script>
|
||
window.DBN_TOOLS_AUTHENTICATED = true;
|
||
window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
||
</script>
|
||
|
||
<?php include __DIR__ . '/includes/nav.php'; ?>
|
||
|
||
<div class="dbn-context-bar" role="note">
|
||
<span class="dbn-context-bar__tag"><?= htmlspecialchars(dbnToolsT('context_bar_tag', $uiLang)) ?></span>
|
||
<nav class="dbn-context-bar__links" aria-label="About">
|
||
<a href="/why-ours.php<?= $langSuffix ?>"><?= htmlspecialchars(dbnToolsT('context_bar_why', $uiLang)) ?></a>
|
||
<a href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars(dbnToolsT('context_bar_pricing', $uiLang)) ?></a>
|
||
<a href="/mcp-tool.php<?= $langSuffix ?>"><?= htmlspecialchars(dbnToolsT('context_bar_mcp', $uiLang)) ?></a>
|
||
<a href="/privacy.php<?= $langSuffix ?>"><?= htmlspecialchars(dbnToolsT('context_bar_privacy', $uiLang)) ?></a>
|
||
</nav>
|
||
</div>
|
||
|
||
<main id="appShell" class="app-shell acct-shell">
|
||
|
||
<header class="acct-header">
|
||
<h1><?= htmlspecialchars($l['page_title']) ?></h1>
|
||
<p class="lede"><?= htmlspecialchars($l['page_lede']) ?></p>
|
||
<?php if ($email !== ''): ?>
|
||
<p class="meta"><?= htmlspecialchars($l['signed_in_as']) ?>: <strong><?= htmlspecialchars($email) ?></strong></p>
|
||
<?php endif; ?>
|
||
</header>
|
||
|
||
<nav class="acct-subnav" aria-label="Account sections">
|
||
<a href="#plan"><?= htmlspecialchars($l['jump_plan']) ?></a>
|
||
<a href="#profile"><?= htmlspecialchars($l['jump_profile']) ?></a>
|
||
<a href="#workspace"><?= htmlspecialchars($l['jump_workspace']) ?></a>
|
||
<a href="#case"><?= htmlspecialchars($l['jump_case']) ?></a>
|
||
<?php if ($isPaidTier): ?>
|
||
<a href="#analyses"><?= htmlspecialchars($l['jump_analyses']) ?></a>
|
||
<?php endif; ?>
|
||
<a href="#usage"><?= htmlspecialchars($l['jump_usage']) ?></a>
|
||
<?php if ($isPaidTier): ?>
|
||
<a href="#mcp"><?= htmlspecialchars($l['jump_mcp']) ?></a>
|
||
<?php endif; ?>
|
||
</nav>
|
||
|
||
<?php if ($paidToast): ?>
|
||
<p class="acct-toast"><?= htmlspecialchars($l['payment_confirmed']) ?></p>
|
||
<?php endif; ?>
|
||
|
||
<!-- ── #plan ─────────────────────────────────────────────── -->
|
||
<section class="acct-section" id="plan">
|
||
<h2><?= htmlspecialchars($l['sec_plan']) ?></h2>
|
||
<div class="acct-plan-grid">
|
||
<div class="acct-plan-cell">
|
||
<h3><?= htmlspecialchars($l['current_plan']) ?></h3>
|
||
<p style="margin:.25rem 0;">
|
||
<span class="acct-tier-pill" style="background:<?= $tierLabel[1] ?>; color:<?= $tierLabel[2] ?>;"><?= htmlspecialchars($tierLabel[0]) ?></span>
|
||
<?php if (!empty($detail['trial_active'])): ?>
|
||
<span class="acct-trial-pill"><?= htmlspecialchars(sprintf($l['trial_badge'], (int)$detail['trial_days_remaining'])) ?></span>
|
||
<?php endif; ?>
|
||
</p>
|
||
<?php if (!empty($detail['subscription_period_end'])): ?>
|
||
<p class="acct-credits-break"><?= htmlspecialchars($l['renews_on']) ?>: <strong><?= htmlspecialchars(date($dateLocale, strtotime((string)$detail['subscription_period_end']))) ?></strong></p>
|
||
<?php elseif ($tier === 'free'): ?>
|
||
<p class="acct-credits-break"><?= htmlspecialchars($l['no_active_sub']) ?> <a href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars($l['see_plans']) ?></a></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="acct-plan-cell">
|
||
<h3><?= htmlspecialchars($l['available_credits']) ?></h3>
|
||
<p class="acct-credits-big"><?= number_format($effective, 0, ',', ' ') ?></p>
|
||
<p class="acct-credits-break">
|
||
<?= (int)$detail['balance'] ?> <?= htmlspecialchars($l['monthly']) ?>
|
||
· <?= (int)$detail['bonus_balance'] ?> <?= htmlspecialchars($l['prepaid']) ?>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="acct-actions">
|
||
<?php if ($isPaidTier): ?>
|
||
<button type="button" id="portalBtn" class="btn btn-secondary"><?= htmlspecialchars($l['manage_subscription']) ?></button>
|
||
<?php else: ?>
|
||
<a class="btn btn-primary" href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars($l['upgrade_plan']) ?> →</a>
|
||
<?php endif; ?>
|
||
<a class="btn btn-secondary" href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars($l['see_plans']) ?></a>
|
||
<a class="btn btn-accent" href="/pricing.php<?= $langSuffix ?>#topup"><?= htmlspecialchars($l['top_up_credits']) ?> →</a>
|
||
</div>
|
||
|
||
<?php if ($isSso): ?>
|
||
<div class="acct-bonus">
|
||
<h3><?= htmlspecialchars($l['bonus_survey_title']) ?></h3>
|
||
<?php if (!empty($detail['survey_completed_at'])): ?>
|
||
<p style="color:#059669;"><?= htmlspecialchars($l['bonus_survey_done']) ?></p>
|
||
<?php else: ?>
|
||
<p><?= htmlspecialchars($l['bonus_survey_pitch']) ?></p>
|
||
<div class="acct-actions">
|
||
<a class="btn btn-primary" href="https://dobetternorge.no/survey.php"><?= htmlspecialchars($l['take_survey_btn']) ?></a>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<!-- ── #profile ──────────────────────────────────────────── -->
|
||
<section class="acct-section" id="profile">
|
||
<h2>
|
||
<?= htmlspecialchars($l['sec_profile']) ?>
|
||
<span class="chip"><?= htmlspecialchars($l['profile_optional']) ?></span>
|
||
</h2>
|
||
<form id="profileForm" class="acct-profile-form">
|
||
<label>
|
||
<span><?= htmlspecialchars($l['f_display_name']) ?></span>
|
||
<input name="display_name" type="text" maxlength="100" autocomplete="name" value="<?= htmlspecialchars($profileVal('display_name')) ?>">
|
||
</label>
|
||
<label>
|
||
<span><?= htmlspecialchars($l['f_phone']) ?></span>
|
||
<input name="phone" type="tel" maxlength="40" autocomplete="tel" value="<?= htmlspecialchars($profileVal('phone')) ?>">
|
||
</label>
|
||
<label class="span-2">
|
||
<span><?= htmlspecialchars($l['f_address_1']) ?></span>
|
||
<input name="address_line1" type="text" maxlength="180" autocomplete="address-line1" value="<?= htmlspecialchars($profileVal('address_line1')) ?>">
|
||
</label>
|
||
<label class="span-2">
|
||
<span><?= htmlspecialchars($l['f_address_2']) ?></span>
|
||
<input name="address_line2" type="text" maxlength="180" autocomplete="address-line2" value="<?= htmlspecialchars($profileVal('address_line2')) ?>">
|
||
</label>
|
||
<label>
|
||
<span><?= htmlspecialchars($l['f_postal']) ?></span>
|
||
<input name="postal_code" type="text" maxlength="32" autocomplete="postal-code" value="<?= htmlspecialchars($profileVal('postal_code')) ?>">
|
||
</label>
|
||
<label>
|
||
<span><?= htmlspecialchars($l['f_city']) ?></span>
|
||
<input name="city" type="text" maxlength="100" autocomplete="address-level2" value="<?= htmlspecialchars($profileVal('city')) ?>">
|
||
</label>
|
||
<label>
|
||
<span><?= htmlspecialchars($l['f_region']) ?></span>
|
||
<input name="address_region" type="text" maxlength="100" autocomplete="address-level1" value="<?= htmlspecialchars($profileVal('address_region')) ?>">
|
||
</label>
|
||
<label>
|
||
<span><?= htmlspecialchars($l['f_country']) ?></span>
|
||
<input name="country" type="text" maxlength="2" autocomplete="country" placeholder="NO" value="<?= htmlspecialchars($profileVal('country')) ?>">
|
||
</label>
|
||
<input type="hidden" name="preferred_language" value="<?= htmlspecialchars($uiLang) ?>">
|
||
<div class="acct-profile-actions span-2">
|
||
<button type="submit" class="btn btn-primary"><?= htmlspecialchars($l['save_profile']) ?></button>
|
||
<p id="profileStatus" class="acct-profile-status" role="status" aria-live="polite"></p>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
|
||
<!-- ── #workspace ────────────────────────────────────────── -->
|
||
<section class="acct-section" id="workspace">
|
||
<h2>
|
||
<?= htmlspecialchars($l['sec_workspace']) ?>
|
||
<span class="chip"><?= htmlspecialchars($tierLabel[0]) ?></span>
|
||
</h2>
|
||
<div class="acct-list">
|
||
<div>
|
||
<span><?= htmlspecialchars($l['w_email']) ?></span>
|
||
<strong><?= htmlspecialchars($email) ?></strong>
|
||
</div>
|
||
<div>
|
||
<span><?= htmlspecialchars($l['w_seats']) ?></span>
|
||
<strong><?= htmlspecialchars($seatsLabel) ?></strong>
|
||
</div>
|
||
<div>
|
||
<span><?= htmlspecialchars($l['w_role']) ?></span>
|
||
<strong><?= htmlspecialchars($l['w_owner']) ?></strong>
|
||
</div>
|
||
<div>
|
||
<span><?= htmlspecialchars($l['w_plan_storage']) ?></span>
|
||
<strong><?= $isPaidTier ? htmlspecialchars($quotaMb . ' MB') : htmlspecialchars($l['w_upgrade_for_storage']) ?></strong>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── #case ─────────────────────────────────────────────── -->
|
||
<section class="acct-section" id="case">
|
||
<h2><?= htmlspecialchars($l['sec_case']) ?></h2>
|
||
<?php if (!$isPaidTier): ?>
|
||
<p class="acct-lede"><?= $l['case_free_gate_pitch'] /* contains <strong> */ ?></p>
|
||
<ul style="margin:0 0 1rem; padding-left: 1.25rem; line-height: 1.9; color: #1f2937;">
|
||
<li><?= htmlspecialchars($l['case_free_gate_b1']) ?></li>
|
||
<li><?= htmlspecialchars($l['case_free_gate_b2']) ?></li>
|
||
<li><?= htmlspecialchars($l['case_free_gate_b3']) ?></li>
|
||
<li><?= htmlspecialchars($l['case_free_gate_b4']) ?></li>
|
||
</ul>
|
||
<div class="acct-actions">
|
||
<a class="btn btn-primary" href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars($l['case_free_gate_cta']) ?></a>
|
||
</div>
|
||
<?php else: ?>
|
||
<p class="acct-lede"><?= htmlspecialchars($l['case_lede']) ?></p>
|
||
|
||
<div class="acct-case-status">
|
||
<div class="acct-plan-cell">
|
||
<h3 style="margin:0 0 .25rem; font-size:.82rem; color:#6b7280; text-transform:uppercase; letter-spacing:.05em;"><?= htmlspecialchars($l['case_storage']) ?></h3>
|
||
<p style="margin:0; font-size:1.4rem; font-weight:700; color:#00205B;"><?= $storageMb ?> MB <span style="color:#6b7280;font-size:.95rem;font-weight:400;">/ <?= $quotaMb ?> MB</span></p>
|
||
<div class="acct-storage-bar"><div style="width: <?= $storagePct ?>%;"></div></div>
|
||
<p style="margin:0; color:#6b7280; font-size:.85rem;"><?= htmlspecialchars(sprintf($l['case_documents_count'], count($docs))) ?></p>
|
||
</div>
|
||
<div class="acct-plan-cell">
|
||
<h3 style="margin:0 0 .25rem; font-size:.82rem; color:#6b7280; text-transform:uppercase; letter-spacing:.05em;"><?= htmlspecialchars($l['current_plan']) ?></h3>
|
||
<p style="margin:0; font-size:1.1rem; font-weight:700; color:#00205B;">
|
||
<?= htmlspecialchars($tierLabel[0]) ?>
|
||
<?php if (!empty($detail['trial_active'])): ?>
|
||
<span class="acct-trial-pill"><?= htmlspecialchars(sprintf($l['trial_badge'], (int)$detail['trial_days_remaining'])) ?></span>
|
||
<?php endif; ?>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<label for="acctFileInput" class="acct-upload" id="acctUploadZone">
|
||
<input id="acctFileInput" type="file" accept="application/pdf" multiple>
|
||
<div class="acct-upload-icon">📤</div>
|
||
<p><strong><?= htmlspecialchars($l['case_drop_pdf']) ?></strong></p>
|
||
<p class="hint"><?= htmlspecialchars($l['case_hint']) ?></p>
|
||
</label>
|
||
|
||
<div id="acctFlash" class="acct-flash" role="status" aria-live="polite"></div>
|
||
|
||
<div class="acct-docs">
|
||
<?php if (empty($docs)): ?>
|
||
<p class="acct-empty"><?= htmlspecialchars($l['case_no_docs']) ?></p>
|
||
<?php else: foreach ($docs as $d): ?>
|
||
<div class="acct-doc" data-doc-id="<?= (int)$d['id'] ?>">
|
||
<div style="font-size:1.6rem;line-height:1;">📄</div>
|
||
<div class="acct-doc-info">
|
||
<div class="acct-doc-name"><?= htmlspecialchars($d['filename']) ?></div>
|
||
<div class="acct-doc-meta">
|
||
<?= round((int)$d['size_bytes'] / 1024, 0) ?> KB
|
||
<?php if (!empty($d['page_count'])): ?> · <?= (int)$d['page_count'] ?> <?= htmlspecialchars($l['case_pages']) ?><?php endif; ?>
|
||
<?php if (!empty($d['doc_type'])): ?> · <?= htmlspecialchars($d['doc_type']) ?><?php endif; ?>
|
||
· <?= htmlspecialchars($l['case_uploaded_label']) ?> <?= htmlspecialchars(date($dateLocale, strtotime((string)$d['uploaded_at']))) ?>
|
||
</div>
|
||
</div>
|
||
<?php $ocrKey = 'case_ocr_' . $d['ocr_status']; ?>
|
||
<span class="acct-status-pill acct-status-<?= htmlspecialchars($d['ocr_status']) ?>">
|
||
<?= htmlspecialchars($l[$ocrKey] ?? $d['ocr_status']) ?>
|
||
</span>
|
||
<div>
|
||
<button type="button" class="btn-mini acct-delete" data-id="<?= (int)$d['id'] ?>"><?= htmlspecialchars($l['case_delete']) ?></button>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<!-- ── #analyses (paid only) ─────────────────────────────── -->
|
||
<?php if ($isPaidTier): ?>
|
||
<section class="acct-section" id="analyses">
|
||
<h2><?= htmlspecialchars($l['sec_analyses']) ?></h2>
|
||
<p class="acct-lede"><?= htmlspecialchars($l['analyses_lede']) ?></p>
|
||
<div class="acct-docs">
|
||
<?php if (empty($results)): ?>
|
||
<p class="acct-empty"><?= htmlspecialchars($l['no_analyses']) ?></p>
|
||
<?php else: foreach ($results as $r): ?>
|
||
<div class="acct-result" data-result-id="<?= (int)$r['id'] ?>">
|
||
<div style="font-size:1.4rem;line-height:1;"><?= htmlspecialchars(CaseResults::toolIcon((string)$r['tool'])) ?></div>
|
||
<div style="flex:1; min-width:0;">
|
||
<div class="acct-result-title">
|
||
<a href="/case-result.php?id=<?= (int)$r['id'] ?>">
|
||
<?= htmlspecialchars((string)($r['title'] ?? CaseResults::toolLabel((string)$r['tool']))) ?>
|
||
</a>
|
||
<?php if (!empty($r['pinned'])): ?>
|
||
<span title="<?= htmlspecialchars($l['pinned_title']) ?>" style="color:#c9a84c;margin-left:.4rem;">★</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="acct-result-meta">
|
||
<?= htmlspecialchars(CaseResults::toolLabel((string)$r['tool'])) ?>
|
||
· <?= htmlspecialchars(date($dateLocale . ' H:i', strtotime((string)$r['created_at']))) ?>
|
||
<?php if (!empty($r['used_case_context'])): ?>
|
||
· <span style="background:#dbeafe;color:#1e3a8a;padding:1px 8px;border-radius:999px;font-size:.75rem;font-weight:600;"><?= htmlspecialchars($l['used_case_badge']) ?></span>
|
||
<?php endif; ?>
|
||
<?php if ($r['tool'] === 'korrespond'): ?>
|
||
· <select class="korr-status-select" data-id="<?= (int)$r['id'] ?>" style="font-size:.8rem;padding:1px 4px;border-radius:4px;border:1px solid #d1d5db;background:#fff;cursor:pointer;">
|
||
<option value="draft" <?= ($r['korr_status'] ?? 'draft') === 'draft' ? 'selected' : '' ?>>Draft</option>
|
||
<option value="sent" <?= ($r['korr_status'] ?? '') === 'sent' ? 'selected' : '' ?>>Sent</option>
|
||
<option value="replied" <?= ($r['korr_status'] ?? '') === 'replied' ? 'selected' : '' ?>>Reply received</option>
|
||
<option value="resolved" <?= ($r['korr_status'] ?? '') === 'resolved' ? 'selected' : '' ?>>Resolved</option>
|
||
</select>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<div class="acct-result-actions">
|
||
<button type="button" class="btn-mini acct-pin" data-id="<?= (int)$r['id'] ?>">
|
||
<?= htmlspecialchars(!empty($r['pinned']) ? $l['unpin'] : $l['pin']) ?>
|
||
</button>
|
||
<a class="btn-mini btn-mini-primary" href="/case-result.php?id=<?= (int)$r['id'] ?>"><?= htmlspecialchars($l['open_analysis']) ?></a>
|
||
<button type="button" class="btn-mini acct-result-delete" data-id="<?= (int)$r['id'] ?>"><?= htmlspecialchars($l['delete_analysis']) ?></button>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; endif; ?>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<!-- ── #usage ────────────────────────────────────────────── -->
|
||
<section class="acct-section" id="usage">
|
||
<h2><?= htmlspecialchars($l['sec_usage']) ?></h2>
|
||
<?php if (empty($recent)): ?>
|
||
<p class="acct-empty" style="border:1px solid #f3f4f6; border-radius:8px;"><?= htmlspecialchars($l['no_usage']) ?></p>
|
||
<?php else: ?>
|
||
<table class="acct-usage-table">
|
||
<thead><tr>
|
||
<th><?= htmlspecialchars($l['usage_col_tool']) ?></th>
|
||
<th><?= htmlspecialchars($l['usage_col_credits']) ?></th>
|
||
<th><?= htmlspecialchars($l['usage_col_when']) ?></th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<?php foreach ($recent as $r): $credits = (int)$r['credits_used']; ?>
|
||
<tr>
|
||
<td><?= htmlspecialchars($r['tool']) ?></td>
|
||
<td class="<?= $credits < 0 ? 'credit-neg' : 'credit-pos' ?>"><?= $credits < 0 ? '+' . abs($credits) : '−' . $credits ?></td>
|
||
<td><?= htmlspecialchars((string)$r['created_at']) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<!-- ── #mcp (paid only) ──────────────────────────────────── -->
|
||
<?php if ($isPaidTier): ?>
|
||
<section class="acct-section" id="mcp">
|
||
<h2><?= htmlspecialchars($l['sec_mcp']) ?></h2>
|
||
<p class="acct-lede"><?= htmlspecialchars($l['mcp_desc']) ?></p>
|
||
|
||
<div class="acct-mcp-row">
|
||
<span style="font-weight:600; color:#374151; white-space:nowrap;"><?= htmlspecialchars($l['mcp_token_lbl']) ?>:</span>
|
||
<code id="acctMcpTokenPrefix"><?= htmlspecialchars($l['mcp_loading']) ?></code>
|
||
<a href="/mcp.php<?= $langSuffix ?>" style="font-size:.85rem; color:#00205B; font-weight:600; text-decoration:none; white-space:nowrap;"><?= htmlspecialchars($l['mcp_manage']) ?></a>
|
||
</div>
|
||
|
||
<p style="margin:1rem 0 .35rem; font-size:.85rem; font-weight:600; color:#374151;"><?= htmlspecialchars($l['mcp_stdio_lbl']) ?></p>
|
||
<div class="acct-mcp-pre-wrap">
|
||
<pre id="acctStdioBlock" class="acct-mcp-pre">claude mcp add dobetternorge -- npx -y @bluenotelogic/mcp dobetternorge-mcp --stdio
|
||
|
||
# Set token (add to ~/.bashrc or ~/.zshrc to persist):
|
||
export DBN_MCP_TOKEN=<span id="acctStdioToken" style="color:#86efac;">dbn_user_mcp_…</span></pre>
|
||
<button type="button" onclick="acctCopyBlock('acctStdioBlock', this)"><?= htmlspecialchars($l['mcp_copy']) ?></button>
|
||
</div>
|
||
|
||
<p style="margin:1rem 0 .35rem; font-size:.85rem; font-weight:600; color:#374151;"><?= htmlspecialchars($l['mcp_remote_lbl']) ?></p>
|
||
<div class="acct-mcp-pre-wrap">
|
||
<pre class="acct-mcp-pre">URL: https://mcp.dobetternorge.no/mcp
|
||
Authorization: Bearer <span id="acctRemoteToken" style="color:#86efac;">dbn_user_mcp_…</span></pre>
|
||
<button type="button" onclick="acctCopyRemote(this)"><?= htmlspecialchars($l['mcp_copy']) ?></button>
|
||
</div>
|
||
|
||
<p style="margin:1rem 0 0; font-size:.9rem;">
|
||
<a href="/mcp.php<?= $langSuffix ?>" style="color:#00205B; font-weight:600;"><?= htmlspecialchars($l['mcp_full_setup']) ?></a>
|
||
</p>
|
||
</section>
|
||
<?php elseif ($isSso): ?>
|
||
<section class="acct-section" id="mcp">
|
||
<h2><?= htmlspecialchars($l['sec_mcp']) ?></h2>
|
||
<div class="acct-mcp-locked">
|
||
<span><?= htmlspecialchars($l['mcp_locked']) ?></span>
|
||
<a href="/pricing.php<?= $langSuffix ?>" class="btn btn-primary"><?= htmlspecialchars($l['upgrade_plan']) ?> →</a>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
</main>
|
||
|
||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||
<script>
|
||
window.ACCT_L = <?= json_encode($l, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||
|
||
/* ── Sub-nav active highlight ──────────────────────────────────── */
|
||
(function () {
|
||
var links = document.querySelectorAll('.acct-subnav a');
|
||
var sections = Array.prototype.map.call(links, function (a) {
|
||
var id = a.getAttribute('href').slice(1);
|
||
return { link: a, el: document.getElementById(id) };
|
||
}).filter(function (s) { return s.el; });
|
||
|
||
function onScroll() {
|
||
var y = window.scrollY + 80;
|
||
var current = null;
|
||
sections.forEach(function (s) {
|
||
if (s.el.offsetTop <= y) current = s;
|
||
});
|
||
sections.forEach(function (s) {
|
||
s.link.classList.toggle('is-current', s === current);
|
||
});
|
||
}
|
||
window.addEventListener('scroll', onScroll, { passive: true });
|
||
onScroll();
|
||
}());
|
||
|
||
/* ── Profile save ──────────────────────────────────────────────── */
|
||
(function () {
|
||
var form = document.getElementById('profileForm');
|
||
var status = document.getElementById('profileStatus');
|
||
if (!form) return;
|
||
|
||
form.addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
var data = {};
|
||
Array.prototype.forEach.call(form.elements, function (el) {
|
||
if (!el.name) return;
|
||
data[el.name] = el.value || '';
|
||
});
|
||
data.dismiss_prompt = true;
|
||
|
||
if (status) { status.textContent = window.ACCT_L.profile_saving; status.style.color = '#059669'; }
|
||
fetch('/api/profile.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'same-origin',
|
||
body: JSON.stringify(data)
|
||
}).then(function (r) {
|
||
return r.json().then(function (j) {
|
||
if (!r.ok || !j.ok) {
|
||
throw new Error((j.error && j.error.message) || window.ACCT_L.profile_error_default);
|
||
}
|
||
if (status) { status.textContent = window.ACCT_L.profile_saved; status.style.color = '#059669'; }
|
||
});
|
||
}).catch(function (err) {
|
||
if (status) { status.textContent = err.message; status.style.color = '#b91c1c'; }
|
||
});
|
||
});
|
||
}());
|
||
|
||
/* ── Case upload / delete ──────────────────────────────────────── */
|
||
(function () {
|
||
var fileInput = document.getElementById('acctFileInput');
|
||
var dropZone = document.getElementById('acctUploadZone');
|
||
var flash = document.getElementById('acctFlash');
|
||
if (!fileInput || !dropZone) return;
|
||
|
||
function showFlash(msg, isError) {
|
||
flash.className = 'acct-flash ' + (isError ? 'is-err' : 'is-ok');
|
||
flash.textContent = msg;
|
||
}
|
||
|
||
function uploadFile(file) {
|
||
if (file.size > 25 * 1024 * 1024) {
|
||
showFlash(window.ACCT_L.case_over_25mb.replace('%s', file.name), true);
|
||
return;
|
||
}
|
||
var data = new FormData();
|
||
data.append('file', file);
|
||
fetch('/api/case/upload.php', { method: 'POST', body: data })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (json) {
|
||
if (json.ok) {
|
||
showFlash(window.ACCT_L.case_uploaded_ok.replace('%s', file.name), false);
|
||
setTimeout(function () { window.location.reload(); }, 1200);
|
||
} else {
|
||
showFlash(file.name + ': ' + ((json.error && json.error.message) || window.ACCT_L.case_unknown_err), true);
|
||
}
|
||
})
|
||
.catch(function (e) { showFlash(window.ACCT_L.network_error + e.message, true); });
|
||
}
|
||
|
||
fileInput.addEventListener('change', function () {
|
||
for (var i = 0; i < fileInput.files.length; i++) uploadFile(fileInput.files[i]);
|
||
});
|
||
|
||
['dragenter', 'dragover'].forEach(function (ev) {
|
||
dropZone.addEventListener(ev, function (e) { e.preventDefault(); dropZone.classList.add('is-drag'); });
|
||
});
|
||
['dragleave', 'drop'].forEach(function (ev) {
|
||
dropZone.addEventListener(ev, function (e) { e.preventDefault(); dropZone.classList.remove('is-drag'); });
|
||
});
|
||
dropZone.addEventListener('drop', function (e) {
|
||
for (var i = 0; i < e.dataTransfer.files.length; i++) {
|
||
var f = e.dataTransfer.files[i];
|
||
if (f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')) uploadFile(f);
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('.acct-delete').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
if (!confirm(window.ACCT_L.case_confirm_delete)) return;
|
||
var id = btn.getAttribute('data-id');
|
||
fetch('/api/case/delete.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ doc_id: parseInt(id, 10) })
|
||
}).then(function (r) { return r.json(); })
|
||
.then(function (json) {
|
||
if (json.ok) window.location.reload();
|
||
else alert((json.error && json.error.message) || window.ACCT_L.case_delete_failed);
|
||
})
|
||
.catch(function (e) { alert(window.ACCT_L.network_error + e.message); });
|
||
});
|
||
});
|
||
}());
|
||
|
||
/* ── Saved analyses: pin / delete ──────────────────────────────── */
|
||
(function () {
|
||
document.querySelectorAll('.acct-pin').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
var id = btn.getAttribute('data-id');
|
||
fetch('/api/case/result-action.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'pin', id: parseInt(id, 10) })
|
||
}).then(function (r) { return r.json(); })
|
||
.then(function (json) {
|
||
if (json.ok) window.location.reload();
|
||
else alert((json.error && json.error.message) || window.ACCT_L.pin_failed);
|
||
})
|
||
.catch(function (e) { alert(window.ACCT_L.network_error + e.message); });
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.korr-status-select').forEach(function (sel) {
|
||
sel.addEventListener('change', function () {
|
||
fetch('/api/case/result-action.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'set_status', id: parseInt(sel.getAttribute('data-id'), 10), status: sel.value })
|
||
}).catch(function () {});
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.acct-result-delete').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
if (!confirm(window.ACCT_L.confirm_delete_analysis)) return;
|
||
var id = btn.getAttribute('data-id');
|
||
fetch('/api/case/result-action.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', id: parseInt(id, 10) })
|
||
}).then(function (r) { return r.json(); })
|
||
.then(function (json) {
|
||
if (json.ok) window.location.reload();
|
||
else alert((json.error && json.error.message) || window.ACCT_L.case_delete_failed);
|
||
})
|
||
.catch(function (e) { alert(window.ACCT_L.network_error + e.message); });
|
||
});
|
||
});
|
||
}());
|
||
|
||
/* ── Stripe portal ─────────────────────────────────────────────── */
|
||
(function () {
|
||
var btn = document.getElementById('portalBtn');
|
||
if (!btn) return;
|
||
btn.addEventListener('click', function () {
|
||
btn.disabled = true;
|
||
var original = btn.textContent;
|
||
btn.textContent = window.ACCT_L.portal_loading;
|
||
fetch('/api/stripe-portal.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: '{}'
|
||
}).then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
if (data.ok && data.url) {
|
||
window.location.href = data.url;
|
||
} else {
|
||
alert((data.error && data.error.message) || window.ACCT_L.portal_error);
|
||
}
|
||
})
|
||
.catch(function (e) { alert(window.ACCT_L.network_error + e.message); })
|
||
.finally(function () { btn.disabled = false; btn.textContent = original; });
|
||
});
|
||
}());
|
||
|
||
<?php if ($isPaidTier): ?>
|
||
/* ── MCP token prefix fetch ────────────────────────────────────── */
|
||
(function () {
|
||
fetch('/api/mcp-tokens.php', { credentials: 'same-origin' })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (data) {
|
||
if (!data || !data.ok) return;
|
||
var active = (data.tokens || []).filter(function (t) { return t.is_active; });
|
||
var prefixEl = document.getElementById('acctMcpTokenPrefix');
|
||
if (prefixEl) {
|
||
prefixEl.textContent = active.length > 0
|
||
? active[0].token_prefix + '…'
|
||
: window.ACCT_L.mcp_no_token;
|
||
}
|
||
if (active.length > 0) {
|
||
var p = active[0].token_prefix + '…';
|
||
var s = document.getElementById('acctStdioToken');
|
||
var rr = document.getElementById('acctRemoteToken');
|
||
if (s) s.textContent = p;
|
||
if (rr) rr.textContent = p;
|
||
}
|
||
})
|
||
.catch(function () {});
|
||
}());
|
||
|
||
window.acctCopyBlock = function (id, btn) {
|
||
var el = document.getElementById(id);
|
||
if (!el) return;
|
||
navigator.clipboard.writeText(el.innerText || el.textContent).then(function () {
|
||
var orig = btn.textContent; btn.textContent = '✓';
|
||
setTimeout(function () { btn.textContent = orig; }, 1500);
|
||
}).catch(function () {});
|
||
};
|
||
|
||
window.acctCopyRemote = function (btn) {
|
||
var tok = document.getElementById('acctRemoteToken');
|
||
var txt = 'URL: https://mcp.dobetternorge.no/mcp\nAuthorization: Bearer ' + (tok ? tok.textContent : 'dbn_user_mcp_…');
|
||
navigator.clipboard.writeText(txt).then(function () {
|
||
var orig = btn.textContent; btn.textContent = '✓';
|
||
setTimeout(function () { btn.textContent = orig; }, 1500);
|
||
}).catch(function () {});
|
||
};
|
||
<?php endif; ?>
|
||
</script>
|
||
</body>
|
||
</html>
|