Compare commits

..

3 Commits

20 changed files with 764 additions and 176 deletions

View File

@@ -70,9 +70,7 @@
setTimeout(() => {
const betragHeader = document.querySelector('.ag-header-cell[col-id="numericAmount"]');
if (betragHeader) {
console.log('Found Betrag header:', betragHeader);
console.log('Header classes:', betragHeader.className);
console.log('Header HTML:', betragHeader.innerHTML);
} else {
console.log('Could not find Betrag header with col-id="numericAmount"');
// Try to find it by text content

View File

@@ -1,10 +1,11 @@
import React, { Component } from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { Container, AppBar, Toolbar, Typography, Button, Box } from '@mui/material';
import { Container, AppBar, Toolbar, Typography, Button, Box, Tabs, Tab } from '@mui/material';
import LoginIcon from '@mui/icons-material/Login';
import DashboardIcon from '@mui/icons-material/Dashboard';
import DownloadIcon from '@mui/icons-material/Download';
import TableChart from '@mui/icons-material/TableChart';
import AuthService from './services/AuthService';
import DataViewer from './components/DataViewer';
import Login from './components/Login';
@@ -29,6 +30,7 @@ class App extends Component {
user: null,
loading: true,
exportData: null, // { selectedMonth, canExport, onExport }
currentView: 'dashboard', // 'dashboard' or 'tables'
};
this.authService = new AuthService();
}
@@ -77,8 +79,12 @@ class App extends Component {
this.setState({ exportData });
};
handleViewChange = (event, newValue) => {
this.setState({ currentView: newValue });
};
render() {
const { isAuthenticated, user, loading } = this.state;
const { isAuthenticated, user, loading, currentView } = this.state;
if (loading) {
return (
@@ -99,7 +105,7 @@ class App extends Component {
<AppBar position="static">
<Toolbar>
<DashboardIcon sx={{ mr: { xs: 1, sm: 2 } }} />
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<Typography variant="h6" component="div" sx={{ mr: 3 }}>
FibDash
</Typography>
{isAuthenticated && user && (
@@ -107,12 +113,42 @@ class App extends Component {
<Typography
variant="body2"
sx={{
mr: { xs: 1, sm: 2 },
mr: { xs: 2, sm: 3 },
display: { xs: 'none', sm: 'block' }
}}
>
Willkommen, {user.name}
</Typography>
<Tabs
value={currentView}
onChange={this.handleViewChange}
sx={{
mr: 'auto',
'& .MuiTab-root': {
color: 'rgba(255, 255, 255, 0.7)',
minHeight: 48,
'&.Mui-selected': {
color: 'white'
}
},
'& .MuiTabs-indicator': {
backgroundColor: 'white'
}
}}
>
<Tab
icon={<DashboardIcon />}
label="Dashboard"
value="dashboard"
sx={{ minHeight: 48 }}
/>
<Tab
icon={<TableChart />}
label="Stammdaten"
value="tables"
sx={{ minHeight: 48 }}
/>
</Tabs>
{this.state.exportData && (
<Button
color="inherit"
@@ -153,7 +189,12 @@ class App extends Component {
<Box sx={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}>
<Container maxWidth={false} sx={{ mt: 4, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}>
{isAuthenticated ? (
<DataViewer user={user} onUpdateExportData={this.updateExportData} />
<DataViewer
user={user}
onUpdateExportData={this.updateExportData}
currentView={currentView}
onViewChange={this.handleViewChange}
/>
) : (
<Login onLogin={this.handleLogin} />
)}

View File

@@ -7,7 +7,6 @@ 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';
@@ -21,7 +20,7 @@ class DataViewer extends Component {
summary: null,
loading: true,
error: null,
currentView: 'dashboard', // 'dashboard' or 'tables'
};
this.authService = new AuthService();
}
@@ -114,21 +113,17 @@ class DataViewer extends Component {
if (this.props.onUpdateExportData) {
this.props.onUpdateExportData({
selectedMonth,
canExport: !!selectedMonth && !this.state.loading && this.state.currentView === 'dashboard',
canExport: !!selectedMonth && !this.state.loading && this.props.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, currentView } = this.state;
const { user } = this.props;
const { months, selectedMonth, transactions, summary, loading, error } = this.state;
const { user, currentView } = this.props;
if (loading && !transactions.length) {
return (
@@ -148,11 +143,6 @@ 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 }}>

View File

@@ -41,12 +41,48 @@ class KreditorSelector extends Component {
componentDidMount() {
this.loadKreditors();
// If prefilled data is provided, set it in the newKreditor state
const updates = {};
if (this.props.prefilledIban) {
updates.iban = this.props.prefilledIban;
}
if (this.props.prefilledName) {
updates.name = this.props.prefilledName;
}
if (Object.keys(updates).length > 0) {
this.setState({
newKreditor: {
...this.state.newKreditor,
...updates
}
});
}
}
componentDidUpdate(prevProps) {
if (prevProps.selectedKreditorId !== this.props.selectedKreditorId) {
this.setState({ selectedKreditorId: this.props.selectedKreditorId || '' });
}
// If prefilled props change, update the newKreditor state
const updates = {};
if (prevProps.prefilledIban !== this.props.prefilledIban && this.props.prefilledIban) {
updates.iban = this.props.prefilledIban;
}
if (prevProps.prefilledName !== this.props.prefilledName && this.props.prefilledName) {
updates.name = this.props.prefilledName;
}
if (Object.keys(updates).length > 0) {
this.setState({
newKreditor: {
...this.state.newKreditor,
...updates
}
});
}
}
loadKreditors = async () => {
@@ -83,7 +119,11 @@ class KreditorSelector extends Component {
handleCreateDialogClose = () => {
this.setState({
createDialogOpen: false,
newKreditor: { iban: '', name: '', kreditorId: '' },
newKreditor: {
iban: this.props.prefilledIban || '',
name: this.props.prefilledName || '',
kreditorId: ''
},
validationErrors: [],
error: null
});
@@ -181,10 +221,12 @@ class KreditorSelector extends Component {
{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>
{(this.props.allowCreate !== false) && (
<MenuItem value="create_new" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
<AddIcon sx={{ mr: 1 }} />
Neuen Kreditor erstellen
</MenuItem>
)}
</Select>
</FormControl>

View File

@@ -51,6 +51,31 @@ class TransactionsTable extends Component {
};
window.addEventListener('resize', this.handleResize);
// Add dialog open listener to blur grid focus
this.handleDialogOpen = () => {
if (this.gridApi) {
// Clear any focused cells to prevent aria-hidden conflicts
this.gridApi.clearFocusedCell();
// Also blur any focused elements within the grid
const gridElement = document.querySelector('.ag-root-wrapper');
if (gridElement) {
const focusedElement = gridElement.querySelector(':focus');
if (focusedElement) {
focusedElement.blur();
}
}
}
};
// Listen for dialog open events (Material-UI dialogs)
this.handleFocusIn = (event) => {
// If focus moves to a dialog, blur the grid
if (event.target.closest('[role="dialog"]')) {
this.handleDialogOpen();
}
};
document.addEventListener('focusin', this.handleFocusIn);
}
componentWillUnmount() {
@@ -59,7 +84,13 @@ class TransactionsTable extends Component {
window.removeEventListener('resize', this.handleResize);
}
if (this.gridApi) {
// Clean up dialog focus listener
if (this.handleFocusIn) {
document.removeEventListener('focusin', this.handleFocusIn);
}
// Check if grid API is still valid before removing listeners
if (this.gridApi && !this.gridApi.isDestroyed()) {
this.gridApi.removeEventListener('modelUpdated', this.onModelUpdated);
this.gridApi.removeEventListener('filterChanged', this.onFilterChanged);
}
@@ -317,7 +348,6 @@ class TransactionsTable extends Component {
animateRows={true}
// Maintain state across data updates
maintainColumnOrder={true}
suppressColumnStateEvents={false}
/>
</div>
</Box>

View File

@@ -44,6 +44,11 @@ class BUTable extends Component {
},
};
this.authService = new AuthService();
// Focus management refs
this.triggerRef = React.createRef();
this.dialogRef = React.createRef();
this.confirmDialogRef = React.createRef();
}
componentDidMount() {
@@ -66,13 +71,16 @@ class BUTable extends Component {
};
handleOpenDialog = (bu = null) => {
// Store reference to the trigger element for focus restoration
this.triggerRef.current = document.activeElement;
this.setState({
dialogOpen: true,
editingBU: bu,
formData: bu ? {
bu: bu.bu,
name: bu.name,
vst: bu.vst || '',
vst: bu.vst !== null && bu.vst !== undefined ? bu.vst.toString() : '',
} : {
bu: '',
name: '',
@@ -91,6 +99,13 @@ class BUTable extends Component {
vst: '',
},
});
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (this.triggerRef.current && this.triggerRef.current.focus) {
this.triggerRef.current.focus();
}
}, 100);
};
handleInputChange = (field) => (event) => {
@@ -102,13 +117,20 @@ class BUTable extends Component {
});
};
isFormValid = () => {
const { formData } = this.state;
return formData.bu.trim() !== '' &&
formData.name.trim() !== '' &&
formData.vst !== '';
};
handleSave = async () => {
const { editingBU, formData } = this.state;
// Convert vst to number or null
const payload = {
...formData,
vst: formData.vst ? parseFloat(formData.vst) : null,
vst: formData.vst !== '' ? parseFloat(formData.vst) : null,
};
try {
@@ -138,6 +160,9 @@ class BUTable extends Component {
};
handleDeleteClick = (bu) => {
// Store reference to the trigger element for focus restoration
this.triggerRef.current = document.activeElement;
this.setState({
confirmDialogOpen: true,
itemToDelete: bu,
@@ -149,6 +174,13 @@ class BUTable extends Component {
if (!itemToDelete) return;
this.setState({ confirmDialogOpen: false, itemToDelete: null });
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (this.triggerRef.current && this.triggerRef.current.focus) {
this.triggerRef.current.focus();
}
}, 100);
try {
const response = await this.authService.apiCall(`/admin/buchungsschluessel/${itemToDelete.id}`, {
@@ -172,6 +204,13 @@ class BUTable extends Component {
confirmDialogOpen: false,
itemToDelete: null,
});
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (this.triggerRef.current && this.triggerRef.current.focus) {
this.triggerRef.current.focus();
}
}, 100);
};
render() {
@@ -220,7 +259,7 @@ class BUTable extends Component {
<TableCell>{bu.bu}</TableCell>
<TableCell>{bu.name}</TableCell>
<TableCell align="right">
{bu.vst ? `${bu.vst}%` : '-'}
{bu.vst !== null && bu.vst !== undefined ? `${bu.vst}%` : '-'}
</TableCell>
<TableCell align="right">
<IconButton
@@ -243,11 +282,22 @@ class BUTable extends Component {
</Table>
</TableContainer>
<Dialog open={dialogOpen} onClose={this.handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
<Dialog
open={dialogOpen}
onClose={this.handleCloseDialog}
maxWidth="sm"
fullWidth
ref={this.dialogRef}
disableAutoFocus={false}
disableEnforceFocus={false}
disableRestoreFocus={true}
aria-labelledby="bu-dialog-title"
aria-describedby="bu-dialog-content"
>
<DialogTitle id="bu-dialog-title">
{editingBU ? 'Buchungsschlüssel bearbeiten' : 'Neuer Buchungsschlüssel'}
</DialogTitle>
<DialogContent>
<DialogContent id="bu-dialog-content">
<TextField
autoFocus
margin="dense"
@@ -287,7 +337,11 @@ class BUTable extends Component {
</DialogContent>
<DialogActions>
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
<Button onClick={this.handleSave} variant="contained">
<Button
onClick={this.handleSave}
variant="contained"
disabled={!this.isFormValid()}
>
Speichern
</Button>
</DialogActions>
@@ -299,9 +353,15 @@ class BUTable extends Component {
onClose={this.handleDeleteCancel}
maxWidth="sm"
fullWidth
ref={this.confirmDialogRef}
disableAutoFocus={false}
disableEnforceFocus={false}
disableRestoreFocus={true}
aria-labelledby="confirm-dialog-title"
aria-describedby="confirm-dialog-content"
>
<DialogTitle>Löschen bestätigen</DialogTitle>
<DialogContent>
<DialogTitle id="confirm-dialog-title">Löschen bestätigen</DialogTitle>
<DialogContent id="confirm-dialog-content">
<Typography>
{this.state.itemToDelete &&
`Buchungsschlüssel "${this.state.itemToDelete.bu} - ${this.state.itemToDelete.name}" wirklich löschen?`

View File

@@ -43,6 +43,11 @@ class KontoTable extends Component {
},
};
this.authService = new AuthService();
// Focus management refs
this.triggerRef = React.createRef();
this.dialogRef = React.createRef();
this.confirmDialogRef = React.createRef();
}
componentDidMount() {
@@ -65,6 +70,9 @@ class KontoTable extends Component {
};
handleOpenDialog = (konto = null) => {
// Store reference to the trigger element for focus restoration
this.triggerRef.current = document.activeElement;
this.setState({
dialogOpen: true,
editingKonto: konto,
@@ -87,6 +95,13 @@ class KontoTable extends Component {
name: '',
},
});
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (this.triggerRef.current && this.triggerRef.current.focus) {
this.triggerRef.current.focus();
}
}, 100);
};
handleInputChange = (field) => (event) => {
@@ -98,6 +113,12 @@ class KontoTable extends Component {
});
};
isFormValid = () => {
const { formData } = this.state;
return formData.konto.trim() !== '' &&
formData.name.trim() !== '';
};
handleSave = async () => {
const { editingKonto, formData } = this.state;
@@ -128,6 +149,9 @@ class KontoTable extends Component {
};
handleDeleteClick = (konto) => {
// Store reference to the trigger element for focus restoration
this.triggerRef.current = document.activeElement;
this.setState({
confirmDialogOpen: true,
itemToDelete: konto,
@@ -139,6 +163,13 @@ class KontoTable extends Component {
if (!itemToDelete) return;
this.setState({ confirmDialogOpen: false, itemToDelete: null });
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (this.triggerRef.current && this.triggerRef.current.focus) {
this.triggerRef.current.focus();
}
}, 100);
try {
const response = await this.authService.apiCall(`/admin/konten/${konto.id}`, {
@@ -162,6 +193,13 @@ class KontoTable extends Component {
confirmDialogOpen: false,
itemToDelete: null,
});
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (this.triggerRef.current && this.triggerRef.current.focus) {
this.triggerRef.current.focus();
}
}, 100);
};
render() {
@@ -229,11 +267,22 @@ class KontoTable extends Component {
</Table>
</TableContainer>
<Dialog open={dialogOpen} onClose={this.handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
<Dialog
open={dialogOpen}
onClose={this.handleCloseDialog}
maxWidth="sm"
fullWidth
ref={this.dialogRef}
disableAutoFocus={false}
disableEnforceFocus={false}
disableRestoreFocus={true}
aria-labelledby="konto-dialog-title"
aria-describedby="konto-dialog-content"
>
<DialogTitle id="konto-dialog-title">
{editingKonto ? 'Konto bearbeiten' : 'Neues Konto'}
</DialogTitle>
<DialogContent>
<DialogContent id="konto-dialog-content">
<TextField
autoFocus
margin="dense"
@@ -257,7 +306,11 @@ class KontoTable extends Component {
</DialogContent>
<DialogActions>
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
<Button onClick={this.handleSave} variant="contained">
<Button
onClick={this.handleSave}
variant="contained"
disabled={!this.isFormValid()}
>
Speichern
</Button>
</DialogActions>
@@ -269,9 +322,15 @@ class KontoTable extends Component {
onClose={this.handleDeleteCancel}
maxWidth="sm"
fullWidth
ref={this.confirmDialogRef}
disableAutoFocus={false}
disableEnforceFocus={false}
disableRestoreFocus={true}
aria-labelledby="konto-confirm-dialog-title"
aria-describedby="konto-confirm-dialog-content"
>
<DialogTitle>Löschen bestätigen</DialogTitle>
<DialogContent>
<DialogTitle id="konto-confirm-dialog-title">Löschen bestätigen</DialogTitle>
<DialogContent id="konto-confirm-dialog-content">
<Typography>
{this.state.itemToDelete &&
`Konto "${this.state.itemToDelete.konto} - ${this.state.itemToDelete.name}" wirklich löschen?`

View File

@@ -44,6 +44,11 @@ class KreditorTable extends Component {
},
};
this.authService = new AuthService();
// Focus management refs
this.triggerRef = React.createRef();
this.dialogRef = React.createRef();
this.confirmDialogRef = React.createRef();
}
componentDidMount() {
@@ -66,6 +71,9 @@ class KreditorTable extends Component {
};
handleOpenDialog = (kreditor = null) => {
// Store reference to the trigger element for focus restoration
this.triggerRef.current = document.activeElement;
this.setState({
dialogOpen: true,
editingKreditor: kreditor,
@@ -91,6 +99,13 @@ class KreditorTable extends Component {
kreditorId: '',
},
});
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (this.triggerRef.current && this.triggerRef.current.focus) {
this.triggerRef.current.focus();
}
}, 100);
};
handleInputChange = (field) => (event) => {
@@ -102,6 +117,13 @@ class KreditorTable extends Component {
});
};
isFormValid = () => {
const { formData } = this.state;
return formData.iban.trim() !== '' &&
formData.name.trim() !== '' &&
formData.kreditorId.trim() !== '';
};
handleSave = async () => {
const { editingKreditor, formData } = this.state;
@@ -132,6 +154,9 @@ class KreditorTable extends Component {
};
handleDeleteClick = (kreditor) => {
// Store reference to the trigger element for focus restoration
this.triggerRef.current = document.activeElement;
this.setState({
confirmDialogOpen: true,
itemToDelete: kreditor,
@@ -143,6 +168,13 @@ class KreditorTable extends Component {
if (!itemToDelete) return;
this.setState({ confirmDialogOpen: false, itemToDelete: null });
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (this.triggerRef.current && this.triggerRef.current.focus) {
this.triggerRef.current.focus();
}
}, 100);
try {
const response = await this.authService.apiCall(`/admin/kreditoren/${kreditor.id}`, {
@@ -166,6 +198,13 @@ class KreditorTable extends Component {
confirmDialogOpen: false,
itemToDelete: null,
});
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (this.triggerRef.current && this.triggerRef.current.focus) {
this.triggerRef.current.focus();
}
}, 100);
};
render() {
@@ -235,11 +274,22 @@ class KreditorTable extends Component {
</Table>
</TableContainer>
<Dialog open={dialogOpen} onClose={this.handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
<Dialog
open={dialogOpen}
onClose={this.handleCloseDialog}
maxWidth="sm"
fullWidth
ref={this.dialogRef}
disableAutoFocus={false}
disableEnforceFocus={false}
disableRestoreFocus={true}
aria-labelledby="kreditor-dialog-title"
aria-describedby="kreditor-dialog-content"
>
<DialogTitle id="kreditor-dialog-title">
{editingKreditor ? 'Kreditor bearbeiten' : 'Neuer Kreditor'}
</DialogTitle>
<DialogContent>
<DialogContent id="kreditor-dialog-content">
<TextField
autoFocus
margin="dense"
@@ -270,7 +320,11 @@ class KreditorTable extends Component {
</DialogContent>
<DialogActions>
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
<Button onClick={this.handleSave} variant="contained">
<Button
onClick={this.handleSave}
variant="contained"
disabled={!this.isFormValid()}
>
Speichern
</Button>
</DialogActions>
@@ -282,9 +336,15 @@ class KreditorTable extends Component {
onClose={this.handleDeleteCancel}
maxWidth="sm"
fullWidth
ref={this.confirmDialogRef}
disableAutoFocus={false}
disableEnforceFocus={false}
disableRestoreFocus={true}
aria-labelledby="kreditor-confirm-dialog-title"
aria-describedby="kreditor-confirm-dialog-content"
>
<DialogTitle>Löschen bestätigen</DialogTitle>
<DialogContent>
<DialogTitle id="kreditor-confirm-dialog-title">Löschen bestätigen</DialogTitle>
<DialogContent id="kreditor-confirm-dialog-content">
<Typography>
{this.state.itemToDelete &&
`Kreditor "${this.state.itemToDelete.name}" wirklich löschen?`

View File

@@ -15,7 +15,9 @@ import {
Divider,
Tabs,
Tab,
Alert
Alert,
Chip,
Paper
} from '@mui/material';
import {
PictureAsPdf as PdfIcon,
@@ -35,6 +37,10 @@ const DocumentRenderer = (params) => {
const [tabValue, setTabValue] = useState(0);
const [error, setError] = useState(null);
// Focus management refs
const triggerRef = React.useRef(null);
const dialogRef = React.useRef(null);
// Always show something clickable, even if no documents
const hasDocuments = pdfs.length > 0 || links.length > 0;
@@ -47,6 +53,8 @@ const DocumentRenderer = (params) => {
const totalCount = allDocuments.length;
const handleClick = () => {
// Store reference to the trigger element for focus restoration
triggerRef.current = document.activeElement;
setDialogOpen(true);
};
@@ -54,6 +62,13 @@ const DocumentRenderer = (params) => {
setDialogOpen(false);
setTabValue(0); // Reset to first tab when closing
setError(null); // Clear any errors when closing
// Restore focus to the trigger element after dialog closes
setTimeout(() => {
if (triggerRef.current && triggerRef.current.focus) {
triggerRef.current.focus();
}
}, 100);
};
const handleTabChange = (event, newValue) => {
@@ -321,11 +336,22 @@ const DocumentRenderer = (params) => {
)}
</Box>
<Dialog open={dialogOpen} onClose={handleClose} maxWidth="lg" fullWidth>
<DialogTitle>
<Dialog
open={dialogOpen}
onClose={handleClose}
maxWidth="lg"
fullWidth
ref={dialogRef}
disableAutoFocus={false}
disableEnforceFocus={false}
disableRestoreFocus={true}
aria-labelledby="document-dialog-title"
aria-describedby="document-dialog-content"
>
<DialogTitle id="document-dialog-title">
{hasDocuments ? `Dokumente (${totalCount})` : 'Dokumentinformationen'}
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<DialogContent sx={{ p: 0 }} id="document-dialog-content">
{error && (
<Alert severity="error" sx={{ m: 2 }} onClose={() => setError(null)}>
{error}
@@ -335,6 +361,23 @@ const DocumentRenderer = (params) => {
<Tabs value={tabValue} onChange={handleTabChange}>
<Tab label="Dokumente" />
<Tab label={`Buchungen (${lineItems.length})`} />
<Tab
label="Kreditor"
sx={{
color: !params.data['Kontonummer/IBAN']
? 'text.secondary'
: params.data.hasKreditor
? 'success.main'
: 'warning.main',
'&.Mui-selected': {
color: !params.data['Kontonummer/IBAN']
? 'text.secondary'
: params.data.hasKreditor
? 'success.main'
: 'warning.main',
}
}}
/>
</Tabs>
</Box>
@@ -431,6 +474,108 @@ const DocumentRenderer = (params) => {
)}
</Box>
)}
{tabValue === 2 && (
<Box sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>
Kreditor Information
</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
IBAN
</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
{params.data['Kontonummer/IBAN'] || 'Keine IBAN verfügbar'}
</Typography>
{/* Show different content based on IBAN availability and Kreditor status */}
{!params.data['Kontonummer/IBAN'] ? (
<Box>
<Chip
label="Keine IBAN verfügbar"
color="default"
size="small"
sx={{ mb: 2 }}
/>
<Typography variant="body2" color="textSecondary">
Ohne IBAN kann kein Kreditor zugeordnet werden.
</Typography>
</Box>
) : params.data.hasKreditor ? (
<Box>
<Chip
label="Kreditor gefunden"
color="success"
size="small"
sx={{ mb: 2 }}
/>
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Kreditor Details
</Typography>
<Typography variant="body2">
<strong>Name:</strong> {params.data.kreditor.name}
</Typography>
<Typography variant="body2">
<strong>Kreditor ID:</strong> {params.data.kreditor.kreditorId}
</Typography>
</Box>
</Box>
) : (
<Box>
<Chip
label="Kein Kreditor gefunden"
color="warning"
size="small"
sx={{ mb: 2 }}
/>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
Sie können einen neuen Kreditor für diese IBAN erstellen:
</Typography>
<KreditorSelector
selectedKreditorId=""
onKreditorChange={(kreditor) => {
console.log('Kreditor selected/created:', kreditor);
if (kreditor) {
// Update the transaction data to reflect the new kreditor
params.data.kreditor = kreditor;
params.data.hasKreditor = true;
// Update all transactions with the same IBAN in the grid
if (params.api && kreditor.iban) {
const nodesToRefresh = [];
params.api.forEachNode((node) => {
if (node.data && node.data['Kontonummer/IBAN'] === kreditor.iban) {
node.data.kreditor = kreditor;
node.data.hasKreditor = true;
nodesToRefresh.push(node);
}
});
// Refresh specific cells to show updated colors and data
if (nodesToRefresh.length > 0) {
params.api.refreshCells({
rowNodes: nodesToRefresh,
columns: ['Kontonummer/IBAN'],
force: true
});
}
}
// Close and reopen dialog to show updated status
setDialogOpen(false);
setTimeout(() => setDialogOpen(true), 100);
}
}}
prefilledIban={params.data['Kontonummer/IBAN']}
prefilledName={params.data['Beguenstigter/Zahlungspflichtiger']}
allowCreate={true}
/>
</Box>
)}
</Paper>
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Schließen</Button>

View File

@@ -21,17 +21,44 @@ const RecipientRenderer = (params) => {
}
};
// Determine color based on Kreditor status for IBAN column
const getIbanColor = () => {
if (!isIbanColumn || !value) return 'inherit';
// Check if this transaction has Kreditor information
if (params.data && params.data.hasKreditor) {
return '#2e7d32'; // Green for found Kreditor
} else if (params.data && value) {
return '#ed6c02'; // Orange for IBAN without Kreditor
}
return '#1976d2'; // Default blue for clickable IBAN
};
const getTitle = () => {
if (!isIbanColumn || !value) return undefined;
if (params.data && params.data.hasKreditor) {
return `IBAN "${value}" - Kreditor: ${params.data.kreditor?.name || 'Unbekannt'} (zum Filtern klicken)`;
} else if (params.data && value) {
return `IBAN "${value}" - Kein Kreditor gefunden (zum Filtern klicken)`;
}
return `Nach IBAN "${value}" filtern`;
};
return (
<span
style={{
fontSize: '0.7rem',
lineHeight: '1.2',
cursor: isIbanColumn && value ? 'pointer' : 'default',
color: isIbanColumn && value ? '#1976d2' : 'inherit',
textDecoration: isIbanColumn && value ? 'underline' : 'none'
color: getIbanColor(),
textDecoration: isIbanColumn && value ? 'underline' : 'none',
fontWeight: isIbanColumn && params.data && params.data.hasKreditor ? 'bold' : 'normal'
}}
onClick={isIbanColumn && value ? handleClick : undefined}
title={isIbanColumn && value ? `Nach IBAN "${value}" filtern` : undefined}
title={getTitle()}
>
{value}
</span>

View File

@@ -28,7 +28,7 @@ export const getColumnDefs = () => [
sortable: false,
filter: false,
resizable: false,
suppressMenu: true,
suppressHeaderMenuButton: true,
cellRenderer: SelectionRenderer,
headerComponent: SelectionHeader,
headerComponentParams: {
@@ -101,7 +101,6 @@ export const getColumnDefs = () => [
width: 70,
cellRenderer: TypeRenderer,
sortable: false,
suppressSorting: true,
filter: CheckboxFilter,
filterParams: {
filterOptions: [
@@ -138,7 +137,6 @@ export const getColumnDefs = () => [
width: 70,
cellRenderer: JtlRenderer,
sortable: false,
suppressSorting: true,
filter: CheckboxFilter,
filterParams: {
filterOptions: [
@@ -212,7 +210,10 @@ export const defaultColDef = {
export const gridOptions = {
animateRows: true,
rowSelection: false,
rowSelection: {
mode: 'multiRow',
enableClickSelection: false
},
rowBuffer: 10,
// Enable virtualization (default behavior)
suppressRowVirtualisation: false,
@@ -225,8 +226,7 @@ export const gridOptions = {
// Pagination (optional - can be removed for infinite scrolling)
pagination: false,
paginationPageSize: 100,
// Disable cell selection
suppressCellSelection: true,
suppressRowClickSelection: true,
// Disable cell selection and focus
cellSelection: false,
suppressCellFocus: true
};

View File

@@ -103,9 +103,13 @@ export default class CheckboxFilter {
};
destroy() {
if (this.reactRoot) {
this.reactRoot.unmount();
}
// Use setTimeout to avoid unmounting during render
setTimeout(() => {
if (this.reactRoot) {
this.reactRoot.unmount();
this.reactRoot = null;
}
}, 0);
}
renderReactComponent() {
@@ -172,10 +176,13 @@ export default class CheckboxFilter {
);
// Recreate React root every time to avoid state corruption
if (this.reactRoot) {
this.reactRoot.unmount();
}
this.reactRoot = createRoot(this.eGui);
this.reactRoot.render(<FilterComponent />);
// Use setTimeout to avoid unmounting during render
setTimeout(() => {
if (this.reactRoot) {
this.reactRoot.unmount();
}
this.reactRoot = createRoot(this.eGui);
this.reactRoot.render(<FilterComponent />);
}, 0);
}
}

View File

@@ -8,20 +8,27 @@ import {
ListItemText,
Box,
Chip,
Button
Button,
TextField,
Typography,
Divider
} from '@mui/material';
export default class IbanSelectionFilter {
constructor() {
this.state = {
selectedValues: [],
availableValues: []
availableValues: [],
partialIban: ''
};
// Create the DOM element that AG Grid expects
this.eGui = document.createElement('div');
this.eGui.style.minWidth = '250px';
this.eGui.style.padding = '8px';
// Create a ref for the text input
this.textInputRef = React.createRef();
}
init(params) {
@@ -35,10 +42,13 @@ export default class IbanSelectionFilter {
}
destroy() {
if (this.reactRoot) {
this.reactRoot.unmount();
this.reactRoot = null;
}
// Use setTimeout to avoid unmounting during render
setTimeout(() => {
if (this.reactRoot) {
this.reactRoot.unmount();
this.reactRoot = null;
}
}, 0);
}
updateAvailableValues() {
@@ -72,15 +82,27 @@ export default class IbanSelectionFilter {
}
isFilterActive() {
return this.state.selectedValues.length > 0;
return this.state.selectedValues.length > 0 || this.state.partialIban.trim() !== '';
}
doesFilterPass(params) {
const { selectedValues } = this.state;
if (selectedValues.length === 0) return true;
const { selectedValues, partialIban } = this.state;
const value = params.data['Kontonummer/IBAN'];
return selectedValues.includes(value);
// If no filters are active, show all rows
if (selectedValues.length === 0 && partialIban.trim() === '') {
return true;
}
// Check if row matches selected IBANs
const matchesSelected = selectedValues.length === 0 || selectedValues.includes(value);
// Check if row matches partial IBAN (case-insensitive)
const matchesPartial = partialIban.trim() === '' ||
(value && value.toLowerCase().includes(partialIban.toLowerCase()));
// Both conditions must be true (AND logic)
return matchesSelected && matchesPartial;
}
getModel() {
@@ -88,16 +110,28 @@ export default class IbanSelectionFilter {
return {
filterType: 'iban-selection',
values: this.state.selectedValues
values: this.state.selectedValues,
partialIban: this.state.partialIban
};
}
setModel(model) {
if (!model) {
this.state.selectedValues = [];
this.state.partialIban = '';
} else {
this.state.selectedValues = model.values || [];
this.state.partialIban = model.partialIban || '';
}
// Update the text field value directly if it exists
if (this.textInputRef.current) {
const inputElement = this.textInputRef.current.querySelector('input');
if (inputElement) {
inputElement.value = this.state.partialIban;
}
}
this.renderReactComponent();
}
@@ -111,8 +145,39 @@ export default class IbanSelectionFilter {
}
};
handlePartialIbanChange = (partialIban) => {
this.state.partialIban = partialIban;
// Update the clear button visibility without full re-render
this.updateClearButtonVisibility();
// Notify AG Grid that filter changed
if (this.params && this.params.filterChangedCallback) {
this.params.filterChangedCallback();
}
};
updateClearButtonVisibility = () => {
// Find the clear button container and update its visibility
const clearButtonContainer = this.eGui.querySelector('.clear-button-container');
if (clearButtonContainer) {
const shouldShow = this.state.selectedValues.length > 0 || this.state.partialIban.trim() !== '';
clearButtonContainer.style.display = shouldShow ? 'block' : 'none';
}
};
clearFilter = () => {
this.state.selectedValues = [];
this.state.partialIban = '';
// Clear the text field directly using the ref
if (this.textInputRef.current) {
const inputElement = this.textInputRef.current.querySelector('input');
if (inputElement) {
inputElement.value = '';
}
}
this.renderReactComponent();
if (this.params && this.params.filterChangedCallback) {
@@ -127,6 +192,27 @@ export default class IbanSelectionFilter {
const FilterComponent = () => (
<Box sx={{ minWidth: 250 }} className="ag-filter-custom">
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
IBAN Filter
</Typography>
<TextField
ref={this.textInputRef}
fullWidth
size="small"
label="IBAN eingeben"
placeholder="z.B. DE89, 1234..."
defaultValue={this.state.partialIban}
onChange={(event) => this.handlePartialIbanChange(event.target.value)}
sx={{ mb: 2 }}
/>
<Divider sx={{ mb: 2 }} />
<Typography variant="body2" sx={{ mb: 1, color: 'text.secondary' }}>
Oder aus der Liste auswählen:
</Typography>
<FormControl fullWidth size="small">
<Select
multiple
@@ -138,7 +224,7 @@ export default class IbanSelectionFilter {
return <em>Alle IBANs</em>;
}
return (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.slice(0, 2).map((value) => {
const ibanData = this.availableValues.find(item => item.iban === value);
return (
@@ -170,7 +256,7 @@ export default class IbanSelectionFilter {
},
},
}}
>
>
{this.availableValues.map((item) => (
<MenuItem key={item.iban} value={item.iban}>
<Checkbox
@@ -188,19 +274,21 @@ export default class IbanSelectionFilter {
</Select>
</FormControl>
{this.state.selectedValues.length > 0 && (
<Box sx={{ mt: 1, textAlign: 'right' }}>
<Button
onClick={this.clearFilter}
size="small"
variant="text"
color="primary"
sx={{ fontSize: '0.75rem' }}
>
Filter löschen
</Button>
</Box>
)}
<Box
className="clear-button-container"
sx={{ mt: 1, textAlign: 'right' }}
style={{ display: (this.state.selectedValues.length > 0 || this.state.partialIban.trim() !== '') ? 'block' : 'none' }}
>
<Button
onClick={this.clearFilter}
size="small"
variant="text"
color="primary"
sx={{ fontSize: '0.75rem' }}
>
Alle Filter löschen
</Button>
</Box>
</Box>
);

View File

@@ -18,7 +18,10 @@ const SortHeader = (params) => {
if (params.api) {
params.api.addEventListener('sortChanged', updateSortState);
return () => {
params.api.removeEventListener('sortChanged', updateSortState);
// Check if grid API is still valid before removing listener
if (params.api && !params.api.isDestroyed()) {
params.api.removeEventListener('sortChanged', updateSortState);
}
};
}
}, [params.api, params.column]);

View File

@@ -36,8 +36,8 @@ class HeaderComponent extends Component {
}
componentWillUnmount() {
// Clean up event listener
if (this.props.params && this.props.params.api) {
// Clean up event listener - check if grid API is still valid
if (this.props.params && this.props.params.api && !this.props.params.api.isDestroyed()) {
this.props.params.api.removeEventListener('filterChanged', this.onFilterChanged);
}
}
@@ -147,7 +147,7 @@ class HeaderComponent extends Component {
const showTextFilter = isTextColumn;
// Check if sorting is disabled for this column
const isSortingDisabled = column.colDef.sortable === false || column.colDef.suppressSorting === true;
const isSortingDisabled = column.colDef.sortable === false;
return (
<Box sx={{
@@ -290,21 +290,19 @@ export default class TextHeaderWithFilter {
this.eGui.style.height = '100%';
this.eGui.style.display = 'flex';
this.eGui.style.flexDirection = 'column';
console.log('TextHeaderWithFilter constructor');
}
init(params) {
this.params = params;
console.log('TextHeaderWithFilter init params:', params);
// Listen for menu close events to keep state in sync
if (params.api) {
params.api.addEventListener('popupMenuVisibleChanged', (event) => {
this.popupMenuListener = (event) => {
if (!event.visible && this.headerComponent) {
this.headerComponent.setState({ menuOpen: false });
}
});
};
params.api.addEventListener('popupMenuVisibleChanged', this.popupMenuListener);
}
// Render React component into the DOM element
@@ -312,11 +310,15 @@ export default class TextHeaderWithFilter {
}
getGui() {
console.log('TextHeaderWithFilter getGui called');
return this.eGui;
}
destroy() {
// Clean up event listener if grid API is still valid
if (this.params && this.params.api && !this.params.api.isDestroyed() && this.popupMenuListener) {
this.params.api.removeEventListener('popupMenuVisibleChanged', this.popupMenuListener);
}
// Use setTimeout to avoid unmounting during render
setTimeout(() => {
if (this.reactRoot) {

View File

@@ -12,9 +12,7 @@ export const processTransactionData = (transactions) => {
export const getRowStyle = (params, selectedRows) => {
const rowId = params.data?.id || params.rowIndex;
const isSelected = selectedRows && selectedRows.has && selectedRows.has(rowId);
console.log('getRowStyle called for row:', rowId, 'isSelected:', isSelected, 'selectedRows size:', selectedRows?.size);
if (params.data.isJTLOnly) {
return {
backgroundColor: isSelected ? '#e3f2fd' : '#ffebee',

View File

@@ -8,6 +8,7 @@ GO
-- Create Kreditor table
-- Multiple IBANs can have the same kreditor name and kreditorId
CREATE TABLE fibdash.Kreditor (
id INT IDENTITY(1,1) PRIMARY KEY,
iban NVARCHAR(34) NOT NULL,
@@ -15,9 +16,10 @@ CREATE TABLE fibdash.Kreditor (
kreditorId NVARCHAR(50) NOT NULL
);
-- Ensure kreditorId is unique to support FK references
-- Create unique index on IBAN to prevent duplicate IBANs
-- but allow same kreditorId and name for multiple IBANs
ALTER TABLE fibdash.Kreditor
ADD CONSTRAINT UQ_Kreditor_kreditorId UNIQUE (kreditorId);
ADD CONSTRAINT UQ_Kreditor_IBAN UNIQUE (iban);
-- Create AccountingItems table
-- Based on CSV structure: umsatz brutto, soll/haben kz, konto, gegenkonto, bu, buchungsdatum, rechnungsnummer, buchungstext, beleglink

View File

@@ -22,7 +22,7 @@ router.get('/system-info', authenticateToken, (req, res) => {
// Get all kreditoren
router.get('/kreditoren', authenticateToken, async (req, res) => {
try {
const result = await executeQuery('SELECT * FROM fibdash.Kreditor ORDER BY name');
const result = await executeQuery('SELECT id, iban, name, kreditorId FROM fibdash.Kreditor ORDER BY name, iban');
res.json({ kreditoren: result.recordset });
} catch (error) {
console.error('Error fetching kreditoren:', error);
@@ -199,7 +199,7 @@ router.post('/buchungsschluessel', authenticateToken, async (req, res) => {
try {
await executeQuery(
'INSERT INTO fibdash.BU (bu, name, vst) VALUES (@bu, @name, @vst)',
{ bu, name, vst: vst || null }
{ bu, name, vst: vst !== undefined && vst !== '' ? vst : null }
);
res.json({ message: 'Buchungsschlüssel erfolgreich erstellt' });
} catch (error) {
@@ -224,7 +224,7 @@ router.put('/buchungsschluessel/:id', authenticateToken, async (req, res) => {
try {
await executeQuery(
'UPDATE fibdash.BU SET bu = @bu, name = @name, vst = @vst WHERE id = @id',
{ bu, name, vst: vst || null, id }
{ bu, name, vst: vst !== undefined && vst !== '' ? vst : null, id }
);
res.json({ message: 'Buchungsschlüssel erfolgreich aktualisiert' });
} catch (error) {

View File

@@ -235,7 +235,18 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
});
}
// Add JTL status to each CSV transaction
// Get Kreditor information for IBAN lookup
let kreditorData = [];
try {
const { executeQuery } = require('../config/database');
const kreditorQuery = `SELECT id, iban, name, kreditorId FROM fibdash.Kreditor`;
const kreditorResult = await executeQuery(kreditorQuery);
kreditorData = kreditorResult.recordset || [];
} catch (error) {
console.log('Kreditor database not available, continuing without Kreditor data');
}
// Add JTL status and Kreditor information to each CSV transaction
const transactionsWithJTL = monthTransactions.map(transaction => {
// Try to match by amount and date (approximate matching)
const amount = transaction.numericAmount;
@@ -255,6 +266,10 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
return amountMatch && dateMatch;
});
// Look up Kreditor by IBAN
const transactionIban = transaction['Kontonummer/IBAN'];
const kreditorMatch = transactionIban ? kreditorData.find(k => k.iban === transactionIban) : null;
return {
...transaction,
hasJTL: jtlDatabaseAvailable ? !!jtlMatch : undefined,
@@ -263,7 +278,15 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
jtlDatabaseAvailable,
// Include document data from JTL match
pdfs: jtlMatch ? jtlMatch.pdfs || [] : [],
links: jtlMatch ? jtlMatch.links || [] : []
links: jtlMatch ? jtlMatch.links || [] : [],
// Include Kreditor information
kreditor: kreditorMatch ? {
id: kreditorMatch.id,
name: kreditorMatch.name,
kreditorId: kreditorMatch.kreditorId,
iban: kreditorMatch.iban
} : null,
hasKreditor: !!kreditorMatch
};
});
@@ -299,6 +322,7 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
'Verwendungszweck': jtl.cVerwendungszweck || '',
'Buchungstext': 'JTL Transaction',
'Beguenstigter/Zahlungspflichtiger': jtl.cName || '',
'Kontonummer/IBAN': '', // JTL transactions don't have IBAN data
'Betrag': jtl.fBetrag ? jtl.fBetrag.toString().replace('.', ',') : '0,00',
numericAmount: parseFloat(jtl.fBetrag) || 0,
parsedDate: new Date(jtl.dBuchungsdatum),
@@ -309,7 +333,10 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
isJTLOnly: true,
// Include document data from JTL transaction
pdfs: jtl.pdfs || [],
links: jtl.links || []
links: jtl.links || [],
// JTL transactions don't have IBAN data, so no Kreditor match
kreditor: null,
hasKreditor: false
}));
// Combine CSV and JTL-only transactions
@@ -565,10 +592,9 @@ 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
SELECT id, iban, name, kreditorId
FROM fibdash.Kreditor
ORDER BY name ASC, iban ASC
`;
const result = await executeQuery(query);
@@ -586,14 +612,12 @@ router.get('/kreditors/:id', authenticateToken, async (req, res) => {
const { id } = req.params;
const query = `
SELECT id, iban, name, kreditorId, created_at, updated_at
FROM Kreditor
WHERE id = @id AND is_active = 1
SELECT id, iban, name, kreditorId
FROM fibdash.Kreditor
WHERE id = @id
`;
const result = await executeQuery(query, [
{ name: 'id', type: 'int', value: parseInt(id) }
]);
const result = await executeQuery(query, { id: parseInt(id) });
if (result.recordset.length === 0) {
return res.status(404).json({ error: 'Kreditor not found' });
@@ -617,32 +641,25 @@ router.post('/kreditors', authenticateToken, async (req, res) => {
return res.status(400).json({ error: 'IBAN, name, and kreditorId are required' });
}
// Check if kreditor with same IBAN or kreditorId already exists
// Check if IBAN already exists (only IBAN needs to be unique)
const checkQuery = `
SELECT id FROM Kreditor
WHERE (iban = @iban OR kreditorId = @kreditorId) AND is_active = 1
SELECT id FROM fibdash.Kreditor
WHERE iban = @iban
`;
const checkResult = await executeQuery(checkQuery, [
{ name: 'iban', type: 'nvarchar', value: iban },
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
]);
const checkResult = await executeQuery(checkQuery, { iban });
if (checkResult.recordset.length > 0) {
return res.status(409).json({ error: 'Kreditor with this IBAN or kreditorId already exists' });
return res.status(409).json({ error: 'Kreditor with this IBAN 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())
INSERT INTO fibdash.Kreditor (iban, name, kreditorId)
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId
VALUES (@iban, @name, @kreditorId)
`;
const result = await executeQuery(insertQuery, [
{ name: 'iban', type: 'nvarchar', value: iban },
{ name: 'name', type: 'nvarchar', value: name },
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
]);
const result = await executeQuery(insertQuery, { iban, name, kreditorId });
res.status(201).json(result.recordset[0]);
} catch (error) {
@@ -664,44 +681,33 @@ router.put('/kreditors/:id', authenticateToken, async (req, res) => {
}
// 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) }
]);
const checkQuery = `SELECT id FROM fibdash.Kreditor WHERE id = @id`;
const checkResult = await executeQuery(checkQuery, { id: parseInt(id) });
if (checkResult.recordset.length === 0) {
return res.status(404).json({ error: 'Kreditor not found' });
}
// Check for conflicts with other kreditors
// Check for conflicts with other kreditors (only IBAN needs to be unique)
const conflictQuery = `
SELECT id FROM Kreditor
WHERE (iban = @iban OR kreditorId = @kreditorId) AND id != @id AND is_active = 1
SELECT id FROM fibdash.Kreditor
WHERE iban = @iban AND id != @id
`;
const conflictResult = await executeQuery(conflictQuery, [
{ name: 'iban', type: 'nvarchar', value: iban },
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId },
{ name: 'id', type: 'int', value: parseInt(id) }
]);
const conflictResult = await executeQuery(conflictQuery, { iban, id: parseInt(id) });
if (conflictResult.recordset.length > 0) {
return res.status(409).json({ error: 'Another kreditor with this IBAN or kreditorId already exists' });
return res.status(409).json({ error: 'Another kreditor with this IBAN 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
UPDATE fibdash.Kreditor
SET iban = @iban, name = @name, kreditorId = @kreditorId
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId
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) }
]);
const result = await executeQuery(updateQuery, { iban, name, kreditorId, id: parseInt(id) });
res.json(result.recordset[0]);
} catch (error) {
@@ -710,21 +716,18 @@ router.put('/kreditors/:id', authenticateToken, async (req, res) => {
}
});
// Delete kreditor (soft delete)
// Delete kreditor (hard 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
DELETE FROM fibdash.Kreditor
WHERE id = @id
`;
const result = await executeQuery(query, [
{ name: 'id', type: 'int', value: parseInt(id) }
]);
const result = await executeQuery(query, { id: parseInt(id) });
if (result.rowsAffected[0] === 0) {
return res.status(404).json({ error: 'Kreditor not found' });

View File

@@ -5,12 +5,20 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
require('dotenv').config();
module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: './client/src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/',
},
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'),
},
module: {
rules: [
{
@@ -72,6 +80,31 @@ module.exports = {
},
},
},
watchOptions: {
ignored: /node_modules/,
aggregateTimeout: 300,
poll: false,
},
snapshot: {
managedPaths: [path.resolve(__dirname, 'node_modules')],
immutablePaths: [],
buildDependencies: {
hash: true,
timestamp: true,
},
module: {
timestamp: true,
hash: true,
},
resolve: {
timestamp: true,
hash: true,
},
resolveBuildDependencies: {
timestamp: true,
hash: true,
},
},
resolve: {
extensions: ['.js', '.jsx'],
},