Enhance webpack configuration for improved performance and development experience. Add filesystem caching and watch options. Update KreditorSelector to handle prefilled data and improve state management. Refactor TransactionsTable to manage focus during dialog interactions. Update admin tables to manage focus restoration and improve dialog handling. Implement IBAN filtering in IbanSelectionFilter and enhance document rendering with Kreditor information. Update SQL schema to allow multiple IBANs for the same Kreditor and adjust API routes for better data handling.
This commit is contained in:
@@ -70,9 +70,7 @@
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const betragHeader = document.querySelector('.ag-header-cell[col-id="numericAmount"]');
|
const betragHeader = document.querySelector('.ag-header-cell[col-id="numericAmount"]');
|
||||||
if (betragHeader) {
|
if (betragHeader) {
|
||||||
console.log('Found Betrag header:', betragHeader);
|
|
||||||
console.log('Header classes:', betragHeader.className);
|
console.log('Header classes:', betragHeader.className);
|
||||||
console.log('Header HTML:', betragHeader.innerHTML);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Could not find Betrag header with col-id="numericAmount"');
|
console.log('Could not find Betrag header with col-id="numericAmount"');
|
||||||
// Try to find it by text content
|
// Try to find it by text content
|
||||||
|
|||||||
@@ -41,12 +41,48 @@ class KreditorSelector extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.loadKreditors();
|
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) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.selectedKreditorId !== this.props.selectedKreditorId) {
|
if (prevProps.selectedKreditorId !== this.props.selectedKreditorId) {
|
||||||
this.setState({ 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 () => {
|
loadKreditors = async () => {
|
||||||
@@ -83,7 +119,11 @@ class KreditorSelector extends Component {
|
|||||||
handleCreateDialogClose = () => {
|
handleCreateDialogClose = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
createDialogOpen: false,
|
createDialogOpen: false,
|
||||||
newKreditor: { iban: '', name: '', kreditorId: '' },
|
newKreditor: {
|
||||||
|
iban: this.props.prefilledIban || '',
|
||||||
|
name: this.props.prefilledName || '',
|
||||||
|
kreditorId: ''
|
||||||
|
},
|
||||||
validationErrors: [],
|
validationErrors: [],
|
||||||
error: null
|
error: null
|
||||||
});
|
});
|
||||||
@@ -181,10 +221,12 @@ class KreditorSelector extends Component {
|
|||||||
{kreditor.name} ({kreditor.kreditorId}) - {kreditor.iban}
|
{kreditor.name} ({kreditor.kreditorId}) - {kreditor.iban}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
<MenuItem value="create_new" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
|
{(this.props.allowCreate !== false) && (
|
||||||
<AddIcon sx={{ mr: 1 }} />
|
<MenuItem value="create_new" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
|
||||||
Neuen Kreditor erstellen
|
<AddIcon sx={{ mr: 1 }} />
|
||||||
</MenuItem>
|
Neuen Kreditor erstellen
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,31 @@ class TransactionsTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
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() {
|
componentWillUnmount() {
|
||||||
@@ -59,7 +84,13 @@ class TransactionsTable extends Component {
|
|||||||
window.removeEventListener('resize', this.handleResize);
|
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('modelUpdated', this.onModelUpdated);
|
||||||
this.gridApi.removeEventListener('filterChanged', this.onFilterChanged);
|
this.gridApi.removeEventListener('filterChanged', this.onFilterChanged);
|
||||||
}
|
}
|
||||||
@@ -317,7 +348,6 @@ class TransactionsTable extends Component {
|
|||||||
animateRows={true}
|
animateRows={true}
|
||||||
// Maintain state across data updates
|
// Maintain state across data updates
|
||||||
maintainColumnOrder={true}
|
maintainColumnOrder={true}
|
||||||
suppressColumnStateEvents={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ class BUTable extends Component {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.authService = new AuthService();
|
this.authService = new AuthService();
|
||||||
|
|
||||||
|
// Focus management refs
|
||||||
|
this.triggerRef = React.createRef();
|
||||||
|
this.dialogRef = React.createRef();
|
||||||
|
this.confirmDialogRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -66,6 +71,9 @@ class BUTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleOpenDialog = (bu = null) => {
|
handleOpenDialog = (bu = null) => {
|
||||||
|
// Store reference to the trigger element for focus restoration
|
||||||
|
this.triggerRef.current = document.activeElement;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
dialogOpen: true,
|
dialogOpen: true,
|
||||||
editingBU: bu,
|
editingBU: bu,
|
||||||
@@ -91,6 +99,13 @@ class BUTable extends Component {
|
|||||||
vst: '',
|
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) => {
|
handleInputChange = (field) => (event) => {
|
||||||
@@ -145,6 +160,9 @@ class BUTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDeleteClick = (bu) => {
|
handleDeleteClick = (bu) => {
|
||||||
|
// Store reference to the trigger element for focus restoration
|
||||||
|
this.triggerRef.current = document.activeElement;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
confirmDialogOpen: true,
|
confirmDialogOpen: true,
|
||||||
itemToDelete: bu,
|
itemToDelete: bu,
|
||||||
@@ -156,6 +174,13 @@ class BUTable extends Component {
|
|||||||
if (!itemToDelete) return;
|
if (!itemToDelete) return;
|
||||||
|
|
||||||
this.setState({ confirmDialogOpen: false, itemToDelete: null });
|
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 {
|
try {
|
||||||
const response = await this.authService.apiCall(`/admin/buchungsschluessel/${itemToDelete.id}`, {
|
const response = await this.authService.apiCall(`/admin/buchungsschluessel/${itemToDelete.id}`, {
|
||||||
@@ -179,6 +204,13 @@ class BUTable extends Component {
|
|||||||
confirmDialogOpen: false,
|
confirmDialogOpen: false,
|
||||||
itemToDelete: null,
|
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() {
|
render() {
|
||||||
@@ -250,11 +282,22 @@ class BUTable extends Component {
|
|||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={this.handleCloseDialog} maxWidth="sm" fullWidth>
|
<Dialog
|
||||||
<DialogTitle>
|
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'}
|
{editingBU ? 'Buchungsschlüssel bearbeiten' : 'Neuer Buchungsschlüssel'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent id="bu-dialog-content">
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
@@ -310,9 +353,15 @@ class BUTable extends Component {
|
|||||||
onClose={this.handleDeleteCancel}
|
onClose={this.handleDeleteCancel}
|
||||||
maxWidth="sm"
|
maxWidth="sm"
|
||||||
fullWidth
|
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>
|
<DialogTitle id="confirm-dialog-title">Löschen bestätigen</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent id="confirm-dialog-content">
|
||||||
<Typography>
|
<Typography>
|
||||||
{this.state.itemToDelete &&
|
{this.state.itemToDelete &&
|
||||||
`Buchungsschlüssel "${this.state.itemToDelete.bu} - ${this.state.itemToDelete.name}" wirklich löschen?`
|
`Buchungsschlüssel "${this.state.itemToDelete.bu} - ${this.state.itemToDelete.name}" wirklich löschen?`
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ class KontoTable extends Component {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.authService = new AuthService();
|
this.authService = new AuthService();
|
||||||
|
|
||||||
|
// Focus management refs
|
||||||
|
this.triggerRef = React.createRef();
|
||||||
|
this.dialogRef = React.createRef();
|
||||||
|
this.confirmDialogRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -65,6 +70,9 @@ class KontoTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleOpenDialog = (konto = null) => {
|
handleOpenDialog = (konto = null) => {
|
||||||
|
// Store reference to the trigger element for focus restoration
|
||||||
|
this.triggerRef.current = document.activeElement;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
dialogOpen: true,
|
dialogOpen: true,
|
||||||
editingKonto: konto,
|
editingKonto: konto,
|
||||||
@@ -87,6 +95,13 @@ class KontoTable extends Component {
|
|||||||
name: '',
|
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) => {
|
handleInputChange = (field) => (event) => {
|
||||||
@@ -134,6 +149,9 @@ class KontoTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDeleteClick = (konto) => {
|
handleDeleteClick = (konto) => {
|
||||||
|
// Store reference to the trigger element for focus restoration
|
||||||
|
this.triggerRef.current = document.activeElement;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
confirmDialogOpen: true,
|
confirmDialogOpen: true,
|
||||||
itemToDelete: konto,
|
itemToDelete: konto,
|
||||||
@@ -145,6 +163,13 @@ class KontoTable extends Component {
|
|||||||
if (!itemToDelete) return;
|
if (!itemToDelete) return;
|
||||||
|
|
||||||
this.setState({ confirmDialogOpen: false, itemToDelete: null });
|
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 {
|
try {
|
||||||
const response = await this.authService.apiCall(`/admin/konten/${konto.id}`, {
|
const response = await this.authService.apiCall(`/admin/konten/${konto.id}`, {
|
||||||
@@ -168,6 +193,13 @@ class KontoTable extends Component {
|
|||||||
confirmDialogOpen: false,
|
confirmDialogOpen: false,
|
||||||
itemToDelete: null,
|
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() {
|
render() {
|
||||||
@@ -235,11 +267,22 @@ class KontoTable extends Component {
|
|||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={this.handleCloseDialog} maxWidth="sm" fullWidth>
|
<Dialog
|
||||||
<DialogTitle>
|
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'}
|
{editingKonto ? 'Konto bearbeiten' : 'Neues Konto'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent id="konto-dialog-content">
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
@@ -279,9 +322,15 @@ class KontoTable extends Component {
|
|||||||
onClose={this.handleDeleteCancel}
|
onClose={this.handleDeleteCancel}
|
||||||
maxWidth="sm"
|
maxWidth="sm"
|
||||||
fullWidth
|
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>
|
<DialogTitle id="konto-confirm-dialog-title">Löschen bestätigen</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent id="konto-confirm-dialog-content">
|
||||||
<Typography>
|
<Typography>
|
||||||
{this.state.itemToDelete &&
|
{this.state.itemToDelete &&
|
||||||
`Konto "${this.state.itemToDelete.konto} - ${this.state.itemToDelete.name}" wirklich löschen?`
|
`Konto "${this.state.itemToDelete.konto} - ${this.state.itemToDelete.name}" wirklich löschen?`
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ class KreditorTable extends Component {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.authService = new AuthService();
|
this.authService = new AuthService();
|
||||||
|
|
||||||
|
// Focus management refs
|
||||||
|
this.triggerRef = React.createRef();
|
||||||
|
this.dialogRef = React.createRef();
|
||||||
|
this.confirmDialogRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -66,6 +71,9 @@ class KreditorTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleOpenDialog = (kreditor = null) => {
|
handleOpenDialog = (kreditor = null) => {
|
||||||
|
// Store reference to the trigger element for focus restoration
|
||||||
|
this.triggerRef.current = document.activeElement;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
dialogOpen: true,
|
dialogOpen: true,
|
||||||
editingKreditor: kreditor,
|
editingKreditor: kreditor,
|
||||||
@@ -91,6 +99,13 @@ class KreditorTable extends Component {
|
|||||||
kreditorId: '',
|
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) => {
|
handleInputChange = (field) => (event) => {
|
||||||
@@ -139,6 +154,9 @@ class KreditorTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDeleteClick = (kreditor) => {
|
handleDeleteClick = (kreditor) => {
|
||||||
|
// Store reference to the trigger element for focus restoration
|
||||||
|
this.triggerRef.current = document.activeElement;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
confirmDialogOpen: true,
|
confirmDialogOpen: true,
|
||||||
itemToDelete: kreditor,
|
itemToDelete: kreditor,
|
||||||
@@ -150,6 +168,13 @@ class KreditorTable extends Component {
|
|||||||
if (!itemToDelete) return;
|
if (!itemToDelete) return;
|
||||||
|
|
||||||
this.setState({ confirmDialogOpen: false, itemToDelete: null });
|
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 {
|
try {
|
||||||
const response = await this.authService.apiCall(`/admin/kreditoren/${kreditor.id}`, {
|
const response = await this.authService.apiCall(`/admin/kreditoren/${kreditor.id}`, {
|
||||||
@@ -173,6 +198,13 @@ class KreditorTable extends Component {
|
|||||||
confirmDialogOpen: false,
|
confirmDialogOpen: false,
|
||||||
itemToDelete: null,
|
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() {
|
render() {
|
||||||
@@ -242,11 +274,22 @@ class KreditorTable extends Component {
|
|||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={this.handleCloseDialog} maxWidth="sm" fullWidth>
|
<Dialog
|
||||||
<DialogTitle>
|
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'}
|
{editingKreditor ? 'Kreditor bearbeiten' : 'Neuer Kreditor'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent id="kreditor-dialog-content">
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
@@ -293,9 +336,15 @@ class KreditorTable extends Component {
|
|||||||
onClose={this.handleDeleteCancel}
|
onClose={this.handleDeleteCancel}
|
||||||
maxWidth="sm"
|
maxWidth="sm"
|
||||||
fullWidth
|
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>
|
<DialogTitle id="kreditor-confirm-dialog-title">Löschen bestätigen</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent id="kreditor-confirm-dialog-content">
|
||||||
<Typography>
|
<Typography>
|
||||||
{this.state.itemToDelete &&
|
{this.state.itemToDelete &&
|
||||||
`Kreditor "${this.state.itemToDelete.name}" wirklich löschen?`
|
`Kreditor "${this.state.itemToDelete.name}" wirklich löschen?`
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Alert
|
Alert,
|
||||||
|
Chip,
|
||||||
|
Paper
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
PictureAsPdf as PdfIcon,
|
PictureAsPdf as PdfIcon,
|
||||||
@@ -35,6 +37,10 @@ const DocumentRenderer = (params) => {
|
|||||||
const [tabValue, setTabValue] = useState(0);
|
const [tabValue, setTabValue] = useState(0);
|
||||||
const [error, setError] = useState(null);
|
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
|
// Always show something clickable, even if no documents
|
||||||
const hasDocuments = pdfs.length > 0 || links.length > 0;
|
const hasDocuments = pdfs.length > 0 || links.length > 0;
|
||||||
|
|
||||||
@@ -47,6 +53,8 @@ const DocumentRenderer = (params) => {
|
|||||||
const totalCount = allDocuments.length;
|
const totalCount = allDocuments.length;
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
|
// Store reference to the trigger element for focus restoration
|
||||||
|
triggerRef.current = document.activeElement;
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,6 +62,13 @@ const DocumentRenderer = (params) => {
|
|||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setTabValue(0); // Reset to first tab when closing
|
setTabValue(0); // Reset to first tab when closing
|
||||||
setError(null); // Clear any errors 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) => {
|
const handleTabChange = (event, newValue) => {
|
||||||
@@ -321,11 +336,22 @@ const DocumentRenderer = (params) => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={handleClose} maxWidth="lg" fullWidth>
|
<Dialog
|
||||||
<DialogTitle>
|
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'}
|
{hasDocuments ? `Dokumente (${totalCount})` : 'Dokumentinformationen'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent sx={{ p: 0 }}>
|
<DialogContent sx={{ p: 0 }} id="document-dialog-content">
|
||||||
{error && (
|
{error && (
|
||||||
<Alert severity="error" sx={{ m: 2 }} onClose={() => setError(null)}>
|
<Alert severity="error" sx={{ m: 2 }} onClose={() => setError(null)}>
|
||||||
{error}
|
{error}
|
||||||
@@ -335,6 +361,23 @@ const DocumentRenderer = (params) => {
|
|||||||
<Tabs value={tabValue} onChange={handleTabChange}>
|
<Tabs value={tabValue} onChange={handleTabChange}>
|
||||||
<Tab label="Dokumente" />
|
<Tab label="Dokumente" />
|
||||||
<Tab label={`Buchungen (${lineItems.length})`} />
|
<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>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -431,6 +474,108 @@ const DocumentRenderer = (params) => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</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>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose}>Schließen</Button>
|
<Button onClick={handleClose}>Schließen</Button>
|
||||||
|
|||||||
@@ -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 (
|
return (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: '0.7rem',
|
fontSize: '0.7rem',
|
||||||
lineHeight: '1.2',
|
lineHeight: '1.2',
|
||||||
cursor: isIbanColumn && value ? 'pointer' : 'default',
|
cursor: isIbanColumn && value ? 'pointer' : 'default',
|
||||||
color: isIbanColumn && value ? '#1976d2' : 'inherit',
|
color: getIbanColor(),
|
||||||
textDecoration: isIbanColumn && value ? 'underline' : 'none'
|
textDecoration: isIbanColumn && value ? 'underline' : 'none',
|
||||||
|
fontWeight: isIbanColumn && params.data && params.data.hasKreditor ? 'bold' : 'normal'
|
||||||
}}
|
}}
|
||||||
onClick={isIbanColumn && value ? handleClick : undefined}
|
onClick={isIbanColumn && value ? handleClick : undefined}
|
||||||
title={isIbanColumn && value ? `Nach IBAN "${value}" filtern` : undefined}
|
title={getTitle()}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const getColumnDefs = () => [
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
filter: false,
|
filter: false,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
suppressMenu: true,
|
suppressHeaderMenuButton: true,
|
||||||
cellRenderer: SelectionRenderer,
|
cellRenderer: SelectionRenderer,
|
||||||
headerComponent: SelectionHeader,
|
headerComponent: SelectionHeader,
|
||||||
headerComponentParams: {
|
headerComponentParams: {
|
||||||
@@ -101,7 +101,6 @@ export const getColumnDefs = () => [
|
|||||||
width: 70,
|
width: 70,
|
||||||
cellRenderer: TypeRenderer,
|
cellRenderer: TypeRenderer,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
suppressSorting: true,
|
|
||||||
filter: CheckboxFilter,
|
filter: CheckboxFilter,
|
||||||
filterParams: {
|
filterParams: {
|
||||||
filterOptions: [
|
filterOptions: [
|
||||||
@@ -138,7 +137,6 @@ export const getColumnDefs = () => [
|
|||||||
width: 70,
|
width: 70,
|
||||||
cellRenderer: JtlRenderer,
|
cellRenderer: JtlRenderer,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
suppressSorting: true,
|
|
||||||
filter: CheckboxFilter,
|
filter: CheckboxFilter,
|
||||||
filterParams: {
|
filterParams: {
|
||||||
filterOptions: [
|
filterOptions: [
|
||||||
@@ -212,7 +210,10 @@ export const defaultColDef = {
|
|||||||
|
|
||||||
export const gridOptions = {
|
export const gridOptions = {
|
||||||
animateRows: true,
|
animateRows: true,
|
||||||
rowSelection: false,
|
rowSelection: {
|
||||||
|
mode: 'multiRow',
|
||||||
|
enableClickSelection: false
|
||||||
|
},
|
||||||
rowBuffer: 10,
|
rowBuffer: 10,
|
||||||
// Enable virtualization (default behavior)
|
// Enable virtualization (default behavior)
|
||||||
suppressRowVirtualisation: false,
|
suppressRowVirtualisation: false,
|
||||||
@@ -225,8 +226,7 @@ export const gridOptions = {
|
|||||||
// Pagination (optional - can be removed for infinite scrolling)
|
// Pagination (optional - can be removed for infinite scrolling)
|
||||||
pagination: false,
|
pagination: false,
|
||||||
paginationPageSize: 100,
|
paginationPageSize: 100,
|
||||||
// Disable cell selection
|
// Disable cell selection and focus
|
||||||
suppressCellSelection: true,
|
cellSelection: false,
|
||||||
suppressRowClickSelection: true,
|
|
||||||
suppressCellFocus: true
|
suppressCellFocus: true
|
||||||
};
|
};
|
||||||
@@ -103,9 +103,13 @@ export default class CheckboxFilter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.reactRoot) {
|
// Use setTimeout to avoid unmounting during render
|
||||||
this.reactRoot.unmount();
|
setTimeout(() => {
|
||||||
}
|
if (this.reactRoot) {
|
||||||
|
this.reactRoot.unmount();
|
||||||
|
this.reactRoot = null;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderReactComponent() {
|
renderReactComponent() {
|
||||||
@@ -172,10 +176,13 @@ export default class CheckboxFilter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Recreate React root every time to avoid state corruption
|
// Recreate React root every time to avoid state corruption
|
||||||
if (this.reactRoot) {
|
// Use setTimeout to avoid unmounting during render
|
||||||
this.reactRoot.unmount();
|
setTimeout(() => {
|
||||||
}
|
if (this.reactRoot) {
|
||||||
this.reactRoot = createRoot(this.eGui);
|
this.reactRoot.unmount();
|
||||||
this.reactRoot.render(<FilterComponent />);
|
}
|
||||||
|
this.reactRoot = createRoot(this.eGui);
|
||||||
|
this.reactRoot.render(<FilterComponent />);
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,20 +8,27 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
Box,
|
Box,
|
||||||
Chip,
|
Chip,
|
||||||
Button
|
Button,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
Divider
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
export default class IbanSelectionFilter {
|
export default class IbanSelectionFilter {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.state = {
|
this.state = {
|
||||||
selectedValues: [],
|
selectedValues: [],
|
||||||
availableValues: []
|
availableValues: [],
|
||||||
|
partialIban: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the DOM element that AG Grid expects
|
// Create the DOM element that AG Grid expects
|
||||||
this.eGui = document.createElement('div');
|
this.eGui = document.createElement('div');
|
||||||
this.eGui.style.minWidth = '250px';
|
this.eGui.style.minWidth = '250px';
|
||||||
this.eGui.style.padding = '8px';
|
this.eGui.style.padding = '8px';
|
||||||
|
|
||||||
|
// Create a ref for the text input
|
||||||
|
this.textInputRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
init(params) {
|
init(params) {
|
||||||
@@ -35,10 +42,13 @@ export default class IbanSelectionFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.reactRoot) {
|
// Use setTimeout to avoid unmounting during render
|
||||||
this.reactRoot.unmount();
|
setTimeout(() => {
|
||||||
this.reactRoot = null;
|
if (this.reactRoot) {
|
||||||
}
|
this.reactRoot.unmount();
|
||||||
|
this.reactRoot = null;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAvailableValues() {
|
updateAvailableValues() {
|
||||||
@@ -72,15 +82,27 @@ export default class IbanSelectionFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isFilterActive() {
|
isFilterActive() {
|
||||||
return this.state.selectedValues.length > 0;
|
return this.state.selectedValues.length > 0 || this.state.partialIban.trim() !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
doesFilterPass(params) {
|
doesFilterPass(params) {
|
||||||
const { selectedValues } = this.state;
|
const { selectedValues, partialIban } = this.state;
|
||||||
if (selectedValues.length === 0) return true;
|
|
||||||
|
|
||||||
const value = params.data['Kontonummer/IBAN'];
|
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() {
|
getModel() {
|
||||||
@@ -88,16 +110,28 @@ export default class IbanSelectionFilter {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
filterType: 'iban-selection',
|
filterType: 'iban-selection',
|
||||||
values: this.state.selectedValues
|
values: this.state.selectedValues,
|
||||||
|
partialIban: this.state.partialIban
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setModel(model) {
|
setModel(model) {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
this.state.selectedValues = [];
|
this.state.selectedValues = [];
|
||||||
|
this.state.partialIban = '';
|
||||||
} else {
|
} else {
|
||||||
this.state.selectedValues = model.values || [];
|
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();
|
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 = () => {
|
clearFilter = () => {
|
||||||
this.state.selectedValues = [];
|
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();
|
this.renderReactComponent();
|
||||||
|
|
||||||
if (this.params && this.params.filterChangedCallback) {
|
if (this.params && this.params.filterChangedCallback) {
|
||||||
@@ -127,6 +192,27 @@ export default class IbanSelectionFilter {
|
|||||||
|
|
||||||
const FilterComponent = () => (
|
const FilterComponent = () => (
|
||||||
<Box sx={{ minWidth: 250 }} className="ag-filter-custom">
|
<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">
|
<FormControl fullWidth size="small">
|
||||||
<Select
|
<Select
|
||||||
multiple
|
multiple
|
||||||
@@ -138,7 +224,7 @@ export default class IbanSelectionFilter {
|
|||||||
return <em>Alle IBANs</em>;
|
return <em>Alle IBANs</em>;
|
||||||
}
|
}
|
||||||
return (
|
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) => {
|
{selected.slice(0, 2).map((value) => {
|
||||||
const ibanData = this.availableValues.find(item => item.iban === value);
|
const ibanData = this.availableValues.find(item => item.iban === value);
|
||||||
return (
|
return (
|
||||||
@@ -170,7 +256,7 @@ export default class IbanSelectionFilter {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.availableValues.map((item) => (
|
{this.availableValues.map((item) => (
|
||||||
<MenuItem key={item.iban} value={item.iban}>
|
<MenuItem key={item.iban} value={item.iban}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -188,19 +274,21 @@ export default class IbanSelectionFilter {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{this.state.selectedValues.length > 0 && (
|
<Box
|
||||||
<Box sx={{ mt: 1, textAlign: 'right' }}>
|
className="clear-button-container"
|
||||||
<Button
|
sx={{ mt: 1, textAlign: 'right' }}
|
||||||
onClick={this.clearFilter}
|
style={{ display: (this.state.selectedValues.length > 0 || this.state.partialIban.trim() !== '') ? 'block' : 'none' }}
|
||||||
size="small"
|
>
|
||||||
variant="text"
|
<Button
|
||||||
color="primary"
|
onClick={this.clearFilter}
|
||||||
sx={{ fontSize: '0.75rem' }}
|
size="small"
|
||||||
>
|
variant="text"
|
||||||
Filter löschen
|
color="primary"
|
||||||
</Button>
|
sx={{ fontSize: '0.75rem' }}
|
||||||
</Box>
|
>
|
||||||
)}
|
Alle Filter löschen
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ const SortHeader = (params) => {
|
|||||||
if (params.api) {
|
if (params.api) {
|
||||||
params.api.addEventListener('sortChanged', updateSortState);
|
params.api.addEventListener('sortChanged', updateSortState);
|
||||||
return () => {
|
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]);
|
}, [params.api, params.column]);
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ class HeaderComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
// Clean up event listener
|
// Clean up event listener - check if grid API is still valid
|
||||||
if (this.props.params && this.props.params.api) {
|
if (this.props.params && this.props.params.api && !this.props.params.api.isDestroyed()) {
|
||||||
this.props.params.api.removeEventListener('filterChanged', this.onFilterChanged);
|
this.props.params.api.removeEventListener('filterChanged', this.onFilterChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ class HeaderComponent extends Component {
|
|||||||
const showTextFilter = isTextColumn;
|
const showTextFilter = isTextColumn;
|
||||||
|
|
||||||
// Check if sorting is disabled for this column
|
// 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 (
|
return (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
@@ -297,11 +297,12 @@ export default class TextHeaderWithFilter {
|
|||||||
|
|
||||||
// Listen for menu close events to keep state in sync
|
// Listen for menu close events to keep state in sync
|
||||||
if (params.api) {
|
if (params.api) {
|
||||||
params.api.addEventListener('popupMenuVisibleChanged', (event) => {
|
this.popupMenuListener = (event) => {
|
||||||
if (!event.visible && this.headerComponent) {
|
if (!event.visible && this.headerComponent) {
|
||||||
this.headerComponent.setState({ menuOpen: false });
|
this.headerComponent.setState({ menuOpen: false });
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
params.api.addEventListener('popupMenuVisibleChanged', this.popupMenuListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render React component into the DOM element
|
// Render React component into the DOM element
|
||||||
@@ -313,6 +314,11 @@ export default class TextHeaderWithFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
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
|
// Use setTimeout to avoid unmounting during render
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.reactRoot) {
|
if (this.reactRoot) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ GO
|
|||||||
|
|
||||||
|
|
||||||
-- Create Kreditor table
|
-- Create Kreditor table
|
||||||
|
-- Multiple IBANs can have the same kreditor name and kreditorId
|
||||||
CREATE TABLE fibdash.Kreditor (
|
CREATE TABLE fibdash.Kreditor (
|
||||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
iban NVARCHAR(34) NOT NULL,
|
iban NVARCHAR(34) NOT NULL,
|
||||||
@@ -15,9 +16,10 @@ CREATE TABLE fibdash.Kreditor (
|
|||||||
kreditorId NVARCHAR(50) NOT NULL
|
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
|
ALTER TABLE fibdash.Kreditor
|
||||||
ADD CONSTRAINT UQ_Kreditor_kreditorId UNIQUE (kreditorId);
|
ADD CONSTRAINT UQ_Kreditor_IBAN UNIQUE (iban);
|
||||||
|
|
||||||
-- Create AccountingItems table
|
-- Create AccountingItems table
|
||||||
-- Based on CSV structure: umsatz brutto, soll/haben kz, konto, gegenkonto, bu, buchungsdatum, rechnungsnummer, buchungstext, beleglink
|
-- Based on CSV structure: umsatz brutto, soll/haben kz, konto, gegenkonto, bu, buchungsdatum, rechnungsnummer, buchungstext, beleglink
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ router.get('/system-info', authenticateToken, (req, res) => {
|
|||||||
// Get all kreditoren
|
// Get all kreditoren
|
||||||
router.get('/kreditoren', authenticateToken, async (req, res) => {
|
router.get('/kreditoren', authenticateToken, async (req, res) => {
|
||||||
try {
|
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 });
|
res.json({ kreditoren: result.recordset });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching kreditoren:', error);
|
console.error('Error fetching kreditoren:', error);
|
||||||
|
|||||||
@@ -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 => {
|
const transactionsWithJTL = monthTransactions.map(transaction => {
|
||||||
// Try to match by amount and date (approximate matching)
|
// Try to match by amount and date (approximate matching)
|
||||||
const amount = transaction.numericAmount;
|
const amount = transaction.numericAmount;
|
||||||
@@ -255,6 +266,10 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
|||||||
return amountMatch && dateMatch;
|
return amountMatch && dateMatch;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Look up Kreditor by IBAN
|
||||||
|
const transactionIban = transaction['Kontonummer/IBAN'];
|
||||||
|
const kreditorMatch = transactionIban ? kreditorData.find(k => k.iban === transactionIban) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...transaction,
|
...transaction,
|
||||||
hasJTL: jtlDatabaseAvailable ? !!jtlMatch : undefined,
|
hasJTL: jtlDatabaseAvailable ? !!jtlMatch : undefined,
|
||||||
@@ -263,7 +278,15 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
|||||||
jtlDatabaseAvailable,
|
jtlDatabaseAvailable,
|
||||||
// Include document data from JTL match
|
// Include document data from JTL match
|
||||||
pdfs: jtlMatch ? jtlMatch.pdfs || [] : [],
|
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 || '',
|
'Verwendungszweck': jtl.cVerwendungszweck || '',
|
||||||
'Buchungstext': 'JTL Transaction',
|
'Buchungstext': 'JTL Transaction',
|
||||||
'Beguenstigter/Zahlungspflichtiger': jtl.cName || '',
|
'Beguenstigter/Zahlungspflichtiger': jtl.cName || '',
|
||||||
|
'Kontonummer/IBAN': '', // JTL transactions don't have IBAN data
|
||||||
'Betrag': jtl.fBetrag ? jtl.fBetrag.toString().replace('.', ',') : '0,00',
|
'Betrag': jtl.fBetrag ? jtl.fBetrag.toString().replace('.', ',') : '0,00',
|
||||||
numericAmount: parseFloat(jtl.fBetrag) || 0,
|
numericAmount: parseFloat(jtl.fBetrag) || 0,
|
||||||
parsedDate: new Date(jtl.dBuchungsdatum),
|
parsedDate: new Date(jtl.dBuchungsdatum),
|
||||||
@@ -309,7 +333,10 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
|||||||
isJTLOnly: true,
|
isJTLOnly: true,
|
||||||
// Include document data from JTL transaction
|
// Include document data from JTL transaction
|
||||||
pdfs: jtl.pdfs || [],
|
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
|
// Combine CSV and JTL-only transactions
|
||||||
@@ -565,10 +592,9 @@ router.get('/kreditors', authenticateToken, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { executeQuery } = require('../config/database');
|
const { executeQuery } = require('../config/database');
|
||||||
const query = `
|
const query = `
|
||||||
SELECT id, iban, name, kreditorId, created_at, updated_at
|
SELECT id, iban, name, kreditorId
|
||||||
FROM Kreditor
|
FROM fibdash.Kreditor
|
||||||
WHERE is_active = 1
|
ORDER BY name ASC, iban ASC
|
||||||
ORDER BY name ASC
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await executeQuery(query);
|
const result = await executeQuery(query);
|
||||||
@@ -586,14 +612,12 @@ router.get('/kreditors/:id', authenticateToken, async (req, res) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT id, iban, name, kreditorId, created_at, updated_at
|
SELECT id, iban, name, kreditorId
|
||||||
FROM Kreditor
|
FROM fibdash.Kreditor
|
||||||
WHERE id = @id AND is_active = 1
|
WHERE id = @id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await executeQuery(query, [
|
const result = await executeQuery(query, { id: parseInt(id) });
|
||||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (result.recordset.length === 0) {
|
if (result.recordset.length === 0) {
|
||||||
return res.status(404).json({ error: 'Kreditor not found' });
|
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' });
|
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 = `
|
const checkQuery = `
|
||||||
SELECT id FROM Kreditor
|
SELECT id FROM fibdash.Kreditor
|
||||||
WHERE (iban = @iban OR kreditorId = @kreditorId) AND is_active = 1
|
WHERE iban = @iban
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const checkResult = await executeQuery(checkQuery, [
|
const checkResult = await executeQuery(checkQuery, { iban });
|
||||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
|
||||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (checkResult.recordset.length > 0) {
|
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 = `
|
const insertQuery = `
|
||||||
INSERT INTO Kreditor (iban, name, kreditorId, created_at, updated_at)
|
INSERT INTO fibdash.Kreditor (iban, name, kreditorId)
|
||||||
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at
|
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId
|
||||||
VALUES (@iban, @name, @kreditorId, GETDATE(), GETDATE())
|
VALUES (@iban, @name, @kreditorId)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await executeQuery(insertQuery, [
|
const result = await executeQuery(insertQuery, { iban, name, kreditorId });
|
||||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
|
||||||
{ name: 'name', type: 'nvarchar', value: name },
|
|
||||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
|
|
||||||
]);
|
|
||||||
|
|
||||||
res.status(201).json(result.recordset[0]);
|
res.status(201).json(result.recordset[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -664,44 +681,33 @@ router.put('/kreditors/:id', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if kreditor exists
|
// Check if kreditor exists
|
||||||
const checkQuery = `SELECT id FROM Kreditor WHERE id = @id AND is_active = 1`;
|
const checkQuery = `SELECT id FROM fibdash.Kreditor WHERE id = @id`;
|
||||||
const checkResult = await executeQuery(checkQuery, [
|
const checkResult = await executeQuery(checkQuery, { id: parseInt(id) });
|
||||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (checkResult.recordset.length === 0) {
|
if (checkResult.recordset.length === 0) {
|
||||||
return res.status(404).json({ error: 'Kreditor not found' });
|
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 = `
|
const conflictQuery = `
|
||||||
SELECT id FROM Kreditor
|
SELECT id FROM fibdash.Kreditor
|
||||||
WHERE (iban = @iban OR kreditorId = @kreditorId) AND id != @id AND is_active = 1
|
WHERE iban = @iban AND id != @id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const conflictResult = await executeQuery(conflictQuery, [
|
const conflictResult = await executeQuery(conflictQuery, { iban, id: parseInt(id) });
|
||||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
|
||||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId },
|
|
||||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (conflictResult.recordset.length > 0) {
|
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 = `
|
const updateQuery = `
|
||||||
UPDATE Kreditor
|
UPDATE fibdash.Kreditor
|
||||||
SET iban = @iban, name = @name, kreditorId = @kreditorId, updated_at = GETDATE()
|
SET iban = @iban, name = @name, kreditorId = @kreditorId
|
||||||
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at
|
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId
|
||||||
WHERE id = @id
|
WHERE id = @id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await executeQuery(updateQuery, [
|
const result = await executeQuery(updateQuery, { iban, name, kreditorId, id: parseInt(id) });
|
||||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
|
||||||
{ name: 'name', type: 'nvarchar', value: name },
|
|
||||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId },
|
|
||||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
|
||||||
]);
|
|
||||||
|
|
||||||
res.json(result.recordset[0]);
|
res.json(result.recordset[0]);
|
||||||
} catch (error) {
|
} 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) => {
|
router.delete('/kreditors/:id', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { executeQuery } = require('../config/database');
|
const { executeQuery } = require('../config/database');
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
UPDATE Kreditor
|
DELETE FROM fibdash.Kreditor
|
||||||
SET is_active = 0, updated_at = GETDATE()
|
WHERE id = @id
|
||||||
WHERE id = @id AND is_active = 1
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await executeQuery(query, [
|
const result = await executeQuery(query, { id: parseInt(id) });
|
||||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (result.rowsAffected[0] === 0) {
|
if (result.rowsAffected[0] === 0) {
|
||||||
return res.status(404).json({ error: 'Kreditor not found' });
|
return res.status(404).json({ error: 'Kreditor not found' });
|
||||||
|
|||||||
@@ -5,12 +5,20 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
mode: process.env.NODE_ENV || 'development',
|
||||||
entry: './client/src/index.js',
|
entry: './client/src/index.js',
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
},
|
},
|
||||||
|
cache: {
|
||||||
|
type: 'filesystem',
|
||||||
|
buildDependencies: {
|
||||||
|
config: [__filename],
|
||||||
|
},
|
||||||
|
cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'),
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
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: {
|
resolve: {
|
||||||
extensions: ['.js', '.jsx'],
|
extensions: ['.js', '.jsx'],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user