/* ELUCENS app — DATA SEAM (rewired for the real product). ---------------------------------------------------------------------------- This is the ONE design file that differs from Claude Design's handoff. Every other file (shell/views/chat/icons/result/settings/actions/search + the CSS) is served byte-identical, so the UI stays pixel-faithful. The handoff hard-coded the "Brouwer Metaalbewerking" demo here. We replace that with the real tenant's data, injected by the FastAPI backend as `window.__ELUCENS_BOOTSTRAP__` (see app/routes/design_app.py + /api/bootstrap). Rules: - Tenant VALUES (actions, mails, crm, integrations, metrics, result, …) come from the bootstrap. When the tenant has no data, the fallback is ZERO / empty — never fake. A fresh tenant renders honest empty states. - DESIGN CONTENT (UI copy, labels, the package pillar definitions, source display metadata, example questions) stays static here — it is design, not tenant data, so keeping it verbatim is what makes the render faithful. */ const B = (typeof window !== 'undefined' && window.__ELUCENS_BOOTSTRAP__) || {}; /* ---- tenant identity (from bootstrap) ---- */ const TENANT = B.tenant || { name: 'Elucens', short: 'Elucens', slug: 'elucens', city: '', letter: 'E', since: '', plan: '', }; const USER = B.user || { name: '', role: '', initials: '' }; /* No tenant switcher in the product (a client is always in their own tenant). */ const TENANTS = B.tenants || [{ ...TENANT, current: true }]; /* ---- team & roles (settings) ---- */ const TEAM = B.team || []; /* ---- global search index (command palette) ---- */ const SEARCH_INDEX = B.searchIndex || []; /* ---- chrome translations (STATIC design copy) ---- */ const LANG = { nl: { grpMain: 'Hoofdmenu', grpPillars: 'Uw pakket', grpData: 'Data', overview: 'Overzicht', ask: 'Vraag Elucens', integrations: 'Integraties', actions: 'Acties', result: 'Resultaat', search: 'Zoek in uw dossier…', tenants: 'Tenant wisselen', calls: 'Gesprekken', timeSaved: 'Tijd bespaard', errors: 'Fouten geëlimineerd', revenue: 'Omzet vrijgemaakt', thisWeek: 'deze week', perWeek: 'per week', vsLast: 't.o.v. vorige maand', allPillars: 'Uw pakket', recent: 'Recente activiteit', context: 'Contextdekking', open: 'Openen', viewAll: 'Alles bekijken', live: 'Live', inDiag: 'In diagnose', new: 'Nieuw', feeds: 'Voedt context', lastSync: 'Laatste sync', records: 'records', connected: 'Gekoppeld', syncing: 'Synchroniseert', available: 'Beschikbaar', addIntegration: 'Bron koppelen', settings: 'Instellingen', }, en: { grpMain: 'Main', grpPillars: 'Your package', grpData: 'Data', overview: 'Overview', ask: 'Ask Elucens', integrations: 'Integrations', actions: 'Actions', result: 'Results', search: 'Search your dossier…', tenants: 'Switch tenant', calls: 'Calls', timeSaved: 'Time saved', errors: 'Errors eliminated', revenue: 'Revenue unlocked', thisWeek: 'this week', perWeek: 'per week', vsLast: 'vs last month', allPillars: 'Your package', recent: 'Recent activity', context: 'Context coverage', open: 'Open', viewAll: 'View all', live: 'Live', inDiag: 'In diagnosis', new: 'New', feeds: 'Feeds context', lastSync: 'Last sync', records: 'records', connected: 'Connected', syncing: 'Syncing', available: 'Available', addIntegration: 'Connect a source', settings: 'Settings', }, }; /* ---- headline metrics: card DEFINITIONS static, values from bootstrap ---- */ const METRICS = B.metrics || [ { key: 'timeSaved', icon: 'clock', value: '0', unit: 'u', foot: 'thisWeek', delta: '', dir: 'up' }, { key: 'errors', icon: 'shield', value: '0', unit: '', foot: 'thisWeek', delta: '', dir: 'up' }, { key: 'revenue', icon: 'euro', value: '€0', unit: '', foot: 'in pijplijn', delta: '', dir: 'up' }, { key: 'context', icon: 'database', value: '0', unit: '%', foot: 'van uw bedrijf gedekt', delta: '', dir: 'up' }, ]; /* ---- pillars: the PACKAGE Elucens offers (design content). Stats come from the bootstrap; the zero-fallback keeps the copy but empties the numbers. ---- */ const PILLARS = B.pillars || [ { id: 'email', icon: 'mail', name: 'E-mailtriage', en: 'Email triage', status: 'live', line: 'Inkomende mail wordt gelezen, gesorteerd en van een concept-antwoord voorzien. U houdt de regie.', stats: [{ v: '0', l: 'reactietijd' }, { v: '0', l: 'gerouteerd' }] }, { id: 'quotes', icon: 'file', name: 'Offerte-automatisering', en: 'Quote automation', status: 'live', line: 'Calculaties lopen automatisch door. Offertes in uren, niet in dagen.', stats: [{ v: '0', l: 'doorlooptijd' }, { v: '0', l: 'rekenfouten' }] }, { id: 'invoicing', icon: 'receipt', name: 'Facturatiekoppeling', en: 'Invoicing link', status: 'live', line: 'Orders stromen vanzelf door naar de facturatie. Geen overtypwerk, geen boetes voor late facturen.', stats: [{ v: '0', l: 'per week terug' }, { v: '€0', l: 'aan boetes' }] }, { id: 'crm', icon: 'users', name: 'CRM', en: 'Relatiebeheer', status: 'live', line: 'Eén klantbeeld, gevoed door al uw bronnen. Wie, wat en wanneer — op één plek.', stats: [{ v: '0', l: 'relaties' }, { v: '0', l: 'open kansen' }] }, { id: 'forecast', icon: 'trendUp', name: 'Voorraadprognose', en: 'Stock forecast', status: 'diag', soon: true, line: 'In diagnose. We onderzoeken waar voorraad vastligt en waar omzet blijft liggen.', stats: [] }, ]; /* ---- integrations: real connected-source status from bootstrap ---- */ const INTEGRATIONS = B.integrations || []; /* ---- context coverage: per-area % from bootstrap ---- */ const COVERAGE = B.coverage || []; /* ---- activity feed from bootstrap ---- */ const ACTIVITY = B.activity || []; /* ---- actions inbox: everything waiting on the owner (from bootstrap) ---- */ const ACTIONS = B.actions || []; /* ---- weekly result report: real aggregates from bootstrap, zeroed fallback ---- */ const RESULT = B.result || { week: '—', period: '—', dossier: '—', hero: [ { lbl: 'Uren bespaard', v: '0', unit: 'u', delta: '', foot: 'handwerk deze week' }, { lbl: 'Reactietijd', v: '0', unit: '%', delta: '', foot: 'op inkomende e-mail' }, { lbl: 'Fouten', v: '0', unit: '', delta: '', foot: 'overgetypt of gemist' }, { lbl: 'Boetes', v: '€0', unit: '', delta: '', foot: 'voor late facturen' }, ], did: [], table: [], trend: [0, 0, 0, 0, 0, 0], trendLabels: ['', '', '', '', '', ''], quote: 'Geen resultaat, geen vervolg.', }; /* ===================== EMAIL TRIAGE ===================== */ /* Category DEFINITIONS static (design); counts (n) from bootstrap, zeroed. */ const MAIL_CATS = B.mailCats || [ { id: 'reply', name: 'Concept klaar', en: 'Draft ready', n: 0, color: '#0E9C9A' }, { id: 'review', name: 'Wacht op u', en: 'Needs you', n: 0, color: '#C98A21' }, { id: 'auto', name: 'Automatisch afgehandeld', en: 'Auto-handled', n: 0, color: '#1E9E63' }, { id: 'routed', name: 'Doorgestuurd', en: 'Routed', n: 0, color: '#737A77' }, ]; const MAILS = B.mails || []; /* ===================== CRM ===================== */ const CRM = B.crm || []; /* ===================== GESPREKKEN (calls) ===================== Real calls from the bootstrap (tenant DB, CallRecording rows). Empty tenant → empty list → the calls view renders its honest empty state. */ const CALLS = B.calls || []; /* Call source label is PER TENANT (Samsung for elucens; Voys/Swyx/Teams for future clients) — shown in the page eyebrow and the drawer's Bron field. */ const CALLS_SRC = B.callsSource || 'Telefoon'; /* ===================== CHAT — grounded knowledge base ===================== */ /* Source display metadata (STATIC design). */ const SRC = B.src || { exact: { abbr: 'E', icon: 'euro', color: '#C0492F', name: 'Exact Online' }, team: { abbr: 'T', icon: 'users', color: '#0A7574', name: 'Teamleader' }, m365: { abbr: 'M', icon: 'grid', color: '#2C6FE0', name: 'Microsoft 365' }, wa: { abbr: 'W', icon: 'whatsapp', color: '#1FA855', name: 'WhatsApp Business' }, orders: { abbr: 'O', icon: 'layers', color: '#7A5410', name: 'Orderportaal' }, docs: { abbr: 'D', icon: 'book', color: '#454C4A', name: 'SharePoint' }, }; /* Example questions (STATIC design copy — shown on the chat empty state). */ const SUGGESTIONS = B.suggestions || [ { q: 'Welke offerte staat het langst open?', src: 'Teamleader · Exact', key: 'offerte' }, { q: 'Hoeveel facturen staan langer dan 30 dagen open?', src: 'Exact Online', key: 'facturen' }, { q: 'Wat was de omzet vorige maand vergeleken met vorig jaar?', src: 'Exact Online', key: 'omzet' }, { q: 'Wie wacht het langst op een antwoord van ons?', src: 'Microsoft 365', key: 'antwoord' }, ]; /* Grounded answers come from the real /chat backend (cli/bedrock + retrieval), not a canned keyword table. Empty here so the prototype's local matcher never fabricates an answer; chat.jsx is wired to POST the question to the backend. */ const CHAT_KB = B.chatKb || []; /* Per-tenant feature-flags (server-side bepaald in app/bootstrap.py). Bv. 'gesprekken' = operator-only calls-tab; afwezig → tab verborgen. */ const FEATURES = B.features || []; const hasFeature = (f) => FEATURES.includes(f); Object.assign(window, { TENANT, USER, TENANTS, TEAM, LANG, METRICS, PILLARS, INTEGRATIONS, COVERAGE, ACTIVITY, ACTIONS, RESULT, MAIL_CATS, MAILS, CRM, CALLS, SUGGESTIONS, CHAT_KB, SRC, SEARCH_INDEX, FEATURES, hasFeature, });