u
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { bootstrapSession } from '../store/authSlice';
|
||||
import { bootstrapSession as bootstrapSessionThunk } from '../store/authSlice';
|
||||
|
||||
let sessionBootstrapped = false;
|
||||
|
||||
class AuthBootstrap extends Component {
|
||||
componentDidMount() {
|
||||
if (sessionBootstrapped) return;
|
||||
sessionBootstrapped = true;
|
||||
|
||||
this.props
|
||||
.bootstrapSession()
|
||||
.unwrap()
|
||||
@@ -24,7 +29,7 @@ class AuthBootstrap extends Component {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
bootstrapSession: () => dispatch(bootstrapSession()),
|
||||
bootstrapSession: () => dispatch(bootstrapSessionThunk()),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(AuthBootstrap);
|
||||
|
||||
55
src/components/BackendError.jsx
Normal file
55
src/components/BackendError.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Component } from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import AlertTitle from '@mui/material/AlertTitle';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { formatBackendError } from '../utils/backendError';
|
||||
|
||||
class BackendError extends Component {
|
||||
render() {
|
||||
const { error, title, onRetry, sx, action } = this.props;
|
||||
if (!error) return null;
|
||||
|
||||
const formatted = formatBackendError(error);
|
||||
const heading = title || formatted.title;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ maxWidth: 720, ...sx }}
|
||||
action={action}
|
||||
>
|
||||
<AlertTitle>{heading}</AlertTitle>
|
||||
{formatted.detail ? (
|
||||
<Typography variant="body2" component="div" sx={{ mb: formatted.request ? 1 : 0 }}>
|
||||
{formatted.detail}
|
||||
</Typography>
|
||||
) : null}
|
||||
{formatted.request ? (
|
||||
<Box
|
||||
component="code"
|
||||
sx={{
|
||||
display: 'block',
|
||||
fontSize: '0.75rem',
|
||||
color: 'text.secondary',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
>
|
||||
{formatted.request}
|
||||
{formatted.status != null ? ` → ${formatted.status}` : ''}
|
||||
</Box>
|
||||
) : null}
|
||||
{onRetry ? (
|
||||
<Box sx={{ mt: 1.5 }}>
|
||||
<Button size="small" variant="outlined" color="inherit" onClick={onRetry}>
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
</Box>
|
||||
) : null}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BackendError;
|
||||
@@ -16,19 +16,21 @@ import TextField from '@mui/material/TextField';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { company } from '../api';
|
||||
import BackendError from './BackendError';
|
||||
import { selectAuthUser } from '../store/authSlice';
|
||||
import {
|
||||
ISO639_OPTIONS,
|
||||
companyLanguageToIso639,
|
||||
parseTaxRate,
|
||||
} from '../utils/companyForm';
|
||||
loadCurrentCompany,
|
||||
selectCompanyStatus,
|
||||
selectCurrentCompany,
|
||||
updateCompany as updateCompanyThunk,
|
||||
} from '../store/companySlice';
|
||||
import { ISO639_OPTIONS, parseTaxRate } from '../utils/companyForm';
|
||||
import { backendErrorFrom, isBackendFailure } from '../utils/backendError';
|
||||
|
||||
class CompanySettingsDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: false,
|
||||
busy: false,
|
||||
error: null,
|
||||
name: '',
|
||||
@@ -37,39 +39,37 @@ class CompanySettingsDialog extends Component {
|
||||
reducedtaxrate: '',
|
||||
};
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.syncFormFromCompany = this.syncFormFromCompany.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { open, user } = this.props;
|
||||
const companyId = user?.selectedCompanyId;
|
||||
if (open && !prevProps.open && companyId) {
|
||||
void this.loadCompany(companyId);
|
||||
const { open, company, companyStatus } = this.props;
|
||||
if (open && !prevProps.open) {
|
||||
this.setState({ error: null });
|
||||
if (company) {
|
||||
this.syncFormFromCompany(company);
|
||||
} else if (companyStatus !== 'loading') {
|
||||
void this.props.loadCurrentCompany();
|
||||
}
|
||||
}
|
||||
if (open && company && company !== prevProps.company) {
|
||||
this.syncFormFromCompany(company);
|
||||
}
|
||||
}
|
||||
|
||||
async loadCompany(companyId) {
|
||||
this.setState({ loading: true, error: null });
|
||||
try {
|
||||
const data = await company.getById(companyId);
|
||||
const item = data.company;
|
||||
this.setState({
|
||||
name: item.name ?? '',
|
||||
iso639: companyLanguageToIso639(item),
|
||||
taxrate: item.taxrate != null ? String(item.taxrate) : '',
|
||||
reducedtaxrate: item.reducedtaxrate != null ? String(item.reducedtaxrate) : '',
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({ error: err.message });
|
||||
} finally {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
syncFormFromCompany(company) {
|
||||
this.setState({
|
||||
name: company.name ?? '',
|
||||
iso639: company.iso639 ?? 'de',
|
||||
taxrate: company.taxrate != null ? String(company.taxrate) : '',
|
||||
reducedtaxrate: company.reducedtaxrate != null ? String(company.reducedtaxrate) : '',
|
||||
});
|
||||
}
|
||||
|
||||
async handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
const { user, onClose } = this.props;
|
||||
const { onClose } = this.props;
|
||||
const { name, iso639, taxrate, reducedtaxrate } = this.state;
|
||||
const companyId = user?.selectedCompanyId;
|
||||
const parsedTaxrate = parseTaxRate(taxrate);
|
||||
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
|
||||
const canSave =
|
||||
@@ -78,31 +78,34 @@ class CompanySettingsDialog extends Component {
|
||||
parsedTaxrate != null &&
|
||||
parsedReducedtaxrate != null;
|
||||
|
||||
if (!companyId || !canSave) {
|
||||
if (!canSave) {
|
||||
this.setState({ error: 'Bitte alle Pflichtfelder ausfüllen.' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ busy: true, error: null });
|
||||
try {
|
||||
await company.update(companyId, {
|
||||
name: name.trim(),
|
||||
iso639,
|
||||
taxrate: parsedTaxrate,
|
||||
reducedtaxrate: parsedReducedtaxrate,
|
||||
});
|
||||
await this.props
|
||||
.updateCompany({
|
||||
name: name.trim(),
|
||||
iso639,
|
||||
taxrate: parsedTaxrate,
|
||||
reducedtaxrate: parsedReducedtaxrate,
|
||||
})
|
||||
.unwrap();
|
||||
enqueueSnackbar('Unternehmen gespeichert.', { variant: 'success' });
|
||||
onClose();
|
||||
} catch (err) {
|
||||
this.setState({ error: err.message });
|
||||
this.setState({ error: backendErrorFrom(err) });
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { open, onClose } = this.props;
|
||||
const { loading, busy, error, name, iso639, taxrate, reducedtaxrate } = this.state;
|
||||
const { open, onClose, companyStatus } = this.props;
|
||||
const { busy, error, name, iso639, taxrate, reducedtaxrate } = this.state;
|
||||
const loading = companyStatus === 'loading' && !busy;
|
||||
const parsedTaxrate = parseTaxRate(taxrate);
|
||||
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
|
||||
const canSave =
|
||||
@@ -117,9 +120,13 @@ class CompanySettingsDialog extends Component {
|
||||
<Box component="form" onSubmit={this.handleSubmit} noValidate>
|
||||
<DialogContent>
|
||||
{error ? (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
isBackendFailure(error) ? (
|
||||
<BackendError error={error} sx={{ mb: 2 }} />
|
||||
) : (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{typeof error === 'string' ? error : error.message}
|
||||
</Alert>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
@@ -212,6 +219,13 @@ class CompanySettingsDialog extends Component {
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
user: selectAuthUser(state),
|
||||
company: selectCurrentCompany(state),
|
||||
companyStatus: selectCompanyStatus(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(CompanySettingsDialog);
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
loadCurrentCompany: () => dispatch(loadCurrentCompany()),
|
||||
updateCompany: (payload) => dispatch(updateCompanyThunk(payload)),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CompanySettingsDialog);
|
||||
|
||||
@@ -20,13 +20,19 @@ 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 { createCompany, logout, selectCompany } from '../store/authSlice';
|
||||
import {
|
||||
createCompany as createCompanyThunk,
|
||||
logout as logoutThunk,
|
||||
selectCompany as selectCompanyThunk,
|
||||
} from '../store/authSlice';
|
||||
import {
|
||||
defaultIso639,
|
||||
ISO639_OPTIONS,
|
||||
parseTaxRate,
|
||||
} from '../utils/companyForm';
|
||||
import { backendErrorFrom, isBackendFailure } from '../utils/backendError';
|
||||
|
||||
class CompanySetupDialog extends Component {
|
||||
constructor(props) {
|
||||
@@ -44,6 +50,7 @@ class CompanySetupDialog extends Component {
|
||||
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() {
|
||||
@@ -67,7 +74,7 @@ class CompanySetupDialog extends Component {
|
||||
try {
|
||||
await this.props.selectCompany(companyId).unwrap();
|
||||
} catch (err) {
|
||||
this.setState({ error: err.message });
|
||||
this.setState({ error: backendErrorFrom(err) });
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
@@ -100,7 +107,7 @@ class CompanySetupDialog extends Component {
|
||||
})
|
||||
.unwrap();
|
||||
} catch (err) {
|
||||
this.setState({ error: err.message });
|
||||
this.setState({ error: backendErrorFrom(err) });
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
@@ -110,6 +117,10 @@ class CompanySetupDialog extends Component {
|
||||
void this.props.logout();
|
||||
}
|
||||
|
||||
handleClose(_event, reason) {
|
||||
if (reason === 'backdropClick' || reason === 'escapeKeyDown') return;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { companies, loading, busy, error, name, iso639, taxrate, reducedtaxrate } =
|
||||
this.state;
|
||||
@@ -123,7 +134,7 @@ class CompanySetupDialog extends Component {
|
||||
parsedReducedtaxrate != null;
|
||||
|
||||
return (
|
||||
<Dialog open disableEscapeKeyDown maxWidth="sm" fullWidth>
|
||||
<Dialog open onClose={this.handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{loading
|
||||
? 'Unternehmen'
|
||||
@@ -141,9 +152,13 @@ class CompanySetupDialog extends Component {
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
isBackendFailure(error) ? (
|
||||
<BackendError error={error} sx={{ mb: 2 }} />
|
||||
) : (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{typeof error === 'string' ? error : error.message}
|
||||
</Alert>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
@@ -268,9 +283,9 @@ class CompanySetupDialog extends Component {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
createCompany: (payload) => dispatch(createCompany(payload)),
|
||||
selectCompany: (companyId) => dispatch(selectCompany(companyId)),
|
||||
logout: () => dispatch(logout()),
|
||||
createCompany: (payload) => dispatch(createCompanyThunk(payload)),
|
||||
selectCompany: (companyId) => dispatch(selectCompanyThunk(companyId)),
|
||||
logout: () => dispatch(logoutThunk()),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(CompanySetupDialog);
|
||||
|
||||
18
src/components/tiles/ActionTile.jsx
Normal file
18
src/components/tiles/ActionTile.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Tile } from './Tile';
|
||||
|
||||
export class ActionTile extends Tile {
|
||||
renderIcon() {
|
||||
return <AddIcon color="primary" sx={{ fontSize: 40 }} />;
|
||||
}
|
||||
|
||||
renderLabel() {
|
||||
const { label = 'Aktion' } = this.props;
|
||||
return (
|
||||
<Typography variant="body2" fontWeight={600} sx={{ lineHeight: 1.2 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
}
|
||||
27
src/components/tiles/ArticleTile.jsx
Normal file
27
src/components/tiles/ArticleTile.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Tile } from './Tile';
|
||||
import { formatPrice } from '../../utils/price';
|
||||
|
||||
export class ArticleTile extends Tile {
|
||||
renderIcon() {
|
||||
return <Inventory2OutlinedIcon color="action" sx={{ fontSize: 36 }} />;
|
||||
}
|
||||
|
||||
renderSecondary() {
|
||||
const { article } = this.props;
|
||||
if (!article) return null;
|
||||
return (
|
||||
<>
|
||||
<Typography variant="caption" color="text.secondary" noWrap sx={{ maxWidth: '100%' }}>
|
||||
{article.sku}
|
||||
</Typography>
|
||||
{article.price != null && (
|
||||
<Typography variant="caption" fontWeight={600}>
|
||||
{formatPrice(article.price)}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
18
src/components/tiles/BackTile.jsx
Normal file
18
src/components/tiles/BackTile.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Tile } from './Tile';
|
||||
|
||||
export class BackTile extends Tile {
|
||||
renderIcon() {
|
||||
return <ArrowBackIcon color="action" sx={{ fontSize: 36 }} />;
|
||||
}
|
||||
|
||||
renderLabel() {
|
||||
const { label = 'Zurück' } = this.props;
|
||||
return (
|
||||
<Typography variant="body2" fontWeight={600} sx={{ lineHeight: 1.2 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
}
|
||||
8
src/components/tiles/CategoryTile.jsx
Normal file
8
src/components/tiles/CategoryTile.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import { Tile } from './Tile';
|
||||
|
||||
export class CategoryTile extends Tile {
|
||||
renderIcon() {
|
||||
return <FolderIcon color="primary" sx={{ fontSize: 40 }} />;
|
||||
}
|
||||
}
|
||||
81
src/components/tiles/Tile.jsx
Normal file
81
src/components/tiles/Tile.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Component } from 'react';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export const TILE_SIZE = 140;
|
||||
|
||||
export const tileBaseSx = {
|
||||
aspectRatio: '1 / 1',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
textAlign: 'center',
|
||||
transition: 'border-color 0.15s, background-color 0.15s',
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
borderColor: 'primary.main',
|
||||
},
|
||||
};
|
||||
|
||||
export class Tile extends Component {
|
||||
handleClick = () => {
|
||||
this.props.onClick?.();
|
||||
};
|
||||
|
||||
renderIcon() {
|
||||
return null;
|
||||
}
|
||||
|
||||
renderLabel() {
|
||||
const { label } = this.props;
|
||||
if (!label) return null;
|
||||
return (
|
||||
<Typography variant="body2" fontWeight={600} sx={{ lineHeight: 1.2 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
renderSecondary() {
|
||||
const { secondary } = this.props;
|
||||
if (!secondary) return null;
|
||||
return (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ lineHeight: 1.2 }}>
|
||||
{secondary}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { sx, disabled } = this.props;
|
||||
return (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
component="button"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
sx={{
|
||||
...tileBaseSx,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
...(disabled
|
||||
? { opacity: 0.5, pointerEvents: 'none' }
|
||||
: {}),
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{this.renderIcon()}
|
||||
{this.renderLabel()}
|
||||
{this.renderSecondary()}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
||||
5
src/components/tiles/index.js
Normal file
5
src/components/tiles/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { TILE_SIZE, Tile, tileBaseSx } from './Tile';
|
||||
export { BackTile } from './BackTile';
|
||||
export { CategoryTile } from './CategoryTile';
|
||||
export { ArticleTile } from './ArticleTile';
|
||||
export { ActionTile } from './ActionTile';
|
||||
9
src/components/withNavigate.jsx
Normal file
9
src/components/withNavigate.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function withNavigate(Wrapped) {
|
||||
function NavigateWrapper(props) {
|
||||
const navigate = useNavigate();
|
||||
return <Wrapped {...props} navigate={navigate} />;
|
||||
}
|
||||
return NavigateWrapper;
|
||||
}
|
||||
Reference in New Issue
Block a user