Compare commits

...

4 Commits

17 changed files with 2162 additions and 152 deletions

View File

@@ -2,3 +2,5 @@
alwaysApply: true
---
sqlcmd -C -S tcp:192.168.56.1,1497 -U app -P 'readonly' -d eazybusiness -W
sqlcmd -C -S tcp:192.168.56.1,1497 -U sa -P 'sa_tekno23' -d eazybusiness -W

10
.env
View File

@@ -1,10 +0,0 @@
GOOGLE_CLIENT_ID=928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
REACT_APP_GOOGLE_CLIENT_ID=928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com
AUTHORIZED_EMAILS=sebgreenbus@gmail.com,growsdd@gmail.com
JWT_SECRET=7vK2gQp9zX1wR4eT6sB8uN0cLmY5aV3j
DB_SERVER=192.168.56.1
DB_DATABASE=eazybusiness
DB_USERNAME=app
DB_PASSWORD=readonly
DB_PORT=1497

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',
},
};

1
.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules
.env

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,6 +148,13 @@ class DataViewer extends Component {
return (
<Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Navigation
currentView={currentView}
onViewChange={this.handleViewChange}
/>
{currentView === 'dashboard' ? (
<>
<Box sx={{ flexShrink: 0 }}>
<SummaryHeader
months={months}
@@ -154,6 +172,12 @@ class DataViewer extends Component {
loading={loading}
/>
</Box>
</>
) : (
<Box sx={{ flex: 1, minHeight: 0, overflow: 'auto', p: 2 }}>
<TableManagement user={user} />
</Box>
)}
</Box>
);
}

View File

@@ -0,0 +1,288 @@
import React, { Component } from 'react';
import {
Select,
MenuItem,
FormControl,
InputLabel,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Box,
Typography,
Alert,
CircularProgress
} from '@mui/material';
import { Add as AddIcon } from '@mui/icons-material';
import KreditorService from '../services/KreditorService';
class KreditorSelector extends Component {
constructor(props) {
super(props);
this.state = {
kreditors: [],
selectedKreditorId: props.selectedKreditorId || '',
loading: false,
createDialogOpen: false,
newKreditor: {
iban: '',
name: '',
kreditorId: ''
},
validationErrors: [],
error: null,
creating: false
};
this.kreditorService = new KreditorService();
}
componentDidMount() {
this.loadKreditors();
}
componentDidUpdate(prevProps) {
if (prevProps.selectedKreditorId !== this.props.selectedKreditorId) {
this.setState({ selectedKreditorId: this.props.selectedKreditorId || '' });
}
}
loadKreditors = async () => {
this.setState({ loading: true, error: null });
try {
const kreditors = await this.kreditorService.getAllKreditors();
this.setState({ kreditors, loading: false });
} catch (error) {
console.error('Error loading kreditors:', error);
this.setState({
error: error.message,
loading: false
});
}
};
handleKreditorChange = (event) => {
const selectedKreditorId = event.target.value;
if (selectedKreditorId === 'create_new') {
this.setState({ createDialogOpen: true });
return;
}
this.setState({ selectedKreditorId });
if (this.props.onKreditorChange) {
const selectedKreditor = this.state.kreditors.find(k => k.id === selectedKreditorId);
this.props.onKreditorChange(selectedKreditor);
}
};
handleCreateDialogClose = () => {
this.setState({
createDialogOpen: false,
newKreditor: { iban: '', name: '', kreditorId: '' },
validationErrors: [],
error: null
});
};
handleNewKreditorChange = (field, value) => {
this.setState({
newKreditor: {
...this.state.newKreditor,
[field]: value
},
validationErrors: [] // Clear validation errors when user types
});
};
generateKreditorId = () => {
// Generate a kreditorId starting with 70 followed by random digits
const randomDigits = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
const kreditorId = `70${randomDigits}`;
this.setState({
newKreditor: {
...this.state.newKreditor,
kreditorId
}
});
};
handleCreateKreditor = async () => {
const { newKreditor } = this.state;
// Validate the data
const validationErrors = this.kreditorService.validateKreditorData(newKreditor);
if (validationErrors.length > 0) {
this.setState({ validationErrors });
return;
}
this.setState({ creating: true, error: null });
try {
const createdKreditor = await this.kreditorService.createKreditor(newKreditor);
// Add the new kreditor to the list and select it
const updatedKreditors = [...this.state.kreditors, createdKreditor];
this.setState({
kreditors: updatedKreditors,
selectedKreditorId: createdKreditor.id,
creating: false
});
// Notify parent component
if (this.props.onKreditorChange) {
this.props.onKreditorChange(createdKreditor);
}
this.handleCreateDialogClose();
} catch (error) {
console.error('Error creating kreditor:', error);
this.setState({
error: error.message,
creating: false
});
}
};
render() {
const {
kreditors,
selectedKreditorId,
loading,
createDialogOpen,
newKreditor,
validationErrors,
error,
creating
} = this.state;
const { label = "Kreditor", disabled = false, fullWidth = true } = this.props;
return (
<>
<FormControl fullWidth={fullWidth} disabled={disabled || loading}>
<InputLabel>{label}</InputLabel>
<Select
value={selectedKreditorId}
onChange={this.handleKreditorChange}
label={label}
>
<MenuItem value="">
<em>Keinen Kreditor auswählen</em>
</MenuItem>
{kreditors.map((kreditor) => (
<MenuItem key={kreditor.id} value={kreditor.id}>
{kreditor.name} ({kreditor.kreditorId}) - {kreditor.iban}
</MenuItem>
))}
<MenuItem value="create_new" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
<AddIcon sx={{ mr: 1 }} />
Neuen Kreditor erstellen
</MenuItem>
</Select>
</FormControl>
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
{error}
</Alert>
)}
{/* Create Kreditor Dialog */}
<Dialog
open={createDialogOpen}
onClose={this.handleCreateDialogClose}
maxWidth="sm"
fullWidth
>
<DialogTitle>Neuen Kreditor erstellen</DialogTitle>
<DialogContent>
<Box sx={{ pt: 1 }}>
{validationErrors.length > 0 && (
<Alert severity="error" sx={{ mb: 2 }}>
<ul style={{ margin: 0, paddingLeft: '20px' }}>
{validationErrors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<TextField
label="Name"
value={newKreditor.name}
onChange={(e) => this.handleNewKreditorChange('name', e.target.value)}
fullWidth
margin="normal"
required
/>
<TextField
label="IBAN"
value={newKreditor.iban}
onChange={(e) => this.handleNewKreditorChange('iban', e.target.value.toUpperCase())}
fullWidth
margin="normal"
required
placeholder="DE89 3704 0044 0532 0130 00"
/>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
<TextField
label="Kreditor-ID"
value={newKreditor.kreditorId}
onChange={(e) => this.handleNewKreditorChange('kreditorId', e.target.value)}
margin="normal"
required
placeholder="70001"
sx={{ flexGrow: 1 }}
/>
<Button
onClick={this.generateKreditorId}
variant="outlined"
sx={{ mb: 1 }}
>
Generieren
</Button>
</Box>
<Typography variant="caption" color="textSecondary" sx={{ mt: 1, display: 'block' }}>
Die Kreditor-ID muss mit "70" beginnen, gefolgt von mindestens 3 Ziffern.
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button
onClick={this.handleCreateDialogClose}
disabled={creating}
>
Abbrechen
</Button>
<Button
onClick={this.handleCreateKreditor}
variant="contained"
disabled={creating}
startIcon={creating ? <CircularProgress size={16} /> : null}
>
{creating ? 'Erstellen...' : 'Erstellen'}
</Button>
</DialogActions>
</Dialog>
</>
);
}
}
export default KreditorSelector;

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

@@ -25,6 +25,7 @@ import {
ContentCopy as CopyIcon
} from '@mui/icons-material';
import { AgGridReact } from 'ag-grid-react';
import KreditorSelector from '../KreditorSelector';
const DocumentRenderer = (params) => {
// Check for pdfs and links regardless of transaction source
@@ -145,7 +146,10 @@ const DocumentRenderer = (params) => {
currency: extractionData.currency || 'EUR',
invoiceNumber: extractionData.invoice_number || '',
date: extractionData.date || '',
sender: extractionData.sender || ''
sender: extractionData.sender || '',
kreditorId: extractionData.kreditor_id || null,
kreditorName: extractionData.kreditor_name || '',
kreditorCode: extractionData.kreditor_code || ''
});
});
}
@@ -184,6 +188,34 @@ const DocumentRenderer = (params) => {
width: 150,
tooltipField: 'sender'
},
{
headerName: 'Kreditor',
field: 'kreditor',
width: 200,
cellRenderer: (params) => {
return (
<Box sx={{ height: '100%', display: 'flex', alignItems: 'center' }}>
<KreditorSelector
selectedKreditorId={params.data.kreditorId}
onKreditorChange={(kreditor) => {
// Update the line item with the selected kreditor
if (params.data && params.api) {
params.data.kreditorId = kreditor ? kreditor.id : null;
params.data.kreditorName = kreditor ? kreditor.name : '';
params.data.kreditorCode = kreditor ? kreditor.kreditorId : '';
params.api.refreshCells({ rowNodes: [params.node] });
}
}}
label=""
fullWidth={false}
/>
</Box>
);
},
editable: false,
sortable: false,
filter: false
},
{
headerName: 'Netto',
field: 'netAmount',
@@ -371,7 +403,7 @@ const DocumentRenderer = (params) => {
)}
{tabValue === 1 && (
<Box sx={{ p: 2, height: 400 }}>
<Box sx={{ p: 2, height: 500 }}>
{lineItems.length > 0 ? (
<div style={{ height: '100%', width: '100%' }}>
<AgGridReact
@@ -379,7 +411,7 @@ const DocumentRenderer = (params) => {
rowData={lineItems}
defaultColDef={defaultColDef}
suppressRowTransform={true}
rowHeight={35}
rowHeight={50}
headerHeight={35}
domLayout="normal"
/>

View File

@@ -0,0 +1,178 @@
class KreditorService {
constructor() {
this.baseURL = '/api';
}
async getAuthHeaders() {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
};
}
async handleResponse(response) {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.log('Server error response:', errorData);
// Handle different types of errors with clearer messages
if (response.status === 502 || response.status === 503) {
throw new Error('FibDash Service nicht verfügbar - Bitte versuchen Sie es später erneut');
} else if (response.status === 500) {
throw new Error('FibDash Server Fehler - Bitte kontaktieren Sie den Administrator');
} else if (response.status === 403) {
const message = errorData.message || 'Zugriff verweigert';
throw new Error(message);
} else if (response.status === 401) {
throw new Error('Authentifizierung fehlgeschlagen - Bitte melden Sie sich erneut an');
} else if (response.status === 404) {
throw new Error('Kreditor nicht gefunden');
} else if (response.status === 409) {
const message = errorData.error || 'Kreditor bereits vorhanden';
throw new Error(message);
} else if (response.status === 400) {
const message = errorData.error || 'Ungültige Daten';
throw new Error(message);
} else {
const errorMessage = errorData.error || errorData.message || `HTTP ${response.status}: Unbekannter Fehler`;
throw new Error(errorMessage);
}
}
return await response.json();
}
async getAllKreditors() {
try {
const response = await fetch(`${this.baseURL}/data/kreditors`, {
method: 'GET',
headers: await this.getAuthHeaders(),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error fetching kreditors:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
async getKreditorById(id) {
try {
const response = await fetch(`${this.baseURL}/data/kreditors/${id}`, {
method: 'GET',
headers: await this.getAuthHeaders(),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error fetching kreditor:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
async createKreditor(kreditorData) {
try {
const response = await fetch(`${this.baseURL}/data/kreditors`, {
method: 'POST',
headers: await this.getAuthHeaders(),
body: JSON.stringify(kreditorData),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error creating kreditor:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
async updateKreditor(id, kreditorData) {
try {
const response = await fetch(`${this.baseURL}/data/kreditors/${id}`, {
method: 'PUT',
headers: await this.getAuthHeaders(),
body: JSON.stringify(kreditorData),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error updating kreditor:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
async deleteKreditor(id) {
try {
const response = await fetch(`${this.baseURL}/data/kreditors/${id}`, {
method: 'DELETE',
headers: await this.getAuthHeaders(),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error deleting kreditor:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
// Utility method to validate kreditor data
validateKreditorData(kreditorData) {
const errors = [];
if (!kreditorData.iban || kreditorData.iban.trim() === '') {
errors.push('IBAN ist erforderlich');
}
if (!kreditorData.name || kreditorData.name.trim() === '') {
errors.push('Name ist erforderlich');
}
if (!kreditorData.kreditorId || kreditorData.kreditorId.trim() === '') {
errors.push('Kreditor-ID ist erforderlich');
}
// Basic IBAN format validation (simplified)
if (kreditorData.iban && !/^[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}$/i.test(kreditorData.iban.replace(/\s/g, ''))) {
errors.push('IBAN Format ist ungültig');
}
// Validate kreditorId format (should start with 70xxx)
if (kreditorData.kreditorId && !/^70\d{3,}$/.test(kreditorData.kreditorId)) {
errors.push('Kreditor-ID muss mit 70 beginnen gefolgt von mindestens 3 Ziffern');
}
return errors;
}
}
export default KreditorService;

View File

@@ -1,36 +1,125 @@
-- FibDash Database Schema
-- Run these commands in your MSSQL database
-- Create Users table
CREATE TABLE Users (
-- Create fibdash schema
IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = 'fibdash')
BEGIN
EXEC('CREATE SCHEMA fibdash')
END
GO
-- Create Kreditor table
CREATE TABLE fibdash.Kreditor (
id INT IDENTITY(1,1) PRIMARY KEY,
google_id NVARCHAR(255) UNIQUE NOT NULL,
email NVARCHAR(255) UNIQUE NOT NULL,
iban NVARCHAR(34) NOT NULL,
name NVARCHAR(255) NOT NULL,
picture NVARCHAR(500),
created_at DATETIME2 DEFAULT GETDATE(),
last_login DATETIME2,
is_active BIT DEFAULT 1
kreditorId NVARCHAR(50) NOT NULL
);
-- Create UserPreferences table
CREATE TABLE UserPreferences (
-- Ensure kreditorId is unique to support FK references
ALTER TABLE fibdash.Kreditor
ADD CONSTRAINT UQ_Kreditor_kreditorId UNIQUE (kreditorId);
-- Create AccountingItems table
-- Based on CSV structure: umsatz brutto, soll/haben kz, konto, gegenkonto, bu, buchungsdatum, rechnungsnummer, buchungstext, beleglink
CREATE TABLE fibdash.AccountingItems (
id INT IDENTITY(1,1) PRIMARY KEY,
user_id INT NOT NULL,
theme NVARCHAR(50) DEFAULT 'light',
language NVARCHAR(10) DEFAULT 'en',
notifications_enabled BIT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE(),
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE
umsatz_brutto DECIMAL(15,2) NOT NULL, -- gross turnover amount
soll_haben_kz CHAR(1) NOT NULL CHECK (soll_haben_kz IN ('S', 'H')), -- S = eingang (debit), H = ausgang (credit)
konto NVARCHAR(10) NOT NULL, -- account (e.g. 5400 = wareneingang 19%)
gegenkonto NVARCHAR(50) NOT NULL, -- counter account references Kreditor(kreditorId)
bu NVARCHAR(10), -- tax code (9 = 19%vst, 8 = 7%vst, 506 = dienstleistung aus EU, 511 = dienstleistung ausserhalb EU)
buchungsdatum DATE NOT NULL, -- booking date
rechnungsnummer NVARCHAR(100), -- invoice number (belegfeld 1)
buchungstext NVARCHAR(500), -- booking text (supplier/customer name + purpose)
beleglink NVARCHAR(500) -- document link
);
-- Create Konto table
CREATE TABLE fibdash.Konto (
id INT IDENTITY(1,1) PRIMARY KEY,
konto NVARCHAR(10) NOT NULL,
name NVARCHAR(255) NOT NULL
);
-- Ensure Konto.konto is unique to support FK references
ALTER TABLE fibdash.Konto
ADD CONSTRAINT UQ_Konto_konto UNIQUE (konto);
-- Create BU table
CREATE TABLE fibdash.BU (
id INT IDENTITY(1,1) PRIMARY KEY,
bu NVARCHAR(10) 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
ALTER TABLE fibdash.BU
ADD CONSTRAINT UQ_BU_bu UNIQUE (bu);
/*
CSV
umsatz brutto ,
soll / haben kz ( S = eingang, H = ausgang),
,,,
konto (XXXX , z.b. 5400 = wareneingang 19%),
gegenkonto (70XXX),
bu (9 = 19%vst , 8 = 7%vst, 506 = dienstleistung aus EU, 511 = dienstleistung ausserhalb EU),
buchungsdatum, (MDD)
rechnungsnummer (belegfeld 1),
,,
buchungstext (lierferantenname / kundenname , + verwendungszweck)
,,,,,
beleglink
--
nicht abziehbare vorstreuer buchen auf 5600
*/
-- Create indexes for better performance
CREATE INDEX IX_Users_Email ON Users(email);
CREATE INDEX IX_Users_GoogleId ON Users(google_id);
CREATE INDEX IX_UserPreferences_UserId ON UserPreferences(user_id);
CREATE INDEX IX_Kreditor_IBAN ON fibdash.Kreditor(iban);
CREATE INDEX IX_Kreditor_KreditorId ON fibdash.Kreditor(kreditorId);
CREATE INDEX IX_AccountingItems_Buchungsdatum ON fibdash.AccountingItems(buchungsdatum);
CREATE INDEX IX_AccountingItems_Konto ON fibdash.AccountingItems(konto);
CREATE INDEX IX_AccountingItems_Rechnungsnummer ON fibdash.AccountingItems(rechnungsnummer);
CREATE INDEX IX_AccountingItems_SollHabenKz ON fibdash.AccountingItems(soll_haben_kz);
-- Add FK from AccountingItems.bu -> BU(bu)
ALTER TABLE fibdash.AccountingItems
ADD CONSTRAINT FK_AccountingItems_BU_BU
FOREIGN KEY (bu) REFERENCES fibdash.BU(bu);
-- Add FK from AccountingItems.gegenkonto -> Kreditor(kreditorId)
ALTER TABLE fibdash.AccountingItems
ADD CONSTRAINT FK_AccountingItems_Gegenkonto_Kreditor
FOREIGN KEY (gegenkonto) REFERENCES fibdash.Kreditor(kreditorId);
-- Add FK from AccountingItems.konto -> Konto(konto)
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)
-- Note: This will only work after you have real Google user data
-- INSERT INTO Users (google_id, email, name, picture)
-- VALUES ('sample_google_id', 'user@example.com', 'Lorem Ipsum User', 'https://example.com/picture.jpg');
-- INSERT INTO fibdash.Kreditor (iban, name, kreditorId)
-- 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();
};
// 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(',');
// Removed admin access check - all authenticated users can access these routes
// Get system info
router.get('/system-info', authenticateToken, (req, res) => {
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) => {
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;

View File

@@ -558,4 +558,183 @@ router.get('/pdf/pdfobject/:kPdfObjekt', authenticateToken, async (req, res) =>
}
});
// Kreditor API endpoints
// Get all kreditors
router.get('/kreditors', authenticateToken, async (req, res) => {
try {
const { executeQuery } = require('../config/database');
const query = `
SELECT id, iban, name, kreditorId, created_at, updated_at
FROM Kreditor
WHERE is_active = 1
ORDER BY name ASC
`;
const result = await executeQuery(query);
res.json(result.recordset || []);
} catch (error) {
console.error('Error fetching kreditors:', error);
res.status(500).json({ error: 'Failed to fetch kreditors' });
}
});
// Get kreditor by ID
router.get('/kreditors/:id', authenticateToken, async (req, res) => {
try {
const { executeQuery } = require('../config/database');
const { id } = req.params;
const query = `
SELECT id, iban, name, kreditorId, created_at, updated_at
FROM Kreditor
WHERE id = @id AND is_active = 1
`;
const result = await executeQuery(query, [
{ name: 'id', type: 'int', value: parseInt(id) }
]);
if (result.recordset.length === 0) {
return res.status(404).json({ error: 'Kreditor not found' });
}
res.json(result.recordset[0]);
} catch (error) {
console.error('Error fetching kreditor:', error);
res.status(500).json({ error: 'Failed to fetch kreditor' });
}
});
// Create new kreditor
router.post('/kreditors', authenticateToken, async (req, res) => {
try {
const { executeQuery } = require('../config/database');
const { iban, name, kreditorId } = req.body;
// Validate required fields
if (!iban || !name || !kreditorId) {
return res.status(400).json({ error: 'IBAN, name, and kreditorId are required' });
}
// Check if kreditor with same IBAN or kreditorId already exists
const checkQuery = `
SELECT id FROM Kreditor
WHERE (iban = @iban OR kreditorId = @kreditorId) AND is_active = 1
`;
const checkResult = await executeQuery(checkQuery, [
{ name: 'iban', type: 'nvarchar', value: iban },
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
]);
if (checkResult.recordset.length > 0) {
return res.status(409).json({ error: 'Kreditor with this IBAN or kreditorId already exists' });
}
const insertQuery = `
INSERT INTO Kreditor (iban, name, kreditorId, created_at, updated_at)
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at
VALUES (@iban, @name, @kreditorId, GETDATE(), GETDATE())
`;
const result = await executeQuery(insertQuery, [
{ name: 'iban', type: 'nvarchar', value: iban },
{ name: 'name', type: 'nvarchar', value: name },
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
]);
res.status(201).json(result.recordset[0]);
} catch (error) {
console.error('Error creating kreditor:', error);
res.status(500).json({ error: 'Failed to create kreditor' });
}
});
// Update kreditor
router.put('/kreditors/:id', authenticateToken, async (req, res) => {
try {
const { executeQuery } = require('../config/database');
const { id } = req.params;
const { iban, name, kreditorId } = req.body;
// Validate required fields
if (!iban || !name || !kreditorId) {
return res.status(400).json({ error: 'IBAN, name, and kreditorId are required' });
}
// Check if kreditor exists
const checkQuery = `SELECT id FROM Kreditor WHERE id = @id AND is_active = 1`;
const checkResult = await executeQuery(checkQuery, [
{ name: 'id', type: 'int', value: parseInt(id) }
]);
if (checkResult.recordset.length === 0) {
return res.status(404).json({ error: 'Kreditor not found' });
}
// Check for conflicts with other kreditors
const conflictQuery = `
SELECT id FROM Kreditor
WHERE (iban = @iban OR kreditorId = @kreditorId) AND id != @id AND is_active = 1
`;
const conflictResult = await executeQuery(conflictQuery, [
{ name: 'iban', type: 'nvarchar', value: iban },
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId },
{ name: 'id', type: 'int', value: parseInt(id) }
]);
if (conflictResult.recordset.length > 0) {
return res.status(409).json({ error: 'Another kreditor with this IBAN or kreditorId already exists' });
}
const updateQuery = `
UPDATE Kreditor
SET iban = @iban, name = @name, kreditorId = @kreditorId, updated_at = GETDATE()
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at
WHERE id = @id
`;
const result = await executeQuery(updateQuery, [
{ name: 'iban', type: 'nvarchar', value: iban },
{ name: 'name', type: 'nvarchar', value: name },
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId },
{ name: 'id', type: 'int', value: parseInt(id) }
]);
res.json(result.recordset[0]);
} catch (error) {
console.error('Error updating kreditor:', error);
res.status(500).json({ error: 'Failed to update kreditor' });
}
});
// Delete kreditor (soft delete)
router.delete('/kreditors/:id', authenticateToken, async (req, res) => {
try {
const { executeQuery } = require('../config/database');
const { id } = req.params;
const query = `
UPDATE Kreditor
SET is_active = 0, updated_at = GETDATE()
WHERE id = @id AND is_active = 1
`;
const result = await executeQuery(query, [
{ name: 'id', type: 'int', value: parseInt(id) }
]);
if (result.rowsAffected[0] === 0) {
return res.status(404).json({ error: 'Kreditor not found' });
}
res.json({ message: 'Kreditor deleted successfully' });
} catch (error) {
console.error('Error deleting kreditor:', error);
res.status(500).json({ error: 'Failed to delete kreditor' });
}
});
module.exports = router;

View File

@@ -45,7 +45,7 @@ module.exports = {
REACT_APP_GOOGLE_CLIENT_ID: JSON.stringify(process.env.GOOGLE_CLIENT_ID),
},
}),
new ReactRefreshWebpackPlugin(),
...(process.env.NODE_ENV === 'development' ? [new ReactRefreshWebpackPlugin()] : []),
],
devServer: {
static: {