Implement navigation and view management in DataViewer, adding Dashboard and TableManagement components. Update export data handling based on current view. Create new components for managing Kreditor, Konto, and BU tables with CRUD functionality. Refactor admin routes to remove admin access checks and streamline data handling for various entities.

This commit is contained in:
sebseb7
2025-08-01 10:22:43 +02:00
parent 092fa0f8bd
commit 5470bebfc4
9 changed files with 1382 additions and 114 deletions

31
.eslintrc.js Normal file
View File

@@ -0,0 +1,31 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
rules: {
// Prevent browser alert dialogs
'no-alert': 'error',
'no-confirm': 'error',
'no-prompt': 'error',
// Additional helpful rules
'no-console': 'warn',
'no-debugger': 'error',
},
globals: {
// Allow React globals
React: 'readonly',
},
};

View File

@@ -7,6 +7,9 @@ import {
import AuthService from '../services/AuthService';
import SummaryHeader from './SummaryHeader';
import TransactionsTable from './TransactionsTable';
import Navigation from './Navigation';
import Dashboard from './Dashboard';
import TableManagement from './TableManagement';
class DataViewer extends Component {
constructor(props) {
@@ -18,6 +21,7 @@ class DataViewer extends Component {
summary: null,
loading: true,
error: null,
currentView: 'dashboard', // 'dashboard' or 'tables'
};
this.authService = new AuthService();
}
@@ -110,14 +114,21 @@ class DataViewer extends Component {
if (this.props.onUpdateExportData) {
this.props.onUpdateExportData({
selectedMonth,
canExport: !!selectedMonth && !this.state.loading,
canExport: !!selectedMonth && !this.state.loading && this.state.currentView === 'dashboard',
onExport: this.downloadDatev
});
}
};
handleViewChange = (event, newView) => {
this.setState({ currentView: newView });
// Update export data when view changes
this.updateExportData();
};
render() {
const { months, selectedMonth, transactions, summary, loading, error } = this.state;
const { months, selectedMonth, transactions, summary, loading, error, currentView } = this.state;
const { user } = this.props;
if (loading && !transactions.length) {
return (
@@ -137,23 +148,36 @@ class DataViewer extends Component {
return (
<Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ flexShrink: 0 }}>
<SummaryHeader
months={months}
selectedMonth={selectedMonth}
summary={summary}
loading={loading}
onMonthChange={this.handleMonthChange}
/>
</Box>
<Navigation
currentView={currentView}
onViewChange={this.handleViewChange}
/>
<Box sx={{ flex: 1, minHeight: 0 }}>
<TransactionsTable
transactions={transactions}
selectedMonth={selectedMonth}
loading={loading}
/>
</Box>
{currentView === 'dashboard' ? (
<>
<Box sx={{ flexShrink: 0 }}>
<SummaryHeader
months={months}
selectedMonth={selectedMonth}
summary={summary}
loading={loading}
onMonthChange={this.handleMonthChange}
/>
</Box>
<Box sx={{ flex: 1, minHeight: 0 }}>
<TransactionsTable
transactions={transactions}
selectedMonth={selectedMonth}
loading={loading}
/>
</Box>
</>
) : (
<Box sx={{ flex: 1, minHeight: 0, overflow: 'auto', p: 2 }}>
<TableManagement user={user} />
</Box>
)}
</Box>
);
}

View File

@@ -0,0 +1,45 @@
import React, { Component } from 'react';
import {
Box,
Tabs,
Tab,
Paper,
} from '@mui/material';
import {
Dashboard as DashboardIcon,
TableChart as TableIcon,
} from '@mui/icons-material';
class Navigation extends Component {
render() {
const { currentView, onViewChange } = this.props;
return (
<Paper elevation={1} sx={{ mb: 3 }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={currentView}
onChange={onViewChange}
variant="fullWidth"
sx={{ minHeight: 48 }}
>
<Tab
icon={<DashboardIcon />}
label="Dashboard"
value="dashboard"
sx={{ minHeight: 48 }}
/>
<Tab
icon={<TableIcon />}
label="Stammdaten"
value="tables"
sx={{ minHeight: 48 }}
/>
</Tabs>
</Box>
</Paper>
);
}
}
export default Navigation;

View File

@@ -0,0 +1,76 @@
import React, { Component } from 'react';
import {
Box,
Tabs,
Tab,
Paper,
Typography,
} from '@mui/material';
import {
AccountBalance as KreditorIcon,
AccountBalanceWallet as KontoIcon,
Receipt as BUIcon,
} from '@mui/icons-material';
import KreditorTable from './admin/KreditorTable';
import KontoTable from './admin/KontoTable';
import BUTable from './admin/BUTable';
class TableManagement extends Component {
constructor(props) {
super(props);
this.state = {
activeTab: 0,
};
}
handleTabChange = (event, newValue) => {
this.setState({ activeTab: newValue });
};
render() {
const { activeTab } = this.state;
const { user } = this.props;
return (
<Box>
<Typography variant="h4" component="h1" gutterBottom>
Stammdaten verwalten
</Typography>
<Paper elevation={2}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={activeTab}
onChange={this.handleTabChange}
variant="fullWidth"
>
<Tab
icon={<KreditorIcon />}
label="Kreditoren"
sx={{ minHeight: 64 }}
/>
<Tab
icon={<KontoIcon />}
label="Konten"
sx={{ minHeight: 64 }}
/>
<Tab
icon={<BUIcon />}
label="Buchungsschlüssel"
sx={{ minHeight: 64 }}
/>
</Tabs>
</Box>
<Box sx={{ p: 3 }}>
{activeTab === 0 && <KreditorTable user={user} />}
{activeTab === 1 && <KontoTable user={user} />}
{activeTab === 2 && <BUTable user={user} />}
</Box>
</Paper>
</Box>
);
}
}
export default TableManagement;

View File

@@ -0,0 +1,325 @@
import React, { Component } from 'react';
import {
Box,
Button,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
IconButton,
Typography,
Alert,
CircularProgress,
} from '@mui/material';
import {
Add as AddIcon,
Edit as EditIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import AuthService from '../../services/AuthService';
class BUTable extends Component {
constructor(props) {
super(props);
this.state = {
buchungsschluessel: [],
loading: true,
error: null,
dialogOpen: false,
editingBU: null,
confirmDialogOpen: false,
itemToDelete: null,
formData: {
bu: '',
name: '',
vst: '',
},
};
this.authService = new AuthService();
}
componentDidMount() {
this.loadBuchungsschluessel();
}
loadBuchungsschluessel = async () => {
try {
const response = await this.authService.apiCall('/admin/buchungsschluessel');
if (response && response.ok) {
const data = await response.json();
this.setState({ buchungsschluessel: data.buchungsschluessel, loading: false });
} else {
this.setState({ error: 'Fehler beim Laden der Buchungsschlüssel', loading: false });
}
} catch (error) {
console.error('Error loading buchungsschluessel:', error);
this.setState({ error: 'Fehler beim Laden der Buchungsschlüssel', loading: false });
}
};
handleOpenDialog = (bu = null) => {
this.setState({
dialogOpen: true,
editingBU: bu,
formData: bu ? {
bu: bu.bu,
name: bu.name,
vst: bu.vst || '',
} : {
bu: '',
name: '',
vst: '',
},
});
};
handleCloseDialog = () => {
this.setState({
dialogOpen: false,
editingBU: null,
formData: {
bu: '',
name: '',
vst: '',
},
});
};
handleInputChange = (field) => (event) => {
this.setState({
formData: {
...this.state.formData,
[field]: event.target.value,
},
});
};
handleSave = async () => {
const { editingBU, formData } = this.state;
// Convert vst to number or null
const payload = {
...formData,
vst: formData.vst ? parseFloat(formData.vst) : null,
};
try {
const url = editingBU
? `/admin/buchungsschluessel/${editingBU.id}`
: '/admin/buchungsschluessel';
const method = editingBU ? 'PUT' : 'POST';
const response = await this.authService.apiCall(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (response && response.ok) {
this.handleCloseDialog();
this.loadBuchungsschluessel();
} else {
const errorData = await response.json();
this.setState({ error: errorData.error || 'Fehler beim Speichern' });
}
} catch (error) {
console.error('Error saving BU:', error);
this.setState({ error: 'Fehler beim Speichern des Buchungsschlüssels' });
}
};
handleDeleteClick = (bu) => {
this.setState({
confirmDialogOpen: true,
itemToDelete: bu,
});
};
handleDeleteConfirm = async () => {
const { itemToDelete } = this.state;
if (!itemToDelete) return;
this.setState({ confirmDialogOpen: false, itemToDelete: null });
try {
const response = await this.authService.apiCall(`/admin/buchungsschluessel/${itemToDelete.id}`, {
method: 'DELETE',
});
if (response && response.ok) {
this.loadBuchungsschluessel();
} else {
const errorData = await response.json();
this.setState({ error: errorData.error || 'Fehler beim Löschen' });
}
} catch (error) {
console.error('Error deleting BU:', error);
this.setState({ error: 'Fehler beim Löschen des Buchungsschlüssels' });
}
};
handleDeleteCancel = () => {
this.setState({
confirmDialogOpen: false,
itemToDelete: null,
});
};
render() {
const { buchungsschluessel, loading, error, dialogOpen, editingBU, formData } = this.state;
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress />
</Box>
);
}
return (
<Box>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h6">Buchungsschlüssel</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => this.handleOpenDialog()}
>
Neuer Buchungsschlüssel
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>BU</TableCell>
<TableCell>Name</TableCell>
<TableCell align="right">VST %</TableCell>
<TableCell align="right">Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{buchungsschluessel.map((bu) => (
<TableRow key={bu.id}>
<TableCell>{bu.bu}</TableCell>
<TableCell>{bu.name}</TableCell>
<TableCell align="right">
{bu.vst ? `${bu.vst}%` : '-'}
</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => this.handleOpenDialog(bu)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
onClick={() => this.handleDeleteClick(bu)}
color="error"
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Dialog open={dialogOpen} onClose={this.handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{editingBU ? 'Buchungsschlüssel bearbeiten' : 'Neuer Buchungsschlüssel'}
</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="BU"
fullWidth
variant="outlined"
value={formData.bu}
onChange={this.handleInputChange('bu')}
sx={{ mb: 2 }}
helperText="z.B. 9, 8, 506, 511"
/>
<TextField
margin="dense"
label="Name"
fullWidth
variant="outlined"
value={formData.name}
onChange={this.handleInputChange('name')}
sx={{ mb: 2 }}
helperText="z.B. 19% VST, 7% VST, Dienstleistung aus EU"
/>
<TextField
margin="dense"
label="Vorsteuer %"
fullWidth
variant="outlined"
type="number"
value={formData.vst}
onChange={this.handleInputChange('vst')}
helperText="z.B. 19.00 für 19% (optional)"
inputProps={{
step: 0.01,
min: 0,
max: 100,
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
<Button onClick={this.handleSave} variant="contained">
Speichern
</Button>
</DialogActions>
</Dialog>
{/* Confirmation Dialog */}
<Dialog
open={this.state.confirmDialogOpen}
onClose={this.handleDeleteCancel}
maxWidth="sm"
fullWidth
>
<DialogTitle>Löschen bestätigen</DialogTitle>
<DialogContent>
<Typography>
{this.state.itemToDelete &&
`Buchungsschlüssel "${this.state.itemToDelete.bu} - ${this.state.itemToDelete.name}" wirklich löschen?`
}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={this.handleDeleteCancel}>
Abbrechen
</Button>
<Button onClick={this.handleDeleteConfirm} color="error" variant="contained">
Löschen
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
}
export default BUTable;

View File

@@ -0,0 +1,295 @@
import React, { Component } from 'react';
import {
Box,
Button,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
IconButton,
Typography,
Alert,
CircularProgress,
} from '@mui/material';
import {
Add as AddIcon,
Edit as EditIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import AuthService from '../../services/AuthService';
class KontoTable extends Component {
constructor(props) {
super(props);
this.state = {
konten: [],
loading: true,
error: null,
dialogOpen: false,
editingKonto: null,
confirmDialogOpen: false,
itemToDelete: null,
formData: {
konto: '',
name: '',
},
};
this.authService = new AuthService();
}
componentDidMount() {
this.loadKonten();
}
loadKonten = async () => {
try {
const response = await this.authService.apiCall('/admin/konten');
if (response && response.ok) {
const data = await response.json();
this.setState({ konten: data.konten, loading: false });
} else {
this.setState({ error: 'Fehler beim Laden der Konten', loading: false });
}
} catch (error) {
console.error('Error loading konten:', error);
this.setState({ error: 'Fehler beim Laden der Konten', loading: false });
}
};
handleOpenDialog = (konto = null) => {
this.setState({
dialogOpen: true,
editingKonto: konto,
formData: konto ? {
konto: konto.konto,
name: konto.name,
} : {
konto: '',
name: '',
},
});
};
handleCloseDialog = () => {
this.setState({
dialogOpen: false,
editingKonto: null,
formData: {
konto: '',
name: '',
},
});
};
handleInputChange = (field) => (event) => {
this.setState({
formData: {
...this.state.formData,
[field]: event.target.value,
},
});
};
handleSave = async () => {
const { editingKonto, formData } = this.state;
try {
const url = editingKonto
? `/admin/konten/${editingKonto.id}`
: '/admin/konten';
const method = editingKonto ? 'PUT' : 'POST';
const response = await this.authService.apiCall(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response && response.ok) {
this.handleCloseDialog();
this.loadKonten();
} else {
const errorData = await response.json();
this.setState({ error: errorData.error || 'Fehler beim Speichern' });
}
} catch (error) {
console.error('Error saving konto:', error);
this.setState({ error: 'Fehler beim Speichern des Kontos' });
}
};
handleDeleteClick = (konto) => {
this.setState({
confirmDialogOpen: true,
itemToDelete: konto,
});
};
handleDeleteConfirm = async () => {
const { itemToDelete } = this.state;
if (!itemToDelete) return;
this.setState({ confirmDialogOpen: false, itemToDelete: null });
try {
const response = await this.authService.apiCall(`/admin/konten/${konto.id}`, {
method: 'DELETE',
});
if (response && response.ok) {
this.loadKonten();
} else {
const errorData = await response.json();
this.setState({ error: errorData.error || 'Fehler beim Löschen' });
}
} catch (error) {
console.error('Error deleting konto:', error);
this.setState({ error: 'Fehler beim Löschen des Kontos' });
}
};
handleDeleteCancel = () => {
this.setState({
confirmDialogOpen: false,
itemToDelete: null,
});
};
render() {
const { konten, loading, error, dialogOpen, editingKonto, formData } = this.state;
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress />
</Box>
);
}
return (
<Box>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h6">Konten</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => this.handleOpenDialog()}
>
Neues Konto
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Konto</TableCell>
<TableCell>Name</TableCell>
<TableCell align="right">Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{konten.map((konto) => (
<TableRow key={konto.id}>
<TableCell>{konto.konto}</TableCell>
<TableCell>{konto.name}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => this.handleOpenDialog(konto)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
onClick={() => this.handleDeleteClick(konto)}
color="error"
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Dialog open={dialogOpen} onClose={this.handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{editingKonto ? 'Konto bearbeiten' : 'Neues Konto'}
</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Konto"
fullWidth
variant="outlined"
value={formData.konto}
onChange={this.handleInputChange('konto')}
sx={{ mb: 2 }}
helperText="z.B. 5400"
/>
<TextField
margin="dense"
label="Name"
fullWidth
variant="outlined"
value={formData.name}
onChange={this.handleInputChange('name')}
helperText="z.B. Wareneingang 19%"
/>
</DialogContent>
<DialogActions>
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
<Button onClick={this.handleSave} variant="contained">
Speichern
</Button>
</DialogActions>
</Dialog>
{/* Confirmation Dialog */}
<Dialog
open={this.state.confirmDialogOpen}
onClose={this.handleDeleteCancel}
maxWidth="sm"
fullWidth
>
<DialogTitle>Löschen bestätigen</DialogTitle>
<DialogContent>
<Typography>
{this.state.itemToDelete &&
`Konto "${this.state.itemToDelete.konto} - ${this.state.itemToDelete.name}" wirklich löschen?`
}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={this.handleDeleteCancel}>
Abbrechen
</Button>
<Button onClick={this.handleDeleteConfirm} color="error" variant="contained">
Löschen
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
}
export default KontoTable;

View File

@@ -0,0 +1,308 @@
import React, { Component } from 'react';
import {
Box,
Button,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
IconButton,
Typography,
Alert,
CircularProgress,
} from '@mui/material';
import {
Add as AddIcon,
Edit as EditIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import AuthService from '../../services/AuthService';
class KreditorTable extends Component {
constructor(props) {
super(props);
this.state = {
kreditoren: [],
loading: true,
error: null,
dialogOpen: false,
editingKreditor: null,
confirmDialogOpen: false,
itemToDelete: null,
formData: {
iban: '',
name: '',
kreditorId: '',
},
};
this.authService = new AuthService();
}
componentDidMount() {
this.loadKreditoren();
}
loadKreditoren = async () => {
try {
const response = await this.authService.apiCall('/admin/kreditoren');
if (response && response.ok) {
const data = await response.json();
this.setState({ kreditoren: data.kreditoren, loading: false });
} else {
this.setState({ error: 'Fehler beim Laden der Kreditoren', loading: false });
}
} catch (error) {
console.error('Error loading kreditoren:', error);
this.setState({ error: 'Fehler beim Laden der Kreditoren', loading: false });
}
};
handleOpenDialog = (kreditor = null) => {
this.setState({
dialogOpen: true,
editingKreditor: kreditor,
formData: kreditor ? {
iban: kreditor.iban,
name: kreditor.name,
kreditorId: kreditor.kreditorId,
} : {
iban: '',
name: '',
kreditorId: '',
},
});
};
handleCloseDialog = () => {
this.setState({
dialogOpen: false,
editingKreditor: null,
formData: {
iban: '',
name: '',
kreditorId: '',
},
});
};
handleInputChange = (field) => (event) => {
this.setState({
formData: {
...this.state.formData,
[field]: event.target.value,
},
});
};
handleSave = async () => {
const { editingKreditor, formData } = this.state;
try {
const url = editingKreditor
? `/admin/kreditoren/${editingKreditor.id}`
: '/admin/kreditoren';
const method = editingKreditor ? 'PUT' : 'POST';
const response = await this.authService.apiCall(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response && response.ok) {
this.handleCloseDialog();
this.loadKreditoren();
} else {
const errorData = await response.json();
this.setState({ error: errorData.error || 'Fehler beim Speichern' });
}
} catch (error) {
console.error('Error saving kreditor:', error);
this.setState({ error: 'Fehler beim Speichern des Kreditors' });
}
};
handleDeleteClick = (kreditor) => {
this.setState({
confirmDialogOpen: true,
itemToDelete: kreditor,
});
};
handleDeleteConfirm = async () => {
const { itemToDelete } = this.state;
if (!itemToDelete) return;
this.setState({ confirmDialogOpen: false, itemToDelete: null });
try {
const response = await this.authService.apiCall(`/admin/kreditoren/${kreditor.id}`, {
method: 'DELETE',
});
if (response && response.ok) {
this.loadKreditoren();
} else {
const errorData = await response.json();
this.setState({ error: errorData.error || 'Fehler beim Löschen' });
}
} catch (error) {
console.error('Error deleting kreditor:', error);
this.setState({ error: 'Fehler beim Löschen des Kreditors' });
}
};
handleDeleteCancel = () => {
this.setState({
confirmDialogOpen: false,
itemToDelete: null,
});
};
render() {
const { kreditoren, loading, error, dialogOpen, editingKreditor, formData } = this.state;
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress />
</Box>
);
}
return (
<Box>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h6">Kreditoren</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => this.handleOpenDialog()}
>
Neuer Kreditor
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Kreditor ID</TableCell>
<TableCell>Name</TableCell>
<TableCell>IBAN</TableCell>
<TableCell align="right">Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{kreditoren.map((kreditor) => (
<TableRow key={kreditor.id}>
<TableCell>{kreditor.kreditorId}</TableCell>
<TableCell>{kreditor.name}</TableCell>
<TableCell>{kreditor.iban}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => this.handleOpenDialog(kreditor)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
onClick={() => this.handleDeleteClick(kreditor)}
color="error"
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Dialog open={dialogOpen} onClose={this.handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{editingKreditor ? 'Kreditor bearbeiten' : 'Neuer Kreditor'}
</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Kreditor ID"
fullWidth
variant="outlined"
value={formData.kreditorId}
onChange={this.handleInputChange('kreditorId')}
sx={{ mb: 2 }}
/>
<TextField
margin="dense"
label="Name"
fullWidth
variant="outlined"
value={formData.name}
onChange={this.handleInputChange('name')}
sx={{ mb: 2 }}
/>
<TextField
margin="dense"
label="IBAN"
fullWidth
variant="outlined"
value={formData.iban}
onChange={this.handleInputChange('iban')}
/>
</DialogContent>
<DialogActions>
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
<Button onClick={this.handleSave} variant="contained">
Speichern
</Button>
</DialogActions>
</Dialog>
{/* Confirmation Dialog */}
<Dialog
open={this.state.confirmDialogOpen}
onClose={this.handleDeleteCancel}
maxWidth="sm"
fullWidth
>
<DialogTitle>Löschen bestätigen</DialogTitle>
<DialogContent>
<Typography>
{this.state.itemToDelete &&
`Kreditor "${this.state.itemToDelete.name}" wirklich löschen?`
}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={this.handleDeleteCancel}>
Abbrechen
</Button>
<Button onClick={this.handleDeleteConfirm} color="error" variant="contained">
Löschen
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
}
export default KreditorTable;

View File

@@ -49,7 +49,8 @@ ADD CONSTRAINT UQ_Konto_konto UNIQUE (konto);
CREATE TABLE fibdash.BU (
id INT IDENTITY(1,1) PRIMARY KEY,
bu NVARCHAR(10) NOT NULL,
name NVARCHAR(255) NOT NULL
name NVARCHAR(255) NOT NULL,
vst DECIMAL(5,2) -- Vorsteuer percentage (e.g., 19.00 for 19%)
);
-- Ensure BU.bu is unique to support FK references
@@ -103,6 +104,22 @@ ALTER TABLE fibdash.AccountingItems
ADD CONSTRAINT FK_AccountingItems_Konto_Konto
FOREIGN KEY (konto) REFERENCES fibdash.Konto(konto);
-- Add vst column to existing BU table (for databases created before this update)
-- IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('fibdash.BU') AND name = 'vst')
-- BEGIN
-- ALTER TABLE fibdash.BU ADD vst DECIMAL(5,2);
-- END
-- Insert sample data (optional)
-- INSERT INTO fibdash.Kreditor (iban, name, kreditorId)
-- VALUES ('DE89370400440532013000', 'Sample Kreditor', '70001');
-- VALUES ('DE89370400440532013000', 'Sample Kreditor', '70001');
-- INSERT INTO fibdash.Konto (konto, name) VALUES
-- ('5400', 'Wareneingang 19%'),
-- ('5600', 'Nicht abziehbare Vorsteuer');
-- INSERT INTO fibdash.BU (bu, name, vst) VALUES
-- ('9', '19% VST', 19.00),
-- ('8', '7% VST', 7.00),
-- ('506', 'Dienstleistung aus EU', NULL),
-- ('511', 'Dienstleistung außerhalb EU', NULL);

View File

@@ -1,110 +1,257 @@
const express = require('express');
const { authenticateToken } = require('../middleware/auth');
const { checkAuthorizedEmail } = require('../middleware/emailAuth');
const { executeQuery, sql } = require('../config/database');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// Check if user is admin (first email in the list or specific admin email)
const checkAdminAccess = (req, res, next) => {
const authorizedEmails = process.env.AUTHORIZED_EMAILS;
if (!authorizedEmails || authorizedEmails.trim() === '') {
return res.status(403).json({ error: 'No authorized emails configured' });
}
const emailList = authorizedEmails.split(',').map(email => email.trim().toLowerCase());
const userEmail = req.user?.email?.toLowerCase();
// First email in the list is considered admin, or check for specific admin emails
const adminEmails = [emailList[0]]; // First email is admin
if (!adminEmails.includes(userEmail)) {
return res.status(403).json({
error: 'Admin access required',
message: 'Only administrators can access this resource'
});
}
next();
};
// Removed admin access check - all authenticated users can access these routes
// Get current authorized emails (admin only)
router.get('/authorized-emails', authenticateToken, checkAdminAccess, (req, res) => {
const authorizedEmails = process.env.AUTHORIZED_EMAILS;
if (!authorizedEmails) {
return res.json({ emails: [] });
}
const emailList = authorizedEmails.split(',').map(email => email.trim());
res.json({ emails: emailList });
});
// Add authorized email (admin only)
router.post('/authorized-emails', authenticateToken, checkAdminAccess, (req, res) => {
const { email } = req.body;
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Valid email address required' });
}
const authorizedEmails = process.env.AUTHORIZED_EMAILS || '';
const emailList = authorizedEmails.split(',').map(e => e.trim()).filter(e => e);
const newEmail = email.trim().toLowerCase();
if (emailList.map(e => e.toLowerCase()).includes(newEmail)) {
return res.status(400).json({ error: 'Email already authorized' });
}
emailList.push(email.trim());
// Note: This only updates the runtime environment variable
// For persistent changes, you'd need to update the .env file
process.env.AUTHORIZED_EMAILS = emailList.join(',');
res.json({
message: 'Email added successfully',
emails: emailList,
note: 'Changes are temporary. Update .env file for permanent changes.'
});
});
// Remove authorized email (admin only)
router.delete('/authorized-emails/:email', authenticateToken, checkAdminAccess, (req, res) => {
const emailToRemove = req.params.email.toLowerCase();
const authorizedEmails = process.env.AUTHORIZED_EMAILS || '';
const emailList = authorizedEmails.split(',').map(e => e.trim()).filter(e => e);
const filteredEmails = emailList.filter(email => email.toLowerCase() !== emailToRemove);
if (filteredEmails.length === emailList.length) {
return res.status(404).json({ error: 'Email not found in authorized list' });
}
// Don't allow removing the last admin email
if (filteredEmails.length === 0) {
return res.status(400).json({ error: 'Cannot remove all authorized emails' });
}
// Note: This only updates the runtime environment variable
process.env.AUTHORIZED_EMAILS = filteredEmails.join(',');
res.json({
message: 'Email removed successfully',
emails: filteredEmails,
note: 'Changes are temporary. Update .env file for permanent changes.'
});
});
// Get system info (admin only)
router.get('/system-info', authenticateToken, checkAdminAccess, (req, res) => {
// Get system info
router.get('/system-info', authenticateToken, (req, res) => {
res.json({
authorizedEmailsConfigured: !!process.env.AUTHORIZED_EMAILS,
totalAuthorizedEmails: process.env.AUTHORIZED_EMAILS ? process.env.AUTHORIZED_EMAILS.split(',').length : 0,
currentUser: req.user.email,
isAdmin: true,
environment: process.env.NODE_ENV || 'development'
});
});
// ==================== KREDITOR ROUTES ====================
// Get all kreditoren
router.get('/kreditoren', authenticateToken, async (req, res) => {
try {
const result = await executeQuery('SELECT * FROM fibdash.Kreditor ORDER BY name');
res.json({ kreditoren: result.recordset });
} catch (error) {
console.error('Error fetching kreditoren:', error);
res.status(500).json({ error: 'Fehler beim Laden der Kreditoren' });
}
});
// Create new kreditor
router.post('/kreditoren', authenticateToken, async (req, res) => {
const { iban, name, kreditorId } = req.body;
if (!iban || !name || !kreditorId) {
return res.status(400).json({ error: 'IBAN, Name und Kreditor ID sind erforderlich' });
}
try {
await executeQuery(
'INSERT INTO fibdash.Kreditor (iban, name, kreditorId) VALUES (@iban, @name, @kreditorId)',
{ iban, name, kreditorId }
);
res.json({ message: 'Kreditor erfolgreich erstellt' });
} catch (error) {
console.error('Error creating kreditor:', error);
if (error.number === 2627) { // Unique constraint violation
res.status(400).json({ error: 'Kreditor ID bereits vorhanden' });
} else {
res.status(500).json({ error: 'Fehler beim Erstellen des Kreditors' });
}
}
});
// Update kreditor
router.put('/kreditoren/:id', authenticateToken, async (req, res) => {
const { id } = req.params;
const { iban, name, kreditorId } = req.body;
if (!iban || !name || !kreditorId) {
return res.status(400).json({ error: 'IBAN, Name und Kreditor ID sind erforderlich' });
}
try {
await executeQuery(
'UPDATE fibdash.Kreditor SET iban = @iban, name = @name, kreditorId = @kreditorId WHERE id = @id',
{ iban, name, kreditorId, id }
);
res.json({ message: 'Kreditor erfolgreich aktualisiert' });
} catch (error) {
console.error('Error updating kreditor:', error);
if (error.number === 2627) { // Unique constraint violation
res.status(400).json({ error: 'Kreditor ID bereits vorhanden' });
} else {
res.status(500).json({ error: 'Fehler beim Aktualisieren des Kreditors' });
}
}
});
// Delete kreditor
router.delete('/kreditoren/:id', authenticateToken, async (req, res) => {
const { id } = req.params;
try {
await executeQuery('DELETE FROM fibdash.Kreditor WHERE id = @id', { id });
res.json({ message: 'Kreditor erfolgreich gelöscht' });
} catch (error) {
console.error('Error deleting kreditor:', error);
if (error.number === 547) { // Foreign key constraint violation
res.status(400).json({ error: 'Kreditor kann nicht gelöscht werden, da er in Buchungen verwendet wird' });
} else {
res.status(500).json({ error: 'Fehler beim Löschen des Kreditors' });
}
}
});
// ==================== KONTO ROUTES ====================
// Get all konten
router.get('/konten', authenticateToken, async (req, res) => {
try {
const result = await executeQuery('SELECT * FROM fibdash.Konto ORDER BY konto');
res.json({ konten: result.recordset });
} catch (error) {
console.error('Error fetching konten:', error);
res.status(500).json({ error: 'Fehler beim Laden der Konten' });
}
});
// Create new konto
router.post('/konten', authenticateToken, async (req, res) => {
const { konto, name } = req.body;
if (!konto || !name) {
return res.status(400).json({ error: 'Konto und Name sind erforderlich' });
}
try {
await executeQuery(
'INSERT INTO fibdash.Konto (konto, name) VALUES (@konto, @name)',
{ konto, name }
);
res.json({ message: 'Konto erfolgreich erstellt' });
} catch (error) {
console.error('Error creating konto:', error);
if (error.number === 2627) { // Unique constraint violation
res.status(400).json({ error: 'Konto bereits vorhanden' });
} else {
res.status(500).json({ error: 'Fehler beim Erstellen des Kontos' });
}
}
});
// Update konto
router.put('/konten/:id', authenticateToken, async (req, res) => {
const { id } = req.params;
const { konto, name } = req.body;
if (!konto || !name) {
return res.status(400).json({ error: 'Konto und Name sind erforderlich' });
}
try {
await executeQuery(
'UPDATE fibdash.Konto SET konto = @konto, name = @name WHERE id = @id',
{ konto, name, id }
);
res.json({ message: 'Konto erfolgreich aktualisiert' });
} catch (error) {
console.error('Error updating konto:', error);
if (error.number === 2627) { // Unique constraint violation
res.status(400).json({ error: 'Konto bereits vorhanden' });
} else {
res.status(500).json({ error: 'Fehler beim Aktualisieren des Kontos' });
}
}
});
// Delete konto
router.delete('/konten/:id', authenticateToken, async (req, res) => {
const { id } = req.params;
try {
await executeQuery('DELETE FROM fibdash.Konto WHERE id = @id', { id });
res.json({ message: 'Konto erfolgreich gelöscht' });
} catch (error) {
console.error('Error deleting konto:', error);
if (error.number === 547) { // Foreign key constraint violation
res.status(400).json({ error: 'Konto kann nicht gelöscht werden, da es in Buchungen verwendet wird' });
} else {
res.status(500).json({ error: 'Fehler beim Löschen des Kontos' });
}
}
});
// ==================== BU (BUCHUNGSSCHLÜSSEL) ROUTES ====================
// Get all buchungsschluessel
router.get('/buchungsschluessel', authenticateToken, async (req, res) => {
try {
const result = await executeQuery('SELECT * FROM fibdash.BU ORDER BY bu');
res.json({ buchungsschluessel: result.recordset });
} catch (error) {
console.error('Error fetching buchungsschluessel:', error);
res.status(500).json({ error: 'Fehler beim Laden der Buchungsschlüssel' });
}
});
// Create new buchungsschluessel
router.post('/buchungsschluessel', authenticateToken, async (req, res) => {
const { bu, name, vst } = req.body;
if (!bu || !name) {
return res.status(400).json({ error: 'BU und Name sind erforderlich' });
}
try {
await executeQuery(
'INSERT INTO fibdash.BU (bu, name, vst) VALUES (@bu, @name, @vst)',
{ bu, name, vst: vst || null }
);
res.json({ message: 'Buchungsschlüssel erfolgreich erstellt' });
} catch (error) {
console.error('Error creating BU:', error);
if (error.number === 2627) { // Unique constraint violation
res.status(400).json({ error: 'Buchungsschlüssel bereits vorhanden' });
} else {
res.status(500).json({ error: 'Fehler beim Erstellen des Buchungsschlüssels' });
}
}
});
// Update buchungsschluessel
router.put('/buchungsschluessel/:id', authenticateToken, async (req, res) => {
const { id } = req.params;
const { bu, name, vst } = req.body;
if (!bu || !name) {
return res.status(400).json({ error: 'BU und Name sind erforderlich' });
}
try {
await executeQuery(
'UPDATE fibdash.BU SET bu = @bu, name = @name, vst = @vst WHERE id = @id',
{ bu, name, vst: vst || null, id }
);
res.json({ message: 'Buchungsschlüssel erfolgreich aktualisiert' });
} catch (error) {
console.error('Error updating BU:', error);
if (error.number === 2627) { // Unique constraint violation
res.status(400).json({ error: 'Buchungsschlüssel bereits vorhanden' });
} else {
res.status(500).json({ error: 'Fehler beim Aktualisieren des Buchungsschlüssels' });
}
}
});
// Delete buchungsschluessel
router.delete('/buchungsschluessel/:id', authenticateToken, async (req, res) => {
const { id } = req.params;
try {
await executeQuery('DELETE FROM fibdash.BU WHERE id = @id', { id });
res.json({ message: 'Buchungsschlüssel erfolgreich gelöscht' });
} catch (error) {
console.error('Error deleting BU:', error);
if (error.number === 547) { // Foreign key constraint violation
res.status(400).json({ error: 'Buchungsschlüssel kann nicht gelöscht werden, da er in Buchungen verwendet wird' });
} else {
res.status(500).json({ error: 'Fehler beim Löschen des Buchungsschlüssels' });
}
}
});
module.exports = router;