Files
dashfront/src/components/CompanySetupDialog.jsx
2026-09-21 03:19:32 +02:00

290 lines
9.7 KiB
JavaScript

import { Component } from 'react';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import InputAdornment from '@mui/material/InputAdornment';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Typography from '@mui/material/Typography';
import BusinessIcon from '@mui/icons-material/Business';
import BackendError from './BackendError';
import { company } from '../api';
import { StorageManagerContext } from '../api/storageManagerContext';
import authAutomat from '../automats/authAutomat';
import {
defaultIso639,
ISO639_OPTIONS,
parseTaxRate,
} from '../utils/companyForm';
import { backendErrorFrom, isBackendFailure } from '../utils/backendError';
class CompanySetupDialog extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
companies: [],
loading: true,
busy: false,
error: null,
name: '',
iso639: defaultIso639(),
taxrate: '19',
reducedtaxrate: '7',
};
this.handleSelect = this.handleSelect.bind(this);
this.handleCreate = this.handleCreate.bind(this);
this.handleLogout = this.handleLogout.bind(this);
this.handleClose = this.handleClose.bind(this);
}
componentDidMount() {
void this.loadCompanies();
}
async loadCompanies() {
this.setState({ loading: true });
try {
const data = await company.list();
this.setState({ companies: data.companies ?? [] });
} catch {
this.setState({ companies: [] });
} finally {
this.setState({ loading: false });
}
}
async handleSelect(companyId) {
this.setState({ busy: true, error: null });
try {
await authAutomat.actions.selectCompany(companyId);
await this.context.updateAuthMe();
} catch (err) {
this.setState({ error: backendErrorFrom(err) });
} finally {
this.setState({ busy: false });
}
}
async handleCreate(event) {
event.preventDefault();
const { name, iso639, taxrate, reducedtaxrate } = this.state;
const parsedTaxrate = parseTaxRate(taxrate);
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
const canCreate =
name.trim() !== '' &&
ISO639_OPTIONS.includes(iso639) &&
parsedTaxrate != null &&
parsedReducedtaxrate != null;
if (!canCreate) {
this.setState({ error: 'Bitte alle Pflichtfelder ausfüllen.' });
return;
}
this.setState({ busy: true, error: null });
try {
await authAutomat.actions.createCompany({
name: name.trim(),
iso639,
taxrate: parsedTaxrate,
reducedtaxrate: parsedReducedtaxrate,
});
await this.context.updateAuthMe();
await this.context.updateCompany();
} catch (err) {
this.setState({ error: backendErrorFrom(err) });
} finally {
this.setState({ busy: false });
}
}
async handleLogout() {
try {
await authAutomat.actions.logout();
await this.context.updateAuthMe();
} catch (err) {
this.setState({ error: backendErrorFrom(err) });
}
}
handleClose(_event, reason) {
if (reason === 'backdropClick' || reason === 'escapeKeyDown') return;
}
render() {
const { companies, loading, busy, error, name, iso639, taxrate, reducedtaxrate } =
this.state;
const hasCompanies = companies.length > 0;
const parsedTaxrate = parseTaxRate(taxrate);
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
const canCreate =
name.trim() !== '' &&
ISO639_OPTIONS.includes(iso639) &&
parsedTaxrate != null &&
parsedReducedtaxrate != null;
return (
<Dialog open onClose={this.handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
{loading
? 'Unternehmen'
: hasCompanies
? 'Unternehmen auswählen'
: 'Unternehmen anlegen'}
</DialogTitle>
<DialogContent>
{!loading ? (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{hasCompanies
? 'Wählen Sie ein bestehendes Unternehmen oder legen Sie ein neues an.'
: 'Legen Sie Ihr Unternehmen an, um fortzufahren.'}
</Typography>
) : null}
{error ? (
isBackendFailure(error) ? (
<BackendError error={error} sx={{ mb: 2 }} />
) : (
<Alert severity="error" sx={{ mb: 2 }}>
{typeof error === 'string' ? error : error.message}
</Alert>
)
) : null}
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
) : (
<Stack spacing={3}>
{hasCompanies ? (
<Box>
<List disablePadding sx={{ bgcolor: 'background.paper', borderRadius: 1 }}>
{companies.map((item) => (
<ListItemButton
key={item.id}
disabled={busy}
onClick={() => this.handleSelect(item.id)}
>
<BusinessIcon sx={{ mr: 1.5, color: 'primary.main' }} />
<ListItemText primary={item.name} />
</ListItemButton>
))}
</List>
</Box>
) : null}
{hasCompanies ? <Divider>oder</Divider> : null}
<Box component="form" onSubmit={this.handleCreate} noValidate>
<Stack spacing={2}>
{hasCompanies ? (
<Typography variant="subtitle2">Neues Unternehmen anlegen</Typography>
) : null}
<TextField
fullWidth
required
size="small"
label="Name"
value={name}
disabled={busy}
onChange={(e) => this.setState({ name: e.target.value })}
autoComplete="organization"
autoFocus={!hasCompanies}
/>
<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>
<Button type="submit" variant="contained" disabled={busy || !canCreate}>
Anlegen
</Button>
</Stack>
</Box>
<Button
type="button"
variant="text"
color="inherit"
disabled={busy}
onClick={this.handleLogout}
sx={{ alignSelf: 'center' }}
>
Abmelden
</Button>
</Stack>
)}
</DialogContent>
</Dialog>
);
}
}
export default CompanySetupDialog;