This commit is contained in:
seb
2026-08-05 07:29:36 +02:00
parent 89f8804a69
commit 3000d7d45c

View File

@@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions'; import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
@@ -28,18 +27,15 @@ import { enqueueSnackbar } from 'notistack';
import BackendError from './BackendError'; import BackendError from './BackendError';
import SingleLineTextEdit from './SingleLineTextEdit'; import SingleLineTextEdit from './SingleLineTextEdit';
import { StorageManagerContext } from '../api/storageManager'; import { StorageManagerContext } from '../api/storageManager';
import { import { selectAuthUser } from '../store/authSlice';
loadCurrentCompany, import { updateCompany as updateCompanyThunk } from '../store/companySlice';
selectCompanyStatus,
selectCurrentCompany,
updateCompany as updateCompanyThunk,
} from '../store/companySlice';
import { import {
createPosterminal, createPosterminal,
deletePosterminal, deletePosterminal,
enableAllTerminalCategories, enableAllTerminalCategories,
} from '../store/posterminalSlice'; } from '../store/posterminalSlice';
import { loadCategoryTree } from '../store/categorySlice'; import { loadCategoryTree } from '../store/categorySlice';
import { requireDefined } from '../utils/apiShape';
import { ISO639_OPTIONS, parseTaxRate } from '../utils/companyForm'; import { ISO639_OPTIONS, parseTaxRate } from '../utils/companyForm';
import { backendErrorFrom, isBackendFailure } from '../utils/backendError'; import { backendErrorFrom, isBackendFailure } from '../utils/backendError';
import { notifyError } from '../utils/notify'; import { notifyError } from '../utils/notify';
@@ -65,34 +61,45 @@ class CompanySettingsDialog extends Component {
this.syncFormFromCompany = this.syncFormFromCompany.bind(this); this.syncFormFromCompany = this.syncFormFromCompany.bind(this);
this.handleCreateTerminal = this.handleCreateTerminal.bind(this); this.handleCreateTerminal = this.handleCreateTerminal.bind(this);
this.handleDeleteTerminal = this.handleDeleteTerminal.bind(this); this.handleDeleteTerminal = this.handleDeleteTerminal.bind(this);
this.loadCompany = this.loadCompany.bind(this);
this.loadTerminals = this.loadTerminals.bind(this); this.loadTerminals = this.loadTerminals.bind(this);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { open, company, companyStatus } = this.props; const { open } = this.props;
if (open && !prevProps.open) { if (open && !prevProps.open) {
this.setState({ error: null }); this.setState({ error: null });
if (company) { void this.loadCompany();
this.syncFormFromCompany(company);
} else if (companyStatus !== 'loading') {
void this.props.loadCurrentCompany();
}
void this.loadTerminals(); void this.loadTerminals();
} }
if (open && company && company !== prevProps.company) {
this.syncFormFromCompany(company);
}
} }
syncFormFromCompany(company) { syncFormFromCompany(company) {
this.setState({ this.setState({
name: company.name ?? '', name: company.name,
iso639: company.iso639, iso639: company.iso639,
taxrate: company.taxrate != null ? String(company.taxrate) : '', taxrate: company.taxrate != null ? String(company.taxrate) : '',
reducedtaxrate: company.reducedtaxrate != null ? String(company.reducedtaxrate) : '', reducedtaxrate: company.reducedtaxrate != null ? String(company.reducedtaxrate) : '',
}); });
} }
async loadCompany() {
try {
const companyId = requireDefined(
this.props.selectedCompanyId,
'selectedCompanyId',
);
const companies = await this.context.companyPromise;
const company = requireDefined(
companies.find((c) => c.id === companyId),
'company',
);
this.syncFormFromCompany(company);
} catch (err) {
notifyError(err);
}
}
async loadTerminals() { async loadTerminals() {
try { try {
const terminals = await this.context.posterminalPromise; const terminals = await this.context.posterminalPromise;
@@ -156,13 +163,14 @@ class CompanySettingsDialog extends Component {
this.setState({ busy: true, error: null }); this.setState({ busy: true, error: null });
try { try {
await this.props await this.props
.updateCompany({ .saveCompany({
name: name.trim(), name: name.trim(),
iso639, iso639,
taxrate: parsedTaxrate, taxrate: parsedTaxrate,
reducedtaxrate: parsedReducedtaxrate, reducedtaxrate: parsedReducedtaxrate,
}) })
.unwrap(); .unwrap();
await this.context.updateCompany();
enqueueSnackbar('Unternehmen gespeichert.', { variant: 'success' }); enqueueSnackbar('Unternehmen gespeichert.', { variant: 'success' });
onClose(); onClose();
} catch (err) { } catch (err) {
@@ -173,9 +181,8 @@ class CompanySettingsDialog extends Component {
} }
render() { render() {
const { open, onClose, companyStatus } = this.props; const { open, onClose } = this.props;
const { busy, error, name, iso639, taxrate, reducedtaxrate } = this.state; const { busy, error, name, iso639, taxrate, reducedtaxrate } = this.state;
const loading = companyStatus === 'loading' && !busy;
const parsedTaxrate = parseTaxRate(taxrate); const parsedTaxrate = parseTaxRate(taxrate);
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate); const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
const canSave = const canSave =
@@ -199,148 +206,142 @@ class CompanySettingsDialog extends Component {
) )
) : null} ) : null}
{loading ? ( <Stack spacing={2}>
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> <SingleLineTextEdit
<CircularProgress /> required
</Box> size="small"
) : ( label="Name"
<Stack spacing={2}> value={name}
<SingleLineTextEdit disabled={busy}
onChange={(nextName) => this.setState({ name: nextName })}
autoComplete="off"
autoFocus
/>
<FormControl required>
<FormLabel sx={{ mb: 0.5 }}>Sprache</FormLabel>
<ToggleButtonGroup
exclusive
required required
size="small" size="small"
label="Name" value={iso639}
value={name}
disabled={busy} disabled={busy}
onChange={(nextName) => this.setState({ name: nextName })} onChange={(_event, value) => {
autoComplete="off" if (value) this.setState({ iso639: value });
autoFocus }}
>
<ToggleButton value="de">Deutsch</ToggleButton>
<ToggleButton value="en">English</ToggleButton>
</ToggleButtonGroup>
</FormControl>
<Stack direction="row" spacing={1}>
<TextField
fullWidth
required
size="small"
type="number"
label="Normalsatz"
value={taxrate}
disabled={busy}
onChange={(e) => this.setState({ taxrate: e.target.value })}
error={taxrate !== '' && parsedTaxrate == null}
slotProps={{
input: {
endAdornment: <InputAdornment position="end">%</InputAdornment>,
},
htmlInput: { min: 0, max: 100, step: 0.01, required: true },
}}
/>
<TextField
fullWidth
required
size="small"
type="number"
label="Ermäßigter Satz"
value={reducedtaxrate}
disabled={busy}
onChange={(e) => this.setState({ reducedtaxrate: e.target.value })}
error={reducedtaxrate !== '' && parsedReducedtaxrate == null}
slotProps={{
input: {
endAdornment: <InputAdornment position="end">%</InputAdornment>,
},
htmlInput: { min: 0, max: 100, step: 0.01, required: true },
}}
/> />
<FormControl required>
<FormLabel sx={{ mb: 0.5 }}>Sprache</FormLabel>
<ToggleButtonGroup
exclusive
required
size="small"
value={iso639}
disabled={busy}
onChange={(_event, value) => {
if (value) this.setState({ iso639: value });
}}
>
<ToggleButton value="de">Deutsch</ToggleButton>
<ToggleButton value="en">English</ToggleButton>
</ToggleButtonGroup>
</FormControl>
<Stack direction="row" spacing={1}>
<TextField
fullWidth
required
size="small"
type="number"
label="Normalsatz"
value={taxrate}
disabled={busy}
onChange={(e) => this.setState({ taxrate: e.target.value })}
error={taxrate !== '' && parsedTaxrate == null}
slotProps={{
input: {
endAdornment: <InputAdornment position="end">%</InputAdornment>,
},
htmlInput: { min: 0, max: 100, step: 0.01, required: true },
}}
/>
<TextField
fullWidth
required
size="small"
type="number"
label="Ermäßigter Satz"
value={reducedtaxrate}
disabled={busy}
onChange={(e) => this.setState({ reducedtaxrate: e.target.value })}
error={reducedtaxrate !== '' && parsedReducedtaxrate == null}
slotProps={{
input: {
endAdornment: <InputAdornment position="end">%</InputAdornment>,
},
htmlInput: { min: 0, max: 100, step: 0.01, required: true },
}}
/>
</Stack>
<Divider sx={{ my: 1 }} />
<Box>
<List dense sx={{ py: 0 }}>
{this.state.terminals.map((terminal) => (
<ListItem
key={terminal.id}
sx={{ px: 0, py: 0.5 }}
>
<PointOfSaleIcon sx={{ mr: 1, color: 'text.secondary', fontSize: 20 }} />
<ListItemText
primary={terminal.name}
secondary={`PIN: ${terminal.pairingpin}`}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
size="small"
onClick={() => this.handleDeleteTerminal(terminal)}
disabled={busy || this.state.creatingTerminal}
>
<DeleteOutlinedIcon fontSize="small" />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<TextField
size="small"
placeholder="Neues Terminal..."
value={this.state.newTerminalName}
onChange={(e) => this.setState({ newTerminalName: e.target.value })}
disabled={busy || this.state.creatingTerminal}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.handleCreateTerminal();
}
}}
sx={{ flex: 1 }}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<PointOfSaleIcon sx={{ fontSize: 18, color: 'text.secondary' }} />
<Switch
size="small"
checked={this.state.enableAllForNew}
onChange={(e) => this.setState({ enableAllForNew: e.target.checked })}
disabled={busy || this.state.creatingTerminal}
/>
</Box>
<Button
variant="contained"
size="small"
onClick={this.handleCreateTerminal}
disabled={busy || this.state.creatingTerminal || !this.state.newTerminalName.trim()}
>
Hinzufügen
</Button>
</Box>
</Box>
</Stack> </Stack>
)}
<Divider sx={{ my: 1 }} />
<Box>
<List dense sx={{ py: 0 }}>
{this.state.terminals.map((terminal) => (
<ListItem
key={terminal.id}
sx={{ px: 0, py: 0.5 }}
>
<PointOfSaleIcon sx={{ mr: 1, color: 'text.secondary', fontSize: 20 }} />
<ListItemText
primary={terminal.name}
secondary={`PIN: ${terminal.pairingpin}`}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
size="small"
onClick={() => this.handleDeleteTerminal(terminal)}
disabled={busy || this.state.creatingTerminal}
>
<DeleteOutlinedIcon fontSize="small" />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<TextField
size="small"
placeholder="Neues Terminal..."
value={this.state.newTerminalName}
onChange={(e) => this.setState({ newTerminalName: e.target.value })}
disabled={busy || this.state.creatingTerminal}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.handleCreateTerminal();
}
}}
sx={{ flex: 1 }}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<PointOfSaleIcon sx={{ fontSize: 18, color: 'text.secondary' }} />
<Switch
size="small"
checked={this.state.enableAllForNew}
onChange={(e) => this.setState({ enableAllForNew: e.target.checked })}
disabled={busy || this.state.creatingTerminal}
/>
</Box>
<Button
variant="contained"
size="small"
onClick={this.handleCreateTerminal}
disabled={busy || this.state.creatingTerminal || !this.state.newTerminalName.trim()}
>
Hinzufügen
</Button>
</Box>
</Box>
</Stack>
</DialogContent> </DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}> <DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={onClose} disabled={busy}> <Button onClick={onClose} disabled={busy}>
Abbrechen Abbrechen
</Button> </Button>
<Button type="submit" variant="contained" disabled={busy || loading || !canSave}> <Button type="submit" variant="contained" disabled={busy || !canSave}>
Speichern Speichern
</Button> </Button>
</DialogActions> </DialogActions>
@@ -351,13 +352,11 @@ class CompanySettingsDialog extends Component {
} }
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
company: selectCurrentCompany(state), selectedCompanyId: selectAuthUser(state)?.selectedCompanyId,
companyStatus: selectCompanyStatus(state),
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
loadCurrentCompany: () => dispatch(loadCurrentCompany()), saveCompany: (payload) => dispatch(updateCompanyThunk(payload)),
updateCompany: (payload) => dispatch(updateCompanyThunk(payload)),
createPosterminal: (payload) => dispatch(createPosterminal(payload)), createPosterminal: (payload) => dispatch(createPosterminal(payload)),
deletePosterminal: (payload) => dispatch(deletePosterminal(payload)), deletePosterminal: (payload) => dispatch(deletePosterminal(payload)),
enableAllTerminalCategories: (payload) => dispatch(enableAllTerminalCategories(payload)), enableAllTerminalCategories: (payload) => dispatch(enableAllTerminalCategories(payload)),