Compare commits
4 Commits
be7a928ce2
...
5470bebfc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5470bebfc4 | ||
|
|
092fa0f8bd | ||
|
|
fa6690135a | ||
|
|
1ec1e1e5f6 |
@@ -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
10
.env
@@ -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
31
.eslintrc.js
Normal 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
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
node_modules
|
||||
.env
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
288
client/src/components/KreditorSelector.js
Normal file
288
client/src/components/KreditorSelector.js
Normal 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;
|
||||
45
client/src/components/Navigation.js
Normal file
45
client/src/components/Navigation.js
Normal 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;
|
||||
76
client/src/components/TableManagement.js
Normal file
76
client/src/components/TableManagement.js
Normal 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;
|
||||
325
client/src/components/admin/BUTable.js
Normal file
325
client/src/components/admin/BUTable.js
Normal 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;
|
||||
295
client/src/components/admin/KontoTable.js
Normal file
295
client/src/components/admin/KontoTable.js
Normal 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;
|
||||
308
client/src/components/admin/KreditorTable.js
Normal file
308
client/src/components/admin/KreditorTable.js
Normal 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;
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
178
client/src/services/KreditorService.js
Normal file
178
client/src/services/KreditorService.js
Normal 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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user