diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..546b79e
--- /dev/null
+++ b/.eslintrc.js
@@ -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',
+ },
+};
\ No newline at end of file
diff --git a/client/src/components/DataViewer.js b/client/src/components/DataViewer.js
index 40328ea..bf4aa2a 100644
--- a/client/src/components/DataViewer.js
+++ b/client/src/components/DataViewer.js
@@ -7,6 +7,9 @@ import {
import AuthService from '../services/AuthService';
import SummaryHeader from './SummaryHeader';
import TransactionsTable from './TransactionsTable';
+import Navigation from './Navigation';
+import Dashboard from './Dashboard';
+import TableManagement from './TableManagement';
class DataViewer extends Component {
constructor(props) {
@@ -18,6 +21,7 @@ class DataViewer extends Component {
summary: null,
loading: true,
error: null,
+ currentView: 'dashboard', // 'dashboard' or 'tables'
};
this.authService = new AuthService();
}
@@ -110,14 +114,21 @@ class DataViewer extends Component {
if (this.props.onUpdateExportData) {
this.props.onUpdateExportData({
selectedMonth,
- canExport: !!selectedMonth && !this.state.loading,
+ canExport: !!selectedMonth && !this.state.loading && this.state.currentView === 'dashboard',
onExport: this.downloadDatev
});
}
};
+ handleViewChange = (event, newView) => {
+ this.setState({ currentView: newView });
+ // Update export data when view changes
+ this.updateExportData();
+ };
+
render() {
- const { months, selectedMonth, transactions, summary, loading, error } = this.state;
+ const { months, selectedMonth, transactions, summary, loading, error, currentView } = this.state;
+ const { user } = this.props;
if (loading && !transactions.length) {
return (
@@ -137,23 +148,36 @@ class DataViewer extends Component {
return (
-
-
-
+
-
-
-
+ {currentView === 'dashboard' ? (
+ <>
+
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
+ )}
);
}
diff --git a/client/src/components/Navigation.js b/client/src/components/Navigation.js
new file mode 100644
index 0000000..bf87f5a
--- /dev/null
+++ b/client/src/components/Navigation.js
@@ -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 (
+
+
+
+ }
+ label="Dashboard"
+ value="dashboard"
+ sx={{ minHeight: 48 }}
+ />
+ }
+ label="Stammdaten"
+ value="tables"
+ sx={{ minHeight: 48 }}
+ />
+
+
+
+ );
+ }
+}
+
+export default Navigation;
\ No newline at end of file
diff --git a/client/src/components/TableManagement.js b/client/src/components/TableManagement.js
new file mode 100644
index 0000000..1dc288e
--- /dev/null
+++ b/client/src/components/TableManagement.js
@@ -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 (
+
+
+ Stammdaten verwalten
+
+
+
+
+
+ }
+ label="Kreditoren"
+ sx={{ minHeight: 64 }}
+ />
+ }
+ label="Konten"
+ sx={{ minHeight: 64 }}
+ />
+ }
+ label="Buchungsschlüssel"
+ sx={{ minHeight: 64 }}
+ />
+
+
+
+
+ {activeTab === 0 && }
+ {activeTab === 1 && }
+ {activeTab === 2 && }
+
+
+
+ );
+ }
+}
+
+export default TableManagement;
\ No newline at end of file
diff --git a/client/src/components/admin/BUTable.js b/client/src/components/admin/BUTable.js
new file mode 100644
index 0000000..af58f02
--- /dev/null
+++ b/client/src/components/admin/BUTable.js
@@ -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 (
+
+
+
+ );
+ }
+
+ return (
+
+
+ Buchungsschlüssel
+ }
+ onClick={() => this.handleOpenDialog()}
+ >
+ Neuer Buchungsschlüssel
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ BU
+ Name
+ VST %
+ Aktionen
+
+
+
+ {buchungsschluessel.map((bu) => (
+
+ {bu.bu}
+ {bu.name}
+
+ {bu.vst ? `${bu.vst}%` : '-'}
+
+
+ this.handleOpenDialog(bu)}
+ >
+
+
+ this.handleDeleteClick(bu)}
+ color="error"
+ >
+
+
+
+
+ ))}
+
+
+
+
+
+
+ {/* Confirmation Dialog */}
+
+
+ );
+ }
+}
+
+export default BUTable;
\ No newline at end of file
diff --git a/client/src/components/admin/KontoTable.js b/client/src/components/admin/KontoTable.js
new file mode 100644
index 0000000..27d6a71
--- /dev/null
+++ b/client/src/components/admin/KontoTable.js
@@ -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 (
+
+
+
+ );
+ }
+
+ return (
+
+
+ Konten
+ }
+ onClick={() => this.handleOpenDialog()}
+ >
+ Neues Konto
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ Konto
+ Name
+ Aktionen
+
+
+
+ {konten.map((konto) => (
+
+ {konto.konto}
+ {konto.name}
+
+ this.handleOpenDialog(konto)}
+ >
+
+
+ this.handleDeleteClick(konto)}
+ color="error"
+ >
+
+
+
+
+ ))}
+
+
+
+
+
+
+ {/* Confirmation Dialog */}
+
+
+ );
+ }
+}
+
+export default KontoTable;
\ No newline at end of file
diff --git a/client/src/components/admin/KreditorTable.js b/client/src/components/admin/KreditorTable.js
new file mode 100644
index 0000000..7ba85d3
--- /dev/null
+++ b/client/src/components/admin/KreditorTable.js
@@ -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 (
+
+
+
+ );
+ }
+
+ return (
+
+
+ Kreditoren
+ }
+ onClick={() => this.handleOpenDialog()}
+ >
+ Neuer Kreditor
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ Kreditor ID
+ Name
+ IBAN
+ Aktionen
+
+
+
+ {kreditoren.map((kreditor) => (
+
+ {kreditor.kreditorId}
+ {kreditor.name}
+ {kreditor.iban}
+
+ this.handleOpenDialog(kreditor)}
+ >
+
+
+ this.handleDeleteClick(kreditor)}
+ color="error"
+ >
+
+
+
+
+ ))}
+
+
+
+
+
+
+ {/* Confirmation Dialog */}
+
+
+ );
+ }
+}
+
+export default KreditorTable;
\ No newline at end of file
diff --git a/src/database/schema.sql b/src/database/schema.sql
index 1c3efbb..6a9f07b 100644
--- a/src/database/schema.sql
+++ b/src/database/schema.sql
@@ -49,7 +49,8 @@ ADD CONSTRAINT UQ_Konto_konto UNIQUE (konto);
CREATE TABLE fibdash.BU (
id INT IDENTITY(1,1) PRIMARY KEY,
bu NVARCHAR(10) NOT NULL,
- name NVARCHAR(255) NOT NULL
+ name NVARCHAR(255) NOT NULL,
+ vst DECIMAL(5,2) -- Vorsteuer percentage (e.g., 19.00 for 19%)
);
-- Ensure BU.bu is unique to support FK references
@@ -103,6 +104,22 @@ ALTER TABLE fibdash.AccountingItems
ADD CONSTRAINT FK_AccountingItems_Konto_Konto
FOREIGN KEY (konto) REFERENCES fibdash.Konto(konto);
+-- Add vst column to existing BU table (for databases created before this update)
+-- IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('fibdash.BU') AND name = 'vst')
+-- BEGIN
+-- ALTER TABLE fibdash.BU ADD vst DECIMAL(5,2);
+-- END
+
-- Insert sample data (optional)
-- INSERT INTO fibdash.Kreditor (iban, name, kreditorId)
--- VALUES ('DE89370400440532013000', 'Sample Kreditor', '70001');
\ No newline at end of file
+-- 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);
\ No newline at end of file
diff --git a/src/routes/admin.js b/src/routes/admin.js
index 4d85774..7ce59a8 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -1,110 +1,257 @@
const express = require('express');
const { authenticateToken } = require('../middleware/auth');
const { checkAuthorizedEmail } = require('../middleware/emailAuth');
+const { executeQuery, sql } = require('../config/database');
const fs = require('fs');
const path = require('path');
const router = express.Router();
-// Check if user is admin (first email in the list or specific admin email)
-const checkAdminAccess = (req, res, next) => {
- const authorizedEmails = process.env.AUTHORIZED_EMAILS;
- if (!authorizedEmails || authorizedEmails.trim() === '') {
- return res.status(403).json({ error: 'No authorized emails configured' });
- }
-
- const emailList = authorizedEmails.split(',').map(email => email.trim().toLowerCase());
- const userEmail = req.user?.email?.toLowerCase();
-
- // First email in the list is considered admin, or check for specific admin emails
- const adminEmails = [emailList[0]]; // First email is admin
-
- if (!adminEmails.includes(userEmail)) {
- return res.status(403).json({
- error: 'Admin access required',
- message: 'Only administrators can access this resource'
- });
- }
-
- next();
-};
+// Removed admin access check - all authenticated users can access these routes
-// Get current authorized emails (admin only)
-router.get('/authorized-emails', authenticateToken, checkAdminAccess, (req, res) => {
- const authorizedEmails = process.env.AUTHORIZED_EMAILS;
- if (!authorizedEmails) {
- return res.json({ emails: [] });
- }
-
- const emailList = authorizedEmails.split(',').map(email => email.trim());
- res.json({ emails: emailList });
-});
-
-// Add authorized email (admin only)
-router.post('/authorized-emails', authenticateToken, checkAdminAccess, (req, res) => {
- const { email } = req.body;
-
- if (!email || !email.includes('@')) {
- return res.status(400).json({ error: 'Valid email address required' });
- }
-
- const authorizedEmails = process.env.AUTHORIZED_EMAILS || '';
- const emailList = authorizedEmails.split(',').map(e => e.trim()).filter(e => e);
-
- const newEmail = email.trim().toLowerCase();
- if (emailList.map(e => e.toLowerCase()).includes(newEmail)) {
- return res.status(400).json({ error: 'Email already authorized' });
- }
-
- emailList.push(email.trim());
-
- // Note: This only updates the runtime environment variable
- // For persistent changes, you'd need to update the .env file
- process.env.AUTHORIZED_EMAILS = emailList.join(',');
-
- res.json({
- message: 'Email added successfully',
- emails: emailList,
- note: 'Changes are temporary. Update .env file for permanent changes.'
- });
-});
-
-// Remove authorized email (admin only)
-router.delete('/authorized-emails/:email', authenticateToken, checkAdminAccess, (req, res) => {
- const emailToRemove = req.params.email.toLowerCase();
- const authorizedEmails = process.env.AUTHORIZED_EMAILS || '';
- const emailList = authorizedEmails.split(',').map(e => e.trim()).filter(e => e);
-
- const filteredEmails = emailList.filter(email => email.toLowerCase() !== emailToRemove);
-
- if (filteredEmails.length === emailList.length) {
- return res.status(404).json({ error: 'Email not found in authorized list' });
- }
-
- // Don't allow removing the last admin email
- if (filteredEmails.length === 0) {
- return res.status(400).json({ error: 'Cannot remove all authorized emails' });
- }
-
- // Note: This only updates the runtime environment variable
- process.env.AUTHORIZED_EMAILS = filteredEmails.join(',');
-
- res.json({
- message: 'Email removed successfully',
- emails: filteredEmails,
- note: 'Changes are temporary. Update .env file for permanent changes.'
- });
-});
-
-// Get system info (admin only)
-router.get('/system-info', authenticateToken, checkAdminAccess, (req, res) => {
+// Get system info
+router.get('/system-info', authenticateToken, (req, res) => {
res.json({
- authorizedEmailsConfigured: !!process.env.AUTHORIZED_EMAILS,
- totalAuthorizedEmails: process.env.AUTHORIZED_EMAILS ? process.env.AUTHORIZED_EMAILS.split(',').length : 0,
currentUser: req.user.email,
- isAdmin: true,
environment: process.env.NODE_ENV || 'development'
});
});
+// ==================== KREDITOR ROUTES ====================
+
+// Get all kreditoren
+router.get('/kreditoren', authenticateToken, async (req, res) => {
+ try {
+ const result = await executeQuery('SELECT * FROM fibdash.Kreditor ORDER BY name');
+ res.json({ kreditoren: result.recordset });
+ } catch (error) {
+ console.error('Error fetching kreditoren:', error);
+ res.status(500).json({ error: 'Fehler beim Laden der Kreditoren' });
+ }
+});
+
+// Create new kreditor
+router.post('/kreditoren', authenticateToken, async (req, res) => {
+ const { iban, name, kreditorId } = req.body;
+
+ if (!iban || !name || !kreditorId) {
+ return res.status(400).json({ error: 'IBAN, Name und Kreditor ID sind erforderlich' });
+ }
+
+ try {
+ await executeQuery(
+ 'INSERT INTO fibdash.Kreditor (iban, name, kreditorId) VALUES (@iban, @name, @kreditorId)',
+ { iban, name, kreditorId }
+ );
+ res.json({ message: 'Kreditor erfolgreich erstellt' });
+ } catch (error) {
+ console.error('Error creating kreditor:', error);
+ if (error.number === 2627) { // Unique constraint violation
+ res.status(400).json({ error: 'Kreditor ID bereits vorhanden' });
+ } else {
+ res.status(500).json({ error: 'Fehler beim Erstellen des Kreditors' });
+ }
+ }
+});
+
+// Update kreditor
+router.put('/kreditoren/:id', authenticateToken, async (req, res) => {
+ const { id } = req.params;
+ const { iban, name, kreditorId } = req.body;
+
+ if (!iban || !name || !kreditorId) {
+ return res.status(400).json({ error: 'IBAN, Name und Kreditor ID sind erforderlich' });
+ }
+
+ try {
+ await executeQuery(
+ 'UPDATE fibdash.Kreditor SET iban = @iban, name = @name, kreditorId = @kreditorId WHERE id = @id',
+ { iban, name, kreditorId, id }
+ );
+ res.json({ message: 'Kreditor erfolgreich aktualisiert' });
+ } catch (error) {
+ console.error('Error updating kreditor:', error);
+ if (error.number === 2627) { // Unique constraint violation
+ res.status(400).json({ error: 'Kreditor ID bereits vorhanden' });
+ } else {
+ res.status(500).json({ error: 'Fehler beim Aktualisieren des Kreditors' });
+ }
+ }
+});
+
+// Delete kreditor
+router.delete('/kreditoren/:id', authenticateToken, async (req, res) => {
+ const { id } = req.params;
+
+ try {
+ await executeQuery('DELETE FROM fibdash.Kreditor WHERE id = @id', { id });
+ res.json({ message: 'Kreditor erfolgreich gelöscht' });
+ } catch (error) {
+ console.error('Error deleting kreditor:', error);
+ if (error.number === 547) { // Foreign key constraint violation
+ res.status(400).json({ error: 'Kreditor kann nicht gelöscht werden, da er in Buchungen verwendet wird' });
+ } else {
+ res.status(500).json({ error: 'Fehler beim Löschen des Kreditors' });
+ }
+ }
+});
+
+// ==================== KONTO ROUTES ====================
+
+// Get all konten
+router.get('/konten', authenticateToken, async (req, res) => {
+ try {
+ const result = await executeQuery('SELECT * FROM fibdash.Konto ORDER BY konto');
+ res.json({ konten: result.recordset });
+ } catch (error) {
+ console.error('Error fetching konten:', error);
+ res.status(500).json({ error: 'Fehler beim Laden der Konten' });
+ }
+});
+
+// Create new konto
+router.post('/konten', authenticateToken, async (req, res) => {
+ const { konto, name } = req.body;
+
+ if (!konto || !name) {
+ return res.status(400).json({ error: 'Konto und Name sind erforderlich' });
+ }
+
+ try {
+ await executeQuery(
+ 'INSERT INTO fibdash.Konto (konto, name) VALUES (@konto, @name)',
+ { konto, name }
+ );
+ res.json({ message: 'Konto erfolgreich erstellt' });
+ } catch (error) {
+ console.error('Error creating konto:', error);
+ if (error.number === 2627) { // Unique constraint violation
+ res.status(400).json({ error: 'Konto bereits vorhanden' });
+ } else {
+ res.status(500).json({ error: 'Fehler beim Erstellen des Kontos' });
+ }
+ }
+});
+
+// Update konto
+router.put('/konten/:id', authenticateToken, async (req, res) => {
+ const { id } = req.params;
+ const { konto, name } = req.body;
+
+ if (!konto || !name) {
+ return res.status(400).json({ error: 'Konto und Name sind erforderlich' });
+ }
+
+ try {
+ await executeQuery(
+ 'UPDATE fibdash.Konto SET konto = @konto, name = @name WHERE id = @id',
+ { konto, name, id }
+ );
+ res.json({ message: 'Konto erfolgreich aktualisiert' });
+ } catch (error) {
+ console.error('Error updating konto:', error);
+ if (error.number === 2627) { // Unique constraint violation
+ res.status(400).json({ error: 'Konto bereits vorhanden' });
+ } else {
+ res.status(500).json({ error: 'Fehler beim Aktualisieren des Kontos' });
+ }
+ }
+});
+
+// Delete konto
+router.delete('/konten/:id', authenticateToken, async (req, res) => {
+ const { id } = req.params;
+
+ try {
+ await executeQuery('DELETE FROM fibdash.Konto WHERE id = @id', { id });
+ res.json({ message: 'Konto erfolgreich gelöscht' });
+ } catch (error) {
+ console.error('Error deleting konto:', error);
+ if (error.number === 547) { // Foreign key constraint violation
+ res.status(400).json({ error: 'Konto kann nicht gelöscht werden, da es in Buchungen verwendet wird' });
+ } else {
+ res.status(500).json({ error: 'Fehler beim Löschen des Kontos' });
+ }
+ }
+});
+
+// ==================== BU (BUCHUNGSSCHLÜSSEL) ROUTES ====================
+
+// Get all buchungsschluessel
+router.get('/buchungsschluessel', authenticateToken, async (req, res) => {
+ try {
+ const result = await executeQuery('SELECT * FROM fibdash.BU ORDER BY bu');
+ res.json({ buchungsschluessel: result.recordset });
+ } catch (error) {
+ console.error('Error fetching buchungsschluessel:', error);
+ res.status(500).json({ error: 'Fehler beim Laden der Buchungsschlüssel' });
+ }
+});
+
+// Create new buchungsschluessel
+router.post('/buchungsschluessel', authenticateToken, async (req, res) => {
+ const { bu, name, vst } = req.body;
+
+ if (!bu || !name) {
+ return res.status(400).json({ error: 'BU und Name sind erforderlich' });
+ }
+
+ try {
+ await executeQuery(
+ 'INSERT INTO fibdash.BU (bu, name, vst) VALUES (@bu, @name, @vst)',
+ { bu, name, vst: vst || null }
+ );
+ res.json({ message: 'Buchungsschlüssel erfolgreich erstellt' });
+ } catch (error) {
+ console.error('Error creating BU:', error);
+ if (error.number === 2627) { // Unique constraint violation
+ res.status(400).json({ error: 'Buchungsschlüssel bereits vorhanden' });
+ } else {
+ res.status(500).json({ error: 'Fehler beim Erstellen des Buchungsschlüssels' });
+ }
+ }
+});
+
+// Update buchungsschluessel
+router.put('/buchungsschluessel/:id', authenticateToken, async (req, res) => {
+ const { id } = req.params;
+ const { bu, name, vst } = req.body;
+
+ if (!bu || !name) {
+ return res.status(400).json({ error: 'BU und Name sind erforderlich' });
+ }
+
+ try {
+ await executeQuery(
+ 'UPDATE fibdash.BU SET bu = @bu, name = @name, vst = @vst WHERE id = @id',
+ { bu, name, vst: vst || null, id }
+ );
+ res.json({ message: 'Buchungsschlüssel erfolgreich aktualisiert' });
+ } catch (error) {
+ console.error('Error updating BU:', error);
+ if (error.number === 2627) { // Unique constraint violation
+ res.status(400).json({ error: 'Buchungsschlüssel bereits vorhanden' });
+ } else {
+ res.status(500).json({ error: 'Fehler beim Aktualisieren des Buchungsschlüssels' });
+ }
+ }
+});
+
+// Delete buchungsschluessel
+router.delete('/buchungsschluessel/:id', authenticateToken, async (req, res) => {
+ const { id } = req.params;
+
+ try {
+ await executeQuery('DELETE FROM fibdash.BU WHERE id = @id', { id });
+ res.json({ message: 'Buchungsschlüssel erfolgreich gelöscht' });
+ } catch (error) {
+ console.error('Error deleting BU:', error);
+ if (error.number === 547) { // Foreign key constraint violation
+ res.status(400).json({ error: 'Buchungsschlüssel kann nicht gelöscht werden, da er in Buchungen verwendet wird' });
+ } else {
+ res.status(500).json({ error: 'Fehler beim Löschen des Buchungsschlüssels' });
+ }
+ }
+});
+
module.exports = router;
\ No newline at end of file