Skip to content

EES (Employee Engagement Survey) — Frontend Developer Documentation

Audience: Frontend developers only. Goal: A complete onboarding + reference guide so a new frontend developer can understand the project, navigate the code, and start contributing without a live walkthrough.

Ground rule: Everything here is derived from the actual source code at d:/Downloads/EES Documentation/EES-2026/EES-V2.0-Frontend. Where something cannot be determined from the code, it says "Unable to determine from the available code." Nothing is invented.

ℹ️ Scope note. This is a single React SPA — the client/admin-facing report portal for the Employee Engagement Survey product. It renders survey analytics (participation, benchmarks, demographic cuts, consolidated insights, line-manager effectiveness, NPS, AI-generated open-ended-feedback synthesis, an "Ask Your Data" chatbot) for a logged-in company user. It consumes a Laravel backend's Sanctum-authenticated JSON API (documented separately at EES - Report backend). The frontend package is named ees-ui-with-ai (package.json:2).


Table of Contents

# Section
1 Introduction
2 Project Structure
3 Frontend Architecture
4 Routing
5 Feature/Page Documentation
6 Pages
7 Components
8 State Management
9 API Layer
10 Forms & Validation
11 Authentication & Authorization
12 Shared Utilities
13 Styling System
14 Assets
15 Environment Configuration
16 Performance Optimizations
17 Error Handling
18 Development Workflow
19 Coding Standards
20 Debugging Guide
21 Common Pitfalls
22 Best Practices
23 Developer Onboarding Checklist
24 Appendix — Unable to determine from the available code

1. Introduction

Purpose of the frontend

This app is the reporting portal a consultancy's clients log into after their Employee Engagement Survey has closed. A single company user signs in and explores their results: overall participation and data-integrity, benchmark comparisons (Top Group / Industry / Benchmark / previous-year), demographic cuts, a consolidated "item detail" drill-down from Combine → Cluster → Driver → Dimension, Line Manager Effectiveness, Organisational NPS, an AI-written Executive Summary, and an AI-synthesised "Voice of the Employee" open-feedback view. It also offers a build-your-own AG Grid pivot table and an "Ask Your Data" chat assistant.

The page <title> is literally "EC Employee Engagement Survey" (index.html:6) — "EC" branding appears throughout (logo file neweclogo.png, EC fallback initial in the collapsed sidebar, Sidebar.tsx:154).

High-level overview

  • One SPA, one login, one company/survey scope per session (there is no company switcher or multi-tenant picker in the code — the logged-in user is scoped to a single survey returned at login, see §11).
  • All data is fetched from a single hardcoded backend host via RTK Query (src/services/baseApi.ts:28).
  • The UI is built almost entirely from bespoke Tailwind components — there is no third-party UI kit (no Ant Design, no MUI) except react-select (multi-filters), @headlessui/react (Listbox dropdowns), and AG Grid Enterprise for the two grid-heavy screens (Pivot Table, and grid usage inside comparison/statement views).
  • The app supports exporting sections of itself to PDF (html2canvas + html-to-image + jsPDF) and to Excel/CSV (AG Grid Enterprise export modules).

Technology stack

Concern Choice Version (package.json)
Language TypeScript ~6.0.2 (via typescript-eslint ^8.59.2)
Framework React ^19.2.6
Build tooling Vite ^8.0.12, @vitejs/plugin-react ^6.0.1
State Redux Toolkit + RTK Query @reduxjs/toolkit ^2.12.0, react-redux ^9.3.0
State persistence None (redux-persist is not used)
Routing React Router react-router-dom ^7.16.0 (v7, BrowserRouter)
Utility CSS Tailwind CSS v4 tailwindcss ^4.3.0, @tailwindcss/vite ^4.3.0
UI primitives Headless UI @headlessui/react ^2.2.10 (Listbox)
Data grid AG Grid Enterprise + React wrapper ag-grid-enterprise ^36.0.0, ag-grid-react ^36.0.0
Charts Recharts ^3.8.1
Select inputs react-select ^5.10.2
Alerts/dialogs SweetAlert2 ^11.26.25
PDF/image export jsPDF, html2canvas, html-to-image ^4.2.1, ^1.4.1, ^1.11.13
Icons lucide-react + react-icons ^1.38.0, ^5.6.0
HTTP RTK Query fetchBaseQuery (fetch-based) via @reduxjs/toolkit/query/react
Linting ESLint 10 (flat config) + typescript-eslint eslint.config.js
Git hooks None found

Design principles (as observed in the code)

  1. Page = data-fetch container + Layout wrapper. Nearly every page in src/pages/ follows the same shape: call one or two RTK Query hooks, show a full-screen CSS spinner while isLoading, then render a tree of presentational components inside <Layout>. See the standard page pattern in §6.
  2. One RTK Query "slice API" per backend feature, all injected into a single baseApi via injectEndpoints (src/services/*.ts), so there is one shared cache/reducer (api) rather than N separate reducers.
  3. Flat, feature-named components. All 95 files in src/components/ sit in one flat directory (no subfolders) — feature identity is carried entirely by filename (e.g. AdvocacyNetPromoterScores.tsx vs AdvocacyNetPromoterScoresForPDF.tsx), not by folder structure.
  4. data_type (eng vs avg) is a pervasive cross-cutting toggle. Almost every page reads useGetDataTypeQuery() and threads a dataType: "eng" | "avg" value into its main data query and into every child component that renders a score, so the same report can be viewed as "% engaged" or "1–5 average."
  5. AI content is first-class, not bolted on. Several API responses embed an ai_content_json object with narrative insight/action text per section (see ExecutiveSummary.tsx:356-403, OpenFeedbackAISynthesis.tsx), and pages pass insAndAction={...} props straight through to presentational components that render it inline next to the charts.
  6. Client-only session timeout. A 24-hour auto-logout timer is implemented purely in App.tsx (useEffect + setTimeout), independent of the backend token's actual expiry — see §11.
  7. Print/PDF is a parallel rendering concern, not an afterthought: many chart/section components ship in two versions — a normal one and a *ForPDF variant (e.g. AdvocacyNetPromoterScoresForPDF.tsx, CustomScatterChartForPDF.tsx, LeaderShipClusterSectionForPDF.tsx, OpenFeedbackCardForPDF.tsx, WhereToFocusDriverPrioritiesForPDF.tsx) because html2canvas/html-to-image cannot rasterize some interactive/animated chart output reliably, and because Tailwind v4's oklch() colors break canvas capture (see generatePDF.ts:54-64).

2. Project Structure

EES-V2.0-Frontend/
├── index.html                 # Vite entry HTML; <title> "EC Employee Engagement Survey"
├── vite.config.ts             # react() + tailwindcss() plugins only
├── eslint.config.js           # Flat ESLint config (js + typescript-eslint + react-hooks + react-refresh)
├── tsconfig.json / tsconfig.app.json / tsconfig.node.json
├── package.json                # name: "ees-ui-with-ai"
├── src/
│   ├── main.tsx                # ReactDOM entry: StrictMode + Provider(store) + App
│   ├── App.tsx                 # BrowserRouter, all <Route> definitions, 24h auto-logout timer
│   ├── App.css                 # (default CRA/Vite template leftover; largely unused)
│   ├── index.css               # Tailwind import, @theme design tokens, global/AG-Grid CSS overrides
│   ├── app/                    # Redux store wiring
│   │   ├── store.ts             # configureStore: baseApi + auth + preferences reducers
│   │   └── hooks.ts             # useAppDispatch / useAppSelector typed hooks
│   ├── config/
│   │   └── menuConfig.ts        # Sidebar navigation tree (menuItems[])
│   ├── features/                # Redux slices (hand-written UI/session state)
│   │   ├── auth/authSlice.ts     # user, survey, token — persisted to localStorage manually
│   │   └── preferences/preferencesSlice.ts  # dataType ("eng" | "avg") toggle
│   ├── services/                 # RTK Query API layer — one file per backend feature (§9)
│   ├── pages/                    # Route-level screens (§6)
│   ├── components/               # 95 flat, mostly presentational components (§7)
│   ├── types/                    # Shared TypeScript interfaces/types
│   ├── utils/                    # Pure helpers: PDF export, AI mock, misc helpers, mock user
│   └── assets/                   # Logo, hero/cluster images, Montserrat font files

Folder-by-folder (why each exists)

Folder Purpose Interacts with
src/app/ Redux store configuration and the two typed hooks every component should use instead of raw useSelector/useDispatch. services/baseApi.ts, features/*
src/config/ A single static file, menuConfig.ts, describing the sidebar's nested navigation tree (label, icon, path, roles, optional children). components/Sidebar.tsx
src/features/ Redux Toolkit slices for state that is not server data: the logged-in user/token (auth) and the eng/avg score-display preference (preferences). Despite the name, there is no further nesting — just two slice files. app/store.ts, most pages/components that read state.auth or state.preferences
src/services/ The entire API layer. One file per backend "page"/feature (overviewApi.ts, statementBmApi.ts, …), each calling baseApi.injectEndpoints. See §9. app/store.ts (via baseApi.reducerPath), types/type.ts
src/pages/ One file per route. Each page owns its own data-fetching and composes many components/* together. There is no pages/admin vs pages/client split — this app serves exactly one audience (the client/company user). components/, services/, features/
src/components/ All reusable and feature-specific UI, in one flat directory. Includes chart wrappers (Recharts), the AG-Grid pivot table, modals, the sidebar/topbar/layout shell, and numerous single-purpose report-section components. See §7. services/ (a few components fetch their own data, e.g. Layout.tsx, Sidebar.tsx), utils/, types/
src/types/ Shared TypeScript interfaces: the giant type.ts (API response shapes for Overview, Statement Benchmark, Consolidated Statement Ranking, Demographic Data Insight, Statement by Demographic, etc.), auth.ts (User), sidebar.ts (MenuItem, UserRole), and chatBlocks.ts (the "Ask Your Data" chat-block union type). Nearly everywhere
src/utils/ Pure/standalone helper modules: generatePDF.ts (the html2canvas/html-to-image → jsPDF pipeline), askAI.ts (a mocked chatbot backend — see §21), helper.ts (small numeric/CSS helpers), mockUser.ts (a hardcoded fake user used for sidebar role-filtering — see §21). components/Sidebar.tsx, pages/AskYourData.tsx, pages/ExecutiveSummary.tsx
src/assets/ The EC logo (neweclogo.png), two illustration/marketing images (hero.png, clusters.png), the Montserrat font family, and unused Vite/React template SVGs (react.svg, vite.svg). index.css (@font-face), Sidebar.tsx/Layout.tsx/Login.tsx (logo)

💡 No path aliases. Unlike many larger React codebases, this project uses plain relative imports everywhere (../services/overviewApi, ../../types/type) — there is no @components/@services alias configured in tsconfig.app.json or vite.config.ts. Keep this in mind when adding new files deep in the tree.


3. Frontend Architecture

Overall architecture

flowchart TB
    subgraph Browser
        subgraph "React SPA"
            IDX["main.tsx - StrictMode + Provider"]
            APP["App.tsx - BrowserRouter + routes - + 24h auto-logout timer"]
            RT["Routes - flat route list, no nesting"]
            PG["Pages src/pages"]
            CM["Components src/components"]
            UT["utils: generatePDF, askAI, helper"]
        end
        RTKQ["RTK Query: baseApi - + 19 injected endpoint files"]
        STORE[("Redux store - api + auth + preferences")]
        LS[("localStorage - user, survey, token, dataType, loginTime")]
    end
    API[("Laravel backend REST API - Sanctum bearer token")]

    IDX --> APP --> RT --> PG
    PG --> CM
    PG --> RTKQ
    CM --> RTKQ
    CM --> UT
    RTKQ --> STORE
    RTKQ -->|fetch + Bearer token| API
    STORE -.->|"auth.token read by prepareHeaders"| RTKQ
    APP --> LS
    STORE -.->|"auth slice initial state reads"| LS

Application lifecycle (from load to rendered page)

sequenceDiagram
    participant B as Browser
    participant M as main.tsx
    participant S as Redux store
    participant A as App.tsx
    participant PR as ProtectedRoute
    participant P as Page component
    participant RQ as RTK Query hook
    participant API as Backend

    B->>M: Load index.html -> src/main.tsx
    M->>S: createRoot dot render, Provider store plus App
    S->>S: authSlice initial state reads localStorage (user, survey, token)
    M->>A: Render App (BrowserRouter)
    A->>A: useEffect: check localStorage.loginTime vs 24h -> schedule/trigger logout
    A->>PR: Render matched Route wrapped in ProtectedRoute
    PR->>S: useSelector state auth token
    alt token present
        PR->>P: Render the page
        P->>RQ: useGetXQuery (e.g. useGetDataTypeQuery, useGetOverviewQuery)
        RQ->>API: fetch with Authorization Bearer token
        API-->>RQ: JSON res object or success object
        RQ-->>P: data / isLoading / error
        P->>B: Render Layout + Sidebar + Topbar + report sections
    else no token
        PR->>B: Navigate to /login, replace
    end

Component architecture

  • Container pages, presentational components. Every file in src/pages/ is a container: it calls RTK Query hooks, computes derived values (percentages, snapshot-card arrays, sort/filter state), and passes plain props down. Components in src/components/ are almost all presentational, taking data via props — with three notable exceptions that read Redux/services directly: Layout.tsx (fetches data_type), Sidebar.tsx (reads mockUser, dispatches logout), Topbar.tsx (fetches data_type, reads localStorage.user).
  • No colocation. Unlike a "page folder owns its subcomponents" pattern, every component lives in the single flat src/components/ directory regardless of which page(s) use it. Discoverability relies entirely on naming (e.g. everything related to Line Manager Effectiveness is prefixed/suffixed accordingly: ManagerAdvocacyLineManagerNPS.tsx, ManagerNPSByDepartment.tsx, WhereToFocusManagerBehaviourPriorities.tsx).
  • Two rendering targets per visual, in places. For charts/sections that must appear both on-screen and inside a PDF export, there are parallel X.tsx / XForPDF.tsx component pairs (see §1 design principle 7 and §17).

Design patterns used

Pattern Where
Provider pattern Provider store={store} in main.tsx:10-12
Guard component (route wrapper) ProtectedRoute.tsx wraps every authenticated <Route element> in App.tsx
RTK Query injectEndpoints Every file in src/services/ extends the single baseApi instead of declaring its own createApi — one shared cache/reducer.
Typed dispatch/selector hooks useAppDispatch / useAppSelector in app/hooks.ts (though many components still call the untyped useDispatch/useSelector from react-redux directly — see §21)
Slice pattern authSlice, preferencesSlice
Render-off-screen-then-capture ExecutiveSummary.tsx:243-278 mounts a hidden, fully separate React tree (createRoot + Provider + BrowserRouter) off-screen purely to render FullExecutiveSummaryPDF, waits 3s, screenshots it via generatePDF, then unmounts it.
Discriminated-union renderer BlockRenderer.tsx switches on ChatBlock["type"] to render each AI chat response block (text, comparison_bars, bullet_list, table, suppressed, quick_replies, stat) — new block types are added by extending the union in types/chatBlocks.ts and adding one case.

4. Routing

Library & mode

React Router v7 (react-router-dom ^7.16.0) using BrowserRouter (App.tsx:2,216) — i.e. real path-based URLs (/participation, /organisational-nps), not hash routing. This requires the hosting/deploy target to rewrite all paths to index.html (a typical SPA server rewrite rule); no such rewrite config (e.g. vercel.json, _redirects, nginx conf) was found in the repository — see §24.

Route configuration structure

Routing is not config-driven here (unlike a routeConfig.ts array pattern) — every route is a literal <Route path=... element=...> written directly inside AppContent() in App.tsx:44-186.

// App.tsx (structure, abbreviated)
function AppContent() {
  const location = useLocation();
  const isLoginPage = location.pathname === "/login";

  if (isLoginPage) {
    return (
      <Routes>
        <Route path="/login" element={<Login />} />
      </Routes>
    );
  }

  return (
    <Routes>
      <Route path="/" element={<ProtectedRoute><ExecutiveSummary /></ProtectedRoute>} />
      <Route path="/organisational-nps" element={<ProtectedRoute><OrganisationalNps /></ProtectedRoute>} />
      {/* ...14 more protected routes... */}
    </Routes>
  );
}

All routes (from App.tsx:44-186)

Path Page component Protected
/login Login No
/ ExecutiveSummary Yes
/organisational-nps OrganisationalNps Yes
/line-manager-effectiveness LIneManagerEffectiveness (sic — typo preserved from the source filename) Yes
/voice-of-the-Employee VoiceOfTheEmployee (mixed-case path segment, preserved as-is) Yes
/participation Participation Yes
/overview Overview Yes
/strength-gaps StrengthAndGaps Yes
/scores-by-cluster ScoreByCluster Yes
/all-questions AllQuestions Yes
/build-your-own-view BuildYourOwnView Yes
/pivot-table Pivot Yes
/demographic-cut/results-by-group DemographicDataInsight Yes
/demographic-cut/statement-by-demographic StatementByDemographic Yes
/demographic-cut/item-detail DemographicItemDetail Yes
/ask-your-data AskYourData Yes
/how-to-read-this-report HowToReadThisReport Yes
/methodology-validation MethodologyAndValidation Yes

Route guard logic (ProtectedRoute.tsx)

flowchart TB
    R(["Route requested"]) --> T{"state.auth.token - present?"}
    T -->|No| NAV["Navigate to /login, replace"]
    T -->|Yes| RENDER["Render children"]

The entire guard is:

// components/ProtectedRoute.tsx:11-20
const ProtectedRoute: React.FC<Props> = ({ children }) => {
  const token = useSelector((state: RootState) => state.auth.token);
  if (!token) {
    return <Navigate to="/login" replace />;
  }
  return children;
};

There is no permission/role check at the route level — ProtectedRoute only checks for a truthy token, nothing more granular. (Sidebar-level role filtering exists but is broken — see §21.)

Public vs protected routes

Type Guarded by Routes
Public none /login only
Protected ProtectedRoute (token presence) All 17 remaining routes

Fallback / 404 route

None found. There is no catch-all <Route path="*"> in App.tsx. Navigating to an undefined path inside AppContent()'s <Routes> renders nothing (React Router v7 simply matches no route and renders null). See §21 and §24.

Lazy-loaded routes

None. All 18 page components are imported eagerly at the top of App.tsx:7-24 via static import. There is no React.lazy/Suspense code-splitting anywhere in the codebase (confirmed by search). See §16.

Login-page layout branch

Note the isLoginPage special case at App.tsx:34-42: when the pathname is exactly /login, AppContent returns an entirely different <Routes> tree with only the login route, so the sidebar/topbar/footer chrome (which every other page renders via <Layout>) never mounts on the login screen. This is a manual branch, not a nested-layout route pattern.


5. Feature/Page Documentation

Each subsection below covers one report/feature area: what it shows, which service(s) and components it uses, and any business logic worth knowing.

5.1 Executive Summary (/, pages/ExecutiveSummary.tsx)

The landing page after login. An AI-narrated, scroll-tracked, 8-section executive summary: Overall Snapshot, Engagement, Clusters, Priorities, Advocacy, Risk, Feedback, Actions.

  • Data: useGetExecutiveSummaryQuery (services/executiveSummaryApi.tsGET na2/get-executive-summary), useGetDataTypeQuery, useChangeFirstLoginMutation.
  • Two viewing modes: a "guided" first-login mode (showInd, driven by apiDataType.first_login !== 1) that shows one section at a time with Previous/Next buttons and a GuidedReading progress component, and a "scroll" mode for returning users that renders all 8 sections stacked with an IntersectionObserver (ExecutiveSummary.tsx:161-193) driving a sticky ExSummaryScrollTab tab bar so the active tab tracks scroll position.
  • Exiting guided mode: handleFirstLogin (ExecutiveSummary.tsx:57-107) shows a SweetAlert2 confirm dialog, optimistically flips UI state, calls changeFirstLogin({ first_login: 1 }), shows a success/error SweetAlert2 toast, refetches both queries, then navigates to /.
  • PDF export: handleDownload (ExecutiveSummary.tsx:236-279) mounts an entirely separate off-screen React tree containing FullExecutiveSummaryPDF, waits 3 seconds for it to render, then calls generatePDF("exec-summary-pdf-section", "Executive Summary PDF") and tears the hidden tree down.
  • Components used: WelcomeCard, GuidedReading, ExSummaryScrollTab, SnapshotSection, OverallEngagementScores, LeaderShipClusterSection, WhereToFocusDriverPriorities, AdvocacyNetPromoterScores, EngagementRiskMatrix, OpenFeedbackAISynthesis, SuggestedActions, FullExecutiveSummaryPDF.

5.2 Participation (/participation, pages/Participation.tsx)

Summarizes who responded and how trustworthy the results are.

  • Data: useGetParticipationSummaryQuery (services/participationSummaryApi.tsna2/get-participation-summary).
  • Business logic: computes nonParticipants = totalInvites - participants and both percentages client-side (Participation.tsx:14-19); builds a 4-tile snapShotData array (Participation %, Accepted responses, Integrity score, Non-participants).
  • Components: WelcomeCard, SnapshotCard ×4, ResponseAndIntegrity, ParticipationByDemographic.

5.3 Line Manager Effectiveness (/line-manager-effectiveness, pages/LIneManagerEffectiveness.tsx)

Rates how effective managers are and whether teams would recommend them.

  • Data: useGetLineMngerEffQuery (services/lineMngerEffApi.tsGET na2/get-line-manager-effectiveness), plus useGetDataTypeQuery.
  • Business logic: lowestDim is computed client-side via Array.reduce over data.res.dimensions to find the dimension with the lowest yrs.avg (LIneManagerEffectiveness.tsx:21-45), used to populate the "Lowest dimension" snapshot tile. NPS gap-to-benchmark and gap-to-Top-Group are also computed inline.
  • Departmental drill-down: ManagerNPSByDepartment is driven by a local activeTab index into data.res.nps.by_demg[], with matching AI narrative pulled from ai_content_json.by_demg[demg${activeTab+1}].
  • Components: WelcomeCard, SnapshotCard ×4, TheIndex, ManagerAdvocacyLineManagerNPS, WhereToFocusManagerBehaviourPriorities, ManagerNPSByDepartment.

5.4 Organisational NPS (/organisational-nps, pages/OrganisationalNps.tsx)

Company-wide Net Promoter Score (would-you-recommend-us-as-an-employer).

  • Data: useGetOrganistionalNpsQuery (services/organisationalNPSApi.tsGET na2/get-organisational-nps).
  • Business logic: promoter/detractor percentages, gap-to-benchmark, and net-positive/negative/zero labeling are all computed client-side from data.res.nps.summary[0] (OrganisationalNps.tsx:14-82). Note data.res.nps.summary[1] is separately referenced for lmeNps passed into AdvocacySegments (OrganisationalNps.tsx:126) — i.e. index 1 of the same summary array is assumed to be the Line Manager NPS figure.
  • Demographic matrix tab: AdvocacySegments is driven by activeTab into data.res.nps.demg_nps_matrix[].
  • Components: WelcomeCard, SnapshotCard ×4, TheScore, WhatPromotersAndDetractorsRate, AdvocacySegments.

5.5 Voice of the Employee (/voice-of-the-Employee, pages/VoiceOfTheEmployee.tsx)

AI-synthesised open-ended feedback (the "Good / Bad / Ugly" themes).

  • Data: useGetVoiceOfEmpQuery (services/voiceOfTheEmpApi.tsGET na2/get-voice-of-the-employee).
  • Components: wraps OpenFeedbackAISynthesis, which itself composes ClusterMiniCard (Insight/Action) and OpenFeedbackCard/OpenFeedbackCardForPDF for the three themed columns.

5.6 Overview (/overview, pages/Overview.tsx)

The first "Explore the Data" screen — overall + per-cluster scores, impact drivers, NPS, NPS matrix, and top/bottom 5 statements, all benchmarked.

  • Data: useGetOverviewQuery({ data_type }) (services/overviewApi.tsPOST overView), gated by skip: !apiDataType so it never fires before the data-type preference is known.
  • Components: OverallEngagementScoresForOverview, ClusterScoresForOverView, ImpactSection, NetPromoterScore, NPSMatrixForOverview, Top5andbottom5Statements.

5.7 Strengths & Gaps (/strength-gaps, pages/StrengthAndGaps.tsx)

Highlights statistically notable positive and negative gaps versus benchmark/Top Group/industry/prior year, plus an "On the Fence" (neutral-response) view.

  • Data: Uses the statementBmApi (services/statementBmApi.tsPOST statement-benchmark) which returns StatementGaps (top_p/top_n/ind_p/ind_n/bmk_p/bmk_n/pv1_p/pv1_n — positive/negative gap arrays per comparison group) and OnTheFence.
  • Components: StatementGaps, StatementGapsCard, OnTheFence.

5.8 Scores by Cluster (/scores-by-cluster, pages/ScoreByCluster.tsx)

The largest single page file (773 lines). Renders the full Cluster → Driver → Dimension score hierarchy with the Consolidated Statement Ranking API.

  • Data: csStatementRankingApi (services/csStatementRankingApi.ts) exposes two related endpoints: getCsStatementRanking (consolidated/statement-ranking) and getStatementRanking (statement-ranking).
  • Components: DimStatementRanking, DimStatementRankingTable, DimensionTable, ClusterCard.

5.9 All Questions / Item Detail (/all-questions, pages/AllQuestions.tsx)

A dense, filterable, sortable table of every statement/dimension score at every rollup level (Overall → Cluster → Component/Driver → Dimension), each with up to 5 comparison-group columns × 8 metric columns.

  • Data: useGetCsItemDetailQuery({ data_type }) (services/csItemDetailApi.tsPOST item-detail).
  • Client-side data reshaping: transformApiData (AllQuestions.tsx:54-177) manually rebuilds a nested combine → clusters[] → components[] → dimensions[] tree from four flat arrays (combine, cluster, driver, dimension) returned by the API, using Maps keyed by clid/drid/dmid — this is real business logic living in the page, not the service layer.
  • Rendering: delegates to ItemDetailTable (components/ItemDetailTable.tsx, ~1200 lines), which independently implements column/row filtering (ItemDetailModal), per-column sorting by clicking headers, tooltip descriptions per metric (Avg./Eng%/Dis%/SA/A/N/D/SD), and a hand-rolled CSV export (exportFilteredData, ItemDetailTable.tsx:770-819) that builds CSV text manually rather than using AG Grid's export.
  • Group-label mapping: every group column header (user.abbr, "Top Group", "Industry", "Benchmark", user.abbr + "'25") is mapped to its backend key (yrs/top/ind/bmk/pv1) via inline groupMap/newGroupMap objects — duplicated in at least 6 places inside ItemDetailTable.tsx.

5.10 Build Your Own View → Comparisons (/build-your-own-view, pages/BuildYourOwnView.tsx)

A thin page that renders ComparisonReport, which lets the user pick up to 3 demographics (X/Y/Z axes) and view a cross-tab of scores.

  • Data: comparisonApi (services/comparisonApi.ts) — getComparison (demographic/comparison, tagged Comparison) and changeComparison (demographic/change-comparison, a mutation that invalidatesTags: ['Comparison'] so the comparison view refetches automatically after the axis selection changes). This is the only service in the codebase using RTK Query cache tags.

5.11 Build Your Own View → Pivot Table (/pivot-table, pages/Pivot.tsx)

A full AG Grid Enterprise pivot table over individual response-level rows.

  • Data: useGetPivotQuery (services/pivotApi.tsPOST demographic/pivot-table), returning { page_name, pivotData: PivotData[], demographics: Demographics[] } where each PivotData row carries Score, Dimension/Driver/Cluster/Combine and many demographic breakdown fields (BU, Designation, Grade, Department, Location, Unit, Time Period, Gender, Religion, Age).
  • Rendering: components/PivotTable.tsx (PivotGrid) — see §7.2 for full AG Grid detail. Key business rule: minCountAvg (PivotTable.tsx:68-85) suppresses the average and returns null whenever a pivoted group has fewer than 5 underlying values, to protect respondent anonymity — the same "n≥5" suppression rule referenced throughout the app (see 5.13 below).

5.12 Demographic Cut → Results by Group (/demographic-cut/results-by-group, pages/DemographicDataInsight.tsx)

Lets the user build an arbitrary demographic "cut" (e.g. Male + <1yr tenure) via MultiFilter dropdowns per demographic, then view that cut's overall/cluster scores, NPS, and two Recharts scatter charts showing which drivers most impact Line Manager NPS / Commitment.

  • Data: dGDIApi (services/dGDIApi.ts) — getDGDI (demographic/data-insight, a query keyed by data_type + two Listbox-selected demographic IDs cm_dg_id/lm_dg_id) and getChnageDG as a lazy query (useLazyGetChnageDGQuery) triggered by the "CHANGE DEMOGRAPHIC SELECTION" button (DemographicDataInsight.tsx:43-61), which posts the full filter_demographic: Record<demographicId, optionId[]> map, then calls refetch() on the main query.
  • Anonymity threshold UI: if the resulting cut has no overall_score/cluster_score, the page shows an inline warning that the minimum-5-respondent threshold was not met (DemographicDataInsight.tsx:216-224).
  • Components: MultiFilter (×N, one per demographic), Button, ProgressBarRow, SimpleTextCard, NetPromoterScore, DGDIClusterScores, MyScatterChart (×2, via HeadlessUI Listbox driver pickers), Top5andbottom5Statements.

5.13 Demographic Cut → Statements by Demographic (/demographic-cut/statement-by-demographic, pages/StatementByDemographic.tsx)

The largest statement-level demographic comparison screen (526 lines) — every dimension's score broken out per demographic group side-by-side.

  • Data: statementByDemographicApi (services/statementByDemographicApi.tsPOST demographic/ranking).

5.14 Demographic Cut → Item Detail (/demographic-cut/item-detail, pages/DemographicItemDetail.tsx)

Same hierarchical item-detail concept as 5.9 (All Questions) but scoped to demographic cuts, reusing much of the same table-rendering approach.

  • Data: dcItemDetailApi (services/dcItemDetailApi.tsPOST demographic/item-detail).

5.15 Ask Your Data (/ask-your-data, pages/AskYourData.tsx)

A chat-style Q&A assistant over the survey results.

  • Data: Not wired to the real backend. Calls askAI(question) from utils/askAI.ts, which is a fully client-side mock: it pattern-matches on substrings in the question ("hotel", "career growth"/"finance", "weakest driver"/"department", "nps") and returns one of four hardcoded BotResponsePayload fixtures after an artificial 600ms setTimeout delay, falling back to a generic "couldn't find a match" response otherwise (askAI.ts:24-111). The file's own doc comment (askAI.ts:3-23) explicitly documents how to swap it for a real fetch("/api/ask-your-data") call. See §21.
  • Rendering: BlockRenderer draws whatever mix of ChatBlocks (text, comparison_bars, bullet_list, table, suppressed, quick_replies, stat) the response contains — this renderer itself is real, generic, and reusable; only its data source is mocked.
  • UX details: chat auto-scrolls to bottom on new messages (AskYourData.tsx:42-44), suggested-question chips in a side rail, an anonymity disclaimer ("results for groups under 5 respondents are hidden"), and an "AI generated, double-check" disclaimer under the input box.

5.16 How to Read This Report (/how-to-read-this-report) and Methodology & Validation (/methodology-validation)

Static, mostly-copy informational pages composed from small presentational components (WhatThisReportIs, HowTheReportIsOrganised, TheComparisonGroups, ReadingTheNumbers, HowTheSurveyWasRun, WhyTheResultsCanBeTrusted). No API calls — MethodologyAndValidation.tsx is only 31 lines and purely compositional.

5.17 Login (/login, pages/Login.tsx)

See §11 for the full flow. Notable UX: a useEffect that immediately redirects to / if a token already exists in localStorage (Login.tsx:20-25), a password show/hide toggle, and errors surfaced from err.data.message with a generic fallback.


6. Pages

Pages live flat in src/pages/ (no subfolders). Every page is a default-exported function component named after its file.

The standard page pattern

flowchart TB
    A["Page mounts"] --> B["useGetDataTypeQuery for eng/avg toggle"]
    B --> C["useGetXQuery for the page's main data"]
    C --> D{"isLoading?"}
    D -->|Yes| E["Layout wrapping a centered .loader spinner div"]
    D -->|No| F["Compute derived values: percentages, snapshot tiles, sort/filter state"]
    F --> G["Layout wraps WelcomeCard + N presentational report-section components"]
    G -->|props| H["Each section reads its own slice of the response - plus insAndAction from ai_content_json"]

Concretely, almost every page (Overview.tsx, Participation.tsx, OrganisationalNps.tsx, LIneManagerEffectiveness.tsx, DemographicDataInsight.tsx, AllQuestions.tsx, Pivot.tsx, …) repeats this exact isLoading guard:

if (isLoading) {
  return (
    <Layout>
      <div className="flex justify-center items-center h-[90vh]">
        <div className="loader"></div>
      </div>
    </Layout>
  );
}

.loader is a pure-CSS conic-gradient spinner defined once in index.css:57-74 — there is no shared <Spinner>/<LoadingScreen> component; the markup above is copy-pasted into each page.

Page inventory

Page file Route Primary hook(s) Notes
ExecutiveSummary.tsx / useGetExecutiveSummaryQuery, useGetDataTypeQuery, useChangeFirstLoginMutation Guided vs scroll mode, PDF export, IntersectionObserver
Participation.tsx /participation useGetParticipationSummaryQuery 4-tile snapshot + breakdown
LIneManagerEffectiveness.tsx /line-manager-effectiveness useGetLineMngerEffQuery, useGetDataTypeQuery Client-side reduce for lowest dimension
OrganisationalNps.tsx /organisational-nps useGetOrganistionalNpsQuery Promoter/detractor math client-side
VoiceOfTheEmployee.tsx /voice-of-the-Employee useGetVoiceOfEmpQuery AI Good/Bad/Ugly synthesis
Overview.tsx /overview useGetOverviewQuery, useGetDataTypeQuery skip until data-type known
StrengthAndGaps.tsx /strength-gaps statement-benchmark query Gaps + On-the-Fence
ScoreByCluster.tsx /scores-by-cluster consolidated statement ranking Largest page (773 lines)
AllQuestions.tsx /all-questions useGetCsItemDetailQuery Manual flat→tree reshape
BuildYourOwnView.tsx /build-your-own-view (delegates to ComparisonReport) Thin wrapper
Pivot.tsx /pivot-table useGetPivotQuery AG Grid Enterprise pivot
DemographicDataInsight.tsx /demographic-cut/results-by-group dGDIApi (query + lazy query) Build-a-cut UI
StatementByDemographic.tsx /demographic-cut/statement-by-demographic statementByDemographicApi 526 lines
DemographicItemDetail.tsx /demographic-cut/item-detail dcItemDetailApi 374 lines
AskYourData.tsx /ask-your-data askAI() (mocked, not RTK Query) Chat UI
HowToReadThisReport.tsx /how-to-read-this-report none Static content
MethodologyAndValidation.tsx /methodology-validation none Static content, 31 lines
Login.tsx /login useLoginMutation See §11

7. Components

src/components/ holds 95 files, all flat, all .tsx. There is no generic/feature-specific folder split in the filesystem, so the categorization below is inferred from actual reuse.

7.1 Layout shell (Generic, used on every protected page)

Layout.tsx

The page chrome: a responsive flex layout with a collapsible desktop Sidebar, an overlay mobile Sidebar, Topbar, a <main> content slot (children), and a "Powered by [logo]" footer. - Props: children: React.ReactNode only. - State: sidebarExpanded (desktop collapse toggle), mobileSidebarOpen (mobile drawer toggle) — both local useState, not Redux. - Own data fetch: useGetDataTypeQuery + useChangeDataTypeMutation, passed down into ToggleTopBar (both the mobile inline toggle at Layout.tsx:60-66 and the one inside Topbar).

Sidebar.tsx

Renders the navigation tree from config/menuConfig.ts, recursively (renderMenuItem, self-calling for children), with expand/collapse per parent item (openMenus state) and a maxHeight CSS transition for the collapse animation (Sidebar.tsx:79-88). Special-cases a "Download PDF" menu label to call generatePDF instead of navigating. Logout button dispatches logout() and baseApi.util.resetApiState() before navigating to /login (Sidebar.tsx:171-177) — this is the only place the RTK Query cache is explicitly cleared. See §21 for the mockUser issue here.

Topbar.tsx

Hamburger buttons (desktop + mobile variants), a route→title lookup table (getPageTitle, Topbar.tsx:20-54) that maps every known pathname to a human title (falls back to "Introduction"), the desktop ToggleTopBar, and a user-initials avatar badge computed from localStorage.user.name.

ToggleTopBar.tsx

The eng%/avg toggle control shared between Layout (mobile) and Topbar (desktop); calls the passed-in changeDataType mutation and refetchDataType.

7.2 The AG Grid pivot table (components/PivotTable.tsx)

The single most complex component in the app (PivotGrid, 523 lines). Registers AG Grid Enterprise modules explicitly at module load time (PivotTable.tsx:24-33): ClientSideRowModelModule, RowGroupingModule, PivotModule, SideBarModule, ColumnsToolPanelModule, FiltersToolPanelModule, ExcelExportModule, CsvExportModule.

Key behaviors: - Custom aggregation with anonymity suppression: minCountAvg (PivotTable.tsx:68-85) is passed as aggFunc on the Score value column — it returns null (blank cell) whenever fewer than 5 numeric values are aggregated into a pivot cell. - Excel export with custom cell/header formatting: exportToExcel (PivotTable.tsx:87-174) builds an AG Grid Enterprise exportDataAsExcel params object with a processCellCallback that rounds numeric/pivot-aggregate cells to 2 decimals and a processHeaderCallback/excelStyles for bold headers and number formatting. - processPivotResultColDef (PivotTable.tsx:472-496) relabels AG Grid's auto-generated pivot column headers (which are normally a raw pivot-key string) into human labels like "Dim: <name>", "Driver: <name>", "Cluster: <name>", "Combine: <name>" based on pivotKeys.length. - onGridReady (PivotTable.tsx:387-418) programmatically enables pivot mode, adds Dimension as the default pivot column and Score as the default value column via a setTimeout(..., 500) — a timing-based workaround rather than an AG Grid state/event API, since the grid API needs a tick to settle after columnDefs mount. - Theming: uses AG Grid's newer theme={themeMaterial} object API (imported from ag-grid-community) layered with an extensive custom CSS class .custom-ag-grid defined in index.css:100-291 (custom header colors, hover row highlight in the app's teal brand color, pivot/group header gradients, scrollbar styling) — i.e. AG Grid Material theme is used only as a base, almost everything visual is overridden by hand-written CSS. - ⚠️ import "ag-grid-community/styles/..." and ModuleRegistry/ClientSideRowModelModule come from the ag-grid-community package, which is not listed in package.json dependencies (only ag-grid-enterprise and ag-grid-react are) — it works today only because ag-grid-enterprise pulls in ag-grid-community transitively. See §21.

7.3 Chart components (Recharts-based)

Component Chart type Notes
DonutChart.tsx Recharts PieChart/Pie Custom foreignObject-based data labels (percent badges) positioned trigonometrically around the ring (DonutChart.tsx:43-80); custom colored tooltip.
MyScatterChart.tsx Recharts scatter Drives the "impact of drivers on X" charts in Demographic Data Insight.
CustomScatterChart.tsx / CustomScatterChartForPDF.tsx Recharts scatter On-screen vs PDF-safe variants.
NPSMatrix.tsx / NPSMatrixForOverview.tsx Custom grid/heatmap-style matrix 3×3 engagement-risk-style matrix.
DivergingBarChart.tsx Recharts bar Positive/negative diverging bars (used for gap views).
PieChartWithStyledOuterLabels.tsx Recharts pie Outer-label variant of the donut pattern.
EngagementRiskMatrix.tsx Composite (matrix + narrative) Executive Summary "Risk" section.
DottedCirclechart.tsx Custom SVG Non-Recharts circular progress-style visualization.

7.4 Modals

Component Notes
ItemDetailModal.tsx The filter panel for ItemDetailTable — toggles which comparison groups, metric columns, clusters, components are shown.
Ad-hoc SweetAlert2 dialogs Not components — Swal.fire({...}) is called inline in page code (e.g. ExecutiveSummary.tsx:59-67, 80-85, 100-105) for confirm/success/error dialogs rather than a wrapped <Modal> component.

There is no generic reusable <Modal> component in the codebase — every modal-like UI is either a single-purpose component (ItemDetailModal) or an imperative SweetAlert2 call.

7.5 Form / input primitives

Component Base Notes
MultiFilter.tsx react-select (isMulti) Generic labeled multi-select used throughout Demographic Cut screens; typed OptionType = { value, label }.
Button.tsx Plain <button> Generic action button (title, navigateHandler) used for the "CHANGE DEMOGRAPHIC SELECTION" action etc.
Toggle.tsx Plain <button>/checkbox styling Boolean toggle switch.
ToggleTopBar.tsx Composed of two buttons The eng%/avg data-type switch.
CustomTabs.tsx Plain <button> group Generic pill-style tab switcher (data, activeTab, setActiveTab — all typed any), reused for demographic-tab UIs (Line Manager NPS by department, Advocacy Segments).
StepDots.tsx Plain <div> dots Step indicator, likely for the guided-reading flow.
Native <input> (Login) Plain HTML Login.tsx does not use any shared input component — email/password fields are raw <input> elements styled with Tailwind classes directly in the page.

7.6 Feature-specific report-section components (not for reuse)

The remaining ~70 components are single-purpose sections tied to one or two specific pages, following consistent naming by feature:

  • Executive Summary: WelcomeCard, GuidedReading, ExSummaryScrollTab, SnapshotSection, OverallEngagementScores(ForOverview), LeaderShipClusterSection(ForPDF), WhereToFocusDriverPriorities(ForPDF), AdvocacyNetPromoterScores(ForPDF), SuggestedActions, FullExecutiveSummaryPDF, TitlePage.
  • Participation: SnapshotCard, ResponseAndIntegrity, ParticipantProgressBar, ParticipationByDemographic.
  • Line Manager Effectiveness: TheIndex, ManagerAdvocacyLineManagerNPS, WhereToFocusManagerBehaviourPriorities, ManagerNPSByDepartment.
  • Organisational NPS: TheScore, WhatPromotersAndDetractorsRate, AdvocacySegments, AdvocacySegmentsChart, AdvocacySegmentsMiniCard, NetPromoterScoreCard.
  • Open feedback / AI synthesis: OpenFeedbackAISynthesis, OpenFeedbackCard(ForPDF), SentimentBreakdown, SentimentRow, ThemesAndSentiment, ExploreTheComments, CommentsList.
  • Cluster/driver/dimension scoring: ClusterCard, ClusterMiniCard, ClusterScoresForOverView, DGDIClusterScores, DimensionTable, DimStatementRanking(Table), DriverPrioritiesCard, DriversTable, StatementGaps(Card), OnTheFence, Top5andbottom5Statements.
  • Demographic cut: DemographicProgressBarRow, ProgressBarRow, ProgressBarForExecutiveSummary, ProgressBar.
  • Item detail: ItemDetailTable, ItemDetailModal.
  • "How to read" content: HowTheReportIsOrganised, HowTheSurveyWasRun, WhatThisReportIs, TheComparisonGroups, ReadingTheNumbers, WhyTheResultsCanBeTrusted, TheEngagementModel, GuidedReading.
  • Chat: BlockRenderer (see §5.15).
  • Misc: CircularProgress, ImpactSection, ImpactEngagementChart, ScoreImpact, OverViewMatrix(ForOverview), FiveStatement, SimpleTextCard.

Type-safety status

  • Typed props (interface/inline type): MultiFilter, ProtectedRoute, Layout, Sidebar, Topbar, ItemDetailTable, PivotGrid (PivotTable.tsx), BlockRenderer.
  • Loosely/untyped (any-typed props): CustomTabs (data, activeTab, setActiveTab all any), DonutChart (whole props object any), and a large share of the ~70 feature-specific section components accept data/insAndAction/overview props typed as any rather than dedicated interfaces — a direct consequence of many services/*.ts endpoints themselves being typed builder.query<any, ...> (see §9).

8. State Management

Approach

Redux Toolkit, with a much smaller footprint than the API surface suggests:

  1. Server state → a single RTK Query baseApi (reducerPath: "api"), with every feature's endpoints merged in via injectEndpoints rather than one createApi per domain.
  2. Client/UI state → exactly two hand-written slices: auth and preferences.

No redux-persist. Despite auth needing to survive reloads, persistence is done manually: authSlice's initialState reads localStorage directly (authSlice.ts:12-14), and setCredentials/logout write/clear localStorage as a side effect inside the reducer body (authSlice.ts:29-33, 40-44) — this works but means the slice's reducers are not pure with respect to the store alone (they also mutate window.localStorage).

Store structure (app/store.ts)

flowchart TB
    subgraph "Redux Store (configureStore)"
        subgraph "RTK Query (server state, cached, one reducer)"
            API["api: baseApi.reducer - 19 injected endpoint files"]:::api
        end
        subgraph "Hand-written slices (UI/session state)"
            AUTH["auth - user, survey, token - mirrored to localStorage"]:::slice
            PREF["preferences - dataType: 'eng' | 'avg' - mirrored to localStorage"]:::slice
        end
    end
    classDef api fill:#e3f2fd,stroke:#1565c0;
    classDef slice fill:#f3e5f5,stroke:#6a1b9a;
// app/store.ts:6-14
export const store = configureStore({
  reducer: {
    [baseApi.reducerPath]: baseApi.reducer,
    auth: authReducer,
    preferences: preferencesReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(baseApi.middleware),
});

Data flow

sequenceDiagram
    participant C as Component/Page
    participant H as RTK Query hook
    participant BQ as baseQueryWithAuth
    participant ST as Redux store
    participant API as Backend

    C->>H: useGetOverviewQuery({ data_type })
    H->>ST: check api cache for this query key
    alt cache miss / refetch requested
        H->>BQ: fetchBaseQuery("url, method, body")
        BQ->>ST: prepareHeaders reads state.auth.token
        BQ->>API: fetch with Authorization Bearer token
        API-->>BQ: JSON response
        alt result.error.status === 401
            BQ->>ST: dispatch(logout())
            BQ->>C: window.location.href = "/login"
        else success
            BQ-->>H: { data }
            H->>ST: cache result under api reducer
        end
    end
    ST-->>C: data / isLoading / isFetching / error (re-render)

Global vs local state

State kind Where Example
Global — server (cached) baseApi (RTK Query) Overview data, participation summary, pivot rows, executive summary
Global — session auth slice + localStorage mirror user, survey, token
Global — preference preferences slice + localStorage mirror dataType ("eng" vs "avg") — though most pages re-fetch this from the API (useGetDataTypeQuery) rather than reading the Redux preferences slice; see §21
Local useState in a page/component Active tab index, filter selections, modal open/close, chat messages, sidebar collapse

The auth slice

// features/auth/authSlice.ts
interface AuthState {
  user: LoginResponse["user"] | null;     // { id, name, email, role }
  survey: LoginResponse["survey"] | null; // { id, name, invites, data_type, top, ind, bmk, pv1, ... }
  token: string | null;
}
  • setCredentials (dispatched from Login.tsx:33 after a successful login mutation) stores user, survey, token in both Redux and localStorage (user, survey, token, dataType, loginTime keys), where loginTime is Date.now() — the seed for the 24h auto-logout timer (§11).
  • logout clears all five of those localStorage keys and resets the three state fields to null.
  • The token in this slice is read by prepareHeaders in baseApi.ts:31 to attach the Authorization header to every RTK Query request, and by ProtectedRoute.tsx:12 to decide whether to render a page or redirect.

The preferences slice

// features/preferences/preferencesSlice.ts
interface PreferencesState {
  dataType: 'eng' | 'avg';
}

Seeded from localStorage.getItem('dataType'). setDataType mirrors to localStorage. In practice, this slice appears under-used: most pages fetch the current dataType via useGetDataTypeQuery() (a server round-trip) rather than reading state.preferences.dataType, and the toggle mutation useChangeDataTypeMutation updates the server, not this slice — setDataType/preferencesSlice is not dispatched from any file found in src/pages/ or src/components/ (confirmed by search). See §21.

Best practices (this codebase)

  • Use useAppSelector/useAppDispatch (app/hooks.ts) for typed access — though be aware several existing files (ProtectedRoute.tsx, Login.tsx, Sidebar.tsx) still import the raw useSelector/useDispatch from react-redux directly.
  • Add new server data by injecting endpoints into the existing baseApi (see §9) rather than creating a new createApi instance.
  • Keep ephemeral UI state (activeTab, filters, modal visibility) local with useState; only promote to a slice if genuinely shared across routes.

9. API Layer

Architecture

flowchart TB
    C["Page / Component"] -->|useXQuery / useXMutation| SVC["injectEndpoints on baseApi<br/>(19 files in src/services/)"]
    SVC -->|baseQuery| BQA["baseQueryWithAuth - services/baseApi.ts"]
    BQA -->|fetchBaseQuery| API[("Backend REST API - hardcoded host, /api/ prefix")]
    BQA -->|prepareHeaders| TOK["state.auth.token"]
    BQA -->|"result.error.status === 401"| LOGOUT["dispatch logout + redirect /login"]

The base API (src/services/baseApi.ts)

// services/baseApi.ts:27-64 (abridged)
const baseQuery = fetchBaseQuery({
  baseUrl: "https://ec-ees-rpt2-26.engagesurvey.biz/api/",
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).auth.token;
    if (token) headers.set("Authorization", `Bearer ${token}`);
    return headers;
  },
});

const baseQueryWithAuth = async (args, api, extraOptions) => {
  const result = await baseQuery(args, api, extraOptions);
  if (result.error?.status === 401) {
    api.dispatch(logout());
    window.location.href = "/login";
  }
  return result;
};

export const baseApi = createApi({
  reducerPath: "api",
  baseQuery: baseQueryWithAuth,
  tagTypes: ["Comparison"],
  endpoints: () => ({}),
});

Key facts: - Base URL is hardcoded to https://ec-ees-rpt2-26.engagesurvey.biz/api/ directly in source — there is no .env/import.meta.env usage anywhere in the codebase (confirmed by search). A commented-out local override (http://127.0.0.1:8000/api/) sits directly above it (baseApi.ts:8, 29) — switching environments today means editing and re-committing this file. See §15 and §21. - Auth: every request gets Authorization: Bearer <token> automatically via prepareHeaders, matching a Laravel Sanctum-style bearer-token API. - 401 handling is centralized: any query/mutation that gets back a 401 triggers logout() and a hard window.location.href redirect — this is the only automatic session-invalidation the frontend performs against real server responses (compare with the purely client-side 24h timer in App.tsx, §11). - No token refresh. There is no refresh-token flow, no retry-after-refresh logic — a 401 is terminal (immediate logout). - tagTypes: ["Comparison"] is declared globally on baseApi, but only comparisonApi.ts actually uses providesTags/invalidatesTags with it (§5.10). No other service uses cache tags — every other query either never invalidates automatically or relies on manual refetch() calls (e.g. DemographicDataInsight.tsx:59, ExecutiveSummary.tsx:88-89). - There is a large commented-out earlier version of this same file still sitting at the top (baseApi.ts:1-19) showing an evolution from a bare fetchBaseQuery to the current baseQueryWithAuth wrapper — worth cleaning up.

Service layer (src/services/) — one file per backend feature

All 19 service files follow the same injectEndpoints pattern:

// services/overviewApi.ts (representative example)
import { baseApi } from "./baseApi";
import type { OverView } from "../types/type";

export const overviewApi = baseApi.injectEndpoints({
  endpoints: (builder) => ({
    getOverview: builder.query<OverView, { data_type: string }>({
      query: () => ({ url: "overView", method: "POST" }),
    }),
  }),
});

export const { useGetOverviewQuery } = overviewApi;
Service file Endpoint(s) HTTP Backend path Notes
authApi.ts login POST login Untyped-error mutation, typed LoginRequest/LoginResponse
userApi.ts getDataType POST get-datatype Returns { data_type, first_login }
changeDatatypeApi.ts changeDataType, changeFirstLogin POST change-data-type, change-first-login Both fully typed mutations
overviewApi.ts getOverview POST overView Typed via types/type.ts OverView
executiveSummaryApi.ts getExecutiveSummary GET (default) na2/get-executive-summary query<any, void> — untyped
participationSummaryApi.ts getParticipationSummary GET (default) na2/get-participation-summary query<any, void>
lineMngerEffApi.ts getLineMngerEff GET na2/get-line-manager-effectiveness query<any, void>
organisationalNPSApi.ts getOrganistionalNps GET na2/get-organisational-nps query<any, void>
voiceOfTheEmpApi.ts getVoiceOfEmp GET (default) na2/get-voice-of-the-employee query<any, void>
statementBmApi.ts getStatementBm POST statement-benchmark Typed StatementBMResponse
csStatementRankingApi.ts getCsStatementRanking, getStatementRanking POST consolidated/statement-ranking, statement-ranking Two related endpoints, one file
csItemDetailApi.ts getCsItemDetail POST item-detail Typed, large response shape
dcItemDetailApi.ts getDcItemDetail POST demographic/item-detail Typed, demographic-scoped variant
demographicApi.ts getDemographicScores POST demographic-scores Response typed as { success, data: any } — placeholder-typed
dGDIApi.ts getDGDI (query), getChnageDG (lazy query) POST demographic/data-insight, demographic/change-demographic Notable: uses a lazy query as a mutation-like trigger (useLazyGetChnageDGQuery) instead of builder.mutation
statementByDemographicApi.ts getStatementByDemographic POST demographic/ranking Typed StatementByDemographic
pivotApi.ts getPivot POST demographic/pivot-table Typed MainResponse
comparisonApi.ts getComparison, changeComparison POST demographic/comparison, demographic/change-comparison Only service using providesTags/invalidatesTags

Request/response conventions observed

  • Most POST endpoints send filter parameters as query-string params (e.g. params: { data_type }) even on POST requests, rather than a JSON body — see statementBmApi.ts:8-10, comparisonApi.ts:105-109. A few endpoints (demographicApi.ts, dGDIApi.ts's getChnageDG) do send a JSON body.
  • Several na2/* endpoints (Executive Summary, Participation Summary, LME, Organisational NPS, Voice of the Employee) wrap their payload in a top-level res key (observed from page usage, e.g. data?.res?.overall, data?.res?.ai_content_json), while others (Overview, Statement Benchmark, Item Detail) return fields directly at the top level (overviewData?.cluster_score). There is no shared response-envelope type or unwrapping helper — each page manually reaches into whichever shape its endpoint happens to return.
  • No global loading/error interceptor beyond the 401 case — every page independently destructures isLoading/error from its hook and renders its own spinner markup.

Adding a new API call (recipe)

  1. Create src/services/myThingApi.ts:
    import { baseApi } from "./baseApi";
    export const myThingApi = baseApi.injectEndpoints({
      endpoints: (builder) => ({
        getMyThing: builder.query<MyThingResponse, { data_type: string }>({
          query: ({ data_type }) => ({ url: "my-thing", method: "POST", params: { data_type } }),
        }),
      }),
    });
    export const { useGetMyThingQuery } = myThingApi;
    
  2. Define MyThingResponse in src/types/type.ts (or inline in the service file, matching existing convention).
  3. Import the generated hook directly in your page — no registration step is needed in app/store.ts because all services share the single already-registered baseApi reducer.

10. Forms & Validation

There is no form library (no Formik, no React Hook Form, no Yup/Zod) anywhere in package.json or the source. All forms are hand-rolled controlled inputs with useState.

Login form (pages/Login.tsx)

The only true "form" in the app (email + password + submit): - Controlled via three useState fields (email, password, errorMessage) plus a showPassword visibility toggle. - Validation: relies entirely on native HTML required attributes and type="email" (Login.tsx:69-86) — there is no client-side format/length validation logic. - Submission: handleSubmit calls login({ email, password }).unwrap(), dispatches setCredentials on success, navigates to /; on failure, reads err?.data?.message with a fallback string ("Invalid Credentials Please try again.") into errorMessage, rendered as a red banner above the form. - Loading state: the submit button is disabled={isLoading} and swaps its label to "LOGGING IN...".

Filter "forms" elsewhere

Most other user-input surfaces in the app are selection UIs, not submitted forms: MultiFilter (react-select multi-selects for demographic cuts), Listbox dropdowns (HeadlessUI, for driver/comparison pickers), ItemDetailModal (checkbox filter panel). These update local component state immediately on change and typically require an explicit action button (e.g. "CHANGE DEMOGRAPHIC SELECTION", DemographicDataInsight.tsx:175-178) to trigger a mutation/refetch, rather than being validated/submitted as a traditional form.

User feedback

  • Success/error/confirm dialogs: SweetAlert2 (Swal.fire({...})), used directly inside page logic (ExecutiveSummary.tsx) — not wrapped in a shared helper component.
  • Inline errors: plain conditional <div> banners (Login page) — there is no shared <FormError>/<FieldError> component.
  • Loading: the shared full-page .loader CSS spinner (see §6), plus per-button disabled + text-swap patterns (Login's "LOGGING IN...", DemographicDataInsight.tsx's "Please wait...").

11. Authentication & Authorization

Login flow

flowchart TB
    L["Login.tsx: user submits email+password"] --> M["useLoginMutation -> POST login (authApi.ts)"]
    M -->|success| SC["dispatch(setCredentials(LoginResponse))"]
    SC --> LS["localStorage: user, survey, token, dataType, loginTime=Date.now()"]
    SC --> NAV["navigate('/')"]
    M -->|failure| ERR["errorMessage = err.data.message"]
    NAV --> PR["ProtectedRoute reads state.auth.token"]
    PR -->|token present| RENDER["Render requested page"]

LoginResponse (types/type.ts:7-29) carries both the user (id, name, email, role) and the entire survey object (id, name, invites, data_type, top, ind, bmk, pv1, tm_name, …) — i.e. login returns the survey context directly, there is no separate "select your survey/company" step visible in the code.

Token management

Token Stored where Attached how
Bearer token (string) localStorage.token + state.auth.token (mirrored on login/logout) Authorization: Bearer <token> header via prepareHeaders in baseApi.ts:30-38
  • No refresh token concept exists in the code — LoginResponse.token is a single string, not an access/refresh pair.
  • Backend-driven invalidation: any RTK Query call that receives HTTP 401 triggers dispatch(logout()) + a hard redirect to /login (baseApi.ts:49-53).
  • Client-driven invalidation (24-hour auto-logout): implemented entirely in App.tsx:192-214, independent of the token's actual server-side lifetime:
// App.tsx:193-214 (abridged)
useEffect(() => {
  const loginTime = localStorage.getItem("loginTime");
  if (!loginTime) return;

  const DAY = 24 * 60 * 60 * 1000;
  const remaining = DAY - (Date.now() - Number(loginTime));

  if (remaining <= 0) {
    dispatch(logout());
    window.location.href = "/login";
    return;
  }

  const timer = setTimeout(() => {
    dispatch(logout());
    window.location.href = "/login";
  }, remaining);

  return () => clearTimeout(timer);
}, [dispatch]);

This matches the "auto logout after 24hrs" behavior referenced for the backend — here it is a purely client-side setTimeout seeded from loginTime in localStorage, set once at login (authSlice.ts:33). It only fires while the tab stays open and mounted; a closed tab reopened after 24h will still hold a stale token in localStorage until the next API call happens to 401, since App.tsx's check only runs remaining <= 0 once on mount (not polled).

Session handling

  • No redux-persist/PersistGate — session survives reload purely because authSlice's initialState re-reads localStorage synchronously on every module load (authSlice.ts:12-14).
  • Login.tsx:20-25 also independently checks localStorage.getItem("token") on mount and redirects to / if already logged in — a second, redundant check alongside the Redux-backed ProtectedRoute.

Protected pages & authorization

Only one level of protection exists:

  1. Route levelProtectedRoute.tsx (token presence only, see §4). There is no route-level role or permission check.
  2. UI element level (broken)config/menuConfig.ts attaches roles: ["user"] to every single sidebar menu item, and Sidebar.tsx:25-28 filters items with item.roles.includes(user?.role) — but user here is mockUser (utils/mockUser.ts, hardcoded { id: 2354568974568, name: "Paul Keijzer", role: "user" }), not the real logged-in user from Redux/localStorage. Since every menu item's role list is ["user"] and the mock user's role is also "user", the filter currently always passes — but it is not actually reading the authenticated session, so a differently-role'd real user would see an unfiltered/incorrect menu, or (if the mock were ever changed) the menu could silently break for everyone. See §21.

There is no backend-permission-string equivalent (checkPermission(...)) anywhere in this codebase — authorization is materially thinner here than a typical multi-role admin panel: this app assumes exactly one role ("company user viewing their own survey").


12. Shared Utilities

src/utils/

File Exports Purpose
helper.ts getWidthPercent(value, type), getCSSVariable(name) getWidthPercent converts a raw score into a CSS width percentage differently for "avg" (out of 5) vs "eng" (already a percent) score types — used to size progress bars. getCSSVariable reads a CSS custom property off document.documentElement (used to fetch the brand teal color for SweetAlert2 button theming, ExecutiveSummary.tsx:84,104).
generatePDF.ts generatePDF(reportClass, fileName) Finds all elements with class reportClass, waits for document.fonts.ready, captures each as a JPEG via html-to-image's toJpeg (chosen specifically because it supports Tailwind v4's oklch() colors, per the inline comment generatePDF.ts:14), and assembles a multi-page A4 jsPDF. Falls back to a html2canvas-based capture (fallbackCapture) that manually strips any oklch() color from every descendant element's computed style (rewriting to plain hex) if the primary method throws.
askAI.ts askAI(question): Promise<BotResponsePayload> Mocked chat backend for /ask-your-data — see §5.15/§21.
mockUser.ts mockUser: User Hardcoded fake user ({ id, name: "Paul Keijzer", role: "user" }) used by Sidebar.tsx instead of the real session — see §11/§21.

Custom hooks (src/app/hooks.ts)

Hook Purpose
useAppDispatch useDispatch.withTypes<AppDispatch>() — typed dispatch
useAppSelector useSelector.withTypes<RootState>() — typed selector

No other custom hooks exist in the codebase (no useDebounce, no useFormValidation, no useBreadcrumbs — all absent, unlike a larger design-system-driven app). Debounce, form validation, and breadcrumb logic are simply not implemented anywhere.

Constants

There is no dedicated src/constants/ folder. The closest equivalents are: - config/menuConfig.ts — the sidebar navigation tree (also doubles as the de-facto route→label source, duplicated independently in Topbar.tsx's titleMap). - Inline color/style literals scattered through components (e.g. hardcoded hex colors like "#176B23", "#B72D31", "#0F7B7B" for good/bad/neutral score coloring, repeated across Participation.tsx, OrganisationalNps.tsx, LIneManagerEffectiveness.tsx rather than centralized as named constants). - Tailwind @theme tokens in index.css (see §13) are the closest thing to a real design-token constants file.


13. Styling System

Approach

Tailwind CSS v4, integrated via the official Vite plugin (@tailwindcss/vite, vite.config.ts:3,9) rather than a PostCSS config file — this is the new Tailwind v4 zero-config approach. There is no tailwind.config.js/.ts in the repo; configuration instead lives directly in CSS via the @theme at-rule.

Design tokens (src/index.css:4-17)

@import "tailwindcss";

@theme {
  --breakpoint-sm: 640px;
  --breakpoint-md: 768px;
  --breakpoint-lg: 1024px;
  --breakpoint-xl: 1280px;

  --color-default: #12777D;       /* brand teal — primary UI color */
  --color-default-light: #ECF9F9;
  --color-hover: #0D5E62;
  --color-dark: #094145;
  --color-light: #1ba39c;

  --font-sans: "MyCustomFont", sans-serif;
}

These generate real Tailwind utility classes automatically (Tailwind v4's @theme token → utility class mechanism), which is why bg-default, text-default, hover:bg-hover, border-default etc. are used freely throughout components (e.g. Login.tsx:104, Sidebar.tsx:122) without any extra config file.

Fonts

Two custom @font-face declarations (index.css:19-30) load local Montserrat .ttf files from src/assets/font/Montserrat/static/: a default "MyCustomFont" (weight 300, mapped to --font-sans) and a separate "Montserrat Black" used only via the utility class .bold-title (index.css:32-34).

Global / component-specific CSS

Beyond Tailwind utilities, index.css hand-writes: - .loader (57-74): the CSS-only conic-gradient spinner used by every page's loading state. - .sidebar-menu-scroll (38-54): custom thin scrollbar styling (WebKit only) for the sidebar and the Ask Your Data chat panel. - AG Grid theme overrides (78-291): a large block of .custom-ag-grid/.ag-theme-* selectors that restyle AG Grid's header, cells, row hover, pivot/group headers, scrollbars, and side panel to match the app's teal brand — layered on top of AG Grid's built-in themeMaterial object theme. - Print/PDF helpers (295-308): #pdf-root (fixed 794px A4-ish width), .pdf-section, .exec-summary-pdf-section background rules used by the off-screen PDF rendering flow. - Numerous inline print: Tailwind variants throughout components (print:hidden, print:px-2 print:py-2, print:break-before-page — e.g. ItemDetailTable.tsx:1117, DemographicDataInsight.tsx:259) indicate the app is also designed to be browser-printed directly (window.print()), as a secondary export path alongside the JS-driven PDF generation.

Responsive design

  • Tailwind responsive prefixes (md:, lg:) are used pervasively for the sidebar (desktop persistent vs mobile overlay drawer, Layout.tsx:26-50), grid layouts (grid md:grid-cols-4 grid-cols-1, used for every snapshot-tile row), and the Ask Your Data two-column layout (grid-cols-1 lg:grid-cols-[1fr_290px], AskYourData.tsx:91).
  • No CSS-in-JS, no styled-components, no CSS Modules — Tailwind utility classes plus the handful of global CSS blocks above are the entirety of the styling approach.

UI consistency notes

  • Brand color (#12777D/--color-default) and its hover/dark variants are used consistently via the default/hover/dark/light Tailwind theme names.
  • Status coloring (positive/negative/neutral score indicators) is not centralized as theme tokens — it is repeated as raw hex literals (#176B23 green, #B72D31 red, #F59E0B amber) inline across many page files rather than defined once in @theme.

14. Assets

Asset type Location Notes
Logo src/assets/neweclogo.png Used in Login.tsx, Sidebar.tsx, Layout.tsx footer ("Powered by")
Illustration images src/assets/hero.png, src/assets/clusters.png Marketing/explanatory imagery (exact usage not traced to a specific component in this pass)
Fonts src/assets/font/Montserrat/static/*.ttf Montserrat-Medium (default sans) and Montserrat-ExtraBold ("Montserrat Black" / .bold-title), loaded via @font-face in index.css
Unused template assets src/assets/react.svg, src/assets/vite.svg Leftovers from the Vite React template scaffold; no import references found
Favicon public/favicon.svg Referenced implicitly by Vite's default index.html favicon convention (not explicitly linked in index.html, which only sets <title>)
Icon sprite public/icons.svg Present in public/; specific usage not traced in this pass
Icon fonts None — all icons are React components from lucide-react and react-icons (multiple sub-packages: react-icons/fi, /fa, /ai, /tb, /bi, /rx, /io5, /tfi, /gi, /lu, /md, /si, /pi) No custom icon font/sprite system

15. Environment Configuration

Environment variables

None are used. A repo-wide search for import.meta.env and any .env* file found zero matches — this is a deliberate deviation from typical Vite convention. All environment-specific values (the API base URL) are hardcoded directly in source:

// services/baseApi.ts:28-29
const baseQuery = fetchBaseQuery({
  baseUrl: "https://ec-ees-rpt2-26.engagesurvey.biz/api/",
  // baseUrl: "http://127.0.0.1:8000/api/",   <-- commented-out local override
  ...

To point the app at a different backend (e.g. local development), a developer must edit and (accidentally or deliberately) commit this file — there is no VITE_API_BASE_URL or equivalent. See §21 and §24.

Build configuration

File Role
vite.config.ts Minimal — just react() and tailwindcss() plugins (vite.config.ts:6-10). No aliasing, no proxy, no build-target overrides, no chunking config.
tsconfig.app.json target: "es2023", moduleResolution: "bundler", jsx: "react-jsx", strict-adjacent lint flags (noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch) — but not strict: true explicitly listed (inherit check not verifiable without the full compiler option list; see §24).
eslint.config.js Flat config: @eslint/js recommended + typescript-eslint recommended + eslint-plugin-react-hooks (flat recommended) + eslint-plugin-react-refresh (vite preset). dist/ is globally ignored.
package.json scripts dev (vite), build (tsc -b && vite build), lint (eslint .), preview (vite preview) — no test script; no test framework/dependency present in package.json at all.

npm scripts

"dev":     "vite",
"build":   "tsc -b && vite build",
"lint":    "eslint .",
"preview": "vite preview"

Deployment

No CI/CD config (no .github/workflows, buildspec.yml, firebase.json, vercel.json, or netlify.toml) was found in the repository. Given BrowserRouter is used (§4), whatever static host serves the built dist/ output must be configured with an SPA fallback rewrite (all paths → index.html), or deep-linking to any route other than / will 404 at the host level. See §24.


16. Performance Optimizations

What exists

Technique Status
RTK Query caching ✅ Server responses cached under the single api reducer; repeated mounts of the same query args reuse cache without a new request.
skip to avoid premature fetches Overview.tsx:18 skips useGetOverviewQuery until apiDataType has resolved.
Lazy query as an on-demand trigger useLazyGetChnageDGQuery (DemographicDataInsight.tsx:41) only fires on button click, not on mount.
useMemo for AG Grid column/def objects PivotTable.tsx:217,357,375 memoizes columnDefs, defaultColDef, autoGroupColumnDef to avoid re-creating grid config on every render.
CSS-only loading spinner ✅ No JS animation library — .loader is a pure CSS keyframe animation (index.css:57-74).

What is NOT present (opportunities)

Technique Status
Route-level code splitting (React.lazy/Suspense) ❌ All 18 pages are imported eagerly at the top of App.tsx — the entire app (including AG Grid Enterprise, Recharts, jsPDF, html2canvas) ships in the initial bundle load path. Given AG Grid Enterprise and the PDF-export libraries are both large dependencies used only on a few routes (Pivot Table; the "Download PDF" action), this is the single biggest available win.
React.memo on presentational components ⚠️ Not used anywhere found — the ~70 feature-specific report-section components re-render whenever their parent page re-renders, even though most receive stable-shaped props derived from a single top-level query.
Debounced inputs ❌ No useDebounce/debounce utility exists; AskYourData.tsx's chat input and MultiFilter selections are not debounced (lower risk here since neither fires a request per keystroke, but worth noting for future search-as-you-type features).
RTK Query cache tags for auto-invalidation ⚠️ Only comparisonApi.ts uses providesTags/invalidatesTags; every other mutation (changeDataType, changeFirstLogin, changeComparison's sibling getChnageDG) relies on manual refetch() calls instead, which is more error-prone to keep in sync as the app grows.
Bundle analysis / manual chunking ❌ Not configured in vite.config.ts (Vite/Rollup defaults only).
Image optimization hero.png/clusters.png/neweclogo.png are used as-is; no responsive srcset, no lazy loading="lazy" attribute usage found.

17. Error Handling

Layers

flowchart TB
    A["RTK Query request"] --> B{"HTTP status"}
    B -->|401| C["baseQueryWithAuth: dispatch logout + redirect to /login"]
    B -->|other error| D["Hook exposes error object to the component"]
    D --> E{"Component handles it?"}
    E -->|Login page| F["errorMessage state -> red banner, from err.data.message"]
    E -->|Most other pages| G["No explicit error UI - only isLoading is checked; error is destructured but frequently unused"]
    H["Render-time exception anywhere in the tree"] --> I["No React error boundary present - whole app unmounts to a blank screen"]
    J["PDF generation failure"] --> K["generatePDF.ts: try/catch per section, falls back to html2canvas + manual oklch-stripping"]

API errors

  • 401 Unauthorized is the only status handled globally (baseApi.ts:48-53): immediate logout + redirect.
  • All other errors (4xx/5xx/network failures) are simply returned as the RTK Query hook's error field. Most pages destructure { data, isLoading } and never destructure or render error at all (confirmed across Overview.tsx, Participation.tsx, OrganisationalNps.tsx, AllQuestions.tsx, Pivot.tsx, etc.) — if a non-401 request fails, the page will exit its isLoading state and attempt to render with data === undefined, relying entirely on optional chaining (data?.res?.overall) to avoid crashing, silently showing an empty/zeroed-out report with no error message to the user.
  • The one page that does surface an error message is Login (Login.tsx:35-40, err?.data?.message with a fallback string).

UI / render errors

No React error boundary exists anywhere in the codebase (confirmed — no componentDidCatch, getDerivedStateFromError, or react-error-boundary usage found in any file). An uncaught render exception in any component — for example the user?.name.trim() call in Topbar.tsx:97 when localStorage.user is "{}" and thus name is undefined (optional chaining stops the crash on user?.name but not on the subsequent .trim() call, since ?. was only applied to the user access) — will unmount the entire React tree to a blank white page with no user-facing recovery UI, only a stack trace in the browser console.

Fallback UI

  • PivotTable.tsx:176-206 explicitly renders "No data available" / "No pivot data available" messages when its data prop is empty.
  • ItemDetailTable.tsx:833-839, 1082-1089 renders "No data available" / "No matching records found" table rows when filters produce zero rows.
  • DemographicDataInsight.tsx:216-224 renders an inline anonymity-threshold warning when a demographic cut has too few respondents.
  • Most other components have no explicit empty-state UI — they rely on optional chaining (data?.field ?? []) to render an empty list/chart rather than an explicit "no data" message.

Logging

Client-side logging is exclusively console.log/console.error, and largely commented out in production code paths (numerous // console.log(...) lines left in place across Overview.tsx:21, ExecutiveSummary.tsx:41,45, LIneManagerEffectiveness.tsx:19,47, PivotTable.tsx:66,214,393,474, etc.) rather than removed — these are debug artifacts, not intentional logging infrastructure. No error-tracking SDK (e.g. Sentry) is present in package.json or source. See §24.


18. Development Workflow

Setting up the project

cd "EES-2026/EES-V2.0-Frontend"
npm install
npm run dev          # Vite dev server (default port 5173, per Vite convention — not overridden in vite.config.ts)

⚠️ Because the backend base URL is hardcoded (§15) and there are no environment variables, npm run dev talks to the live https://ec-ees-rpt2-26.engagesurvey.biz/api/ backend by default. To develop against a local backend, uncomment/edit the baseUrl in src/services/baseApi.ts:28-29 locally and take care not to commit that change — see §21 for a suggested .env fix.

Running the application

  • npm run dev → Vite dev server with HMR.
  • npm run buildtsc -b && vite build (type-checks via project references, then bundles to dist/).
  • npm run preview → serves the built dist/ locally.
  • npm run lint → ESLint over the whole repo per eslint.config.js.
  • There is no test script and no test framework in package.json — no unit/integration/e2e tests exist in the repository at all.

Add a new page

  1. Create src/pages/MyPage.tsx following the standard page pattern (fetch data_type if needed, fetch page data, isLoading spinner guard, <Layout> wrapper).
  2. Add a new <Route path="/my-page" element={<ProtectedRoute><MyPage /></ProtectedRoute>} /> inside AppContent() in App.tsx.
  3. Add an entry to menuItems in src/config/menuConfig.ts (label, icon from react-icons, path, roles: ["user"]) so it appears in the sidebar.
  4. Add the new path → title mapping to titleMap in components/Topbar.tsx:21-43 (this is a second, independent source of truth from the sidebar label — both must be updated).

Add a new component

  1. Create src/components/MyComponent.tsx directly in the flat components/ directory (no subfolder convention exists to follow).
  2. Type its props with an inline object type or a named interface (prefer this over the any-typed pattern seen in older components — see §7's type-safety status and §19).
  3. Import via a relative path from the consuming page/component (no path aliases are configured — see §2).

Connect to a new API endpoint

Follow the recipe in §9 — create a service file that calls baseApi.injectEndpoints, type the response/request in src/types/type.ts or inline, and consume the generated hook directly. No further registration is needed in app/store.ts.

Create a reusable component

  • Put it in src/components/, give it a typed props interface, and keep it presentational (data via props) unless it genuinely needs global session data — in which case follow the Layout.tsx/Topbar.tsx pattern of calling useGetDataTypeQuery/reading localStorage directly rather than threading props through many layers.

19. Coding Standards

Naming conventions (as observed)

Thing Convention Example
Page files PascalCase, matches route intent ExecutiveSummary.tsx, AllQuestions.tsx
Component files PascalCase WelcomeCard.tsx, DonutChart.tsx
PDF-variant components Base name + ForPDF suffix AdvocacyNetPromoterScoresForPDF.tsx
Service files camelCase + Api suffix overviewApi.ts, csItemDetailApi.ts
Slice files camelCase + Slice suffix, inside a feature folder features/auth/authSlice.ts
Hooks useXxx camelCase useAppDispatch, useAppSelector
RTK Query hooks (generated) use<Name>Query / use<Name>Mutation / useLazy<Name>Query useGetOverviewQuery, useChangeDataTypeMutation, useLazyGetChnageDGQuery
Types/interfaces PascalCase, no consistent I-prefix (mixed) OverView, StatementBMResponse, but also plain Props used repeatedly across files with no namespacing

Import organization

  • Plain relative imports only — no path aliases (@components, @services, etc.) are configured anywhere (tsconfig.app.json, vite.config.ts). Deep imports like ../../services/overviewApi or ../types/type are the norm.
  • No barrel (index.ts) files/re-exports were found in services/, components/, or pages/ — every import references the concrete file directly.
  • Import ordering is not enforced by any lint rule (no eslint-plugin-import/simple-import-sort in eslint.config.js) — ordering is inconsistent file-to-file.

TypeScript usage patterns

  • Response types are inconsistently strict: some services fully type both request and response (overviewApi.ts, statementBmApi.ts, pivotApi.ts, comparisonApi.ts), while several na2/* services type the response as bare any (executiveSummaryApi.ts:5, lineMngerEffApi.ts:5, organisationalNPSApi.ts:5, voiceOfTheEmpApi.ts:5, participationSummaryApi.ts:5). This means every page consuming those endpoints loses type safety on data?.res?.… chains — errors there are only caught at runtime.
  • types/type.ts accumulates request/response types for many unrelated features in one 587-line file, annotated with numbered comments (// 1. Login Type, // 8. Statement BM, // 11. Consolidated Statement Ranking) rather than being split per-feature — a strong candidate for refactor into per-service type files.
  • Several components accept props: any wholesale (CustomTabs.tsx:1, DonutChart.tsx:82-88) rather than a typed interface.
  • noUnusedLocals/noUnusedParameters/noFallthroughCasesInSwitch are enabled in tsconfig.app.json:19-22, giving a baseline of dead-code hygiene enforcement at build time (npm run build runs tsc -b first).

Code comments

A distinctive pattern in this codebase: many inline comments are written in Roman-Urdu/English code-mixed style (e.g. // Agar login page hai to sidebar & footer hide kar do in App.tsx:33, // Agar token invalid ya expired hai in baseApi.ts:48, // Save in localStorage mixed with Urdu elsewhere) — useful context for a new developer not expecting bilingual comments, and a sign of the original team's working language.

Linting

eslint.config.js is a flat config combining @eslint/js recommended rules, typescript-eslint recommended rules, eslint-plugin-react-hooks's flat recommended preset (catches useEffect/hook dependency mistakes), and eslint-plugin-react-refresh's Vite preset (warns on files that mix component and non-component exports, which would break Fast Refresh). Run via npm run lint. No Prettier config (.prettierrc) exists in the repo — formatting consistency is not automated.


20. Debugging Guide

Tools

Need Tool
State inspection Redux DevTools browser extension — configureStore's default middleware includes the DevTools enhancer automatically in development. Inspect state.auth, state.preferences, and the api reducer's cache entries.
Network / API Browser DevTools → Network tab. Watch for the Authorization: Bearer header on every request, and check whether the endpoint uses query-string params or a JSON body (mixed convention, see §9).
Component tree React DevTools.
AG Grid internals The gridApi is captured in local component state (PivotTable.tsx:65) and console.logged (PivotTable.tsx:66) — a quick way to inspect the grid API object from the console during a dev session.

Common issues & where to look

Symptom Likely cause / where to look
Redirected to /login unexpectedly Either a real 401 from the backend (baseApi.ts:48-53) or the 24-hour client-side timer expiring (App.tsx:192-214) — check localStorage.loginTime age.
Blank white screen, no error shown An uncaught render exception with no error boundary to catch it (§17) — check the browser console for the actual stack trace; a common culprit is a .method() call chained after ?. that doesn't guard the whole chain (e.g. Topbar.tsx:97).
Page renders but a section is silently empty/zeroed The request likely errored (non-401) and data is undefined; the page never checked error (§17) — verify in the Network tab.
Sidebar shows/hides the wrong menu items for a role Sidebar.tsx:18,25-28 filters using mockUser, not the real session user (§11/§21) — this is model behavior, not a bug in the filter logic itself.
Data type toggle (eng/avg) doesn't seem to persist visually Most pages re-fetch data_type from the server via useGetDataTypeQuery rather than reading the preferences Redux slice — check whether refetchDataType() was called after changeDataType fired.
PDF export produces a broken/blank image for a section Likely an oklch() color that html-to-image couldn't rasterize before the html2canvas fallback kicked in — check the console for the catch block's logged error (generatePDF.ts:27-28) and confirm the section has a *ForPDF variant component if colors look wrong.
AG Grid pivot columns/values reset unexpectedly onGridReady's setTimeout(..., 500) (PivotTable.tsx:397-417) re-applies default pivot/value columns after grid mount — a race with any earlier user interaction within that half-second window.
CSV export from Item Detail table is missing expected columns ItemDetailTable.tsx's exportFilteredData/getFilteredDataForExport build columns from currently active filters (filters.groupCol/filters.dataCol) — toggle the relevant filter checkboxes via the "Filters" modal before exporting.

API debugging tips

  • Because response envelopes are inconsistent ({ res: {...} } for na2/* endpoints vs flat top-level fields for others, §9), always check the raw Network response body shape before assuming a page's data?.res?.x vs data?.x chain is correct.
  • Since several services are typed builder.query<any, ...>, TypeScript will not catch a typo in a deeply-nested data?.res?.ai_content_json?.priorities?.focus-style access — verify against the actual JSON response, not the type signature.

21. Common Pitfalls

Pitfall Why it happens Do this instead
Sidebar role filtering uses a hardcoded fake user Sidebar.tsx:18 sets const user = mockUser; (utils/mockUser.ts) instead of reading state.auth.user — the menu-role filter (Sidebar.tsx:25-28) never actually checks the logged-in user's real role. Replace mockUser with useAppSelector(state => state.auth.user) before relying on role-based menu filtering for anything beyond the current single-role app.
Topbar.tsx:96-100 can throw on a malformed/missing user name user?.name.trim().split(...) — the optional chaining ?. only guards the user access, not the subsequent .trim() call; if localStorage.user is "{}" (as it might transiently be), user.name is undefined and .trim() throws, and with no error boundary anywhere (§17) this blanks the whole app. Guard the full chain: user?.name?.trim()... or compute initials with a null-safe helper function.
utils/askAI.ts is a hardcoded mock, not a real API call The file itself documents this in its top comment (askAI.ts:3-23) — it pattern-matches on substrings and returns 1 of 4 fixed responses. Don't assume "Ask Your Data" answers are real; when wiring the real backend, replace the body of askAI() per the file's own inline instructions rather than adding a parallel implementation elsewhere.
API base URL is hardcoded with no environment switching services/baseApi.ts:28-29 hardcodes the production host; the local override is a commented-out line in the same file. Introduce import.meta.env.VITE_API_BASE_URL with a Vite .env/.env.local split instead of editing source to change environments — avoids accidental commits of a local URL.
ag-grid-community is used but not declared as a dependency PivotTable.tsx imports directly from ag-grid-community (styles, ModuleRegistry, ClientSideRowModelModule, themeMaterial) — only ag-grid-enterprise/ag-grid-react are in package.json; it currently works only because ag-grid-enterprise pulls ag-grid-community in transitively. Add ag-grid-community explicitly to package.json dependencies so a future major-version bump of ag-grid-enterprise cannot silently break this import.
No 404/fallback route App.tsx's <Routes> has no catch-all <Route path="*">. Any unrecognized path under an authenticated session renders nothing; add a fallback route before relying on "any bad link shows something."
No React error boundary anywhere Never implemented in this codebase (confirmed by search). Any render-time exception in any of the 95+ components takes down the entire app to a blank page; wrap <AppContent /> (or at least the <Layout> content slot) in an error boundary.
preferences Redux slice is effectively dead code Pages fetch data_type from the server via useGetDataTypeQuery() rather than reading state.preferences.dataType, and setDataType is never dispatched anywhere in pages//components/. Either wire pages to read/write the slice consistently, or remove it if the server round-trip is intentionally the single source of truth.
Duplicated route→label mapping The sidebar label (config/menuConfig.ts) and Topbar.tsx's titleMap (Topbar.tsx:21-43) are two independent hardcoded maps from path to display text. Adding/renaming a route requires updating both files in lockstep; consider deriving one from the other.
Repeated inline groupMap/newGroupMap objects ItemDetailTable.tsx redefines the same { [user.abbr]: "yrs", "Top Group": "top", ... } mapping object inline at least 6 separate times across different functions in the same file. Hoist to a single module-level constant/function inside ItemDetailTable.tsx (or a shared util) to avoid drift between the copies.
Manual CSV building instead of a library ItemDetailTable.tsx:770-819's exportFilteredData hand-writes CSV rows/escaping instead of using AG Grid's CsvExportModule (already a dependency, used elsewhere in PivotTable.tsx) or a small CSV library. Prefer the already-available AG Grid export modules, or a tested CSV-serialization utility, over hand-rolled quote-escaping logic.
console.log/commented-out debug lines left in shipped code Numerous // console.log(...) lines throughout pages and PivotTable.tsx are debug leftovers rather than intentional logging. Remove before merging, or replace with a real logging utility gated by an environment flag.

22. Best Practices

Components

  • Give new components a typed props interface rather than following the any-typed pattern seen in CustomTabs.tsx/DonutChart.tsx.
  • Keep components presentational (data via props); reserve direct localStorage/Redux/service reads for genuinely global chrome components (Layout, Sidebar, Topbar), matching the existing pattern.
  • When a section must also render inside the PDF export path, follow the existing X/XForPDF pairing convention rather than trying to make one component serve both contexts with conditional rendering (the codebase already made this trade-off deliberately — see §1).

State management

  • Use useAppSelector/useAppDispatch (app/hooks.ts) instead of the raw react-redux hooks for new code.
  • Prefer RTK Query providesTags/invalidatesTags (as comparisonApi.ts does) over manual refetch() calls when adding new mutations that should update related queries — it is less likely to be forgotten as the codebase grows.
  • Type new endpoints' request/response fully in builder.query<Response, Request>(...) rather than <any, ...>, even though several existing na2/* services don't — new code should raise the bar, not match the lowest common denominator.

API integration

  • Add new endpoints via baseApi.injectEndpoints in a new src/services/*.ts file — never create a second createApi instance.
  • Be deliberate about params (query string) vs body (JSON) — check what the specific backend endpoint expects rather than copying whichever convention the nearest existing service file happens to use.

Performance

  • Consider React.lazy/Suspense for at least the Pivot Table route (AG Grid Enterprise) and any PDF-heavy pages, since those pull in the app's largest dependencies but are not needed on first paint of the Executive Summary landing page.
  • Memoize expensive derived-data computations (e.g. AllQuestions.tsx's transformApiData, ItemDetailTable.tsx's repeated sortArray/getCellValue calls) with useMemo if profiling shows re-render cost, following the existing useMemo pattern already established in PivotTable.tsx.

Error handling

  • Always destructure and handle error alongside isLoading/data from RTK Query hooks in new pages, rather than only guarding on isLoading as most current pages do.
  • Add a top-level React error boundary around <AppContent /> before this app grows further — currently one uncaught exception anywhere takes down the whole SPA.

Security / correctness

  • Replace mockUser usage in Sidebar.tsx with the real state.auth.user before this app supports more than one role.
  • Move the hardcoded API base URL into a Vite environment variable before adding a second deployment target (staging, local dev, etc.).

Accessibility

  • Current state: no systematic accessibility patterns found — no consistent aria-* labeling, no visible focus-management strategy beyond native browser defaults, and several interactive <div onClick> elements (e.g. Sidebar.tsx:96-113's "Download PDF" item, CustomTabs.tsx's tab buttons which are at least real <button>s) rather than semantic elements throughout. Unable to determine whether this was a deliberate scope decision — treat as an improvement area for new work.

23. Developer Onboarding Checklist

Environment setup

  • Clone the repo; open EES-2026/EES-V2.0-Frontend/.
  • Read this document's §1§4.
  • npm install.
  • npm run dev and load the app — note it will hit the live production backend by default (§15); confirm with the team whether you need a local/staging override before doing anything destructive.

Understand the project structure

  • Skim src/ and match each folder to §2.
  • Read main.tsx and App.tsx — understand the bootstrap, the isLoginPage branch, and the 24h auto-logout useEffect (§3, §11).
  • Read app/store.ts, features/auth/authSlice.ts, features/preferences/preferencesSlice.ts (§8).

Run & explore

  • Log in via /login (get credentials from the team) and reach the Executive Summary.
  • Open Redux DevTools; inspect state.auth and the api reducer's cache entries.
  • Trigger a page load; watch the Network tab for the Authorization: Bearer header and note whether the request used params or body (§9).

Learn routing & auth

  • Read App.tsx's full <Routes> list and components/ProtectedRoute.tsx (§4).
  • Confirm you understand there is no route-level role/permission check, only token presence (§11).

Learn the shared layout & components

  • Read components/Layout.tsx, Sidebar.tsx, Topbar.tsx, config/menuConfig.ts (§7.1).
  • Note the mockUser issue in Sidebar.tsx (§21) before relying on role-based menu behavior.
  • Read components/PivotTable.tsx if you'll touch the AG Grid pivot screen (§7.2).

Make your first change

  • Add a harmless UI tweak to an existing page's snapshot tile or a small text change in a "How to Read This Report" component.
  • Verify Vite HMR picks it up; run npm run lint before considering the change done (there is no automated test suite to run).

Add something new (stretch)

  • Add a new page end-to-end following the recipe in §18: page file → route in App.tsx → sidebar entry in menuConfig.ts → title entry in Topbar.tsx's titleMap → a new services/*Api.ts file.

Submit a change

  • Run npm run lint and npm run build (which runs tsc -b first) to confirm no type errors.
  • There is no CI config found in the repo and no documented branch/PR policy — confirm the team's process directly. (See §24.)

24. Appendix — "Unable to determine from the available code"

  • Deployment target / SPA rewrite configuration. No CI/CD or hosting config file (vercel.json, netlify.toml, firebase.json, nginx.conf, GitHub Actions workflow) exists in the repository, so how BrowserRouter's client-side routes get an SPA fallback rewrite in production is unknown from the code alone.
  • Whether a .env/environment-variable mechanism is planned but simply not yet adopted, versus a deliberate choice to hardcode the API host — the commented-out local override in baseApi.ts suggests developers do switch it manually, but no environment-variable infrastructure exists to formalize this.
  • Backend response envelope rationale — why some na2/* endpoints wrap data in { res: {...} } while others (Overview, Statement Benchmark) return fields at the top level is a backend design decision outside this codebase's visibility.
  • Exact purpose of src/assets/hero.png and clusters.png — no component reference to either was traced in this pass; they may be unused, used via a component not covered by the read set, or used in a way not surfaced by search (e.g. referenced only from CSS not reviewed).
  • Whether a client-side error-tracking SDK (e.g. Sentry) is planned — none is present in package.json today.
  • The team's branch/PR/code-review policy — no CONTRIBUTING.md, PR template, or CI gate was found.
  • Full list of backend endpoints this frontend is meant to eventually call beyond what is already wired in src/services/ (the task brief mentions /benchmark-view and /na2/* sub-routes generally; only the specific paths listed in §9 were found actually referenced in source).
  • Intended real implementation of askAI/the /ask-your-data backend endpoint — the mock's own comment sketches a POST /api/ask-your-data shape, but the actual backend contract (if implemented) was not visible from this frontend-only review.
  • Whether tsconfig.app.json inherits strict: true from a base config not included in the reviewed file set — the file shown does not list strict explicitly among its compiler options.

This document is derived from static analysis of EES-2026/EES-V2.0-Frontend as of the current codebase state. Line references reflect the code at analysis time and may drift as the app evolves. For backend/API detail, see the companion backend documentation for EES - Report backend.