Files
loyaly-merchant/src/shared/layouts/workspace/account-menu.ts
Suriyakumarvijayanayagam 0b61077e2f Stores, Team and Account run against the platform, not a mock
Stores: each shop's health in one line (offline, losing visits, camera
trouble, faces too poor - in severity order, one verdict), its cameras
as pictures with connection and 'proven to recognise a face' as two
different claims, add/edit/remove camera with the make picker, test
connection and placement checks claimed by the shop PC, and the
enrolment code a new shop PC types. Team: create an account (password
shown once), invite (code shown once), change role, remove access, reset
password, revoke pending invitations - with the rank rules the server
enforces mirrored in what the form offers. Account: who you are and
every device signed in, with 'sign out' per device and everywhere else.

Gone: StoreManagement, SecurityManager and ProfileForm that rendered
hard-coded arrays, the Business form with no backend, the /api/stores
route nothing served, and the cascading account menu's dead links.

Two things found by using the camera form, not by tests: Chrome filled
the operator's own email into 'camera username', and the first camera
saved with a password and no username because autofill wrote to the
input without React seeing it. The dialog sets autocomplete on the real
inputs and reads the credential fields from the DOM at submit.

Every route was exercised against production before this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-18 12:06:28 +05:30

163 lines
5.1 KiB
TypeScript

import {SETTINGS_NAV} from '@/features/settings/config/settingsNav';
import {ICONS} from '@/shared/utils/icons';
import type {IconType} from '@astryxdesign/core/Icon';
/**
* The account menu, as a two-level cascade rather than a flat list.
*
* Panel one is identity plus four branches and a way out. Every branch opens a
* SECOND panel beside the first — it never navigates. Only a leaf navigates,
* and leaves only exist in panel two (and on the sign-out row, which ends the
* session instead).
*
* That distinction is the whole shape of the file: `AccountBranch` carries a
* `panel`, `AccountLeaf` carries a destination or an `action`, and nothing
* carries both. A row cannot accidentally do both things because there is no
* type that permits it.
*/
export type AccountPanelId = 'settings' | 'profile' | 'language' | 'help';
/**
* Rows that resolve to something. `href` routes, `externalHref` opens a tab,
* `action` is handled by the menu itself.
*/
export interface AccountLeaf {
label: string;
icon?: IconType;
/** Shown under the label. Use it for a live value, not for decoration. */
description?: string;
/** Internal destination. */
href?: string;
/** Opens in a new tab instead of routing. Mutually exclusive with href. */
externalHref?: string;
/** Non-navigational rows: the session, and the two help surfaces. */
action?: 'signOut' | 'shortcuts' | 'about';
/** Only sign-out may carry semantic colour — see the monochrome rule. */
tone?: 'danger';
}
/** Rows that open panel two. The one thing they must never do is navigate. */
export interface AccountBranch {
label: string;
icon: IconType;
panel: AccountPanelId;
description?: string;
}
export type AccountRowSpec = AccountLeaf | AccountBranch;
export function isBranch(row: AccountRowSpec): row is AccountBranch {
return 'panel' in row;
}
/**
* Panel one, as two groups separated by a divider.
*
* Four branches and sign-out — deliberately short. Everything that used to sit
* here as a flat list of twelve destinations now lives one level down, where
* it is grouped by what it is rather than stacked by what fit.
*/
export const ACCOUNT_ROOT_GROUPS: AccountRowSpec[][] = [
// Flat: every row is a destination that exists. The cascading panels below
// are unreachable from here and kept only until the menu is simplified.
[
{label: 'Account & devices', icon: ICONS.profile, href: '/settings'},
{label: 'Team', icon: ICONS.staff, href: '/settings/team'},
{label: 'Keyboard shortcuts', icon: ICONS.keyboard, action: 'shortcuts'},
],
[
{
label: 'Log out',
icon: ICONS.signOut,
action: 'signOut',
tone: 'danger',
},
],
];
export interface AccountPanel {
title: string;
rows: AccountLeaf[];
}
/**
* The locales the workspace offers, in the order PreferencesForm lists them.
*
* Display only: there is no locale provider yet, so selecting one marks the
* row and nothing else. The row that actually persists a choice is the
* "Language settings" leaf at the bottom of that panel, which routes to
* Preferences where the Selector writes it.
*/
export interface LocaleOption {
code: 'en' | 'hi' | 'kn' | 'ta';
label: string;
}
export const LOCALE_OPTIONS: LocaleOption[] = [
{code: 'en', label: 'English (US & India)'},
{code: 'hi', label: 'Hindi (हिंदी)'},
{code: 'kn', label: 'Kannada (ಕನ್ನಡ)'},
{code: 'ta', label: 'Tamil (தமிழ்)'},
];
/** The shipped default in PreferencesForm. */
export const DEFAULT_LOCALE: LocaleOption['code'] = 'en';
/**
* Panel two, keyed by the branch that opens it.
*
* `settings` is DERIVED from SETTINGS_NAV rather than restated: the settings
* sub-nav and this panel are the same eleven destinations, and a copy here is
* a copy that goes stale the first time a section is added.
*
* `language` holds only its trailing leaf — the locale rows are stateful and
* come from LOCALE_OPTIONS at render time.
*/
export const ACCOUNT_PANELS: Record<AccountPanelId, AccountPanel> = {
settings: {
title: 'Settings',
rows: SETTINGS_NAV.map(({label, href, icon}) => ({label, href, icon})),
},
profile: {
title: 'Profile',
rows: [
{label: 'Account & devices', icon: ICONS.profile, href: '/settings'},
],
},
language: {
title: 'Language',
rows: [
{
label: 'Language settings',
icon: ICONS.preferences,
href: '/settings',
},
],
},
help: {
title: 'Help',
rows: [
// Placeholder URLs on the brand's own domain — point them at the real
// docs and support hosts when there are any.
{
label: 'Documentation',
icon: ICONS.docs,
externalHref: 'https://loyaly.ai/docs',
},
{label: 'Keyboard Shortcuts', icon: ICONS.keyboard, action: 'shortcuts'},
{
label: 'Report Bug',
icon: ICONS.alert,
externalHref: 'https://loyaly.ai/support/bug',
},
{
label: 'Contact Support',
icon: ICONS.chat,
externalHref: 'https://loyaly.ai/support',
},
{label: 'About Loyaly', icon: ICONS.info, action: 'about'},
],
},
};