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 namedees-ui-with-ai(package.json:2).
Table of Contents¶
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
surveyreturned 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(Listboxdropdowns), 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)¶
- 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 whileisLoading, then render a tree of presentational components inside<Layout>. See the standard page pattern in §6. - One RTK Query "slice API" per backend feature, all injected into a single
baseApiviainjectEndpoints(src/services/*.ts), so there is one shared cache/reducer (api) rather than N separate reducers. - 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.tsxvsAdvocacyNetPromoterScoresForPDF.tsx), not by folder structure. data_type(eng vs avg) is a pervasive cross-cutting toggle. Almost every page readsuseGetDataTypeQuery()and threads adataType: "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."- AI content is first-class, not bolted on. Several API responses embed an
ai_content_jsonobject with narrative insight/action text per section (seeExecutiveSummary.tsx:356-403,OpenFeedbackAISynthesis.tsx), and pages passinsAndAction={...}props straight through to presentational components that render it inline next to the charts. - 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. - Print/PDF is a parallel rendering concern, not an afterthought: many chart/section components ship in two versions — a normal one and a
*ForPDFvariant (e.g.AdvocacyNetPromoterScoresForPDF.tsx,CustomScatterChartForPDF.tsx,LeaderShipClusterSectionForPDF.tsx,OpenFeedbackCardForPDF.tsx,WhereToFocusDriverPrioritiesForPDF.tsx) becausehtml2canvas/html-to-imagecannot rasterize some interactive/animated chart output reliably, and because Tailwind v4'soklch()colors break canvas capture (seegeneratePDF.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/@servicesalias configured intsconfig.app.jsonorvite.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 insrc/components/are almost all presentational, taking data via props — with three notable exceptions that read Redux/services directly:Layout.tsx(fetchesdata_type),Sidebar.tsx(readsmockUser, dispatcheslogout),Topbar.tsx(fetchesdata_type, readslocalStorage.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.tsxcomponent 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.ts→GET na2/get-executive-summary),useGetDataTypeQuery,useChangeFirstLoginMutation. - Two viewing modes: a "guided" first-login mode (
showInd, driven byapiDataType.first_login !== 1) that shows one section at a time with Previous/Next buttons and aGuidedReadingprogress component, and a "scroll" mode for returning users that renders all 8 sections stacked with anIntersectionObserver(ExecutiveSummary.tsx:161-193) driving a stickyExSummaryScrollTabtab 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, callschangeFirstLogin({ 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 containingFullExecutiveSummaryPDF, waits 3 seconds for it to render, then callsgeneratePDF("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.ts→na2/get-participation-summary). - Business logic: computes
nonParticipants = totalInvites - participantsand both percentages client-side (Participation.tsx:14-19); builds a 4-tilesnapShotDataarray (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.ts→GET na2/get-line-manager-effectiveness), plususeGetDataTypeQuery. - Business logic:
lowestDimis computed client-side viaArray.reduceoverdata.res.dimensionsto find the dimension with the lowestyrs.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:
ManagerNPSByDepartmentis driven by a localactiveTabindex intodata.res.nps.by_demg[], with matching AI narrative pulled fromai_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.ts→GET 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). Notedata.res.nps.summary[1]is separately referenced forlmeNpspassed intoAdvocacySegments(OrganisationalNps.tsx:126) — i.e. index1of the same summary array is assumed to be the Line Manager NPS figure. - Demographic matrix tab:
AdvocacySegmentsis driven byactiveTabintodata.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.ts→GET na2/get-voice-of-the-employee). - Components: wraps
OpenFeedbackAISynthesis, which itself composesClusterMiniCard(Insight/Action) andOpenFeedbackCard/OpenFeedbackCardForPDFfor 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.ts→POST overView), gated byskip: !apiDataTypeso 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.ts→POST statement-benchmark) which returnsStatementGaps(top_p/top_n/ind_p/ind_n/bmk_p/bmk_n/pv1_p/pv1_n— positive/negative gap arrays per comparison group) andOnTheFence. - 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) andgetStatementRanking(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.ts→POST item-detail). - Client-side data reshaping:
transformApiData(AllQuestions.tsx:54-177) manually rebuilds a nestedcombine → clusters[] → components[] → dimensions[]tree from four flat arrays (combine,cluster,driver,dimension) returned by the API, usingMaps keyed byclid/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 inlinegroupMap/newGroupMapobjects — duplicated in at least 6 places insideItemDetailTable.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, taggedComparison) andchangeComparison(demographic/change-comparison, a mutation thatinvalidatesTags: ['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.ts→POST demographic/pivot-table), returning{ page_name, pivotData: PivotData[], demographics: Demographics[] }where eachPivotDatarow carriesScore,Dimension/Driver/Cluster/Combineand 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 returnsnullwhenever 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 bydata_type+ twoListbox-selected demographic IDscm_dg_id/lm_dg_id) andgetChnageDGas a lazy query (useLazyGetChnageDGQuery) triggered by the "CHANGE DEMOGRAPHIC SELECTION" button (DemographicDataInsight.tsx:43-61), which posts the fullfilter_demographic: Record<demographicId, optionId[]>map, then callsrefetch()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 HeadlessUIListboxdriver 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.ts→POST 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.ts→POST 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)fromutils/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 hardcodedBotResponsePayloadfixtures after an artificial 600mssetTimeoutdelay, 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 realfetch("/api/ask-your-data")call. See §21. - Rendering:
BlockRendererdraws whatever mix ofChatBlocks (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,setActiveTaballany),DonutChart(whole props objectany), and a large share of the ~70 feature-specific section components acceptdata/insAndAction/overviewprops typed asanyrather than dedicated interfaces — a direct consequence of manyservices/*.tsendpoints themselves being typedbuilder.query<any, ...>(see §9).
8. State Management¶
Approach¶
Redux Toolkit, with a much smaller footprint than the API surface suggests:
- Server state → a single RTK Query
baseApi(reducerPath: "api"), with every feature's endpoints merged in viainjectEndpointsrather than onecreateApiper domain. - Client/UI state → exactly two hand-written slices:
authandpreferences.
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 fromLogin.tsx:33after a successfulloginmutation) storesuser,survey,tokenin both Redux andlocalStorage(user,survey,token,dataType,loginTimekeys), whereloginTimeisDate.now()— the seed for the 24h auto-logout timer (§11).logoutclears all five of thoselocalStoragekeys and resets the three state fields tonull.- The
tokenin this slice is read byprepareHeadersinbaseApi.ts:31to attach theAuthorizationheader to every RTK Query request, and byProtectedRoute.tsx:12to decide whether to render a page or redirect.
The preferences slice¶
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 rawuseSelector/useDispatchfromreact-reduxdirectly. - Add new server data by injecting endpoints into the existing
baseApi(see §9) rather than creating a newcreateApiinstance. - Keep ephemeral UI state (
activeTab, filters, modal visibility) local withuseState; 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 JSONbody— seestatementBmApi.ts:8-10,comparisonApi.ts:105-109. A few endpoints (demographicApi.ts,dGDIApi.ts'sgetChnageDG) do send a JSONbody. - Several
na2/*endpoints (Executive Summary, Participation Summary, LME, Organisational NPS, Voice of the Employee) wrap their payload in a top-levelreskey (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/errorfrom its hook and renders its own spinner markup.
Adding a new API call (recipe)¶
- 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; - Define
MyThingResponseinsrc/types/type.ts(or inline in the service file, matching existing convention). - Import the generated hook directly in your page — no registration step is needed in
app/store.tsbecause all services share the single already-registeredbaseApireducer.
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
.loaderCSS spinner (see §6), plus per-buttondisabled+ 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.tokenis 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 becauseauthSlice'sinitialStatere-readslocalStoragesynchronously on every module load (authSlice.ts:12-14). Login.tsx:20-25also independently checkslocalStorage.getItem("token")on mount and redirects to/if already logged in — a second, redundant check alongside the Redux-backedProtectedRoute.
Protected pages & authorization¶
Only one level of protection exists:
- Route level —
ProtectedRoute.tsx(token presence only, see §4). There is no route-level role or permission check. - UI element level (broken) —
config/menuConfig.tsattachesroles: ["user"]to every single sidebar menu item, andSidebar.tsx:25-28filters items withitem.roles.includes(user?.role)— butuserhere ismockUser(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 thedefault/hover/dark/lightTailwind theme names. - Status coloring (positive/negative/neutral score indicators) is not centralized as theme tokens — it is repeated as raw hex literals (
#176B23green,#B72D31red,#F59E0Bamber) 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¶
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
errorfield. Most pages destructure{ data, isLoading }and never destructure or rendererrorat all (confirmed acrossOverview.tsx,Participation.tsx,OrganisationalNps.tsx,AllQuestions.tsx,Pivot.tsx, etc.) — if a non-401 request fails, the page will exit itsisLoadingstate and attempt to render withdata === 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?.messagewith 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-206explicitly renders "No data available" / "No pivot data available" messages when itsdataprop is empty.ItemDetailTable.tsx:833-839, 1082-1089renders "No data available" / "No matching records found" table rows when filters produce zero rows.DemographicDataInsight.tsx:216-224renders 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 devtalks to the livehttps://ec-ees-rpt2-26.engagesurvey.biz/api/backend by default. To develop against a local backend, uncomment/edit thebaseUrlinsrc/services/baseApi.ts:28-29locally and take care not to commit that change — see §21 for a suggested.envfix.
Running the application¶
npm run dev→ Vite dev server with HMR.npm run build→tsc -b && vite build(type-checks via project references, then bundles todist/).npm run preview→ serves the builtdist/locally.npm run lint→ ESLint over the whole repo pereslint.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¶
- Create
src/pages/MyPage.tsxfollowing the standard page pattern (fetchdata_typeif needed, fetch page data,isLoadingspinner guard,<Layout>wrapper). - Add a new
<Route path="/my-page" element={<ProtectedRoute><MyPage /></ProtectedRoute>} />insideAppContent()inApp.tsx. - Add an entry to
menuItemsinsrc/config/menuConfig.ts(label, icon fromreact-icons,path,roles: ["user"]) so it appears in the sidebar. - Add the new path → title mapping to
titleMapincomponents/Topbar.tsx:21-43(this is a second, independent source of truth from the sidebar label — both must be updated).
Add a new component¶
- Create
src/components/MyComponent.tsxdirectly in the flatcomponents/directory (no subfolder convention exists to follow). - Type its props with an inline object type or a named
interface(prefer this over theany-typed pattern seen in older components — see §7's type-safety status and §19). - 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 theLayout.tsx/Topbar.tsxpattern of callinguseGetDataTypeQuery/readinglocalStoragedirectly 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/overviewApior../types/typeare the norm. - No barrel (
index.ts) files/re-exports were found inservices/,components/, orpages/— every import references the concrete file directly. - Import ordering is not enforced by any lint rule (no
eslint-plugin-import/simple-import-sortineslint.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 severalna2/*services type the response as bareany(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 ondata?.res?.…chains — errors there are only caught at runtime. types/type.tsaccumulates 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: anywholesale (CustomTabs.tsx:1,DonutChart.tsx:82-88) rather than a typed interface. noUnusedLocals/noUnusedParameters/noFallthroughCasesInSwitchare enabled intsconfig.app.json:19-22, giving a baseline of dead-code hygiene enforcement at build time (npm run buildrunstsc -bfirst).
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: {...} }forna2/*endpoints vs flat top-level fields for others, §9), always check the raw Network response body shape before assuming a page'sdata?.res?.xvsdata?.xchain is correct. - Since several services are typed
builder.query<any, ...>, TypeScript will not catch a typo in a deeply-nesteddata?.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
interfacerather than following theany-typed pattern seen inCustomTabs.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/XForPDFpairing 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 rawreact-reduxhooks for new code. - Prefer RTK Query
providesTags/invalidatesTags(ascomparisonApi.tsdoes) over manualrefetch()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 existingna2/*services don't — new code should raise the bar, not match the lowest common denominator.
API integration¶
- Add new endpoints via
baseApi.injectEndpointsin a newsrc/services/*.tsfile — never create a secondcreateApiinstance. - Be deliberate about
params(query string) vsbody(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/Suspensefor 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'stransformApiData,ItemDetailTable.tsx's repeatedsortArray/getCellValuecalls) withuseMemoif profiling shows re-render cost, following the existinguseMemopattern already established inPivotTable.tsx.
Error handling¶
- Always destructure and handle
erroralongsideisLoading/datafrom RTK Query hooks in new pages, rather than only guarding onisLoadingas 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
mockUserusage inSidebar.tsxwith the realstate.auth.userbefore 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 devand 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.tsxandApp.tsx— understand the bootstrap, theisLoginPagebranch, and the 24h auto-logoutuseEffect(§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.authand theapireducer's cache entries. - Trigger a page load; watch the Network tab for the
Authorization: Bearerheader and note whether the request usedparamsorbody(§9).
Learn routing & auth¶
- Read
App.tsx's full<Routes>list andcomponents/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
mockUserissue inSidebar.tsx(§21) before relying on role-based menu behavior. - Read
components/PivotTable.tsxif 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 lintbefore 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 inmenuConfig.ts→ title entry inTopbar.tsx'stitleMap→ a newservices/*Api.tsfile.
Submit a change¶
- Run
npm run lintandnpm run build(which runstsc -bfirst) 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 howBrowserRouter'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 inbaseApi.tssuggests 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.pngandclusters.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.jsontoday. - 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-viewand/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-databackend endpoint — the mock's own comment sketches aPOST /api/ask-your-datashape, but the actual backend contract (if implemented) was not visible from this frontend-only review. - Whether
tsconfig.app.jsoninheritsstrict: truefrom a base config not included in the reviewed file set — the file shown does not liststrictexplicitly 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.