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

LayerTechnologyNotes
RuntimeBunBundled via bun build to dist/app.js
FrameworkReact 18createRoot API, functional components + hooks
StylingTailwind CSSCDN build in index.html + custom config in tailwind.config.js
UI KitTremor ReactCharts (AreaChart, DonutChart, BarChart), layout primitives (Card, Grid, Flex), tabs
FontsInterVia Google Fonts CDN
AuthJWT (custom)sessionStorage-backed with dev bypass
ChartsTremor + RechartsFinancial 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:

StateTypeDriven by
view'search' | 'kundkort'User navigation, URL ?org= param
orgNrstring | nullSelected company
tokenstring | nulluseAuth 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 = true auto-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 --minify

Outputs:

  • dist/app.js — bundled application
  • dist/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:

TokenValueUsage
--bg#0f1117Page background
--bg-surface#161b27Cards, panels
--bg-raised#1c2333Elevated surfaces
--accent#6366f1Primary indigo
--text-primary#f1f5f9Headings
--text-secondary#94a3b8Body text
--text-muted#475569Labels, hints
--status-ok#34d399Success
--status-warn#fbbf24Warning
--status-error#f87171Error

Full light-mode override in @media print — white background, dark text, grid collapse to single column, link URLs exposed, A4 portrait.


Key Design Decisions

  1. Inline styles over CSS-in-JS: All components use style={{...}} props for theming consistency. No styled-components, no CSS modules (except ErrorPanel.css).
  2. No router library: Simple useState view switching with URL sync. Two views = no need for React Router.
  3. Tremor for charts only: Layout and typography are hand-rolled; Tremor provides chart primitives and some grid helpers.
  4. Dev bypass hardcoded: DEV_MODE = true in useAuth.ts — must be set to false before production.
  5. Session-based auth: JWT in sessionStorage (not localStorage) for security.

See also

See also