Reports
Generate and download water-data reports. Pick a report, configure the date, interval, units, and format, then download or preview. The report itself is rendered by the server — the frontend only ever builds a URL.
At a glance
| Route | /reports → ReportsV2Page + subpages (libs/reportsV2/) |
| Available in | production demo |
| Sidebar gate | The DISABLE_REPORTS permission; individual reports are gated per service and per permission |
| Route guard | None — the route element is not wrapped |
| Endpoint | GET {baseUrl}report — one endpoint for everything |
| Client libraries | No charting, PDF or Excel library — rendering is entirely server-side |
| Delivery | window.open for files, an <iframe> for previews |
| Unit list source | Your stored login response, not an API |
- It does not render anything. No charting, PDF, or spreadsheet library is installed for this feature — the server produces the document.
- It does not use
apiClient. The URL is assembled by hand and handed towindow.openor an<iframe>, which is why the JWT travels in the query string. - It does not fetch the unit list. Units come from your stored login response.
- It does not report server errors. Nothing reads the response, so a failure appears as a blank tab or a broken preview.
legacyReportsis not a fallback — the directory contains no source files.
Which reports library is which
| Library | Status | Where it runs | What it is |
|---|---|---|---|
reportsV2 | Current | production, demo | Routed multi-page generator — this page |
reports | Standalone apps | rwi, uwms, lakepulse | Older single-page tabbed generator against the same backend |
legacyReports | Dead | Nowhere | Contains no source files at all |
The standalone reports lib builds its URL in ReportController.js and opens it the
same way, but gates on NOT_GENERAL_REPORT / EXECUTIVE_SUMMARY_REPORT —
keys reportsV2 never uses — and adds format=v2 conditionally rather than
always.
Three ways in
Summary: Every route converges on one URL builder. The only thing that distinguishes a download from a preview is the
reportFormatvalue.
| Path | Subpage | What it does |
|---|---|---|
/reports | ReportMainSelectionPage | The chooser — frequent cards, executive cards, "create new" |
/reports/frequent_reports/:reportId | FrequentReports | A preset with inline-editable fields |
/reports/summary_report | SummaryReportPage | Daily / Weekly / Monthly summary — PDF only |
/reports/water_balance | WaterBalanceReportPage | Daily / Monthly water-balance report — Excel |
/reports/new_report | NewReportPage | The four-step wizard |
The new-report wizard
Summary: Four steps, each validated before you can continue. Rain Water selects every unit automatically and skips the unit step entirely.
| Step | Choices | Notes |
|---|---|---|
| 1. Report type | Flowmeter · Stock · GW Level · Quality · Rain Water · Energy | Filtered by your services; Rain Water additionally needs RWI |
| 2. Time & date | Every 5 Minutes · Every Hour · Every Day · Every Month, then single or multiple dates | Each interval carries its own range cap |
| 3. Categories & units | Searchable unit picker, or select-all | Skipped for Rain Water |
| 4. Format & export | Excel or PDF, plus a live preview | Preview goes stale if you change params |
A breadcrumb of chips (StepSummaryChips) shows the chosen service, interval,
units, and format as you go. Continue runs validateParams, which returns the list
of missing field labels and renders them as an inline error — unitIds counts as
satisfied when "select all" is on.
Parameter vocabulary
service — the report family
| Key | Value |
|---|---|
WATER | water |
WATER_CONSOLIDATED | consolidated |
QUALITY | quality |
LEVEL | level |
SUMMARY | summary |
WATER_BALANCE | water_balance |
BOREWELL | borewell |
RAIN_WATER | rain_water |
ENERGY | energy |
ENERGY_CONSOLIDATED | energy_consolidated |
EXECUTIVE_SUMMARY | executive |
reportType and reportFormat
reportType | Value |
|---|---|
GRANULAR | granular |
HOURLY | hourly |
DAILY | daily |
DAILY_RANGE | daily_range |
MONTHLY | monthly |
MONTHLY_RANGE | monthly_range |
WEEKLY | custom — the one key whose value doesn't match its name |
reportFormat | Value | Behaviour |
|---|---|---|
EXCEL | xlsx | Download |
PDF | pdf | Download |
PREVIEW | html | Rendered in an <iframe> |
Only Excel and PDF are offered to the user; html is internal to the preview pane.
Interval to report type and range cap
| Interval | Range cap (days/months) | Single date | Multiple dates |
|---|---|---|---|
| Every 5 Minutes | 6 | — | granular |
| Every Hour | 6 | — | hourly |
| Every Day | 30 | daily | daily_range |
| Every Month | 12 | monthly | monthly_range |
Preset and executive report defaults
Frequent reports
| Preset | reportType | Format | Default range | Extra gate |
|---|---|---|---|---|
| Flowmeter Report | granular | Excel | Today | — |
| Rain Water Report | monthly | Excel | Month to date | RWI |
| Water Quality Report | daily | Excel | Today | — |
| Energy Report | monthly | Excel | Week to date | — |
Executive reports
| Report | service | Format | Max date |
|---|---|---|---|
| Daily Summary | summary | Yesterday | |
| Weekly Summary | executive | End of last week | |
| Monthly Summary | executive | End of last month | |
| Daily Water Balance | water_balance | Excel | Yesterday |
| Monthly Water Balance | water_balance | Excel | End of last month |
Note that Daily Summary uses the summary service while Weekly and
Monthly use executive — three summary variants across two services.
The Summary card requires DAILY_SUMMARY; the Water Balance card requires
WATER_BALANCE.
The one endpoint
buildReportUrl(format) in reportStore.jsx assembles the whole thing client-side —
there is no apiClient call anywhere in this feature:
GET {baseUrl}report
?useShift=true
&reportType=<granular|hourly|daily|daily_range|monthly|monthly_range|custom>
&reportFormat=<pdf|xlsx|html>
&service=<water|quality|summary|water_balance|...>
&jwt=<token>
&startDate=<DD/MM/YYYY>
&format=v2
[&endDate=...] [&unitIds=a,b,c] [&startTime=...] [&endTime=...]
The token comes from loginData.token with any Bearer prefix stripped. The four
trailing parameters are appended only when present, and unitIds is comma-joined.
Because the report is fetched by window.open or an <iframe> rather than through
apiClient, the token is passed as a jwt query parameter. It lands in browser
history, and in the logs of anything between the browser and the server. The same
pattern is used by the alerts export.
Visibility & access
Which apps register the route
| App | Library | Shape |
|---|---|---|
production · demo | reportsV2 | /reports with nested subpage routes |
rwi · uwms · lakepulse | reports | /reports as a single tabbed page |
Both register /reports/water_balance — in the standalone apps that path is the
only water-balance surface, since the diagram is not routed
there.
Gating layers
| Layer | Rule |
|---|---|
/reports route | Unguarded |
| Sidebar entry | DISABLE_REPORTS |
| Report type cards | Shown when your services include that category |
| Rain Water | Additionally requires RWI |
| Summary card | Requires DAILY_SUMMARY |
| Water Balance card | Requires WATER_BALANCE |
Standalone reports lib | NOT_GENERAL_REPORT / EXECUTIVE_SUMMARY_REPORT |
Cards that declare a permission are checked through PermissionController.isPermitted;
the rest fall back to a service check.
Render states
| State | What the user sees |
|---|---|
| Missing fields | Continue is blocked and the missing field labels are listed inline |
| Rain Water selected | All units auto-selected, unit step skipped |
| More than 5 units | A non-blocking notice that the report will take longer |
| Preview requested | The report renders in an <iframe> |
| Params changed after preview | A "refresh preview" affordance appears |
| Download | A new tab opens; the file arrives from the server |
| Server error | Surfaces inside the downloaded file or the iframe — there is no client-side error UI |
Dependencies
Shared store & services
| Import | Used for |
|---|---|
Urls.baseUrl | The root of the hand-assembled report URL |
AppStore | loginData.token for the jwt parameter |
PermissionController | isPermitted for cards that declare a permission |
Permissions · StandardCategoryTypeUppercase | Gating vocabulary |
DatepickerEnum | Picker modes per interval |
NavigationHelper | Routing between the chooser and the report subpages |
Notably absent: apiClient. Nothing in this feature makes an XHR — the report is
fetched by the browser itself.
Component library
AppDatePickerSelection, plus MUI primitives for the wizard, cards, chips, and the
preview frame.
Lib-internal
ReportDataProvider / ReportDataContext (params + buildReportUrl) and
ReportHelper, which holds every enum, preset, interval, validation routine, and
display helper in one 500-line module.
External npm
moment, @mui/material, @iconify/react. No charting, PDF, or spreadsheet
library — rendering is entirely server-side, and delivery is window.open or an
<iframe>.
Usage examples
Build a report URL
import { useContext } from 'react';
import { ReportDataContext } from '@aquagen-mf-webapp/reportsV2/store/reportStore';
import { ReportHelper } from '@aquagen-mf-webapp/reportsV2/helper/reportHelper';
function DownloadButton() {
const { buildReportUrl } = useContext(ReportDataContext);
const download = () =>
window.open(buildReportUrl(ReportHelper.ReportFormat.EXCEL), '_blank', 'noreferrer');
const preview = () => buildReportUrl(ReportHelper.ReportFormat.PREVIEW); // 'html'
return <button onClick={download}>Download</button>;
}
Configure params the way a preset does
const { params, setParams } = useContext(ReportDataContext);
setParams({
...params,
serviceType: ReportHelper.ServiceType.WATER, // 'water'
reportType: ReportHelper.ReportType.GRANULAR, // 'granular'
format: ReportHelper.ReportFormat.EXCEL, // 'xlsx'
startDate: '29/07/2026',
endDate: '29/07/2026',
unitIds: ['unit-a', 'unit-b'], // joined with commas in the URL
});
Validate a wizard step
const { selectAllUnits } = useContext(ReportDataContext);
const missing = ReportHelper.validateParams(
params,
['serviceType', 'reportType', 'unitIds', 'format'],
{ selectAllUnits } // 'unitIds' counts as satisfied when select-all is on
);
if (missing.length) return setStepErrors(missing); // array of human labels
Read the breadcrumb display values
ReportHelper.getServiceTypeDisplay(params); // 'Flowmeter'
ReportHelper.getIntervalDisplay(params); // 'Every Day'
ReportHelper.getFormatDisplay(params); // 'Excel'
ReportHelper.getUnitsDisplay(params, selectAllUnits); // 'All Units' or a joined list
Troubleshooting
A blank tab opens and nothing downloads
Nothing in the frontend reads the server's response, so a server-side failure looks exactly like this. Copy the opened URL and inspect the response directly.
getUnitsDisplay throws
It calls JSON.parse(localStorage.getItem('loginResponse')) with no null check, and
uses the raw storage key rather than LocalDBInstance. It fails hard when the session
is missing.
The Rain Water report skipped the unit step
Intentional — Rain Water auto-selects every unit and bypasses step 3.
A report type is missing from the wizard
Type cards are filtered by your services. Rain Water additionally requires RWI.
The Summary or Water Balance card is absent
They require DAILY_SUMMARY and WATER_BALANCE respectively.
The same report is gated differently in AquaRain
The standalone apps run libs/reports, which gates on NOT_GENERAL_REPORT /
EXECUTIVE_SUMMARY_REPORT — keys reportsV2 never uses.
Backend logs show reportType=custom and I never chose that
ReportType.WEEKLY has the value custom. Weekly Summary is the only path that emits
it.
The preview looks stale
Changing params after a preview does not re-fetch it; a refresh affordance appears instead.
Known issues & gotchas
| Issue | Where | Impact |
|---|---|---|
| JWT in the URL | buildReportUrl, and the standalone lib's ReportController | The token is exposed in history and intermediary logs |
Raw localStorage with a literal key | getUnitsDisplay does JSON.parse(localStorage.getItem('loginResponse')), bypassing LocalDBInstance, with no null check | Throws if the session is absent, and duplicates knowledge of the storage key |
| Non-aliased deep import | reportStore.jsx imports Urls from src/services/api/urls instead of the @aquagen-mf-webapp/shared alias | Fragile path that breaks if the shared lib moves |
WEEKLY maps to custom | ReportType.WEEKLY = 'custom' | Easy to misread when correlating frontend and backend logs |
Urls.reportUrl(...) is dead | The helper exists in urls.js but nothing calls it — both live libs build their URL inline | Two competing sources of truth for the same URL shape |
legacyReports is an empty shell | The lib directory contains no source files | Delete candidate; do not build on it |
| No error surface | Nothing inspects the server's response, because nothing reads it | A failed report looks like a blank tab or a broken iframe |
| Two divergent gating vocabularies | reportsV2 uses services + DAILY_SUMMARY / WATER_BALANCE; reports uses NOT_GENERAL_REPORT / EXECUTIVE_SUMMARY_REPORT | The same product gates differently depending on which app you are in |
Code reference
| File | Role |
|---|---|
ReportsV2Page.jsx | Layout shell, <Outlet />, provider |
store/reportStore.jsx | Params + buildReportUrl |
helper/reportHelper.js | Every enum, preset, interval, validation, and display helper |
subpages/ReportMainSelectionPage.jsx | The chooser |
subpages/NewReportPage.jsx · NewReportStepContents.jsx | The wizard and its steps |
subpages/SummaryReportPage.jsx · WaterBalanceReportPage.jsx · FrequentReports.jsx | The dedicated report pages |
components/ReportUnitSelector.jsx | Unit picker + the five-unit notice |
components/Report{Interval,Format,ServiceType}Selector.jsx | Field selectors |
components/ReportPreviewComponent.jsx | The iframe preview |
components/StepSummaryChips.jsx | The breadcrumb chips |
libs/reports/src/controller/ReportController.js | The standalone apps' URL builder |
Related
- Water Balance — the feature behind the water-balance report
- Alerts & Notifications — its export uses this same report service
- Applications → Standalone apps — where the older
reportslib runs - Permissions · Application Routes