Compare commits
4 Commits
bc11a2c3ab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90936a449d | ||
|
|
283ce3dc5f | ||
|
|
863422df8e | ||
|
|
9841002398 |
@@ -40,6 +40,7 @@ class CarrierItem extends PureComponent {
|
||||
};
|
||||
|
||||
handleBlur = () => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.carrier ?? this.props.item;
|
||||
if (!item) return;
|
||||
const draft = this.state.draft.trim();
|
||||
@@ -63,6 +64,7 @@ class CarrierItem extends PureComponent {
|
||||
}
|
||||
|
||||
handleToggleHasPackages = (e) => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.carrier ?? this.props.item;
|
||||
if (!item) return;
|
||||
const nextVal = e.target.checked;
|
||||
@@ -70,6 +72,7 @@ class CarrierItem extends PureComponent {
|
||||
};
|
||||
|
||||
handleToggleIsReverse = (e) => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.carrier ?? this.props.item;
|
||||
if (!item) return;
|
||||
const nextVal = e.target.checked;
|
||||
@@ -77,6 +80,7 @@ class CarrierItem extends PureComponent {
|
||||
};
|
||||
|
||||
handleKeyDown = (event) => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.carrier ?? this.props.item;
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
@@ -90,6 +94,7 @@ class CarrierItem extends PureComponent {
|
||||
};
|
||||
|
||||
handleDelete = () => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.carrier ?? this.props.item;
|
||||
if (!item) return;
|
||||
if (!window.confirm(`Versandart „${item.name}“ wirklich löschen?`)) return;
|
||||
|
||||
@@ -37,6 +37,7 @@ class KundeGruppe extends PureComponent {
|
||||
};
|
||||
|
||||
handleBlur = () => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.group ?? this.props.item;
|
||||
if (!item) return;
|
||||
const editable = item.editable !== false;
|
||||
@@ -79,6 +80,7 @@ class KundeGruppe extends PureComponent {
|
||||
};
|
||||
|
||||
handleDelete = () => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.group ?? this.props.item;
|
||||
if (!item) return;
|
||||
const editable = item.editable !== false;
|
||||
|
||||
@@ -44,6 +44,8 @@ import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { vfs } from '../../api';
|
||||
import backupAutomat from '../../automats/backupAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { isProtectedVfsFile } from '../vfs/vfsUtils';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import {
|
||||
BORDER,
|
||||
@@ -93,6 +95,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
super(props);
|
||||
const { status, error, backupStatus, backups, vfsFilesByBackup, vfsLoadingByBackup } = backupAutomat.state;
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
status,
|
||||
error,
|
||||
backupStatus,
|
||||
@@ -115,6 +118,9 @@ export default class BackupSettingsPanel extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
this.unsub = backupAutomat.subscribe(this, (state) => ({
|
||||
status: state.status,
|
||||
error: state.error,
|
||||
@@ -130,6 +136,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubCompany?.();
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
@@ -155,6 +162,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
}
|
||||
|
||||
handleOpenConfirm(type) {
|
||||
if (!this.state.isAdmin) return;
|
||||
this.setState({ confirmRestore: type });
|
||||
}
|
||||
|
||||
@@ -215,6 +223,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
isAdmin,
|
||||
status,
|
||||
error,
|
||||
backupStatus,
|
||||
@@ -463,6 +472,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
<BackupTableRow
|
||||
key={item.name}
|
||||
item={item}
|
||||
isAdmin={isAdmin}
|
||||
isExpanded={isExpanded}
|
||||
manifest={manifest}
|
||||
vfsFiles={vfsFiles}
|
||||
@@ -549,7 +559,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
variant="contained"
|
||||
startIcon={restoringSkills ? <CircularProgress size={14} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
||||
onClick={() => this.handleOpenConfirm('skills')}
|
||||
disabled={restoringSkills}
|
||||
disabled={!isAdmin || restoringSkills}
|
||||
sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
|
||||
>
|
||||
Skills wiederherstellen
|
||||
@@ -607,7 +617,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
variant="contained"
|
||||
startIcon={restoringApplets ? <CircularProgress size={14} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
||||
onClick={() => this.handleOpenConfirm('applets')}
|
||||
disabled={restoringApplets}
|
||||
disabled={!isAdmin || restoringApplets}
|
||||
sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
|
||||
>
|
||||
Applets wiederherstellen
|
||||
@@ -740,6 +750,7 @@ class BackupTableRow extends Component {
|
||||
// MARK: calls POST /backup/:id/vfs/recover via backupAutomat.actions.recoverBackupVfs from ./api/index.js
|
||||
async handleRecoverFile(e, path) {
|
||||
e?.stopPropagation?.();
|
||||
if (!this.props.isAdmin && isProtectedVfsFile(path)) return;
|
||||
if (this.state.recoveringPath) return;
|
||||
|
||||
this.setState({ recoveringPath: path });
|
||||
@@ -991,7 +1002,7 @@ class BackupTableRow extends Component {
|
||||
color="primary"
|
||||
startIcon={isRecovering ? <CircularProgress size={12} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
||||
onClick={(e) => this.handleRecoverFile(e, f.name)}
|
||||
disabled={Boolean(recoveringPath)}
|
||||
disabled={Boolean(recoveringPath) || (!this.props.isAdmin && isProtectedVfsFile(f.name))}
|
||||
sx={{ textTransform: 'none', py: 0.25, px: 1, minHeight: 26, fontSize: '0.75rem' }}
|
||||
>
|
||||
{isRecovering ? 'Wiederherstellen...' : 'Wiederherstellen'}
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class CompanySettingsPanel extends Component {
|
||||
super(props);
|
||||
const current = companyAutomat.state.current;
|
||||
this.state = {
|
||||
isAdmin: current?.role === 'admin',
|
||||
status: current ? 'ready' : 'loading',
|
||||
busy: false,
|
||||
error: null,
|
||||
@@ -42,6 +43,9 @@ export default class CompanySettingsPanel extends Component {
|
||||
if (state.current && this.state.status === 'loading') {
|
||||
this.syncFormFromCompany(state.current);
|
||||
}
|
||||
return {
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
};
|
||||
});
|
||||
if (companyAutomat.state.current) {
|
||||
this.syncFormFromCompany(companyAutomat.state.current);
|
||||
@@ -86,6 +90,7 @@ export default class CompanySettingsPanel extends Component {
|
||||
|
||||
async handleSubmit(event) {
|
||||
event?.preventDefault?.();
|
||||
if (!this.state.isAdmin) return;
|
||||
const { name, iso639, taxrate, reducedtaxrate } = this.state;
|
||||
const parsedTaxrate = parseTaxRate(taxrate);
|
||||
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
|
||||
@@ -123,7 +128,7 @@ export default class CompanySettingsPanel extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, busy, error, name, iso639, taxrate, reducedtaxrate } = this.state;
|
||||
const { isAdmin, status, busy, error, name, iso639, taxrate, reducedtaxrate } = this.state;
|
||||
|
||||
if (status === 'loading' && !name) {
|
||||
return (
|
||||
@@ -146,6 +151,7 @@ export default class CompanySettingsPanel extends Component {
|
||||
taxrate.trim() !== this._initial.taxrate.trim() ||
|
||||
reducedtaxrate.trim() !== this._initial.reducedtaxrate.trim());
|
||||
const canSave =
|
||||
isAdmin &&
|
||||
isDirty &&
|
||||
name.trim() !== '' &&
|
||||
ISO639_OPTIONS.includes(iso639) &&
|
||||
@@ -175,7 +181,7 @@ export default class CompanySettingsPanel extends Component {
|
||||
size="small"
|
||||
label="Name"
|
||||
value={name}
|
||||
disabled={busy}
|
||||
disabled={!isAdmin || busy}
|
||||
onChange={(nextName) => this.setState({ name: nextName })}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
@@ -188,7 +194,7 @@ export default class CompanySettingsPanel extends Component {
|
||||
required
|
||||
size="small"
|
||||
value={iso639}
|
||||
disabled={busy}
|
||||
disabled={!isAdmin || busy}
|
||||
onChange={(_event, value) => {
|
||||
if (value) this.setState({ iso639: value });
|
||||
}}
|
||||
@@ -206,7 +212,7 @@ export default class CompanySettingsPanel extends Component {
|
||||
type="number"
|
||||
label="Normalsatz"
|
||||
value={taxrate}
|
||||
disabled={busy}
|
||||
disabled={!isAdmin || busy}
|
||||
onChange={(e) => this.setState({ taxrate: e.target.value })}
|
||||
error={taxrate !== '' && parsedTaxrate == null}
|
||||
slotProps={{
|
||||
@@ -223,7 +229,7 @@ export default class CompanySettingsPanel extends Component {
|
||||
type="number"
|
||||
label="Ermäßigter Satz"
|
||||
value={reducedtaxrate}
|
||||
disabled={busy}
|
||||
disabled={!isAdmin || busy}
|
||||
onChange={(e) => this.setState({ reducedtaxrate: e.target.value })}
|
||||
error={reducedtaxrate !== '' && parsedReducedtaxrate == null}
|
||||
slotProps={{
|
||||
@@ -240,7 +246,7 @@ export default class CompanySettingsPanel extends Component {
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color={canSave ? 'success' : 'inherit'}
|
||||
disabled={busy || !canSave}
|
||||
disabled={!isAdmin || busy || !canSave}
|
||||
startIcon={busy ? <CircularProgress size={16} color="inherit" /> : null}
|
||||
sx={
|
||||
canSave
|
||||
|
||||
@@ -50,6 +50,7 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
// Export state
|
||||
downloading: false,
|
||||
downloadProgress: null, // null = unknown, number 0-100
|
||||
@@ -84,7 +85,14 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsub?.();
|
||||
if (this.uploadAbortController) {
|
||||
this.uploadAbortController.abort();
|
||||
}
|
||||
@@ -147,6 +155,7 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
}
|
||||
|
||||
handleFileChange(event) {
|
||||
if (!this.state.isAdmin) return;
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
if (!isZipFile(file)) {
|
||||
@@ -169,6 +178,7 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
}
|
||||
|
||||
async handleStartUpload() {
|
||||
if (!this.state.isAdmin) return;
|
||||
const { selectedFile } = this.state;
|
||||
if (!selectedFile) return;
|
||||
|
||||
@@ -282,6 +292,7 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
isAdmin,
|
||||
downloading,
|
||||
downloadProgress,
|
||||
downloadStatus,
|
||||
@@ -583,7 +594,7 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
accept=".zip,application/zip,application/x-zip-compressed"
|
||||
style={{ display: 'none' }}
|
||||
onChange={this.handleFileChange}
|
||||
disabled={uploading}
|
||||
disabled={!isAdmin || uploading}
|
||||
/>
|
||||
|
||||
{!selectedFile ? (
|
||||
@@ -592,7 +603,7 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
variant="outlined"
|
||||
startIcon={<InsertDriveFileOutlinedIcon />}
|
||||
onClick={() => this.fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
disabled={!isAdmin || uploading}
|
||||
>
|
||||
ZIP-Datei auswählen
|
||||
</Button>
|
||||
@@ -625,6 +636,7 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
{!uploading && (
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={!isAdmin}
|
||||
onClick={() => {
|
||||
this.setState({ selectedFile: null });
|
||||
if (this.fileInputRef.current) this.fileInputRef.current.value = '';
|
||||
@@ -760,7 +772,7 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
<CloudUploadOutlinedIcon />
|
||||
)
|
||||
}
|
||||
disabled={uploading || !selectedFile}
|
||||
disabled={!isAdmin || uploading || !selectedFile}
|
||||
onClick={this.handleStartUpload}
|
||||
>
|
||||
{uploading ? 'Import läuft…' : 'Upload & Import starten'}
|
||||
|
||||
@@ -157,7 +157,7 @@ export default class MembersSettingsPanel extends PureComponent {
|
||||
</Stack>
|
||||
{loading && members.length === 0 && <CircularProgress size={20} />}
|
||||
{members.map((m) => (
|
||||
<Stack key={m.id} direction="row" spacing={1.5} alignItems="center" sx={{ py: 0.75 }}>
|
||||
<Stack key={m.id} direction="row" spacing={1.5} sx={{ alignItems: 'center', py: 0.75 }}>
|
||||
<Avatar
|
||||
src={normalizePicture(m.picture) || undefined}
|
||||
alt={m.email}
|
||||
@@ -193,7 +193,7 @@ export default class MembersSettingsPanel extends PureComponent {
|
||||
</Button>
|
||||
</Stack>
|
||||
{invites.map((i) => (
|
||||
<Stack key={i.id} direction="row" spacing={1.5} alignItems="center" sx={{ py: 0.75 }}>
|
||||
<Stack key={i.id} direction="row" spacing={1.5} sx={{ alignItems: 'center', py: 0.75 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 32,
|
||||
|
||||
@@ -59,6 +59,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
||||
: [];
|
||||
const numbersStatus = companyAutomat.state.numbersStatus;
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
status:
|
||||
numbersStatus === 'ready'
|
||||
? 'ready'
|
||||
@@ -81,6 +82,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => {
|
||||
const numbers = Array.isArray(state.numbers) ? state.numbers : [];
|
||||
return {
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
status:
|
||||
state.numbersStatus === 'ready'
|
||||
? 'ready'
|
||||
@@ -121,6 +123,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
||||
}
|
||||
|
||||
async handleSave(id) {
|
||||
if (!this.state.isAdmin) return;
|
||||
const draft = this.state.drafts[id];
|
||||
if (!draft || draft.busy) return;
|
||||
const row = this.state.numbers.find((r) => r.id === id);
|
||||
@@ -211,7 +214,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, numbers, drafts } = this.state;
|
||||
const { isAdmin, status, error, numbers, drafts } = this.state;
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
@@ -249,7 +252,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
||||
<TextInput
|
||||
size="small"
|
||||
value={draft.prefix}
|
||||
disabled={draft.busy}
|
||||
disabled={!isAdmin || draft.busy}
|
||||
onChange={(e) => this.handleChange(row.id, 'prefix', e.target.value)}
|
||||
sx={{ input: { fontFamily: 'monospace' } }}
|
||||
/>
|
||||
@@ -259,7 +262,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
||||
size="small"
|
||||
type="number"
|
||||
value={draft.padding}
|
||||
disabled={draft.busy}
|
||||
disabled={!isAdmin || draft.busy}
|
||||
slotProps={{ htmlInput: { min: 1, step: 1 } }}
|
||||
onChange={(e) => this.handleChange(row.id, 'padding', e.target.value)}
|
||||
sx={{ input: { fontFamily: 'monospace' } }}
|
||||
@@ -270,7 +273,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
||||
size="small"
|
||||
type="number"
|
||||
value={draft.next}
|
||||
disabled={draft.busy}
|
||||
disabled={!isAdmin || draft.busy}
|
||||
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
||||
onChange={(e) => this.handleChange(row.id, 'next', e.target.value)}
|
||||
sx={{ input: { fontFamily: 'monospace' } }}
|
||||
@@ -295,7 +298,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
||||
size="small"
|
||||
aria-label="Speichern"
|
||||
color="primary"
|
||||
disabled={draft.busy}
|
||||
disabled={!isAdmin || draft.busy}
|
||||
onClick={() => void this.handleSave(row.id)}
|
||||
>
|
||||
<SaveOutlinedIcon fontSize="small" />
|
||||
|
||||
@@ -64,6 +64,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
super(props);
|
||||
const terminals = companyAutomat.state.terminals ?? [];
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
status: companyAutomat.state.terminalsStatus === 'succeeded' ? 'ready' : 'loading',
|
||||
error: null,
|
||||
terminals,
|
||||
@@ -93,6 +94,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
terminals: Array.isArray(state.terminals) ? state.terminals : [],
|
||||
status: state.terminalsStatus === 'succeeded' ? 'ready' : state.terminalsStatus === 'failed' ? 'failed' : this.state.status,
|
||||
}));
|
||||
@@ -133,6 +135,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
}
|
||||
|
||||
async handleCreateTerminal() {
|
||||
if (!this.state.isAdmin) return;
|
||||
const { newTerminalName, enableAllForNew, terminals } = this.state;
|
||||
const trimmed = newTerminalName.trim();
|
||||
if (!trimmed) return;
|
||||
@@ -294,6 +297,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
isAdmin,
|
||||
status,
|
||||
error,
|
||||
terminals,
|
||||
@@ -362,7 +366,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
placeholder="Neues Terminal..."
|
||||
value={newTerminalName}
|
||||
onChange={(e) => this.setState({ newTerminalName: e.target.value })}
|
||||
disabled={creatingTerminal}
|
||||
disabled={!isAdmin || creatingTerminal}
|
||||
autoComplete="off"
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
@@ -387,7 +391,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
size="small"
|
||||
checked={enableAllForNew}
|
||||
onChange={(e) => this.setState({ enableAllForNew: e.target.checked })}
|
||||
disabled={creatingTerminal}
|
||||
disabled={!isAdmin || creatingTerminal}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
@@ -396,7 +400,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
size="small"
|
||||
startIcon={creatingTerminal ? <CircularProgress size={16} color="inherit" /> : <AddIcon />}
|
||||
onClick={this.handleCreateTerminal}
|
||||
disabled={creatingTerminal || !newTerminalName.trim()}
|
||||
disabled={!isAdmin || creatingTerminal || !newTerminalName.trim()}
|
||||
>
|
||||
Hinzufügen
|
||||
</Button>
|
||||
@@ -492,7 +496,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
<Switch
|
||||
size="small"
|
||||
checked={isEnabled}
|
||||
disabled={isPatching || isDeleting}
|
||||
disabled={!isAdmin || isPatching || isDeleting}
|
||||
onChange={(e) => void this.handleToggleEnabled(terminal, e.target.checked)}
|
||||
color="success"
|
||||
/>
|
||||
@@ -516,7 +520,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
size="small"
|
||||
aria-label={`Terminal ${terminal.name} löschen`}
|
||||
onClick={() => void this.handleDeleteTerminal(terminal)}
|
||||
disabled={isDeleting || creatingTerminal || isPatching}
|
||||
disabled={!isAdmin || isDeleting || creatingTerminal || isPatching}
|
||||
color="error"
|
||||
>
|
||||
{isDeleting ? (
|
||||
@@ -546,13 +550,14 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
<Chip
|
||||
icon={<FolderIcon />}
|
||||
label={this.getCategoryLabel(terminal)}
|
||||
onClick={() => this.setState({ pickerTerminal: terminal })}
|
||||
onClick={isAdmin ? () => this.setState({ pickerTerminal: terminal }) : undefined}
|
||||
onDelete={
|
||||
terminal.rootCategoryId != null
|
||||
isAdmin && terminal.rootCategoryId != null
|
||||
? () => void this.handleSetRootCategory(terminal, null)
|
||||
: undefined
|
||||
}
|
||||
clickable
|
||||
clickable={isAdmin}
|
||||
disabled={!isAdmin || isPatching || isDeleting}
|
||||
variant={terminal.rootCategoryId != null ? 'filled' : 'outlined'}
|
||||
color={terminal.rootCategoryId != null ? 'primary' : 'default'}
|
||||
size="small"
|
||||
@@ -560,6 +565,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
disabled={!isAdmin || isPatching || isDeleting}
|
||||
onClick={() => this.setState({ pickerTerminal: terminal })}
|
||||
sx={{ minWidth: 'auto', p: 0.5, fontSize: '0.75rem' }}
|
||||
>
|
||||
@@ -576,7 +582,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={index === 0 || isPatching || isDeleting}
|
||||
disabled={!isAdmin || index === 0 || isPatching || isDeleting}
|
||||
onClick={() => void this.handleMoveTerminal(terminal, 'up')}
|
||||
sx={{ p: 0.25 }}
|
||||
>
|
||||
@@ -588,7 +594,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={index === terminals.length - 1 || isPatching || isDeleting}
|
||||
disabled={!isAdmin || index === terminals.length - 1 || isPatching || isDeleting}
|
||||
onClick={() => void this.handleMoveTerminal(terminal, 'down')}
|
||||
sx={{ p: 0.25 }}
|
||||
>
|
||||
@@ -599,7 +605,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
disabled={isPatching || isDeleting}
|
||||
disabled={!isAdmin || isPatching || isDeleting}
|
||||
value={this.state.sortDrafts[terminal.id] ?? terminal.sort ?? 0}
|
||||
onChange={(e) => this.handleSortDraftChange(terminal.id, e.target.value)}
|
||||
onBlur={() => void this.handleSortSubmit(terminal)}
|
||||
@@ -624,7 +630,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
<Switch
|
||||
size="small"
|
||||
checked={Boolean(terminal.syncRootArticles)}
|
||||
disabled={isPatching || isDeleting}
|
||||
disabled={!isAdmin || isPatching || isDeleting}
|
||||
onChange={(e) => void this.handleToggleSyncRootArticles(terminal, e.target.checked)}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
printeragents: Array.isArray(companyAutomat.state.printeragents)
|
||||
? companyAutomat.state.printeragents
|
||||
: [],
|
||||
@@ -254,6 +255,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
printeragents: Array.isArray(state.printeragents) ? state.printeragents : [],
|
||||
agentprinters: Array.isArray(state.agentprinters) ? state.agentprinters : [],
|
||||
livePrinters: Array.isArray(state.livePrinters) ? state.livePrinters : [],
|
||||
@@ -279,6 +281,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
}
|
||||
|
||||
async handleCreateAgent() {
|
||||
if (!this.state.isAdmin) return;
|
||||
const { newAgentName, printeragents } = this.state;
|
||||
const trimmed = newAgentName.trim();
|
||||
if (!trimmed) return;
|
||||
@@ -497,6 +500,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
isAdmin,
|
||||
error,
|
||||
printeragents,
|
||||
agentprinters,
|
||||
@@ -540,23 +544,42 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
Drucker-Agenten
|
||||
</Typography>
|
||||
<Link
|
||||
href="https://github.com/sebseb7/zebragent/releases/latest"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
fontSize: '0.8125rem',
|
||||
color: 'primary.main',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
zebragent Releases
|
||||
<OpenInNewIcon sx={{ fontSize: 15 }} />
|
||||
</Link>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Link
|
||||
href="https://github.com/sebseb7/zebragent/releases/latest"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
fontSize: '0.8125rem',
|
||||
color: 'primary.main',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
zebragent (Linux / Windows)
|
||||
<OpenInNewIcon sx={{ fontSize: 15 }} />
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/QuixPos-com/AndroidPrinterAgent/releases/latest"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
fontSize: '0.8125rem',
|
||||
color: 'primary.main',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Android Agent
|
||||
<OpenInNewIcon sx={{ fontSize: 15 }} />
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Add new agent row */}
|
||||
@@ -572,7 +595,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
void this.handleCreateAgent();
|
||||
}
|
||||
}}
|
||||
disabled={creatingAgent}
|
||||
disabled={!isAdmin || creatingAgent}
|
||||
autoComplete="off"
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
@@ -594,7 +617,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={this.handleCreateAgent}
|
||||
disabled={creatingAgent || !newAgentName.trim()}
|
||||
disabled={!isAdmin || creatingAgent || !newAgentName.trim()}
|
||||
>
|
||||
Hinzufügen
|
||||
</Button>
|
||||
@@ -716,7 +739,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => this.handleStartRename(agent)}
|
||||
disabled={isPatching || isDeleting}
|
||||
disabled={!isAdmin || isPatching || isDeleting}
|
||||
sx={{ p: 0.5, color: 'text.secondary' }}
|
||||
>
|
||||
<EditOutlinedIcon sx={{ fontSize: 16 }} />
|
||||
@@ -764,7 +787,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<Switch
|
||||
size="small"
|
||||
checked={isEnabled}
|
||||
disabled={isPatching || isDeleting}
|
||||
disabled={!isAdmin || isPatching || isDeleting}
|
||||
onChange={(e) => void this.handleToggleAgentEnabled(agent, e.target.checked)}
|
||||
color="success"
|
||||
/>
|
||||
@@ -788,7 +811,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
size="small"
|
||||
aria-label={`Drucker-Agent ${agent.name} löschen`}
|
||||
onClick={() => void this.handleDeleteAgent(agent)}
|
||||
disabled={isDeleting || creatingAgent || isPatching}
|
||||
disabled={!isAdmin || isDeleting || creatingAgent || isPatching}
|
||||
color="error"
|
||||
>
|
||||
<DeleteOutlinedIcon fontSize="small" />
|
||||
@@ -904,7 +927,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={index === 0 || isPatching || isDeleting}
|
||||
disabled={!isAdmin || index === 0 || isPatching || isDeleting}
|
||||
onClick={() => void this.handleMoveAgent(agent, 'up')}
|
||||
sx={{ p: 0.25 }}
|
||||
>
|
||||
@@ -914,7 +937,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={index === printeragents.length - 1 || isPatching || isDeleting}
|
||||
disabled={!isAdmin || index === printeragents.length - 1 || isPatching || isDeleting}
|
||||
onClick={() => void this.handleMoveAgent(agent, 'down')}
|
||||
sx={{ p: 0.25 }}
|
||||
>
|
||||
@@ -924,7 +947,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
disabled={isPatching || isDeleting}
|
||||
disabled={!isAdmin || isPatching || isDeleting}
|
||||
value={this.state.sortDrafts[agent.id] ?? agent.sort ?? 0}
|
||||
onChange={(e) => this.handleSortDraftChange(agent.id, e.target.value)}
|
||||
onBlur={() => void this.handleSortSubmit(agent)}
|
||||
@@ -1078,6 +1101,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<Switch
|
||||
size="small"
|
||||
checked={pEnabled}
|
||||
disabled={!isAdmin}
|
||||
onChange={(e) =>
|
||||
void this.handleTogglePrinterEnabled(p, e.target.checked)
|
||||
}
|
||||
@@ -1099,7 +1123,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={pIdx === 0}
|
||||
disabled={!isAdmin || pIdx === 0}
|
||||
onClick={() => void this.handleMovePrinter(p, 'up')}
|
||||
sx={{ p: 0.25 }}
|
||||
>
|
||||
@@ -1110,7 +1134,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={pIdx === agentprinters.length - 1}
|
||||
disabled={!isAdmin || pIdx === agentprinters.length - 1}
|
||||
onClick={() => void this.handleMovePrinter(p, 'down')}
|
||||
sx={{ p: 0.25 }}
|
||||
>
|
||||
@@ -1124,7 +1148,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
disabled={isOnline}
|
||||
disabled={!isAdmin || isOnline}
|
||||
onClick={() => void this.handleDeletePrinter(p)}
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
|
||||
@@ -26,6 +26,7 @@ import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import BackendError from '../BackendError';
|
||||
import { printer as printerApi } from '../../api';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { requireArray, requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -189,6 +190,7 @@ export default class PrinterSettingsPanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
status: 'loading',
|
||||
error: null,
|
||||
printers: [],
|
||||
@@ -208,6 +210,9 @@ export default class PrinterSettingsPanel extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
void this.load();
|
||||
}
|
||||
|
||||
@@ -326,10 +331,12 @@ export default class PrinterSettingsPanel extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isAdmin,
|
||||
status, error, printers,
|
||||
draggingId, dragOverId,
|
||||
addOpen, editPrinter,
|
||||
@@ -354,6 +361,7 @@ export default class PrinterSettingsPanel extends Component {
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<AddIcon />}
|
||||
disabled={!isAdmin}
|
||||
onClick={this.handleAdd}
|
||||
>
|
||||
Drucker hinzufügen
|
||||
@@ -386,17 +394,17 @@ export default class PrinterSettingsPanel extends Component {
|
||||
<TableRow
|
||||
key={printer.id}
|
||||
hover
|
||||
draggable
|
||||
onDragStart={() => this.handleDragStart(printer.id)}
|
||||
onDragOver={(e) => this.handleDragOver(e, printer.id)}
|
||||
onDragLeave={() => this.handleDragLeave()}
|
||||
onDrop={() => this.handleDrop(printer.id)}
|
||||
onDragEnd={() => this.handleDragEnd()}
|
||||
draggable={isAdmin}
|
||||
onDragStart={isAdmin ? () => this.handleDragStart(printer.id) : undefined}
|
||||
onDragOver={isAdmin ? (e) => this.handleDragOver(e, printer.id) : undefined}
|
||||
onDragLeave={isAdmin ? () => this.handleDragLeave() : undefined}
|
||||
onDrop={isAdmin ? () => this.handleDrop(printer.id) : undefined}
|
||||
onDragEnd={isAdmin ? () => this.handleDragEnd() : undefined}
|
||||
sx={{
|
||||
opacity: isDragging ? 0.4 : 1,
|
||||
outline: isDragOver ? '2px solid' : 'none',
|
||||
outlineColor: 'primary.main',
|
||||
cursor: 'grab',
|
||||
cursor: isAdmin ? 'grab' : 'default',
|
||||
transition: 'opacity 0.15s',
|
||||
}}
|
||||
>
|
||||
@@ -426,7 +434,7 @@ export default class PrinterSettingsPanel extends Component {
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Bearbeiten"
|
||||
disabled={isDeleting}
|
||||
disabled={!isAdmin || isDeleting}
|
||||
onClick={() => this.setState({ editPrinter: printer })}
|
||||
>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
@@ -435,7 +443,7 @@ export default class PrinterSettingsPanel extends Component {
|
||||
size="small"
|
||||
aria-label="Löschen"
|
||||
color="error"
|
||||
disabled={isDeleting}
|
||||
disabled={!isAdmin || isDeleting}
|
||||
onClick={() => void this.handleDelete(printer.id)}
|
||||
>
|
||||
{isDeleting ? (
|
||||
|
||||
@@ -16,6 +16,7 @@ import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import BackendError from '../BackendError';
|
||||
import { setting as settingApi } from '../../api';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -68,6 +69,7 @@ export default class SettingsKeysPanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
status: 'loading',
|
||||
error: null,
|
||||
settings: [],
|
||||
@@ -79,6 +81,9 @@ export default class SettingsKeysPanel extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
void this.load();
|
||||
}
|
||||
|
||||
@@ -124,6 +129,7 @@ export default class SettingsKeysPanel extends Component {
|
||||
}
|
||||
|
||||
async handleSave(key) {
|
||||
if (!this.state.isAdmin) return;
|
||||
const draft = this.state.drafts[key];
|
||||
if (!draft || draft.busy) return;
|
||||
const row = this.state.settings.find((s) => s.key === key);
|
||||
@@ -172,10 +178,11 @@ export default class SettingsKeysPanel extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, settings, drafts } = this.state;
|
||||
const { isAdmin, status, error, settings, drafts } = this.state;
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
@@ -224,7 +231,7 @@ export default class SettingsKeysPanel extends Component {
|
||||
size="small"
|
||||
fullWidth
|
||||
value={draft.value !== undefined && draft.value !== null ? draft.value : (DEFAULT_SETTINGS[row.key] ?? '')}
|
||||
disabled={draft.busy}
|
||||
disabled={!isAdmin || draft.busy}
|
||||
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
||||
slotProps={{ htmlInput: { spellCheck: false } }}
|
||||
>
|
||||
@@ -247,7 +254,7 @@ export default class SettingsKeysPanel extends Component {
|
||||
maxRows={isMultiline ? 6 : undefined}
|
||||
placeholder={DEFAULT_SETTINGS[row.key] ?? undefined}
|
||||
value={draft.value}
|
||||
disabled={draft.busy}
|
||||
disabled={!isAdmin || draft.busy}
|
||||
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (!isMultiline && e.key === 'Enter') {
|
||||
@@ -279,7 +286,7 @@ export default class SettingsKeysPanel extends Component {
|
||||
size="small"
|
||||
aria-label="Speichern"
|
||||
color="primary"
|
||||
disabled={draft.busy}
|
||||
disabled={!isAdmin || draft.busy}
|
||||
onClick={() => void this.handleSave(row.key)}
|
||||
>
|
||||
<SaveOutlinedIcon fontSize="small" />
|
||||
|
||||
@@ -5,19 +5,21 @@ import IconButton from '@mui/material/IconButton';
|
||||
import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import vfsAutomat from '../../automats/vfsAutomat';
|
||||
import VfsFileExplorer from './VfsFileExplorer';
|
||||
import VfsTextEditor from './VfsTextEditor';
|
||||
import VfsFilePreview from './VfsFilePreview';
|
||||
import VfsAppletRunner from './VfsAppletRunner';
|
||||
import VfsNewFileDialog from './VfsNewFileDialog';
|
||||
import { isAppletFile } from './vfsUtils';
|
||||
import { isAppletFile, isProtectedVfsFile } from './vfsUtils';
|
||||
|
||||
export default class VfsEditorPanel extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const s = vfsAutomat.state;
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
files: s.files,
|
||||
loadingFiles: s.loadingFiles,
|
||||
loadError: s.loadError,
|
||||
@@ -39,6 +41,9 @@ export default class VfsEditorPanel extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
this.unsub = vfsAutomat.subscribe(this, (state) => ({
|
||||
files: state.files,
|
||||
loadingFiles: state.loadingFiles,
|
||||
@@ -72,6 +77,7 @@ export default class VfsEditorPanel extends PureComponent {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubCompany?.();
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
@@ -108,6 +114,7 @@ export default class VfsEditorPanel extends PureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
isAdmin,
|
||||
files,
|
||||
loadingFiles,
|
||||
selectedFileName,
|
||||
@@ -124,6 +131,7 @@ export default class VfsEditorPanel extends PureComponent {
|
||||
} = this.state;
|
||||
|
||||
const isApplet = Boolean(selectedFileName && isAppletFile(selectedFileName));
|
||||
const isProtectedFile = Boolean(selectedFileName && isProtectedVfsFile(selectedFileName));
|
||||
const showAppletRunner = isApplet && selectedFile?.isText && appletView === 'run';
|
||||
|
||||
return (
|
||||
@@ -161,6 +169,8 @@ export default class VfsEditorPanel extends PureComponent {
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
loadingContent={loadingContent}
|
||||
isAdmin={isAdmin}
|
||||
readOnly={!isAdmin && isProtectedFile}
|
||||
onChange={(c) => vfsAutomat.actions.updateContent(c)}
|
||||
onSave={() => vfsAutomat.actions.saveCurrentFile()}
|
||||
onClose={() => vfsAutomat.actions.selectFile(null)}
|
||||
@@ -216,6 +226,7 @@ export default class VfsEditorPanel extends PureComponent {
|
||||
<VfsFileExplorer
|
||||
files={files}
|
||||
loading={loadingFiles}
|
||||
isAdmin={isAdmin}
|
||||
selectedFileName={selectedFileName}
|
||||
searchQuery={searchQuery}
|
||||
onSelectFile={(name) => vfsAutomat.actions.selectFile(name)}
|
||||
@@ -238,6 +249,7 @@ export default class VfsEditorPanel extends PureComponent {
|
||||
<VfsNewFileDialog
|
||||
open={newFileDialogOpen}
|
||||
defaultFolder={this.state.newFileDefaultFolder}
|
||||
isAdmin={isAdmin}
|
||||
onClose={() => vfsAutomat.actions.setNewFileDialogOpen(false)}
|
||||
onCreate={(name) => vfsAutomat.actions.createFile(name)}
|
||||
isCreating={isSaving}
|
||||
|
||||
@@ -27,11 +27,25 @@ import CreateNewFolderRoundedIcon from '@mui/icons-material/CreateNewFolderRound
|
||||
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
|
||||
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import VfsFileIcon from './VfsFileIcon';
|
||||
import { formatVfsSize, formatVfsDate, isAppletFile } from './vfsUtils';
|
||||
import { formatVfsSize, formatVfsDate, isAppletFile, isProtectedVfsFile } from './vfsUtils';
|
||||
import VfsNewFolderDialog from './VfsNewFolderDialog';
|
||||
import VfsRenameDialog from './VfsRenameDialog';
|
||||
|
||||
/**
|
||||
* Returns true if the given folder path contains any admin-protected VFS files
|
||||
* (`.skill` or `.applet.js`, case-insensitive).
|
||||
*/
|
||||
function folderContainsProtectedFiles(folderPath, files) {
|
||||
if (!folderPath || !Array.isArray(files)) return false;
|
||||
const prefix = `${String(folderPath).replace(/^\/+/, '').replace(/\/+$/, '')}/`;
|
||||
return files.some((f) => {
|
||||
const clean = String(f?.name || '').replace(/^\/+/, '');
|
||||
return clean.startsWith(prefix) && isProtectedVfsFile(clean);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all ancestor directory paths for a file or folder path.
|
||||
* e.g. "docs/api/v1/spec.json" -> ["docs", "docs/api", "docs/api/v1"]
|
||||
@@ -126,6 +140,7 @@ export default class VfsFileExplorer extends PureComponent {
|
||||
}
|
||||
}
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
menuAnchor: null,
|
||||
menuFile: null,
|
||||
folderMenuAnchor: null,
|
||||
@@ -143,6 +158,16 @@ export default class VfsFileExplorer extends PureComponent {
|
||||
this.fileInputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubCompany?.();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.selectedFileName && this.props.selectedFileName !== prevProps.selectedFileName) {
|
||||
const parents = getParentFolderPaths(this.props.selectedFileName);
|
||||
@@ -632,7 +657,12 @@ export default class VfsFileExplorer extends PureComponent {
|
||||
isRenaming,
|
||||
isDraggingOver,
|
||||
} = this.state;
|
||||
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||
const isText = menuFile?.isText;
|
||||
const isMenuFileProtected = Boolean(menuFile && isProtectedVfsFile(menuFile.name));
|
||||
const isFolderProtected = Boolean(
|
||||
folderMenuNode && folderContainsProtectedFiles(folderMenuNode.path, this.props.files)
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -832,6 +862,7 @@ export default class VfsFileExplorer extends PureComponent {
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isAdmin && isMenuFileProtected}
|
||||
onClick={() => {
|
||||
const file = menuFile;
|
||||
this.handleCloseMenu();
|
||||
@@ -860,6 +891,7 @@ export default class VfsFileExplorer extends PureComponent {
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
disabled={!isAdmin && isMenuFileProtected}
|
||||
onClick={() => {
|
||||
if (menuFile && window.confirm(`Möchtest du "${menuFile.name}" wirklich aus dem VFS löschen?`)) {
|
||||
this.props.onDelete(menuFile.name);
|
||||
@@ -915,6 +947,7 @@ export default class VfsFileExplorer extends PureComponent {
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isAdmin && isFolderProtected}
|
||||
onClick={() => {
|
||||
const folder = folderMenuNode;
|
||||
this.handleCloseFolderMenu();
|
||||
@@ -934,6 +967,7 @@ export default class VfsFileExplorer extends PureComponent {
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isAdmin && isFolderProtected}
|
||||
onClick={() => {
|
||||
const folder = folderMenuNode;
|
||||
this.handleCloseFolderMenu();
|
||||
@@ -963,6 +997,7 @@ export default class VfsFileExplorer extends PureComponent {
|
||||
<VfsRenameDialog
|
||||
open={renameDialogOpen}
|
||||
item={renameItem}
|
||||
isAdmin={isAdmin}
|
||||
onClose={() => this.setState({ renameDialogOpen: false, renameItem: null })}
|
||||
onRename={this.handleRenameItem}
|
||||
isRenaming={isRenaming}
|
||||
|
||||
@@ -7,17 +7,30 @@ import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import NoteAddRoundedIcon from '@mui/icons-material/NoteAddRounded';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { isProtectedVfsFile } from './vfsUtils';
|
||||
|
||||
export default class VfsNewFileDialog extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const folder = props.defaultFolder ? `${String(props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : '';
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
filename: folder,
|
||||
error: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.open && this.props.open) {
|
||||
const folder = this.props.defaultFolder ? `${String(this.props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : '';
|
||||
@@ -27,6 +40,7 @@ export default class VfsNewFileDialog extends PureComponent {
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e?.preventDefault();
|
||||
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||
const clean = this.state.filename.trim().replace(/^[/\\]+/, '');
|
||||
if (!clean) {
|
||||
this.setState({ error: 'Bitte gib einen Dateinamen ein.' });
|
||||
@@ -36,12 +50,21 @@ export default class VfsNewFileDialog extends PureComponent {
|
||||
this.setState({ error: 'Ungültige Zeichen im Dateinamen.' });
|
||||
return;
|
||||
}
|
||||
if (!isAdmin && isProtectedVfsFile(clean)) {
|
||||
this.setState({ error: 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien erstellen.' });
|
||||
return;
|
||||
}
|
||||
this.props.onCreate(clean);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open, onClose, isCreating } = this.props;
|
||||
const { filename, error } = this.state;
|
||||
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||
const clean = filename.trim().replace(/^[/\\]+/, '');
|
||||
const isProtected = Boolean(!isAdmin && clean && isProtectedVfsFile(clean));
|
||||
const displayError =
|
||||
error || (isProtected ? 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien erstellen.' : '');
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -70,8 +93,8 @@ export default class VfsNewFileDialog extends PureComponent {
|
||||
label=""
|
||||
value={filename}
|
||||
onChange={(nextVal) => this.setState({ filename: nextVal, error: '' })}
|
||||
error={Boolean(error)}
|
||||
helperText={error}
|
||||
error={Boolean(displayError)}
|
||||
helperText={displayError}
|
||||
disabled={isCreating}
|
||||
/>
|
||||
</DialogContent>
|
||||
@@ -83,7 +106,7 @@ export default class VfsNewFileDialog extends PureComponent {
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={isCreating || !filename.trim()}
|
||||
disabled={isCreating || !clean || isProtected}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
{isCreating ? 'Erstelle…' : 'Erstellen & Öffnen'}
|
||||
|
||||
@@ -7,16 +7,29 @@ import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { isProtectedVfsFile } from './vfsUtils';
|
||||
|
||||
export default class VfsRenameDialog extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
targetPath: props.item?.path || '',
|
||||
error: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.open && this.props.open) {
|
||||
this.setState({
|
||||
@@ -28,6 +41,7 @@ export default class VfsRenameDialog extends PureComponent {
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e?.preventDefault();
|
||||
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||
const raw = this.state.targetPath.trim();
|
||||
if (!raw) {
|
||||
this.setState({ error: 'Bitte gib einen neuen Namen bzw. Pfad ein.' });
|
||||
@@ -41,6 +55,11 @@ export default class VfsRenameDialog extends PureComponent {
|
||||
const cleanNew = raw.replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
|
||||
const cleanOld = String(this.props.item?.path || '').replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
|
||||
|
||||
if (!isAdmin && (isProtectedVfsFile(cleanOld) || isProtectedVfsFile(cleanNew))) {
|
||||
this.setState({ error: 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien umbenennen.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanNew === cleanOld) {
|
||||
this.props.onClose?.();
|
||||
return;
|
||||
@@ -53,6 +72,11 @@ export default class VfsRenameDialog extends PureComponent {
|
||||
const { open, onClose, isRenaming, item } = this.props;
|
||||
const { targetPath, error } = this.state;
|
||||
const isDir = Boolean(item?.isDirectory);
|
||||
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||
const cleanNew = targetPath.trim().replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
|
||||
const isProtected = Boolean(!isAdmin && cleanNew && isProtectedVfsFile(cleanNew));
|
||||
const displayError =
|
||||
error || (isProtected ? 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien umbenennen.' : '');
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -81,8 +105,8 @@ export default class VfsRenameDialog extends PureComponent {
|
||||
label=""
|
||||
value={targetPath}
|
||||
onChange={(nextVal) => this.setState({ targetPath: nextVal, error: '' })}
|
||||
error={Boolean(error)}
|
||||
helperText={error}
|
||||
error={Boolean(displayError)}
|
||||
helperText={displayError}
|
||||
disabled={isRenaming}
|
||||
/>
|
||||
</DialogContent>
|
||||
@@ -94,7 +118,7 @@ export default class VfsRenameDialog extends PureComponent {
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={isRenaming || !targetPath.trim()}
|
||||
disabled={isRenaming || !cleanNew || isProtected}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
{isRenaming ? 'Speichere…' : 'Umbenennen'}
|
||||
|
||||
@@ -16,8 +16,9 @@ import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineR
|
||||
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||
import WrapTextRoundedIcon from '@mui/icons-material/WrapTextRounded';
|
||||
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import VfsFileIcon from './VfsFileIcon';
|
||||
import { formatVfsSize } from './vfsUtils';
|
||||
import { formatVfsSize, isProtectedVfsFile } from './vfsUtils';
|
||||
|
||||
const CodeMirrorCore = React.lazy(() => import('./CodeMirrorCore'));
|
||||
|
||||
@@ -72,6 +73,7 @@ export default class VfsTextEditor extends PureComponent {
|
||||
super(props);
|
||||
this.editorView = null;
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
wrapLines: true,
|
||||
cursorLine: 1,
|
||||
cursorCol: 1,
|
||||
@@ -79,9 +81,16 @@ export default class VfsTextEditor extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
}));
|
||||
this.focusEditor();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.fileName !== this.props.fileName) {
|
||||
this.focusEditor();
|
||||
@@ -145,6 +154,9 @@ export default class VfsTextEditor extends PureComponent {
|
||||
};
|
||||
|
||||
handleSaveShortcut = () => {
|
||||
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||
const isProtectedFile = isProtectedVfsFile(this.props.fileName);
|
||||
if (this.props.readOnly || (!isAdmin && isProtectedFile)) return;
|
||||
if (this.props.isDirty && !this.props.isSaving) {
|
||||
this.props.onSave?.();
|
||||
}
|
||||
@@ -168,6 +180,9 @@ export default class VfsTextEditor extends PureComponent {
|
||||
} = this.props;
|
||||
|
||||
const { wrapLines, cursorLine, cursorCol } = this.state;
|
||||
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||
const isProtectedFile = isProtectedVfsFile(fileName);
|
||||
const isReadOnly = Boolean(this.props.readOnly || (!isAdmin && isProtectedFile));
|
||||
const text = content || '';
|
||||
const lineCount = text.split('\n').length;
|
||||
const charCount = text.length;
|
||||
@@ -292,17 +307,17 @@ export default class VfsTextEditor extends PureComponent {
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
color={isDirty ? 'success' : 'inherit'}
|
||||
color={!isReadOnly && isDirty ? 'success' : 'inherit'}
|
||||
startIcon={isSaving ? <CircularProgress size={14} color="inherit" /> : <SaveRoundedIcon />}
|
||||
onClick={onSave}
|
||||
disabled={!isDirty || isSaving}
|
||||
disabled={isReadOnly || !isDirty || isSaving}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
px: 1.5,
|
||||
py: 0.4,
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 600,
|
||||
...(isDirty && {
|
||||
...(!isReadOnly && isDirty && {
|
||||
bgcolor: 'success.main',
|
||||
color: '#fff',
|
||||
'&:hover': { bgcolor: 'success.dark' },
|
||||
@@ -396,7 +411,7 @@ export default class VfsTextEditor extends PureComponent {
|
||||
fileName={fileName}
|
||||
mime={file?.mime}
|
||||
wrapLines={wrapLines}
|
||||
readOnly={this.props.readOnly}
|
||||
readOnly={isReadOnly}
|
||||
onSaveShortcut={this.handleSaveShortcut}
|
||||
onCreateEditor={this.handleEditorCreate}
|
||||
onUpdate={this.handleViewUpdate}
|
||||
|
||||
@@ -87,10 +87,30 @@ export function getVfsCategory(filePath, mime = '') {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAppletFile(filePath) {
|
||||
const p = String(filePath || '').toLowerCase();
|
||||
const p = String(filePath || '').trim().toLowerCase();
|
||||
return p.endsWith('.applet.js');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a VFS path is a skill file (`*.skill`, case-insensitive).
|
||||
* @param {string} filePath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSkillFile(filePath) {
|
||||
const p = String(filePath || '').trim().toLowerCase();
|
||||
return p.endsWith('.skill');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a VFS path is an admin-protected file (`*.skill` or `*.applet.js`, case-insensitive).
|
||||
* @param {string} filePath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isProtectedVfsFile(filePath) {
|
||||
const p = String(filePath || '').trim().toLowerCase();
|
||||
return p.endsWith('.skill') || p.endsWith('.applet.js');
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a VFS file path into directory and basename.
|
||||
* @param {string} path
|
||||
|
||||
@@ -37,6 +37,7 @@ class ArtikelLager extends PureComponent {
|
||||
};
|
||||
|
||||
handleBlur = () => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.warehouse ?? this.props.item;
|
||||
if (!item) return;
|
||||
const draft = this.state.draft.trim();
|
||||
@@ -60,6 +61,7 @@ class ArtikelLager extends PureComponent {
|
||||
}
|
||||
|
||||
handleKeyDown = (event) => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.warehouse ?? this.props.item;
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
@@ -73,6 +75,7 @@ class ArtikelLager extends PureComponent {
|
||||
};
|
||||
|
||||
handleDelete = () => {
|
||||
if (this.props.disabled) return;
|
||||
const item = this.props.warehouse ?? this.props.item;
|
||||
if (!item) return;
|
||||
if (!window.confirm(`Lager „${item.name}" wirklich löschen?`)) return;
|
||||
|
||||
38
src/nav.js
38
src/nav.js
@@ -101,13 +101,13 @@ export const NAV_ITEMS = [
|
||||
},
|
||||
...(import.meta.env.DEV
|
||||
? [
|
||||
{
|
||||
path: '/debug',
|
||||
label: 'Debug',
|
||||
icon: BugReportIcon,
|
||||
help: 'Entwickler- und Diagnosewerkzeuge für Automats (nur im Dev-Modus)',
|
||||
},
|
||||
]
|
||||
{
|
||||
path: '/debug',
|
||||
label: 'Debug',
|
||||
icon: BugReportIcon,
|
||||
help: 'Entwickler- und Diagnosewerkzeuge für Automats (nur im Dev-Modus)',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
@@ -282,8 +282,8 @@ export const SUB_NAV = {
|
||||
'/infos': [
|
||||
{
|
||||
segment: '',
|
||||
label: 'ArticleHistory',
|
||||
title: 'ArticleHistory',
|
||||
label: 'Artikel-Historie',
|
||||
title: 'Artikel-Historie',
|
||||
icon: HistoryIcon,
|
||||
help: 'Historie aller Artikeländerungen und Ereignisse',
|
||||
},
|
||||
@@ -311,16 +311,16 @@ export const SUB_NAV = {
|
||||
],
|
||||
...(import.meta.env.DEV
|
||||
? {
|
||||
'/debug': [
|
||||
{
|
||||
segment: '',
|
||||
label: 'Company',
|
||||
title: 'Company Automat',
|
||||
icon: BusinessIcon,
|
||||
help: 'companyAutomat Eingänge, Ausgänge (Actions & Selectors) und internen State inspizieren & steuern',
|
||||
},
|
||||
],
|
||||
}
|
||||
'/debug': [
|
||||
{
|
||||
segment: '',
|
||||
label: 'Company',
|
||||
title: 'Company Automat',
|
||||
icon: BusinessIcon,
|
||||
help: 'companyAutomat Eingänge, Ausgänge (Actions & Selectors) und internen State inspizieren & steuern',
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ class ArtikelLagerSearchField extends PureComponent {
|
||||
super(props);
|
||||
const initial = companyAutomat.state.warehouseFilter || props.prefill || '';
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
input: initial,
|
||||
submitting: false,
|
||||
};
|
||||
@@ -50,6 +51,7 @@ class ArtikelLagerSearchField extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
input: state.warehouseFilter,
|
||||
}));
|
||||
}
|
||||
@@ -64,6 +66,7 @@ class ArtikelLagerSearchField extends PureComponent {
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
if (!this.state.isAdmin) return;
|
||||
const name = this.state.input.trim();
|
||||
if (!name || this.state.submitting) return;
|
||||
|
||||
@@ -84,8 +87,8 @@ class ArtikelLagerSearchField extends PureComponent {
|
||||
|
||||
render() {
|
||||
const { inputRef, returnTo, navigate } = this.props;
|
||||
const { input, submitting } = this.state;
|
||||
const canSubmit = Boolean(input.trim()) && !submitting;
|
||||
const { isAdmin, input, submitting } = this.state;
|
||||
const canCreate = isAdmin && Boolean(input.trim()) && !submitting;
|
||||
|
||||
return (
|
||||
<PanelTileGrid>
|
||||
@@ -141,7 +144,7 @@ class ArtikelLagerSearchField extends PureComponent {
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!canSubmit}
|
||||
disabled={!canCreate}
|
||||
onClick={() => void this.handleSubmit()}
|
||||
>
|
||||
Hinzufügen
|
||||
@@ -159,6 +162,7 @@ class ArtikelLagerPage extends PureComponent {
|
||||
const initialWarehouses =
|
||||
companyAutomat.state.filteredWarehouses ?? companyAutomat.state.warehouses ?? [];
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
status: companyAutomat.state.warehousesStatus === 'ready' ? 'ready' : 'loading',
|
||||
error: companyAutomat.state.warehousesError ? backendErrorFrom(companyAutomat.state.warehousesError) : null,
|
||||
warehouses: initialWarehouses,
|
||||
@@ -178,6 +182,7 @@ class ArtikelLagerPage extends PureComponent {
|
||||
this._cancelled = false;
|
||||
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
warehouses: state.filteredWarehouses ?? state.warehouses,
|
||||
filter: state.warehouseFilter ?? '',
|
||||
status: state.warehousesStatus,
|
||||
@@ -191,6 +196,7 @@ class ArtikelLagerPage extends PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (
|
||||
prevState.isAdmin !== this.state.isAdmin ||
|
||||
prevState.status !== this.state.status ||
|
||||
prevState.warehouses !== this.state.warehouses ||
|
||||
prevState.filter !== this.state.filter
|
||||
@@ -219,7 +225,7 @@ class ArtikelLagerPage extends PureComponent {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
disabled: isFiltered,
|
||||
disabled: !this.state.isAdmin || isFiltered,
|
||||
onEnd: (evt) => {
|
||||
const { oldIndex, newIndex } = evt;
|
||||
if (oldIndex === newIndex) return;
|
||||
@@ -260,7 +266,7 @@ class ArtikelLagerPage extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, warehouses, filter } = this.state;
|
||||
const { isAdmin, status, error, warehouses, filter } = this.state;
|
||||
const { prefill, returnTo } = getInitialParams(this.props);
|
||||
|
||||
if (status === 'loading' || status === 'idle') {
|
||||
@@ -299,6 +305,7 @@ class ArtikelLagerPage extends PureComponent {
|
||||
<ArtikelLager
|
||||
key={wh.id}
|
||||
warehouse={wh}
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -21,6 +21,7 @@ class VersandartenSearchField extends PureComponent {
|
||||
super(props);
|
||||
const initial = companyAutomat.state.carrierFilter || '';
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
input: initial,
|
||||
submitting: false,
|
||||
};
|
||||
@@ -29,6 +30,7 @@ class VersandartenSearchField extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
input: state.carrierFilter,
|
||||
}));
|
||||
}
|
||||
@@ -43,6 +45,7 @@ class VersandartenSearchField extends PureComponent {
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
if (!this.state.isAdmin) return;
|
||||
const name = this.state.input.trim();
|
||||
if (!name || this.state.submitting) return;
|
||||
|
||||
@@ -64,8 +67,8 @@ class VersandartenSearchField extends PureComponent {
|
||||
|
||||
render() {
|
||||
const { inputRef } = this.props;
|
||||
const { input, submitting } = this.state;
|
||||
const canSubmit = Boolean(input.trim()) && !submitting;
|
||||
const { isAdmin, input, submitting } = this.state;
|
||||
const canCreate = isAdmin && Boolean(input.trim()) && !submitting;
|
||||
|
||||
return (
|
||||
<PanelTileGrid>
|
||||
@@ -102,7 +105,7 @@ class VersandartenSearchField extends PureComponent {
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!canSubmit}
|
||||
disabled={!canCreate}
|
||||
onClick={() => void this.handleSubmit()}
|
||||
>
|
||||
Hinzufügen
|
||||
@@ -120,6 +123,7 @@ class AuftragVersandartenPage extends PureComponent {
|
||||
const initialCarriers =
|
||||
companyAutomat.state.filteredCarriers ?? companyAutomat.state.carriers ?? [];
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
status: companyAutomat.state.carriersStatus === 'ready' ? 'ready' : 'loading',
|
||||
error: companyAutomat.state.carriersError ? backendErrorFrom(companyAutomat.state.carriersError) : null,
|
||||
carriers: initialCarriers,
|
||||
@@ -138,6 +142,7 @@ class AuftragVersandartenPage extends PureComponent {
|
||||
this._cancelled = false;
|
||||
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
carriers: state.filteredCarriers ?? state.carriers,
|
||||
filter: state.carrierFilter ?? '',
|
||||
status: state.carriersStatus,
|
||||
@@ -151,6 +156,7 @@ class AuftragVersandartenPage extends PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (
|
||||
prevState.isAdmin !== this.state.isAdmin ||
|
||||
prevState.status !== this.state.status ||
|
||||
prevState.carriers !== this.state.carriers ||
|
||||
prevState.filter !== this.state.filter
|
||||
@@ -179,7 +185,7 @@ class AuftragVersandartenPage extends PureComponent {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
disabled: isFiltered,
|
||||
disabled: !this.state.isAdmin || isFiltered,
|
||||
onEnd: (evt) => {
|
||||
const { oldIndex, newIndex } = evt;
|
||||
if (oldIndex === newIndex) return;
|
||||
@@ -220,7 +226,7 @@ class AuftragVersandartenPage extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, carriers, filter } = this.state;
|
||||
const { isAdmin, status, error, carriers, filter } = this.state;
|
||||
|
||||
if (status === 'loading' || status === 'idle') {
|
||||
return (
|
||||
@@ -253,6 +259,7 @@ class AuftragVersandartenPage extends PureComponent {
|
||||
<CarrierItem
|
||||
key={c.id}
|
||||
carrier={c}
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -22,13 +22,6 @@ export default class StatisticsPage extends PureComponent {
|
||||
}}
|
||||
>
|
||||
<BarChartIcon sx={{ fontSize: 48, opacity: 0.4 }} />
|
||||
<Typography variant="h6" color="text.primary" fontWeight={600}>
|
||||
Statistik & Auswertungen
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 420 }}>
|
||||
Hier entstehen demnächst grafische Auswertungen, Umsatzstatistiken und
|
||||
Bestandskennzahlen.
|
||||
</Typography>
|
||||
</Box>
|
||||
</PanelTile>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ class KundeGruppenSearchField extends PureComponent {
|
||||
super(props);
|
||||
const initial = companyAutomat.state.customergroupFilter || props.prefill || '';
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
input: initial,
|
||||
submitting: false,
|
||||
};
|
||||
@@ -49,6 +50,7 @@ class KundeGruppenSearchField extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
input: state.customergroupFilter,
|
||||
}));
|
||||
}
|
||||
@@ -63,6 +65,7 @@ class KundeGruppenSearchField extends PureComponent {
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
if (!this.state.isAdmin) return;
|
||||
const name = this.state.input.trim();
|
||||
if (!name || this.state.submitting) return;
|
||||
|
||||
@@ -83,8 +86,8 @@ class KundeGruppenSearchField extends PureComponent {
|
||||
|
||||
render() {
|
||||
const { inputRef, returnTo, navigate } = this.props;
|
||||
const { input, submitting } = this.state;
|
||||
const canSubmit = Boolean(input.trim()) && !submitting;
|
||||
const { isAdmin, input, submitting } = this.state;
|
||||
const canCreate = isAdmin && Boolean(input.trim()) && !submitting;
|
||||
|
||||
return (
|
||||
<PanelTileGrid>
|
||||
@@ -141,7 +144,7 @@ class KundeGruppenSearchField extends PureComponent {
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!canSubmit}
|
||||
disabled={!canCreate}
|
||||
onClick={() => void this.handleSubmit()}
|
||||
>
|
||||
Hinzufügen
|
||||
@@ -159,6 +162,7 @@ class KundeGruppenPage extends PureComponent {
|
||||
const initialGroups =
|
||||
companyAutomat.state.filteredCustomergroups ?? companyAutomat.state.customergroups ?? [];
|
||||
this.state = {
|
||||
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||
status: companyAutomat.state.customergroupsStatus === 'ready' ? 'ready' : 'loading',
|
||||
error: companyAutomat.state.customergroupsError ? backendErrorFrom(companyAutomat.state.customergroupsError) : null,
|
||||
groups: initialGroups,
|
||||
@@ -177,6 +181,7 @@ class KundeGruppenPage extends PureComponent {
|
||||
this._cancelled = false;
|
||||
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
isAdmin: state.current?.role === 'admin',
|
||||
groups: state.filteredCustomergroups ?? state.customergroups,
|
||||
filter: state.customergroupFilter ?? '',
|
||||
status: state.customergroupsStatus,
|
||||
@@ -190,6 +195,7 @@ class KundeGruppenPage extends PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (
|
||||
prevState.isAdmin !== this.state.isAdmin ||
|
||||
prevState.status !== this.state.status ||
|
||||
prevState.groups !== this.state.groups ||
|
||||
prevState.filter !== this.state.filter
|
||||
@@ -218,7 +224,7 @@ class KundeGruppenPage extends PureComponent {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
disabled: isFiltered,
|
||||
disabled: !this.state.isAdmin || isFiltered,
|
||||
onEnd: (evt) => {
|
||||
const { oldIndex, newIndex } = evt;
|
||||
if (oldIndex === newIndex) return;
|
||||
@@ -259,7 +265,7 @@ class KundeGruppenPage extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, groups, filter } = this.state;
|
||||
const { isAdmin, status, error, groups, filter } = this.state;
|
||||
const { prefill, returnTo } = getInitialParams(this.props);
|
||||
|
||||
if (status === 'loading' || status === 'idle') {
|
||||
@@ -298,6 +304,7 @@ class KundeGruppenPage extends PureComponent {
|
||||
<KundeGruppe
|
||||
key={group.id}
|
||||
group={group}
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user