True Cost of Water
A dashboard that breaks the cost of water into Raw Water, Energy, and Chemicals & Consumables, with KPI cards, a cost trend line, a cost-distribution donut, and a per-category detail table.
True Cost runs entirely on static dummy data (dummyData/trueCostDummyData.js),
makes no API calls, and has no dataSource, controller, or store layer.
Its route exists only in the demo app. Every figure on the page — amounts,
percentages, totals — is illustrative. Never present this as a live or production
feature.
- It does not read live data. There is no API call, and no
dataSource,controller, orstorelayer to add one to. - It does not exist outside the demo build — neither the route nor the sidebar item.
TRUE_COSTis not a defined permission. The gate isisDemoOption.- It does not compute anything. Amounts and percentages are pre-formatted strings.
- It has no loading, empty, or error states, because nothing is fetched.
At a glance
| Route | /true_cost → TrueCostPage — registered only in apps/demo |
| Available in | demo only |
| Sidebar | A TRUE_COST item in the Efficiency submenu, marked isDemoOption: true |
| Permission | permissionId: 'TRUE_COST' is referenced by the nav config, but TRUE_COST is not defined in the Permissions enum |
| Data | dummyData/trueCostDummyData.js, imported directly |
| Library | libs/trueCost/ — components + a helper, nothing else |
| Charts | recharts — consistent with the app standard |
| State | One local useState object; no context |
Why you only see it in demo
Summary: The real gate is
isDemoOption, not the permission — the sidebar drops any demo-only item when the build is not the demo app. The same flag hides the feature's entry in the feature-locked catalogue (TRUE_COST_OF_WATER).
What's on the page
Selecting a period swaps which slice of the dummy dataset is displayed.
| Area | Component | What it shows | Action |
|---|---|---|---|
| Title bar | SubHeadingView | "True Cost of Water" | — |
| Filter bar | TrueCostFormControl | Current / Custom selector, the period sub-menu, a date picker for custom day and month | Changing a selector re-slices the data. "+ Edit expense" has no handler |
| KPI cards | TrueCostHeaderCards | Total Water Cost, Average Water Cost, Total Water Used | — |
| Cost trend | TrueCostCard → TotalViewGraph / SplitGraphView | A line chart, Total or Split by category | Total/Split radio; in Split, three category checkboxes — unchecking all reverts to Total |
| Cost distribution | TrueCostPiechart | A donut of the three categories with a centred total | — |
| Category detail | TrueCostTableComponent + TrueCostTable | Cost lines for the selected category | Tabs switch category; the sort menu offers A - Z or Cost High to Low |
The dummy data contract
Summary: Two top-level branches, nine periods between them, and the same five blocks inside each — so every selector combination renders the same shape.
Block-by-block shape
| Block | Shape | Feeds |
|---|---|---|
headercard | { totalWaterCost, averageWaterCost, totalWaterUsed } | The three KPI cards |
tableCard | { rawWater, energy, chemicals } | The detail table |
pieChartData | Category totals | The distribution donut |
totalGraph | A single combined series | The trend line in Total view |
splitGraph | Per-category series | The trend line in Split view |
Each tableCard category carries:
| Field | Purpose |
|---|---|
displayName | Heading, including the period, e.g. "Raw Water (Last 7 Days)" |
type[] | The cost-line names |
cost[] | The matching amounts, pre-formatted strings with currency symbols |
totalCost | The category total, also pre-formatted |
distribution[] | Percentage strings |
lastUpdated | Generated from moment() at import time |
Because the amounts are formatted strings rather than numbers, nothing on the page can recompute or re-aggregate them — the display is the data.
Date labels are generated with moment() at module import, so they always read
as relative to today even though the figures never change.
State
All page state lives in a single object in TrueCostPage:
| Field | Purpose |
|---|---|
selection | current or custom |
currentType · customType | Which period within that branch |
selectedMonth | The custom date picker value |
view | total or split |
splitView | { rawWater, energy, chemicals } checkbox state |
tabIndex | The active category tab |
sortOrder · sortText | Sort direction and its button label |
anchorEl | The sort menu anchor |
currentTimePeriod · customTimePeriod | Unused — duplicates of currentType / customType |
helper/trueCostHelper.js supplies the period enum and a set of curried change
handlers. It performs no data access.
Render states
Because the data is a synchronous local object, there is no loading, empty, or error state. The only conditional behaviour is:
| Case | Handling |
|---|---|
| Split view with everything unchecked | Falls back to the Total view |
| Non-array chart data | The donut renders nothing rather than throwing |
| Custom period | The date picker appears for day and month selections |
Visibility & access
Which apps register the route
| App | Path | Renders |
|---|---|---|
demo | /true_cost | TrueCostPage |
production · uwms · rwi · lakepulse | — | Not registered |
Route-level gating: none
The demo app's route element is a bare <TrueCostPage />.
Sidebar gating — and why the permission is not the gate
| Property | Value |
|---|---|
| Nav key | TRUE_COST, inside the Efficiency submenu |
| Display name | True Cost of Water |
permissionId | TRUE_COST — not defined in the Permissions enum |
isDemoOption | true — this is the effective gate |
SideBarMenus drops any item whose isDemoOption is true when constants.isDemo is
false, and FeatureLockedPage skips demo-only rows the same way. So the feature
disappears outside the demo build regardless of permissions. See
Permissions → Sidebar access modes.
Feature-locked catalogue
lockedFeatureHelper carries a TRUE_COST_OF_WATER entry pointing at the same
TRUE_COST permission id, also demo-gated.
Dependencies
Shared store & services
| Import | Used for |
|---|---|
SubPageWrapper · FixedBar | Page shell |
That is the whole list. This feature touches no store, no apiClient, no Urls,
no analytics service, and no navigation helper — a useful signal of how shallow the
prototype is.
Component library
SubPageWrapper, FixedBar, and MUI primitives for the cards, table, tabs, and
filter bar.
Lib-internal
TrueCostHelper (period enum + curried change handlers), trueCostDummyData, and the
eight TrueCost* components. There is deliberately no dataSource, controller,
or store directory.
External npm
recharts (line charts and the donut), @mui/material, moment — the last only to
stamp relative date labels into the dummy data at import time.
Usage examples
Select a slice of the dummy dataset
import { TrueCostHelper } from '@aquagen-mf-webapp/trueCost/helper/trueCostHelper';
import { trueCostDummyData } from '@aquagen-mf-webapp/trueCost/dummyData/trueCostDummyData';
const { CURRENT, LAST_7_DAYS } = TrueCostHelper.TrueCostTimeperiodEnum;
// this is exactly what the page's effect does
const period = trueCostDummyData[CURRENT][LAST_7_DAYS];
period.headercard; // { totalWaterCost, averageWaterCost, totalWaterUsed }
period.tableCard.rawWater; // { displayName, type[], cost[], totalCost, ... }
Read the available periods
Object.keys(trueCostDummyData.current);
// ['last7DaysData', 'month', 'lastMonth', 'last3Months', 'year', 'lastYear']
Object.keys(trueCostDummyData.custom);
// ['days', 'months', 'years']
Wire a change handler the way the page does
const handleStateChange = (key) => (value) =>
setTrueCostState((prev) => ({ ...prev, [key]: value }));
// the helpers are curried: pass the setter, get an event handler back
const onViewChange = TrueCostHelper.handleViewChange(handleStateChange('view'));
const onSplitChange = TrueCostHelper.handleSplitViewChange(
handleStateChange('splitView')
); // reads event.target.name and .checked
Troubleshooting
The page is missing entirely
It is only routed in the demo app, and its sidebar item is isDemoOption: true.
Outside a demo build there is neither a link nor a route.
A permission check against TRUE_COST never matches
TRUE_COST is referenced by the nav and feature-locked configs but is not defined
in libs/shared/src/enums/permissions.js. Gate on constants.isDemo instead.
"+ Edit expense" does nothing
It has no click handler. The button is decorative.
The figures never change
Every number is a hardcoded string in trueCostDummyData.js. Only the date labels
are dynamic, generated by moment() at import.
I cannot recompute the totals
Costs and percentages are stored pre-formatted with currency symbols, so no arithmetic is possible on them. The display is the data.
The trend chart reverted to Total on its own
Unchecking all three category boxes in Split view falls back to Total by design.
There is no loading or error state to test
Correct — the data is a synchronous local import, so no async states exist.
Known issues & gotchas
| Issue | Where | Impact |
|---|---|---|
| The whole feature is fabricated data | dummyData/trueCostDummyData.js | Anyone treating this page as real reporting will be misled. It has no data layer to swap in |
TRUE_COST is not a real permission | The nav config and the feature-locked catalogue both reference permissionId: 'TRUE_COST', but permissions.js does not define it | Permission checks against it can never match a defined key; the effective gate is isDemoOption |
| Sidebar entry can point nowhere | The nav item is registered app-wide but the route exists only in apps/demo | Were the demo flag ever relaxed, the link would resolve to no route |
| "+ Edit expense" is inert | The button is rendered in TrueCostFormControl with no click handler | Looks actionable, does nothing |
| Dead duplicated state | currentTimePeriod and customTimePeriod are initialised and never read | Confusing alongside the fields that are used |
| Redundant props | TrueCostCard receives both splitView and the three showRawWater / showEnergy / showChemicals booleans, plus period and currentType carrying the same value | Two sources of truth for one piece of state |
| Amounts are strings | Costs and percentages are pre-formatted with symbols | No client-side maths is possible; totals cannot be verified |
Code reference
| File | Role |
|---|---|
TrueCostPage.jsx | Orchestrator; selects the dummy-data slice |
dummyData/trueCostDummyData.js | All figures — static |
helper/trueCostHelper.js | Period enum + curried change handlers |
components/TrueCostFormControl.jsx | Period filter bar (and the inert expense button) |
components/TrueCostHeaderCards.jsx | KPI cards |
components/TrueCostCard.jsx | Trend card, Total/Split switching |
components/TotalViewGraph.jsx · SplitGraphView.jsx | The two recharts line views |
components/TrueCostPiechart.jsx | The distribution donut |
components/TrueCostTableComponent.jsx · TrueCostTable.jsx | Category tabs, sort menu, detail table |
Related
- Applications → Products — where the demo app fits
- Water Balance · Water Neutrality · Water Usage Ratio — the real efficiency features
- Components — the recharts standard this prototype follows
- Permissions — and why
TRUE_COSTis not among the defined keys