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 + + + + {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" + > + + + + + ))} + +
+
+ + + + {editingBU ? 'Buchungsschlüssel bearbeiten' : 'Neuer Buchungsschlüssel'} + + + + + + + + + + + + + {/* Confirmation Dialog */} + + Löschen bestätigen + + + {this.state.itemToDelete && + `Buchungsschlüssel "${this.state.itemToDelete.bu} - ${this.state.itemToDelete.name}" wirklich löschen?` + } + + + + + + + +
+ ); + } +} + +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 + + + + {error && ( + + {error} + + )} + + + + + + Konto + Name + Aktionen + + + + {konten.map((konto) => ( + + {konto.konto} + {konto.name} + + this.handleOpenDialog(konto)} + > + + + this.handleDeleteClick(konto)} + color="error" + > + + + + + ))} + +
+
+ + + + {editingKonto ? 'Konto bearbeiten' : 'Neues Konto'} + + + + + + + + + + + + {/* Confirmation Dialog */} + + Löschen bestätigen + + + {this.state.itemToDelete && + `Konto "${this.state.itemToDelete.konto} - ${this.state.itemToDelete.name}" wirklich löschen?` + } + + + + + + + +
+ ); + } +} + +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 + + + + {error && ( + + {error} + + )} + + + + + + Kreditor ID + Name + IBAN + Aktionen + + + + {kreditoren.map((kreditor) => ( + + {kreditor.kreditorId} + {kreditor.name} + {kreditor.iban} + + this.handleOpenDialog(kreditor)} + > + + + this.handleDeleteClick(kreditor)} + color="error" + > + + + + + ))} + +
+
+ + + + {editingKreditor ? 'Kreditor bearbeiten' : 'Neuer Kreditor'} + + + + + + + + + + + + + {/* Confirmation Dialog */} + + Löschen bestätigen + + + {this.state.itemToDelete && + `Kreditor "${this.state.itemToDelete.name}" wirklich löschen?` + } + + + + + + + +
+ ); + } +} + +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