Skip to main content

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.

What Account Management does NOT do
  • It is not four features. All four screens live in one lib (libs/manageAccount) and share one AccountStore instance 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_v2 is not a live screen — the path exists in the enums and is routed nowhere.

At a glance

Layout route/manage_accountManageAccountPage — provides the store and an <Outlet />
Subpagesindex → AccountLandingPage · account_info · account_settings · manual_node_entry
Librarylibs/manageAccount/
PermissionsACCOUNT_SETTINGS, MANUAL_NODE_ENTRY
Extra gateAn in-memory password re-check for settings and manual entry
Endpoints7 — see Data & APIs
StoresAccountStore (shared by all four) + ManualNodeStore (manual entry only)
Side effectSaving settings refreshes the auth token

The four surfaces

Summary: One layout route owns the store, so all four screens read the same accountScreenData without refetching. Account Info is open; settings and manual entry sit behind a password gate that only the landing page can open.

RouteComponentWhat it isGate
/manage_accountAccountLandingPageOverview cards, subscription dates, the password dialogPermission only
/manage_account/account_infoAccountInfoPageEditable account information + category tablesPermission only
/manage_account/account_settingsAccountSettingPageFive-section settings screenPassword
/manage_account/manual_node_entryManualNodePageHand-entered meter readingsPassword

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 stateisVerifiedForAccountSetting and isVerifiedForManualNodeEntry. Nothing is stored, so the protection is per-mount: navigating away and back means entering the password again.

DetailValue
Verification callLoginController.validateCredentials(username, password), expecting status: 'success'
Username sourceappStore.loginData.username — never typed by the user
Local rejectionA password containing a space fails with "Password shouldn't contains space." before any request
Failure message"Wrong password"
DebounceThe submit handler is debounced by 500 ms
AnalyticsACCOUNT_SETTINGS_CLICK on success
EnforcementEach 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:

HelperBehaviour
Joining dateFormatted DD/MM/YYYY
Next payment dateTakes 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:

idTitleCovers
section1AccountAccount details, logo upload
section2UsersUser list management
section3AlertsAlert email recipients
section4ReportsReport configuration — laid out differently from the others
section5SSOSSO login email allow-list
BehaviourHow it works
Section navigationTabs plus a scroll-spy over .section nodes; activeSection starts at section1
Deep linklocation.state.scrollToSection scrolls to that section, then the state is cleared with a replace navigation
Unsaved-change guardReact Router's useBlocker intercepts navigation and shows AccountSettingsExitCard
SavePOST accounts/settings
After saveThe controller refreshes the auth token — see below
Logo uploadA separate multipart call, field name image
Saving settings refreshes your auth token

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:

FunctionRule
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

meterTypeValue labelSave allowed when
RS485Flow TotaliserThe new value is strictly greater than the last recorded value — a totaliser only counts up
PULSETotal ConsumptionThe 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

ControlConstraint
DateCustomSingleDatePicker with minDate = the unit's createdOn — you cannot record a reading before the previous one
TimeMUI TimePicker, defaulting to now, snapped by a rounding helper
Same-day ruleWhen 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

ActionMethodPathNotes
Account dataGETaccounts/landingpageFetched once on layout mount
Update account infoPATCHaccounts/infoHardcoded path, not from Urls; no try/catch
Last updated timeGETaccounts/info/lastupdatedTakes a path param, reads data.lastupdated
Update settingsPOSTaccounts/settingsTriggers a token refresh on success
Upload logoPOST/accounts/logoupdateapiClient.postFile, multipart field image
Manual node dataGETvirtual/device_dataReturns response.data.datadouble-nested
Save manual nodesPOST/virtual/device_dataBody 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:

FieldPurpose
accountScreenDataThe reshaped accounts/landingpage response
lastUpdatedTimePer-section timestamps
updatedOperationsPending account-info edits, sent as one PATCH
isVerifiedForAccountSettingPassword gate for settings
isVerifiedForManualNodeEntryPassword gate for manual entry
isLoadingShared loading flag

Actions: fetchAccountData, fetchLastUpdatedTime, clearLastUpdatedTime, updateAccountInfo.

ManualNodeStore — mounted only on the manual-entry page:

FieldPurpose
manualNodeDataThe units and their last readings
newValuesKeyed map of pending edits; posted as its values array
saveButtonDisableDerived from ManualNodeHelper.validateValues
showMessage{ message, color } banner after a save
fullScreenLoaderCovers the save round-trip

Analytics

EventWhen
PAGE_VIEWLayout mount, with the third argument true
ACCOUNT_SETTINGS_CLICKA 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

LayerMechanismBypassable by URL?
Card visibilityPermissionWrapper on the landing pageYes — the routes are unguarded
ACCOUNT_SETTINGSPermission on the settings cardYes
MANUAL_NODE_ENTRYPermission on the manual-entry cardYes
Password re-checkIn-memory flag checked on mountNo — 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

ImportUsed for
AppStoreloginData (username, refresh token), setMenuIconEnabled
apiClient · UrlsSix of the seven calls — the account-info PATCH hardcodes its path
apiClient.postFileThe logo upload
LoginControllervalidateCredentials for the gate, refreshToken after a settings save
AnalyticsService · AnalyticEventsPAGE_VIEW, ACCOUNT_SETTINGS_CLICK
NavigationHelperThe 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

Verified against the code
IssueWhereImpact
Account-info PATCH is unprotected and hardcodedupdateAccountInfo uses the literal string 'accounts/info' and has no try/catch, while every sibling call has bothA failed save rejects out of the store, leaving isLoading stuck true
Manual-entry validation can throwvalidateValues does a map lookup by unitId then reads .meterType with no null checkA pending edit for a unit missing from the fetched list crashes the validation pass
Validation returns undefined for empty inputThe same function returns early when either array is emptysaveButtonDisable becomes falsy, so Save can enable with nothing validated
roundToNearest30Minutes rounds to 5It computes Math.round(minutes / 5) * 5The name is off by a factor of six; do not trust it as a 30-minute snap
getMinTime guard does not guardIt assigns a fallback when the inputs are not moments, then calls .isSame(...) on them anywayThrows on non-moment input instead of degrading
Duplicated unit logicThe manual-entry card inlines the flowFactor === 1000 check instead of calling getUnitFromFlowFactorTwo places to change if the mapping moves
Dead account_settings_v2The path and nav option exist in the enums, routed nowhere, referenced by nothingMisleading when tracing account routes
The password gate is the only real guardThe routes carry no PermissionWrapper; permissions only hide landing-page cardsRevoking a permission alone does not block direct URL access
SSO TLD limitThe email regex caps the TLD at four charactersValid addresses on longer TLDs are rejected
Inconsistent return typerearrangeAccountData returns [] on falsy input but an object otherwise, and the caller spreads the resultUnreachable today because the caller guards first, but misleading
Product copy error"Password shouldn't contains space."User-visible grammatical error
Typo in a helper namegetNextPaymenyDateCosmetic, but grep-hostile

Code reference

FileRole
ManageAccountPage.jsxLayout route — store provider, <Outlet />, PAGE_VIEW
store/AccountStore.jsShared state, the two verification flags, account fetch
store/ManualNodeStore.jsManual-entry state, the mount guard, save flow
controller/accountController.jsResponse reshaping, token refresh after a settings save
controller/manualNodeController.jsWraps readings as { unitDetails }
dataSource/account.jsAll seven calls
subPages/AccountLandingPage.jsxOverview cards + the password dialog
subPages/AccountInfoPage.jsxThin shell around the info panel
subPages/AccountSettingPage.jsxFive sections, scroll spy, exit blocker
subPages/ManualNodePage.jsxDate/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.jsMeter types, unit labels, validation
helpers/ssoHelper.jsEmail validation and row handling
hooks/useSectionObserver.jsScroll-spy for the settings sections
Two helper directories

This lib has both helper/ (manual node) and helpers/ (SSO). Not a bug, but easy to mistype when adding files.