This commit is contained in:
seb
2026-11-02 00:19:03 +01:00
parent 0181e08657
commit bc284c6d42
10 changed files with 468 additions and 7 deletions

View File

@@ -15,6 +15,7 @@
* - Brands (`brands`, `filteredBrands`, `brandFilter`)
* - Product attributes & values (`attributes`)
* - Payment accounts (`accounts`)
* - Members & pending invites (`members`, `invites`; admin only, loaded on demand)
*
* Lives with the 1st-level navigation shell (`AppShell`) and triggers when
* `authAutomat.state.hasCompany` is true (`hasCompany` requires `hasAuth`).
@@ -39,6 +40,8 @@ import {
attributename as attributenameApi,
attributevalue as attributevalueApi,
account as accountApi,
companyuser as companyuserApi,
autoinvite as autoinviteApi,
} from '../api';
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
import { normalizeCompany } from '../utils/companyForm';
@@ -330,6 +333,14 @@ const initialState = {
accounts: [],
accountsStatus: 'idle',
accountsError: null,
// 14. Members & invites (admin only; never part of loadAll)
members: [],
membersStatus: 'idle',
membersError: null,
invites: [],
invitesStatus: 'idle',
invitesError: null,
};
let companyAutomat;
@@ -350,6 +361,8 @@ let inflightCarriers = null;
let inflightBrands = null;
let inflightAttributes = null;
let inflightAccounts = null;
let inflightMembers = null;
let inflightInvites = null;
// #endregion
// #region internal state updaters & fetchers
@@ -565,6 +578,8 @@ const actions = {
inflightBrands = null;
inflightAttributes = null;
inflightAccounts = null;
inflightMembers = null;
inflightInvites = null;
companyAutomat.setState({
...initialState,
viewerPosition: companyAutomat.state.viewerPosition,
@@ -1709,6 +1724,122 @@ const actions = {
return accounts;
},
// #endregion
// #region 14. Members (companyuser) & invites (autoinvite) — admin only
// MARK: calls companyuser.* / autoinvite.* from ../api/index.js
loadMembers() {
if (inflightMembers) return inflightMembers;
inflightMembers = (async () => {
companyAutomat.setState({ membersStatus: 'loading', membersError: null });
try {
const res = await companyuserApi.list();
const members = requireArray(res?.companyusers, 'companyusers');
companyAutomat.setState({ members, membersStatus: 'ready', membersError: null });
return members;
} catch (error) {
companyAutomat.setState({ membersStatus: 'failed', membersError: error });
throw error;
} finally {
inflightMembers = null;
}
})();
return inflightMembers;
},
async reloadMembers() {
try {
const res = await companyuserApi.list();
const members = requireArray(res?.companyusers, 'companyusers');
companyAutomat.setState({ members, membersStatus: 'ready', membersError: null });
return members;
} catch (error) {
companyAutomat.setState({ membersError: error });
throw error;
}
},
ensureMembersLoaded() {
if (companyAutomat.state.membersStatus === 'ready') {
return Promise.resolve(companyAutomat.state.members);
}
return actions.loadMembers();
},
/** Adds an already registered user by email. Rejects with 404/409 from the backend. */
async addMember(email, role = 'user') {
const res = await companyuserApi.create({ email, role });
const member = requireObject(res?.companyuser, 'companyuser');
companyAutomat.setState({ members: [...companyAutomat.state.members, member] });
return member;
},
/** Rejects with 409 when demoting the last admin. */
async setMemberRole(id, role) {
const res = await companyuserApi.setRole(id, role);
const member = requireObject(res?.companyuser, 'companyuser');
companyAutomat.setState({
members: companyAutomat.state.members.map((m) => (m.id === member.id ? member : m)),
});
return member;
},
/** Rejects with 409 when removing the last admin. */
async removeMember(id) {
await companyuserApi.delete(id);
companyAutomat.setState({ members: companyAutomat.state.members.filter((m) => m.id !== id) });
},
loadInvites() {
if (inflightInvites) return inflightInvites;
inflightInvites = (async () => {
companyAutomat.setState({ invitesStatus: 'loading', invitesError: null });
try {
const res = await autoinviteApi.list();
const invites = requireArray(res?.autoinvites, 'autoinvites');
companyAutomat.setState({ invites, invitesStatus: 'ready', invitesError: null });
return invites;
} catch (error) {
companyAutomat.setState({ invitesStatus: 'failed', invitesError: error });
throw error;
} finally {
inflightInvites = null;
}
})();
return inflightInvites;
},
async reloadInvites() {
try {
const res = await autoinviteApi.list();
const invites = requireArray(res?.autoinvites, 'autoinvites');
companyAutomat.setState({ invites, invitesStatus: 'ready', invitesError: null });
return invites;
} catch (error) {
companyAutomat.setState({ invitesError: error });
throw error;
}
},
ensureInvitesLoaded() {
if (companyAutomat.state.invitesStatus === 'ready') {
return Promise.resolve(companyAutomat.state.invites);
}
return actions.loadInvites();
},
/** Rejects with 409 when the email is already registered (use addMember then). */
async createInvite(email, role = 'user') {
const res = await autoinviteApi.create({ email, role });
const invite = requireObject(res?.autoinvite, 'autoinvite');
companyAutomat.setState({ invites: [...companyAutomat.state.invites, invite] });
return invite;
},
async deleteInvite(id) {
await autoinviteApi.delete(id);
companyAutomat.setState({ invites: companyAutomat.state.invites.filter((i) => i.id !== id) });
},
// #endregion
};
/**
@@ -1807,6 +1938,15 @@ sseAutomat.actions.on('attributename', () => {
sseAutomat.actions.on('account', () => {
void companyAutomat.actions.reloadAccounts().catch(() => {});
});
// Admin-only lists: only refresh once an admin view has loaded them.
sseAutomat.actions.on('companyuser', () => {
if (companyAutomat.state.membersStatus !== 'ready') return;
void companyAutomat.actions.reloadMembers().catch(() => {});
});
sseAutomat.actions.on('autoinvite', () => {
if (companyAutomat.state.invitesStatus !== 'ready') return;
void companyAutomat.actions.reloadInvites().catch(() => {});
});
sseAutomat.actions.on('company', () => {
void companyAutomat.actions.reload().catch(() => {});
});
@@ -1885,6 +2025,16 @@ export const selectAttributeStatus = () => companyAutomat.state.attributesStatus
// Accounts selectors
export const selectAccounts = () => companyAutomat.state.accounts;
export const selectAccountsStatus = () => companyAutomat.state.accountsStatus;
// Own role in the selected company ('admin' | 'user' | undefined)
export const selectCompanyRole = () => companyAutomat.state.current?.role;
export const selectIsCompanyAdmin = () => companyAutomat.state.current?.role === 'admin';
// Members & invites selectors (admin only)
export const selectMembers = () => companyAutomat.state.members;
export const selectMembersStatus = () => companyAutomat.state.membersStatus;
export const selectInvites = () => companyAutomat.state.invites;
export const selectInvitesStatus = () => companyAutomat.state.invitesStatus;
// #endregion
export default companyAutomat;