Compare commits
3 Commits
5470bebfc4
...
6cde543938
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cde543938 | ||
|
|
481f4db389 | ||
|
|
976c802b11 |
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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?`
|
||||
|
||||
@@ -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?`
|
||||
|
||||
@@ -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?`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user