u
This commit is contained in:
@@ -131,7 +131,7 @@ Strip supplierName?: str from T11 (resolve via entityId in cached supplier list)
|
||||
|
||||
# Dash API Routes Overview (All)
|
||||
|
||||
Overview of all 307 API endpoints across all modules.
|
||||
Overview of all 314 API endpoints across all modules.
|
||||
|
||||
### Notation
|
||||
|
||||
@@ -158,7 +158,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
||||
- `T16` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
|
||||
- `T17` = `{ id: int, name: str, sort: int, exclusive: bool, values: T30[], articleCount?: int }`
|
||||
- `T18` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: int|null, first_message?: str|null }`
|
||||
- `T19` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }`
|
||||
- `T19` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num, role: str }`
|
||||
- `T20` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null, shippable: bool }`
|
||||
- `T21` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null, shippable?: bool }`
|
||||
- `T22` = `{ id: int, agentprinterId: int, type: str, state: str, data: str, createdAt: str, printeragentId?: int|null, completedAt?: str|null }`
|
||||
@@ -407,6 +407,18 @@ _none_
|
||||
- Params: `body: { companyId: int }`
|
||||
- Returns: `200 { user: T35 }`
|
||||
|
||||
### Section: /autoinvite
|
||||
|
||||
#### List auto-invitations — `GET /autoinvite`
|
||||
_none_
|
||||
- Returns: `200 { autoinvites: { id: int, email: str, role: str, createdAt: str }[] }`
|
||||
#### Create auto-invitation — `POST /autoinvite`
|
||||
- Params: `body: { email: str, role?: str }`
|
||||
- Returns: `201 { autoinvite: { id: int, email: str, role: str } }`
|
||||
#### Delete auto-invitation — `DELETE /autoinvite/{id}`
|
||||
- Params: `id: int`
|
||||
- Returns: `204 void`
|
||||
|
||||
### Section: /backup
|
||||
|
||||
#### List backups and service status — `GET /backup`
|
||||
@@ -548,6 +560,21 @@ _none_
|
||||
- Params: `id: int, body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
|
||||
- Returns: `200 { company: T19 }`
|
||||
|
||||
### Section: /companyuser
|
||||
|
||||
#### List company users — `GET /companyuser`
|
||||
_none_
|
||||
- Returns: `200 { companyusers: { id: int, googleAuthId: int, email: str, role: str }[] }`
|
||||
#### Add an existing user to the company — `POST /companyuser`
|
||||
- Params: `body: { email: str, role?: str }`
|
||||
- Returns: `201 { companyuser: { id: int, googleAuthId: int, email: str, role: str } }`
|
||||
#### Remove a user from the company — `DELETE /companyuser/{id}`
|
||||
- Params: `id: int`
|
||||
- Returns: `204 void`
|
||||
#### Change a company user's role — `PATCH /companyuser/{id}`
|
||||
- Params: `id: int, body: { role: str }`
|
||||
- Returns: `200 { companyuser: { id: int, googleAuthId: int, role: str } }`
|
||||
|
||||
### Section: /customergroup
|
||||
|
||||
#### List customer groups — `GET /customergroup`
|
||||
@@ -654,6 +681,8 @@ _none_
|
||||
- `ledger`: `{}`
|
||||
- `attributename`: `{}`
|
||||
- `propertyname`: `{}`
|
||||
- `companyuser`: `{}`
|
||||
- `autoinvite`: `{}`
|
||||
- `labeltemplate`: `{}`
|
||||
- `printer`: `{}`
|
||||
- `printeragent`: `{}`
|
||||
|
||||
24
src/api/autoinvite.js
Normal file
24
src/api/autoinvite.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { http } from './http.js';
|
||||
|
||||
/**
|
||||
* Pending invites for emails that are not registered yet (admin only).
|
||||
*/
|
||||
export const autoinvite = {
|
||||
// #region GET /api/autoinvite — list pending invites
|
||||
list() {
|
||||
return http('/api/autoinvite');
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region POST /api/autoinvite — create an invite
|
||||
create(body) {
|
||||
return http('/api/autoinvite', { method: 'POST', body });
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region DELETE /api/autoinvite/:id — delete an invite
|
||||
delete(id) {
|
||||
return http(`/api/autoinvite/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
// #endregion
|
||||
};
|
||||
30
src/api/companyuser.js
Normal file
30
src/api/companyuser.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { http } from './http.js';
|
||||
|
||||
/**
|
||||
* Company members (admin only).
|
||||
*/
|
||||
export const companyuser = {
|
||||
// #region GET /api/companyuser — list members
|
||||
list() {
|
||||
return http('/api/companyuser');
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region POST /api/companyuser — add an existing user by email
|
||||
create(body) {
|
||||
return http('/api/companyuser', { method: 'POST', body });
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region PATCH /api/companyuser/:id — change role
|
||||
setRole(id, role) {
|
||||
return http(`/api/companyuser/${id}`, { method: 'PATCH', body: { role } });
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region DELETE /api/companyuser/:id — remove a member
|
||||
delete(id) {
|
||||
return http(`/api/companyuser/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
// #endregion
|
||||
};
|
||||
@@ -34,6 +34,8 @@ export { shipment } from './shipment.js';
|
||||
export { shipping } from './shipping.js';
|
||||
export { payment } from './payment.js';
|
||||
export { account } from './account.js';
|
||||
export { companyuser } from './companyuser.js';
|
||||
export { autoinvite } from './autoinvite.js';
|
||||
export { mcp } from './mcp.js';
|
||||
export { backup } from './backup.js';
|
||||
export { carrier } from './carrier.js';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -178,6 +178,12 @@ export {
|
||||
selectAttributeStatus,
|
||||
selectAccounts,
|
||||
selectAccountsStatus,
|
||||
selectCompanyRole,
|
||||
selectIsCompanyAdmin,
|
||||
selectMembers,
|
||||
selectMembersStatus,
|
||||
selectInvites,
|
||||
selectInvitesStatus,
|
||||
} from './companyAutomat';
|
||||
|
||||
export { default as categoryAutomat } from './categoryAutomat';
|
||||
|
||||
207
src/components/settings/MembersSettingsPanel.jsx
Normal file
207
src/components/settings/MembersSettingsPanel.jsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import { PureComponent } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import companyAutomat, { selectIsCompanyAdmin } from '../../automats/companyAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
|
||||
const ROLES = [
|
||||
{ value: 'user', label: 'Benutzer' },
|
||||
{ value: 'admin', label: 'Administrator' },
|
||||
];
|
||||
|
||||
const selectState = (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
members: state.members,
|
||||
membersStatus: state.membersStatus,
|
||||
invites: state.invites,
|
||||
invitesStatus: state.invitesStatus,
|
||||
});
|
||||
|
||||
/**
|
||||
* Company members and pending auto-invites. Visible and usable by admins only.
|
||||
*/
|
||||
export default class MembersSettingsPanel extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...selectState(companyAutomat.state),
|
||||
memberEmail: '',
|
||||
inviteEmail: '',
|
||||
inviteRole: 'user',
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, selectState);
|
||||
this.loadIfAdmin();
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps, prevState) {
|
||||
if (!prevState.isAdmin && this.state.isAdmin) this.loadIfAdmin();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
loadIfAdmin() {
|
||||
if (!selectIsCompanyAdmin()) return;
|
||||
companyAutomat.actions.ensureMembersLoaded().catch(notifyError);
|
||||
companyAutomat.actions.ensureInvitesLoaded().catch(notifyError);
|
||||
}
|
||||
|
||||
// Runs a mutation, mapping the backend's 404/409 to friendly messages.
|
||||
run = async (fn, messages = {}) => {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
await fn();
|
||||
return true;
|
||||
} catch (err) {
|
||||
const normalized = backendErrorFrom(err);
|
||||
const text = messages[normalized.status];
|
||||
if (text) enqueueSnackbar(text, { variant: 'warning' });
|
||||
else notifyError(err);
|
||||
return false;
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
};
|
||||
|
||||
handleAddMember = async (event) => {
|
||||
event.preventDefault();
|
||||
const email = this.state.memberEmail.trim();
|
||||
if (!email) return;
|
||||
const ok = await this.run(() => companyAutomat.actions.addMember(email, 'user'), {
|
||||
404: 'Kein registrierter Benutzer mit dieser E-Mail. Bitte stattdessen einladen.',
|
||||
409: 'Benutzer gehört bereits zum Unternehmen.',
|
||||
});
|
||||
if (ok) this.setState({ memberEmail: '' });
|
||||
};
|
||||
|
||||
handleSetRole = (member, role) =>
|
||||
this.run(() => companyAutomat.actions.setMemberRole(member.id, role), {
|
||||
409: 'Der letzte Administrator kann nicht herabgestuft werden.',
|
||||
});
|
||||
|
||||
handleRemoveMember = (member) =>
|
||||
this.run(() => companyAutomat.actions.removeMember(member.id), {
|
||||
409: 'Der letzte Administrator kann nicht entfernt werden.',
|
||||
});
|
||||
|
||||
handleCreateInvite = async (event) => {
|
||||
event.preventDefault();
|
||||
const email = this.state.inviteEmail.trim();
|
||||
if (!email) return;
|
||||
const ok = await this.run(() => companyAutomat.actions.createInvite(email, this.state.inviteRole), {
|
||||
409: 'Diese E-Mail ist bereits eingeladen oder registriert (dann direkt als Mitglied hinzufügen).',
|
||||
});
|
||||
if (ok) this.setState({ inviteEmail: '' });
|
||||
};
|
||||
|
||||
handleDeleteInvite = (invite) => this.run(() => companyAutomat.actions.deleteInvite(invite.id));
|
||||
|
||||
renderRoleSelect(value, onChange, disabled) {
|
||||
return (
|
||||
<Select size="small" value={value} disabled={disabled} onChange={(e) => onChange(e.target.value)}>
|
||||
{ROLES.map((r) => (
|
||||
<MenuItem key={r.value} value={r.value}>
|
||||
{r.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isAdmin, members, membersStatus, invites, invitesStatus, memberEmail, inviteEmail, inviteRole, busy } =
|
||||
this.state;
|
||||
|
||||
if (!isAdmin) {
|
||||
return <Typography color="text.secondary">Nur Administratoren können Mitglieder verwalten.</Typography>;
|
||||
}
|
||||
|
||||
const loading = membersStatus === 'loading' || invitesStatus === 'loading';
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Mitglieder
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Registrierte Benutzer per E-Mail hinzufügen. Sie können dann zu diesem Unternehmen wechseln.
|
||||
</Typography>
|
||||
<Stack component="form" direction="row" spacing={1} onSubmit={this.handleAddMember} sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
type="email"
|
||||
label="E-Mail"
|
||||
value={memberEmail}
|
||||
onChange={(e) => this.setState({ memberEmail: e.target.value })}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<Button type="submit" variant="contained" disabled={busy || !memberEmail.trim()}>
|
||||
Hinzufügen
|
||||
</Button>
|
||||
</Stack>
|
||||
{loading && members.length === 0 && <CircularProgress size={20} />}
|
||||
{members.map((m) => (
|
||||
<Stack key={m.id} direction="row" spacing={1} alignItems="center" sx={{ py: 0.5 }}>
|
||||
<Typography sx={{ flex: 1 }}>{m.email}</Typography>
|
||||
{this.renderRoleSelect(m.role, (role) => this.handleSetRole(m, role), busy)}
|
||||
<IconButton aria-label="Entfernen" disabled={busy} onClick={() => this.handleRemoveMember(m)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Einladungen
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
E-Mails, die bei der Registrierung automatisch zum Unternehmen hinzugefügt werden.
|
||||
</Typography>
|
||||
<Stack component="form" direction="row" spacing={1} onSubmit={this.handleCreateInvite} sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
type="email"
|
||||
label="E-Mail"
|
||||
value={inviteEmail}
|
||||
onChange={(e) => this.setState({ inviteEmail: e.target.value })}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
{this.renderRoleSelect(inviteRole, (role) => this.setState({ inviteRole: role }), busy)}
|
||||
<Button type="submit" variant="contained" disabled={busy || !inviteEmail.trim()}>
|
||||
Einladen
|
||||
</Button>
|
||||
</Stack>
|
||||
{invites.length === 0 && invitesStatus === 'ready' && (
|
||||
<Typography color="text.secondary">Keine offenen Einladungen.</Typography>
|
||||
)}
|
||||
{invites.map((i) => (
|
||||
<Stack key={i.id} direction="row" spacing={1} alignItems="center" sx={{ py: 0.5 }}>
|
||||
<Typography sx={{ flex: 1 }}>{i.email}</Typography>
|
||||
<Typography color="text.secondary">{ROLES.find((r) => r.value === i.role)?.label ?? i.role}</Typography>
|
||||
<IconButton aria-label="Einladung löschen" disabled={busy} onClick={() => this.handleDeleteInvite(i)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ import SettingsKeysPanel from './SettingsKeysPanel';
|
||||
import ImportExportSettingsPanel from './ImportExportSettingsPanel';
|
||||
import PosTerminalSettingsPanel from './PosTerminalSettingsPanel';
|
||||
import McpAuthorizationsPanel from './McpAuthorizationsPanel';
|
||||
import MembersSettingsPanel from './MembersSettingsPanel';
|
||||
import GroupOutlinedIcon from '@mui/icons-material/GroupOutlined';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import BackupSettingsPanel from './BackupSettingsPanel';
|
||||
import HealthSettingsPanel from './HealthSettingsPanel';
|
||||
import MonitorHeartOutlinedIcon from '@mui/icons-material/MonitorHeartOutlined';
|
||||
@@ -42,6 +45,7 @@ const TAB_KEYS = {
|
||||
backup: 'backup',
|
||||
posterminal: 'posterminal',
|
||||
security: 'security',
|
||||
members: 'members',
|
||||
};
|
||||
|
||||
const TABS = [
|
||||
@@ -54,6 +58,7 @@ const TABS = [
|
||||
{ id: TAB_KEYS.importExport, label: 'Import/Export', icon: <ImportExportIcon />, Panel: ImportExportSettingsPanel },
|
||||
{ id: TAB_KEYS.backup, label: 'Backups', icon: <BackupTableOutlinedIcon />, Panel: BackupSettingsPanel },
|
||||
{ id: TAB_KEYS.posterminal, label: 'POS Terminal', icon: <PointOfSaleIcon />, Panel: PosTerminalSettingsPanel },
|
||||
{ id: TAB_KEYS.members, label: 'Mitglieder', icon: <GroupOutlinedIcon />, Panel: MembersSettingsPanel },
|
||||
{ id: TAB_KEYS.security, label: 'Sicherheit & MCP', icon: <SecurityIcon />, Panel: McpAuthorizationsPanel },
|
||||
];
|
||||
|
||||
@@ -61,9 +66,10 @@ function getDefaultTab(vhost) {
|
||||
return vhost ? TAB_KEYS.company : TAB_KEYS.health;
|
||||
}
|
||||
|
||||
function getVisibleTabs(vhost) {
|
||||
if (!vhost) return TABS;
|
||||
return TABS.filter(
|
||||
function getVisibleTabs(vhost, isAdmin) {
|
||||
const tabs = isAdmin ? TABS : TABS.filter((t) => t.id !== TAB_KEYS.members);
|
||||
if (!vhost) return tabs;
|
||||
return tabs.filter(
|
||||
(t) => t.id !== TAB_KEYS.health && t.id !== TAB_KEYS.backup && t.id !== TAB_KEYS.printers
|
||||
);
|
||||
}
|
||||
@@ -76,6 +82,7 @@ export default class SettingsDialog extends Component {
|
||||
tab: getDefaultTab(vhost),
|
||||
schemaVersion: authAutomat.state.schemaVersion,
|
||||
vhost,
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
};
|
||||
this.handleTabChange = this.handleTabChange.bind(this);
|
||||
}
|
||||
@@ -85,10 +92,14 @@ export default class SettingsDialog extends Component {
|
||||
schemaVersion: state.schemaVersion,
|
||||
vhost: Boolean(state.vhost),
|
||||
}));
|
||||
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubAuth?.();
|
||||
this.unsubCompany?.();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -129,8 +140,8 @@ export default class SettingsDialog extends Component {
|
||||
|
||||
render() {
|
||||
const { open, onClose } = this.props;
|
||||
const { tab, schemaVersion, vhost } = this.state;
|
||||
const visibleTabs = getVisibleTabs(vhost);
|
||||
const { tab, schemaVersion, vhost, isAdmin } = this.state;
|
||||
const visibleTabs = getVisibleTabs(vhost, isAdmin);
|
||||
const activeTab = visibleTabs.some((t) => t.id === tab) ? tab : visibleTabs[0]?.id;
|
||||
|
||||
const paperSx = {
|
||||
|
||||
@@ -8,3 +8,4 @@ export { default as ImportExportSettingsPanel } from './ImportExportSettingsPane
|
||||
export { default as McpAuthorizationsPanel } from './McpAuthorizationsPanel';
|
||||
export { default as BackupSettingsPanel } from './BackupSettingsPanel';
|
||||
export { default as HealthSettingsPanel } from './HealthSettingsPanel';
|
||||
export { default as MembersSettingsPanel } from './MembersSettingsPanel';
|
||||
|
||||
@@ -22,5 +22,6 @@ export function normalizeCompany(raw, overrides = {}) {
|
||||
reducedtaxrate: overrides.reducedtaxrate !== undefined ? overrides.reducedtaxrate : raw.reducedtaxrate,
|
||||
taxfreerate: overrides.taxfreerate !== undefined ? overrides.taxfreerate : raw.taxfreerate,
|
||||
rootCategoryId: overrides.rootCategoryId !== undefined ? overrides.rootCategoryId : raw.rootCategoryId,
|
||||
role: overrides.role !== undefined ? overrides.role : raw.role,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user