From 28d6fece4644a05d8370395b1f6261ae87041427 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 30 Oct 2026 16:22:03 +0100 Subject: [PATCH] u --- src/automats/authAutomat.js | 22 ++++++- .../settings/PosTerminalSettingsPanel.jsx | 58 +++++++++++++++++++ src/components/settings/SettingsDialog.jsx | 55 ++++++++++++++---- 3 files changed, 122 insertions(+), 13 deletions(-) diff --git a/src/automats/authAutomat.js b/src/automats/authAutomat.js index d62deb6..3062b89 100644 --- a/src/automats/authAutomat.js +++ b/src/automats/authAutomat.js @@ -31,6 +31,8 @@ import { auth, company, number as numberApi } from '../api'; * @property {AuthUser|null} user Authenticated user, or `null` for anonymous sessions. * @property {string|null} googleClientId Google Identity Services client ID. * @property {number|null} schemaVersion Database schema version returned by the backend. + * @property {boolean} vhost Whether vhost mode is enabled. + * @property {string|null} vhostSettings Vhost configuration string (e.g. posport:41420;posip:147.135.129.77). * @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state. * @property {boolean} hasAuth True when the session has a valid authenticated user. * @property {boolean} hasCompany True when `hasAuth` is true AND a company is selected. @@ -49,6 +51,8 @@ const initialState = { user: null, googleClientId: null, schemaVersion: null, + vhost: false, + vhostSettings: null, status: 'loading', hasAuth: false, hasCompany: false, @@ -124,6 +128,8 @@ function buildAuthState(user, status, extra = {}) { status, hasAuth, hasCompany, + vhost: authAutomat?.state?.vhost ?? false, + vhostSettings: authAutomat?.state?.vhostSettings ?? null, retryNotification: null, ...extra, }; @@ -143,22 +149,30 @@ function applySession(nextSession) { nextSession.schemaVersion !== undefined ? nextSession.schemaVersion : (authAutomat.state.schemaVersion ?? null); + const vhost = + nextSession.vhost !== undefined + ? Boolean(nextSession.vhost) + : (authAutomat.state.vhost ?? false); + const vhostSettings = + nextSession.vhostSettings !== undefined + ? nextSession.vhostSettings + : (authAutomat.state.vhostSettings ?? null); if (!nextSession.user) { if (authAutomat.state.status !== 'authed') { const nextState = authAutomat.setState( - buildAuthState(null, 'anon', { googleClientId, schemaVersion, retryNotification: null }) + buildAuthState(null, 'anon', { googleClientId, schemaVersion, vhost, vhostSettings, retryNotification: null }) ); syncSse(nextState); } else { - authAutomat.setState({ googleClientId, schemaVersion, retryNotification: null }); + authAutomat.setState({ googleClientId, schemaVersion, vhost, vhostSettings, retryNotification: null }); } return nextSession; } const user = mergeUser(authAutomat.state.user, nextSession.user); const nextState = authAutomat.setState( - buildAuthState(user, 'authed', { googleClientId, schemaVersion, retryNotification: null }) + buildAuthState(user, 'authed', { googleClientId, schemaVersion, vhost, vhostSettings, retryNotification: null }) ); syncSse(nextState); emit({ type: AUTH_SESSION_ESTABLISHED }); @@ -352,6 +366,8 @@ export const selectSelectedCompanyId = () => authAutomat.state.hasCompany ? authAutomat.state.user?.selectedCompanyId ?? null : null; export const selectRetryNotification = () => authAutomat.state.retryNotification; export const selectSchemaVersion = () => authAutomat.state.schemaVersion; +export const selectVhost = () => Boolean(authAutomat.state.vhost); +export const selectVhostSettings = () => authAutomat.state.vhostSettings; /** * Re-reads `GET /api/auth/me` from the server. diff --git a/src/components/settings/PosTerminalSettingsPanel.jsx b/src/components/settings/PosTerminalSettingsPanel.jsx index 2fb5ed8..ad5782a 100644 --- a/src/components/settings/PosTerminalSettingsPanel.jsx +++ b/src/components/settings/PosTerminalSettingsPanel.jsx @@ -19,12 +19,29 @@ import PointOfSaleIcon from '@mui/icons-material/PointOfSale'; import { enqueueSnackbar } from 'notistack'; import BackendError from '../BackendError'; import CategoryPickerDialog from '../article/CategoryPickerDialog'; +import authAutomat from '../../automats/authAutomat'; import companyAutomat from '../../automats/companyAutomat'; import categoryAutomat, { whenCategoryTreeReady } from '../../automats/categoryAutomat'; import { categoryDisplayName } from '../../utils/categoryUtils'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; +function parseVhostSettings(str) { + if (!str || typeof str !== 'string') return {}; + const result = {}; + for (const part of str.split(';')) { + const trimmed = part.trim(); + if (!trimmed) continue; + const colonIdx = trimmed.indexOf(':'); + if (colonIdx !== -1) { + const key = trimmed.slice(0, colonIdx).trim().toLowerCase(); + const val = trimmed.slice(colonIdx + 1).trim(); + result[key] = val; + } + } + return result; +} + function formatDateTime(val) { if (!val) return null; try { @@ -52,6 +69,7 @@ export default class PosTerminalSettingsPanel extends Component { terminals, categories: categoryAutomat.state.categories, rootCategoryId: categoryAutomat.state.rootCategoryId, + vhostSettings: authAutomat.state.vhostSettings, newTerminalName: '', creatingTerminal: false, enableAllForNew: true, @@ -82,6 +100,9 @@ export default class PosTerminalSettingsPanel extends Component { categories: state.categories, rootCategoryId: state.rootCategoryId, })); + this.unsubAuth = authAutomat.subscribe(this, (state) => ({ + vhostSettings: state.vhostSettings, + })); if (categoryAutomat.state.treeStatus === 'idle') { void categoryAutomat.actions.loadTree(); } @@ -90,6 +111,7 @@ export default class PosTerminalSettingsPanel extends Component { componentWillUnmount() { this.unsub?.(); this.unsubCategory?.(); + this.unsubAuth?.(); } async loadTerminals() { @@ -275,6 +297,7 @@ export default class PosTerminalSettingsPanel extends Component { status, error, terminals, + vhostSettings, newTerminalName, creatingTerminal, enableAllForNew, @@ -284,6 +307,11 @@ export default class PosTerminalSettingsPanel extends Component { rootCategoryId, } = this.state; + const parsedVhost = parseVhostSettings(vhostSettings); + const posIp = parsedVhost.posip || parsedVhost.ip || null; + const posPort = parsedVhost.posport || parsedVhost.port || null; + const hasVhostSettings = Boolean(posIp || posPort || vhostSettings); + if (status === 'loading' && terminals.length === 0) { return ( @@ -298,6 +326,36 @@ export default class PosTerminalSettingsPanel extends Component { return ( + {hasVhostSettings && ( + + + Pos Sync IP:{' '} + + {posIp ?? '—'} + + + + Pos Sync Port:{' '} + + {posPort ?? '—'} + + + + )} , Panel: McpAuthorizationsPanel }, ]; +function getDefaultTab(vhost) { + return vhost ? TAB_KEYS.company : TAB_KEYS.health; +} + +function getVisibleTabs(vhost) { + if (!vhost) return TABS; + return TABS.filter( + (t) => t.id !== TAB_KEYS.health && t.id !== TAB_KEYS.backup && t.id !== TAB_KEYS.printers + ); +} + export default class SettingsDialog extends Component { constructor(props) { super(props); + const vhost = Boolean(authAutomat.state.vhost); this.state = { - tab: TAB_KEYS.health, + tab: getDefaultTab(vhost), schemaVersion: authAutomat.state.schemaVersion, + vhost, }; this.handleTabChange = this.handleTabChange.bind(this); } componentDidMount() { - this.unsubAuth = authAutomat.subscribe(this, ['schemaVersion']); + this.unsubAuth = authAutomat.subscribe(this, (state) => ({ + schemaVersion: state.schemaVersion, + vhost: Boolean(state.vhost), + })); } componentWillUnmount() { @@ -73,12 +89,29 @@ export default class SettingsDialog extends Component { componentDidUpdate(prevProps) { const { open } = this.props; + const { vhost } = this.state; if (!open && prevProps.open) { - this.setState({ tab: TAB_KEYS.health }); + this.setState({ tab: getDefaultTab(vhost) }); } if (open && !prevProps.open) { + const currentVhost = Boolean(authAutomat.state.vhost); + const updates = {}; if (this.state.schemaVersion !== authAutomat.state.schemaVersion) { - this.setState({ schemaVersion: authAutomat.state.schemaVersion }); + updates.schemaVersion = authAutomat.state.schemaVersion; + } + if (this.state.vhost !== currentVhost) { + updates.vhost = currentVhost; + } + if ( + currentVhost && + (this.state.tab === TAB_KEYS.health || + this.state.tab === TAB_KEYS.backup || + this.state.tab === TAB_KEYS.printers) + ) { + updates.tab = getDefaultTab(currentVhost); + } + if (Object.keys(updates).length > 0) { + this.setState(updates); } } } @@ -89,7 +122,9 @@ export default class SettingsDialog extends Component { render() { const { open, onClose } = this.props; - const { tab, schemaVersion } = this.state; + const { tab, schemaVersion, vhost } = this.state; + const visibleTabs = getVisibleTabs(vhost); + const activeTab = visibleTabs.some((t) => t.id === tab) ? tab : visibleTabs[0]?.id; const paperSx = { height: '85vh', @@ -146,7 +181,7 @@ export default class SettingsDialog extends Component { > - {TABS.map((t) => ( + {visibleTabs.map((t) => ( - {TABS.map((t) => ( + {visibleTabs.map((t) => ( - + ))}