u
This commit is contained in:
@@ -2,6 +2,7 @@ import { Component } from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
@@ -13,14 +14,26 @@ 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 Paper from '@mui/material/Paper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
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 Inventory2Icon from '@mui/icons-material/Inventory2';
|
||||
import PeopleIcon from '@mui/icons-material/People';
|
||||
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import BackendError from './BackendError';
|
||||
import { company } from '../api';
|
||||
import { company, number as numberApi } from '../api';
|
||||
import { StorageManagerContext } from '../api/storageManagerContext';
|
||||
import authAutomat from '../automats/authAutomat';
|
||||
import {
|
||||
@@ -30,6 +43,67 @@ import {
|
||||
} from '../utils/companyForm';
|
||||
import { backendErrorFrom, isBackendFailure } from '../utils/backendError';
|
||||
|
||||
const NUMBER_SCHEMES = [
|
||||
{ type: 'sku', label: 'Artikel-SKU', icon: Inventory2Icon },
|
||||
{ type: 'entity', label: 'Kundennummer', icon: PeopleIcon },
|
||||
{ type: 'delivery', label: 'Lieferschein', icon: LocalShippingIcon },
|
||||
{ type: 'adjustment', label: 'Bestandskorrektur', icon: TuneIcon },
|
||||
];
|
||||
|
||||
const PRESETS = [
|
||||
{
|
||||
id: 'standard',
|
||||
label: 'Standard',
|
||||
hint: 'Klassische Kürzel (ART-, KD-, LS-, KORR-)',
|
||||
values: {
|
||||
sku: { prefix: 'ART-', padding: 6, next: 1 },
|
||||
entity: { prefix: 'KD-', padding: 6, next: 1001 },
|
||||
delivery: { prefix: 'LS-', padding: 6, next: 1 },
|
||||
adjustment: { prefix: 'KORR-', padding: 5, next: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'numeric',
|
||||
label: 'Rein numerisch',
|
||||
hint: 'Ohne Präfix, mit Nummernblöcken (10001, 20001…)',
|
||||
values: {
|
||||
sku: { prefix: '', padding: 5, next: 10001 },
|
||||
entity: { prefix: '', padding: 5, next: 20001 },
|
||||
delivery: { prefix: '', padding: 5, next: 30001 },
|
||||
adjustment: { prefix: '', padding: 5, next: 90001 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'compact',
|
||||
label: 'Kurzkürzel',
|
||||
hint: 'Einbuchstabige Präfixe (A-, K-, L-, B-)',
|
||||
values: {
|
||||
sku: { prefix: 'A-', padding: 5, next: 1 },
|
||||
entity: { prefix: 'K-', padding: 5, next: 1001 },
|
||||
delivery: { prefix: 'L-', padding: 5, next: 1 },
|
||||
adjustment: { prefix: 'B-', padding: 5, next: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'international',
|
||||
label: 'International',
|
||||
hint: 'Englische Kürzel (SKU-, C-, DEL-, ADJ-)',
|
||||
values: {
|
||||
sku: { prefix: 'SKU-', padding: 5, next: 1 },
|
||||
entity: { prefix: 'C-', padding: 5, next: 1001 },
|
||||
delivery: { prefix: 'DEL-', padding: 5, next: 1 },
|
||||
adjustment: { prefix: 'ADJ-', padding: 4, next: 1 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function formatNumberPreview(prefix, padding, next, offset = 0) {
|
||||
const pad = Math.max(0, Number(padding) || 0);
|
||||
const num = Math.max(0, (Number(next) || 1) + offset);
|
||||
const numStr = pad > 0 ? String(num).padStart(pad, '0') : String(num);
|
||||
return `${prefix ?? ''}${numStr}`;
|
||||
}
|
||||
|
||||
class CompanySetupDialog extends Component {
|
||||
static contextType = StorageManagerContext;
|
||||
|
||||
@@ -44,11 +118,15 @@ class CompanySetupDialog extends Component {
|
||||
iso639: defaultIso639(),
|
||||
taxrate: '19',
|
||||
reducedtaxrate: '7',
|
||||
selectedPreset: 'standard',
|
||||
numbers: { ...PRESETS[0].values },
|
||||
};
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
this.handleCreate = this.handleCreate.bind(this);
|
||||
this.handleLogout = this.handleLogout.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.applyPreset = this.applyPreset.bind(this);
|
||||
this.handleNumberChange = this.handleNumberChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -79,9 +157,31 @@ class CompanySetupDialog extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
applyPreset(presetId) {
|
||||
const preset = PRESETS.find((p) => p.id === presetId);
|
||||
if (!preset) return;
|
||||
this.setState({
|
||||
selectedPreset: presetId,
|
||||
numbers: { ...preset.values },
|
||||
});
|
||||
}
|
||||
|
||||
handleNumberChange(type, field, val) {
|
||||
this.setState((prev) => ({
|
||||
selectedPreset: 'custom',
|
||||
numbers: {
|
||||
...prev.numbers,
|
||||
[type]: {
|
||||
...prev.numbers[type],
|
||||
[field]: val,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async handleCreate(event) {
|
||||
event.preventDefault();
|
||||
const { name, iso639, taxrate, reducedtaxrate } = this.state;
|
||||
const { name, iso639, taxrate, reducedtaxrate, numbers } = this.state;
|
||||
const parsedTaxrate = parseTaxRate(taxrate);
|
||||
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
|
||||
const canCreate =
|
||||
@@ -103,8 +203,29 @@ class CompanySetupDialog extends Component {
|
||||
taxrate: parsedTaxrate,
|
||||
reducedtaxrate: parsedReducedtaxrate,
|
||||
});
|
||||
|
||||
// Apply initial number sequences if set
|
||||
if (numbers) {
|
||||
try {
|
||||
const listRes = await numberApi.list();
|
||||
const serverNumbers = listRes?.numbers ?? [];
|
||||
for (const serverRow of serverNumbers) {
|
||||
const userConfig = numbers[serverRow.type];
|
||||
if (userConfig) {
|
||||
const prefix = String(userConfig.prefix ?? '');
|
||||
const padding = Math.max(0, Number(userConfig.padding) || 0);
|
||||
const next = Math.max(1, Number(userConfig.next) || 1);
|
||||
await numberApi.update(serverRow.id, { prefix, padding, next });
|
||||
}
|
||||
}
|
||||
} catch (numErr) {
|
||||
console.warn('Initial number sequences could not be saved:', numErr);
|
||||
}
|
||||
}
|
||||
|
||||
await this.context.updateAuthMe();
|
||||
await this.context.updateCompany();
|
||||
await this.context.updateNumber?.();
|
||||
} catch (err) {
|
||||
this.setState({ error: backendErrorFrom(err) });
|
||||
} finally {
|
||||
@@ -126,8 +247,18 @@ class CompanySetupDialog extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { companies, loading, busy, error, name, iso639, taxrate, reducedtaxrate } =
|
||||
this.state;
|
||||
const {
|
||||
companies,
|
||||
loading,
|
||||
busy,
|
||||
error,
|
||||
name,
|
||||
iso639,
|
||||
taxrate,
|
||||
reducedtaxrate,
|
||||
selectedPreset,
|
||||
numbers,
|
||||
} = this.state;
|
||||
const hasCompanies = companies.length > 0;
|
||||
const parsedTaxrate = parseTaxRate(taxrate);
|
||||
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
|
||||
@@ -138,20 +269,25 @@ class CompanySetupDialog extends Component {
|
||||
parsedReducedtaxrate != null;
|
||||
|
||||
return (
|
||||
<Dialog open onClose={this.handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
<Dialog
|
||||
open
|
||||
onClose={this.handleClose}
|
||||
maxWidth={hasCompanies ? 'sm' : 'md'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle sx={{ pb: 1 }}>
|
||||
{loading
|
||||
? 'Unternehmen'
|
||||
: hasCompanies
|
||||
? 'Unternehmen auswählen'
|
||||
: 'Unternehmen anlegen'}
|
||||
: 'Willkommen bei Dash'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{!loading ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2.5 }}>
|
||||
{hasCompanies
|
||||
? 'Wählen Sie ein bestehendes Unternehmen oder legen Sie ein neues an.'
|
||||
: 'Legen Sie Ihr Unternehmen an, um fortzufahren.'}
|
||||
: 'Legen Sie Ihr erstes Unternehmen an und wählen Sie das Format für Ihre Nummernkreise.'}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
@@ -191,7 +327,7 @@ class CompanySetupDialog extends Component {
|
||||
{hasCompanies ? <Divider>oder</Divider> : null}
|
||||
|
||||
<Box component="form" onSubmit={this.handleCreate} noValidate>
|
||||
<Stack spacing={2}>
|
||||
<Stack spacing={2.5}>
|
||||
{hasCompanies ? (
|
||||
<Typography variant="subtitle2">Neues Unternehmen anlegen</Typography>
|
||||
) : null}
|
||||
@@ -200,7 +336,7 @@ class CompanySetupDialog extends Component {
|
||||
fullWidth
|
||||
required
|
||||
size="small"
|
||||
label="Name"
|
||||
label="Unternehmensname"
|
||||
value={name}
|
||||
disabled={busy}
|
||||
onChange={(e) => this.setState({ name: e.target.value })}
|
||||
@@ -225,7 +361,7 @@ class CompanySetupDialog extends Component {
|
||||
</ToggleButtonGroup>
|
||||
</FormControl>
|
||||
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
required
|
||||
@@ -262,8 +398,143 @@ class CompanySetupDialog extends Component {
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button type="submit" variant="contained" disabled={busy || !canCreate}>
|
||||
Anlegen
|
||||
{/* Nummernkreise Section (when no company was created yet or creating new) */}
|
||||
<Box sx={{ pt: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
Nummernkreise & Belegnummern
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
|
||||
Wählen Sie eine Voreinstellung oder passen Sie Präfix, Stellen und Startnummer an:
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap sx={{ mb: 1.5 }}>
|
||||
{PRESETS.map((p) => {
|
||||
const isSelected = selectedPreset === p.id;
|
||||
return (
|
||||
<Button
|
||||
key={p.id}
|
||||
size="small"
|
||||
variant={isSelected ? 'contained' : 'outlined'}
|
||||
color={isSelected ? 'primary' : 'inherit'}
|
||||
disabled={busy}
|
||||
onClick={() => this.applyPreset(p.id)}
|
||||
startIcon={isSelected ? <AutoAwesomeIcon sx={{ fontSize: 16 }} /> : null}
|
||||
sx={{ textTransform: 'none', borderRadius: 2 }}
|
||||
>
|
||||
{p.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{selectedPreset === 'custom' ? (
|
||||
<Chip
|
||||
size="small"
|
||||
label="Benutzerdefiniert"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
sx={{ height: 30, borderRadius: 2 }}
|
||||
/>
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ borderRadius: 1.5 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow sx={{ bgcolor: 'action.hover' }}>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Bereich</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, width: 130 }}>Präfix</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, width: 90 }}>Stellen</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, width: 110 }}>Startnr.</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Vorschau</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{NUMBER_SCHEMES.map(({ type, label, icon: Icon }) => {
|
||||
const cfg = numbers[type] || { prefix: '', padding: 5, next: 1 };
|
||||
return (
|
||||
<TableRow key={type} hover>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Icon sx={{ fontSize: 18, color: 'text.secondary', flexShrink: 0 }} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, whiteSpace: 'nowrap' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
value={cfg.prefix}
|
||||
disabled={busy}
|
||||
onChange={(e) => this.handleNumberChange(type, 'prefix', e.target.value)}
|
||||
placeholder="z.B. ART-"
|
||||
sx={{ input: { fontFamily: 'monospace', fontSize: '0.85rem' } }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
type="number"
|
||||
value={cfg.padding}
|
||||
disabled={busy}
|
||||
slotProps={{ htmlInput: { min: 0, max: 20, step: 1 } }}
|
||||
onChange={(e) => this.handleNumberChange(type, 'padding', e.target.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
type="number"
|
||||
value={cfg.next}
|
||||
disabled={busy}
|
||||
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
||||
onChange={(e) => this.handleNumberChange(type, 'next', e.target.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
size="small"
|
||||
label={formatNumberPreview(cfg.prefix, cfg.padding, cfg.next, 0)}
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 700,
|
||||
bgcolor: 'primary.50',
|
||||
color: 'primary.dark',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.200',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption" color="text.disabled">
|
||||
→
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
label={formatNumberPreview(cfg.prefix, cfg.padding, cfg.next, 1)}
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.75rem',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={busy || !canCreate}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Unternehmen anlegen
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -287,3 +558,4 @@ class CompanySetupDialog extends Component {
|
||||
}
|
||||
|
||||
export default CompanySetupDialog;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user