Account Management
Everything under /manage_account — the account overview, editable account
information, the password-gated Account Settings screen (users, alert emails,
reports, SSO), and Manual Node Entry for hand-entering meter readings.
- It is not four features. All four screens live in one lib
(
libs/manageAccount) and share oneAccountStoreinstance provided by the layout route. - Account Settings and Manual Node Entry are not reachable by URL. Both redirect to the landing page unless you pass the password check in the current session.
- The password check is not persisted. It lives in memory and is cleared on unmount, so a page refresh always sends you back.
- Manual Node Entry does not accept arbitrary values. Totaliser readings must increase, and you cannot back-date before the last reading.
account_settings_v2is not a live screen — the path exists in the enums and is routed nowhere.
At a glance
| Layout route | /manage_account → ManageAccountPage — provides the store and an <Outlet /> |
| Subpages | index → AccountLandingPage · account_info · account_settings · manual_node_entry |
| Library | libs/manageAccount/ |
| Permissions | ACCOUNT_SETTINGS, MANUAL_NODE_ENTRY |
| Extra gate | An in-memory password re-check for settings and manual entry |
| Endpoints | 7 — see Data & APIs |
| Stores | AccountStore (shared by all four) + ManualNodeStore (manual entry only) |
| Side effect | Saving settings refreshes the auth token |
The four surfaces
Summary: One layout route owns the store, so all four screens read the same
accountScreenDatawithout refetching. Account Info is open; settings and manual entry sit behind a password gate that only the landing page can open.
| Route | Component | What it is | Gate |
|---|---|---|---|
/manage_account | AccountLandingPage | Overview cards, subscription dates, the password dialog | Permission only |
/manage_account/account_info | AccountInfoPage | Editable account information + category tables | Permission only |
/manage_account/account_settings | AccountSettingPage | Five-section settings screen | Password |
/manage_account/manual_node_entry | ManualNodePage | Hand-entered meter readings | Password |
The layout route fires PAGE_VIEW once and calls setMenuIconEnabled(false) — the
subpages do not re-fire it.
The password gate
Settings and Manual Node Entry require re-entering your password even though you are already logged in.
Summary: The gate is two booleans in React state —
isVerifiedForAccountSettingandisVerifiedForManualNodeEntry. Nothing is stored, so the protection is per-mount: navigating away and back means entering the password again.
| Detail | Value |
|---|---|
| Verification call | LoginController.validateCredentials(username, password), expecting status: 'success' |
| Username source | appStore.loginData.username — never typed by the user |
| Local rejection | A password containing a space fails with "Password shouldn't contains space." before any request |
| Failure message | "Wrong password" |
| Debounce | The submit handler is debounced by 500 ms |
| Analytics | ACCOUNT_SETTINGS_CLICK on success |
| Enforcement | Each guarded page redirects to /manage_account if its flag is false, and clears the flag on unmount |
Account Landing
Overview cards plus the password dialog. Two date helpers shape what you see:
| Helper | Behaviour |
|---|---|
| Joining date | Formatted DD/MM/YYYY |
| Next payment date | Takes the stored date and advances it a year at a time until it is in the future — it is derived, not stored |
Account Info
The lightest subpage — AccountInfoPage is a shell around AccountInfoRightSide.
Edits are collected as an operations array (updatedOperations) rather than a
whole object, and sent as a single PATCH when you save. The store clears the array
afterwards.
The controller reshapes the response for the category tables: it walks
AccountInfoPage, builds a pointsArray of { keyName, name, unitsCount } — each
missing field defaulting to 'N/A' — and computes maxUnitsCount across the numeric
counts only.
A last-updated timestamp per section comes from a separate call taking a path
argument and reading data.lastupdated.
Account Settings
Five scroll-linked sections behind the password gate:
id | Title | Covers |
|---|---|---|
section1 | Account | Account details, logo upload |
section2 | Users | User list management |
section3 | Alerts | Alert email recipients |
section4 | Reports | Report configuration — laid out differently from the others |
section5 | SSO | SSO login email allow-list |
| Behaviour | How it works |
|---|---|
| Section navigation | Tabs plus a scroll-spy over .section nodes; activeSection starts at section1 |
| Deep link | location.state.scrollToSection scrolls to that section, then the state is cleared with a replace navigation |
| Unsaved-change guard | React Router's useBlocker intercepts navigation and shows AccountSettingsExitCard |
| Save | POST accounts/settings |
| After save | The controller refreshes the auth token — see below |
| Logo upload | A separate multipart call, field name image |
accountSettingsUpdate calls LoginController.refreshToken(loginData.refreshToken, setLoginData)
on success, because settings can change branding and permissions carried in the JWT.
A settings save is therefore not read-only with respect to session state — expect
loginData to be replaced underneath any component holding it.
SSO email rules
helpers/ssoHelper.js provides the validation used by the SSO section:
| Function | Rule |
|---|---|
isValidEmail(email) | Regex ^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$, case-insensitive |
isRegisteredEmail(email, originalSSOUsers) | Matches an active existing user with that exact address |
isDuplicateEmail(email, users, currentIndex) | Rejects a repeat within the form, ignoring the row being edited |
handleEmailChange(...) | Updates the row, recomputes errors, and flags that changes happened |
Note the TLD is capped at four characters, so addresses on longer TLDs such as
.museum or .travel are rejected as invalid.
Manual Node Entry
For units without live telemetry, an operator records the reading by hand.
Summary: Validation runs on every change and drives a disabled Save button rather than an error message — so an invalid reading simply makes the button unclickable, with nothing on screen explaining why.
Meter types and their rules
meterType | Value label | Save allowed when |
|---|---|---|
RS485 | Flow Totaliser | The new value is strictly greater than the last recorded value — a totaliser only counts up |
PULSE | Total Consumption | The new value is not negative |
The value label is composed as "{valueType} Value in {unit}", where the unit is
Kilo Liters when flowFactor is 1000 and Liters otherwise. An unknown meter
type falls back to "Flow Totaliser".
Date and time constraints
| Control | Constraint |
|---|---|
| Date | CustomSingleDatePicker with minDate = the unit's createdOn — you cannot record a reading before the previous one |
| Time | MUI TimePicker, defaulting to now, snapped by a rounding helper |
| Same-day rule | When the selected date equals createdOn, the minimum selectable time is that timestamp; on any other day there is no minimum |
Each card shows Last Updated: <value> <unit> on <createdOn> for context. Leaving
with unsaved values raises an unsaved-changes dialog.
Data & APIs
| Action | Method | Path | Notes |
|---|---|---|---|
| Account data | GET | accounts/landingpage | Fetched once on layout mount |
| Update account info | PATCH | accounts/info | Hardcoded path, not from Urls; no try/catch |
| Last updated time | GET | accounts/info/lastupdated | Takes a path param, reads data.lastupdated |
| Update settings | POST | accounts/settings | Triggers a token refresh on success |
| Upload logo | POST | /accounts/logoupdate | apiClient.postFile, multipart field image |
| Manual node data | GET | virtual/device_data | Returns response.data.data — double-nested |
| Save manual nodes | POST | /virtual/device_data | Body wrapped as { unitDetails: [...] } |
The two virtual-node URLs differ only by a leading slash.
Store shapes
AccountStore — provided by the layout route, shared by all four screens:
| Field | Purpose |
|---|---|
accountScreenData | The reshaped accounts/landingpage response |
lastUpdatedTime | Per-section timestamps |
updatedOperations | Pending account-info edits, sent as one PATCH |
isVerifiedForAccountSetting | Password gate for settings |
isVerifiedForManualNodeEntry | Password gate for manual entry |
isLoading | Shared loading flag |
Actions: fetchAccountData, fetchLastUpdatedTime, clearLastUpdatedTime,
updateAccountInfo.
ManualNodeStore — mounted only on the manual-entry page:
| Field | Purpose |
|---|---|
manualNodeData | The units and their last readings |
newValues | Keyed map of pending edits; posted as its values array |
saveButtonDisable | Derived from ManualNodeHelper.validateValues |
showMessage | { message, color } banner after a save |
fullScreenLoader | Covers the save round-trip |
Analytics
| Event | When |
|---|---|
PAGE_VIEW | Layout mount, with the third argument true |
ACCOUNT_SETTINGS_CLICK | A successful password verification |
Visibility & access
Route registration
/manage_account and its four children form a nested route group with
ManageAccountPage as the layout element.
Route-level gating: none
No child is wrapped in PermissionWrapper. Access is governed by the landing page's
cards, the two permissions, and the password gate.
The gates, in order
| Layer | Mechanism | Bypassable by URL? |
|---|---|---|
| Card visibility | PermissionWrapper on the landing page | Yes — the routes are unguarded |
ACCOUNT_SETTINGS | Permission on the settings card | Yes |
MANUAL_NODE_ENTRY | Permission on the manual-entry card | Yes |
| Password re-check | In-memory flag checked on mount | No — the guarded pages redirect |
So the password gate, not the permission, is what actually stops a direct URL hit.
Dead navigation entry
NavigationRoutes.ACCOUNT_SETTINGS_V2 (/monitoring/manage_account/account_settings_v2)
and NavigationOptions.accountSettingsV2 exist in the enums but are routed
nowhere and referenced by no component.
Dependencies
Shared store & services
| Import | Used for |
|---|---|
AppStore | loginData (username, refresh token), setMenuIconEnabled |
apiClient · Urls | Six of the seven calls — the account-info PATCH hardcodes its path |
apiClient.postFile | The logo upload |
LoginController | validateCredentials for the gate, refreshToken after a settings save |
AnalyticsService · AnalyticEvents | PAGE_VIEW, ACCOUNT_SETTINGS_CLICK |
NavigationHelper | The redirect target for failed gate checks |
Component library
SubPageWrapper, If / IfNot, PermissionWrapper, CustomSingleDatePicker
(the legacy picker — see Components), plus MUI
primitives and @mui/x-date-pickers' TimePicker.
Lib-internal
AccountStoreContextProvider, ManualNodeContextProvider, AccountController,
ManualNodeController, AccountDataSource, ManualNodeHelper, SSOHelper,
useSectionObserver, and the accountInfo/, accountSettings/, and
manualNodeEntry/ component groups.
External npm
moment, lodash, @mui/material, @mui/x-date-pickers, react-router-dom
(useBlocker). No charting library.
Usage examples
Read shared account data
import { useContext } from 'react';
import { AccountStoreContext } from '@aquagen-mf-webapp/manageAccount/store/AccountStore';
function CategorySummary() {
const { accountScreenData, isLoading } = useContext(AccountStoreContext);
if (isLoading || !accountScreenData) return null;
// pointsArray and maxUnitsCount are added by the controller
return accountScreenData.pointsArray.map((p) => (
<div key={p.keyName}>{p.name}: {p.unitsCount}</div>
));
}
Save queued account-info edits
const { updateAccountInfo } = useContext(AccountStoreContext);
// edits accumulate in updatedOperations, then go out as one PATCH
await updateAccountInfo(); // sends the queued operations, then clears them
Check the password gate before rendering
const { isVerifiedForAccountSetting } = useContext(AccountStoreContext);
// this is exactly what AccountSettingPage does on mount
if (!isVerifiedForAccountSetting) {
navigate(`../../${NavigationHelper.i.routes.MANAGE_ACCOUNT}`);
}
Validate a manual reading
import { ManualNodeHelper } from '@aquagen-mf-webapp/manageAccount/helper/manualNodeHelperInstance';
// returns TRUE to DISABLE the save button
const disabled = ManualNodeHelper.i.validateValues(
Object.values(newValues), // [{ unitId, value }]
manualNodeData // [{ unitId, value, meterType }]
);
ManualNodeHelper.i.getValueTypeWithUnit('RS485', 1000);
// 'Flow Totaliser Value in Kilo Liters'
ManualNodeHelper.i.getUnitFromFlowFactor(1);
// 'Liters'
Submit manual readings
import { ManualNodeContext } from '@aquagen-mf-webapp/manageAccount/store/ManualNodeStore';
const { newValues, setNewValues, postManualNodeData } = useContext(ManualNodeContext);
setNewValues({ ...newValues, [unitId]: { unitId, value, date, time } });
await postManualNodeData(); // wraps as { unitDetails: [...] }, refetches, then clears
Validate an SSO address
import { SSOHelper } from '@aquagen-mf-webapp/manageAccount/helpers/ssoHelper';
SSOHelper.isValidEmail('ops@example.com'); // true
SSOHelper.isDuplicateEmail(email, users, rowIndex); // true blocks the row
SSOHelper.isRegisteredEmail(email, originalSSOUsers); // matches active users only
Troubleshooting
Opening /manage_account/account_settings bounces me to the landing page
Expected. The page checks isVerifiedForAccountSetting on mount, and that flag only
becomes true by passing the password dialog in the current mount. The same applies
to manual node entry.
I passed the password, refreshed, and lost access
The flags live in React state and are explicitly reset on unmount. There is no persistence, by design.
The password dialog rejects a correct password
Check for a space. A password containing one is rejected locally, before any request, with "Password shouldn't contains space."
My permissions or logo changed after saving settings
Saving triggers a token refresh, so loginData is replaced. Components holding a
stale copy need to re-read it.
The Save button on manual entry stays greyed out
Validation returns "disable" when a totaliser (RS485) reading is not greater than
the last one, or a pulse (PULSE) reading is negative. Nothing explains this on
screen — compare your value against the "Last Updated" line on the card.
The Save button is enabled but nothing has been validated
The validation effect skips entirely while newValues is empty, so the initial state
is not recomputed until the first edit.
An SSO address is rejected as invalid
The regex caps the TLD at four characters. Longer TLDs fail regardless of how valid the address is.
I cannot back-date a manual reading
minDate is the unit's createdOn. Readings only move forward in time.
An account-info save failed silently, and the page stayed busy
updateAccountInfo in the data source has no try/catch, unlike its six
siblings. The rejection propagates out of the store's updateAccountInfo, which does
not catch it either, so setIsLoading(false) never runs.
Known issues & gotchas
| Issue | Where | Impact |
|---|---|---|
| Account-info PATCH is unprotected and hardcoded | updateAccountInfo uses the literal string 'accounts/info' and has no try/catch, while every sibling call has both | A failed save rejects out of the store, leaving isLoading stuck true |
| Manual-entry validation can throw | validateValues does a map lookup by unitId then reads .meterType with no null check | A pending edit for a unit missing from the fetched list crashes the validation pass |
Validation returns undefined for empty input | The same function returns early when either array is empty | saveButtonDisable becomes falsy, so Save can enable with nothing validated |
roundToNearest30Minutes rounds to 5 | It computes Math.round(minutes / 5) * 5 | The name is off by a factor of six; do not trust it as a 30-minute snap |
getMinTime guard does not guard | It assigns a fallback when the inputs are not moments, then calls .isSame(...) on them anyway | Throws on non-moment input instead of degrading |
| Duplicated unit logic | The manual-entry card inlines the flowFactor === 1000 check instead of calling getUnitFromFlowFactor | Two places to change if the mapping moves |
Dead account_settings_v2 | The path and nav option exist in the enums, routed nowhere, referenced by nothing | Misleading when tracing account routes |
| The password gate is the only real guard | The routes carry no PermissionWrapper; permissions only hide landing-page cards | Revoking a permission alone does not block direct URL access |
| SSO TLD limit | The email regex caps the TLD at four characters | Valid addresses on longer TLDs are rejected |
| Inconsistent return type | rearrangeAccountData returns [] on falsy input but an object otherwise, and the caller spreads the result | Unreachable today because the caller guards first, but misleading |
| Product copy error | "Password shouldn't contains space." | User-visible grammatical error |
| Typo in a helper name | getNextPaymenyDate | Cosmetic, but grep-hostile |
Code reference
| File | Role |
|---|---|
ManageAccountPage.jsx | Layout route — store provider, <Outlet />, PAGE_VIEW |
store/AccountStore.js | Shared state, the two verification flags, account fetch |
store/ManualNodeStore.js | Manual-entry state, the mount guard, save flow |
controller/accountController.js | Response reshaping, token refresh after a settings save |
controller/manualNodeController.js | Wraps readings as { unitDetails } |
dataSource/account.js | All seven calls |
subPages/AccountLandingPage.jsx | Overview cards + the password dialog |
subPages/AccountInfoPage.jsx | Thin shell around the info panel |
subPages/AccountSettingPage.jsx | Five sections, scroll spy, exit blocker |
subPages/ManualNodePage.jsx | Date/time pickers, per-unit cards, unsaved dialog |
components/accountInfo/* | Info tables, password card, save panel |
components/accountSettings/* | The five sections, save/cancel, exit card |
components/accountSettings/ssologin/* | SSO section, rows, dialogs |
components/manualNodeEntry/* | Entry cards and dialogs |
helper/manualNodeHelperInstance.js | Meter types, unit labels, validation |
helpers/ssoHelper.js | Email validation and row handling |
hooks/useSectionObserver.js | Scroll-spy for the settings sections |
This lib has both helper/ (manual node) and helpers/ (SSO). Not a bug, but easy to
mistype when adding files.
Related
- Permissions —
ACCOUNT_SETTINGS,MANUAL_NODE_ENTRY, and the sidebar access modes - Authentication —
validateCredentialsand the refresh-token flow this page reuses - Reports — configured from the Reports section of settings
- Alerts & Notifications — recipients configured from the Alerts section
- Components —
CustomSingleDatePickeris the legacy picker used here - Application Routes · API Call Flow