Kundkort — Frontend Overview
The Kundkort frontend is a single-page React application for viewing enriched Swedish company data. It is the primary user-facing interface of the EnrichNode platform.
Source of truth:
frontend/kundkort/in the DBPOC repo.
What it does
- Search for Swedish companies by name or org number
- View a rich “kundkort” (customer card) with identity, financials, contacts, trademarks, and group structure
- Enrich a company on-demand via the enrichment pipeline
- Export kundkort as PDF (print styles) or CSV (search results)
- Monitor enrichment errors and retry failed jobs
Tech Stack
| Layer | Technology | Notes |
|---|---|---|
| Runtime | Bun | Bundled via bun build to dist/app.js |
| Framework | React 18 | createRoot API, functional components + hooks |
| Styling | Tailwind CSS | CDN build in index.html + custom config in tailwind.config.js |
| UI Kit | Tremor React | Charts (AreaChart, DonutChart, BarChart), layout primitives (Card, Grid, Flex), tabs |
| Fonts | Inter | Via Google Fonts CDN |
| Auth | JWT (custom) | sessionStorage-backed with dev bypass |
| Charts | Tremor + Recharts | Financial trends, contact distribution, data completeness |
Note
The app uses inline styles extensively (not Tailwind utility classes) for most components, with Tailwind/Tremor used primarily for chart layouts and grid systems. This is an intentional design choice for fine-grained dark-theme control.
Architecture
Entry Point
app.tsx — mounts the React app to #root and manages top-level routing between two views:
┌─────────────┐ ┌─────────────────┐
│ Search │────▶│ Kundkort │
│ (list) │◀────│ (detail) │
└─────────────┘ └─────────────────┘
- Search view (
SearchPage): Company search with filters, recent history, CSV export - Kundkort view (
KundkortPage): Full company profile with tabs (Overview / Analytics / Contacts)
View State Management
No external state library. View state is managed via useState in App:
| State | Type | Driven by |
|---|---|---|
view | 'search' | 'kundkort' | User navigation, URL ?org= param |
orgNr | string | null | Selected company |
token | string | null | useAuth hook |
URL sync: ?org=5560000000 deep-links directly to a kundkort. Browser back/forward supported via popstate.
Auth Flow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ App mount │───▶│ useAuth() │───▶│ DEV_MODE? │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
┌────────────────────────┘
│ Yes → auto-set 'dev-token'
│ No → check sessionStorage
▼
┌─────────────┐
│ token set │───▶ render SearchPage
│ token null │───▶ render LoginModal
└─────────────┘
- Production: JWT stored in
sessionStorage, login via/api/auth/login - Development:
DEV_MODE = trueauto-sets a dummy token, bypassing login entirely - Token passed down as prop to all pages/hooks that need API access
See Hooks Reference for useAuth details.
Data Flow
Kundkort Detail Page
KundkortPage
├── useKundkort(orgNr, token) ──▶ GET /api/kundkort/:orgNr
│ └── Main company data (identity, contacts, financials, etc.)
│
└── useEcoApi(orgNr, token) ──▶ Multiple parallel calls
├── fetchInsights() ──▶ GET /api/companies/:orgNr/financials (ECOAPI)
├── fetchGaps() ──▶ GET /api/kundkort/:orgNr (gap analysis)
├── fetchFinancialTrend() ──▶ Transformed financials
└── fetchContactDistribution() ──▶ GET /api/kundkort/:orgNr (contact grouping)
Both hooks expose refetch() and enrich() for manual refresh and on-demand enrichment.
Search Page
SearchPage
└── useSearch(query, token, filters?) ──▶ GET /api/kundkort/search?q=...
or /api/kundkort/search/advanced?...
- Debounced 300ms
- Supports filters:
sni,city,legal_form,active - Recent searches persisted to
localStorage
Build Process
# From project root
cd frontend/kundkort
bun build app.tsx --outdir dist --minifyOutputs:
dist/app.js— bundled applicationdist/app.css— Tailwind + custom styles
index.html loads both from ./dist/. The HTML also includes the Tailwind CDN script for runtime JIT compilation (used by Tremor components).
File Structure
frontend/kundkort/
├── app.tsx # Entry point, routing, auth gate
├── index.html # HTML shell, Tailwind CDN, fonts
├── index.css # CSS variables, scrollbar, animations, print styles
├── tailwind.config.js # Custom theme (dark palette + Tremor tokens)
├── types/
│ └── kundkort.ts # All TypeScript interfaces
├── utils/
│ └── formatOrgNr.ts # Org number formatting (10-digit with dash)
├── services/
│ └── ecoApiClient.ts # ECOAPI fetch helpers + gap analysis
├── hooks/
│ ├── useAuth.ts # JWT auth with dev bypass
│ ├── useKundkort.ts # Main company data fetcher
│ ├── useEcoApi.ts # Analytics/insights fetcher
│ └── useSearch.ts # Search with debounce
├── components/
│ ├── CompanyHeader.tsx # Avatar, name, status, lead score, completeness bar
│ ├── KundkortPage.tsx # Detail page layout, tabs, action bar
│ ├── SearchPage.tsx # Search input, results, filters, recent, export
│ ├── LoginModal.tsx # Email/password login form
│ ├── ErrorPanel.tsx # Floating enrichment error monitor + retry
│ ├── Footer.tsx # Fixed footer with dev mode badge + enrichment counter
│ ├── sections/ # Kundkort content sections
│ │ ├── IdentityCard.tsx
│ │ ├── ContactInfoCard.tsx
│ │ ├── SummarySection.tsx
│ │ ├── FinancialsSection.tsx
│ │ ├── ContactsSection.tsx
│ │ ├── KoncernSection.tsx
│ │ └── VarumarkenSection.tsx
│ └── ui/ # Reusable UI primitives + charts
│ ├── SectionHeading.tsx
│ ├── KvRow.tsx
│ ├── ContactCard.tsx
│ ├── GapBadge.tsx
│ ├── SkeletonLoader.tsx
│ ├── ErrorState.tsx
│ ├── LoadingCard.tsx
│ ├── FinancialChart.tsx
│ ├── ContactDistributionChart.tsx
│ ├── DataCompletenessChart.tsx
│ ├── GapsPanel.tsx
│ └── InsightsPanel.tsx
Theming
Dark Theme (default)
CSS custom properties in index.css:
| Token | Value | Usage |
|---|---|---|
--bg | #0f1117 | Page background |
--bg-surface | #161b27 | Cards, panels |
--bg-raised | #1c2333 | Elevated surfaces |
--accent | #6366f1 | Primary indigo |
--text-primary | #f1f5f9 | Headings |
--text-secondary | #94a3b8 | Body text |
--text-muted | #475569 | Labels, hints |
--status-ok | #34d399 | Success |
--status-warn | #fbbf24 | Warning |
--status-error | #f87171 | Error |
Print Theme
Full light-mode override in @media print — white background, dark text, grid collapse to single column, link URLs exposed, A4 portrait.
Key Design Decisions
- Inline styles over CSS-in-JS: All components use
style={{...}}props for theming consistency. No styled-components, no CSS modules (exceptErrorPanel.css). - No router library: Simple
useStateview switching with URL sync. Two views = no need for React Router. - Tremor for charts only: Layout and typography are hand-rolled; Tremor provides chart primitives and some grid helpers.
- Dev bypass hardcoded:
DEV_MODE = trueinuseAuth.ts— must be set tofalsebefore production. - Session-based auth: JWT in
sessionStorage(notlocalStorage) for security.
See also
- Components Reference — Full component catalog
- Hooks Reference — Hook APIs and internals
- Services Reference — ECOAPI client
- Stack — Backend stack context