Compare commits
3 Commits
5470bebfc4
...
6cde543938
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cde543938 | ||
|
|
481f4db389 | ||
|
|
976c802b11 |
@@ -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
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
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 LoginIcon from '@mui/icons-material/Login';
|
||||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
import TableChart from '@mui/icons-material/TableChart';
|
||||||
import AuthService from './services/AuthService';
|
import AuthService from './services/AuthService';
|
||||||
import DataViewer from './components/DataViewer';
|
import DataViewer from './components/DataViewer';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
@@ -29,6 +30,7 @@ class App extends Component {
|
|||||||
user: null,
|
user: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
exportData: null, // { selectedMonth, canExport, onExport }
|
exportData: null, // { selectedMonth, canExport, onExport }
|
||||||
|
currentView: 'dashboard', // 'dashboard' or 'tables'
|
||||||
};
|
};
|
||||||
this.authService = new AuthService();
|
this.authService = new AuthService();
|
||||||
}
|
}
|
||||||
@@ -77,8 +79,12 @@ class App extends Component {
|
|||||||
this.setState({ exportData });
|
this.setState({ exportData });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleViewChange = (event, newValue) => {
|
||||||
|
this.setState({ currentView: newValue });
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isAuthenticated, user, loading } = this.state;
|
const { isAuthenticated, user, loading, currentView } = this.state;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -99,7 +105,7 @@ class App extends Component {
|
|||||||
<AppBar position="static">
|
<AppBar position="static">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<DashboardIcon sx={{ mr: { xs: 1, sm: 2 } }} />
|
<DashboardIcon sx={{ mr: { xs: 1, sm: 2 } }} />
|
||||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
<Typography variant="h6" component="div" sx={{ mr: 3 }}>
|
||||||
FibDash
|
FibDash
|
||||||
</Typography>
|
</Typography>
|
||||||
{isAuthenticated && user && (
|
{isAuthenticated && user && (
|
||||||
@@ -107,12 +113,42 @@ class App extends Component {
|
|||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
mr: { xs: 1, sm: 2 },
|
mr: { xs: 2, sm: 3 },
|
||||||
display: { xs: 'none', sm: 'block' }
|
display: { xs: 'none', sm: 'block' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Willkommen, {user.name}
|
Willkommen, {user.name}
|
||||||
</Typography>
|
</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 && (
|
{this.state.exportData && (
|
||||||
<Button
|
<Button
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -153,7 +189,12 @@ class App extends Component {
|
|||||||
<Box sx={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}>
|
<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%' }}>
|
<Container maxWidth={false} sx={{ mt: 4, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<DataViewer user={user} onUpdateExportData={this.updateExportData} />
|
<DataViewer
|
||||||
|
user={user}
|
||||||
|
onUpdateExportData={this.updateExportData}
|
||||||
|
currentView={currentView}
|
||||||
|
onViewChange={this.handleViewChange}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Login onLogin={this.handleLogin} />
|
<Login onLogin={this.handleLogin} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import AuthService from '../services/AuthService';
|
import AuthService from '../services/AuthService';
|
||||||
import SummaryHeader from './SummaryHeader';
|
import SummaryHeader from './SummaryHeader';
|
||||||
import TransactionsTable from './TransactionsTable';
|
import TransactionsTable from './TransactionsTable';
|
||||||
import Navigation from './Navigation';
|
|
||||||
import Dashboard from './Dashboard';
|
import Dashboard from './Dashboard';
|
||||||
import TableManagement from './TableManagement';
|
import TableManagement from './TableManagement';
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ class DataViewer extends Component {
|
|||||||
summary: null,
|
summary: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
currentView: 'dashboard', // 'dashboard' or 'tables'
|
|
||||||
};
|
};
|
||||||
this.authService = new AuthService();
|
this.authService = new AuthService();
|
||||||
}
|
}
|
||||||
@@ -114,21 +113,17 @@ class DataViewer extends Component {
|
|||||||
if (this.props.onUpdateExportData) {
|
if (this.props.onUpdateExportData) {
|
||||||
this.props.onUpdateExportData({
|
this.props.onUpdateExportData({
|
||||||
selectedMonth,
|
selectedMonth,
|
||||||
canExport: !!selectedMonth && !this.state.loading && this.state.currentView === 'dashboard',
|
canExport: !!selectedMonth && !this.state.loading && this.props.currentView === 'dashboard',
|
||||||
onExport: this.downloadDatev
|
onExport: this.downloadDatev
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleViewChange = (event, newView) => {
|
|
||||||
this.setState({ currentView: newView });
|
|
||||||
// Update export data when view changes
|
|
||||||
this.updateExportData();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { months, selectedMonth, transactions, summary, loading, error, currentView } = this.state;
|
const { months, selectedMonth, transactions, summary, loading, error } = this.state;
|
||||||
const { user } = this.props;
|
const { user, currentView } = this.props;
|
||||||
|
|
||||||
if (loading && !transactions.length) {
|
if (loading && !transactions.length) {
|
||||||
return (
|
return (
|
||||||
@@ -148,11 +143,6 @@ class DataViewer extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Navigation
|
|
||||||
currentView={currentView}
|
|
||||||
onViewChange={this.handleViewChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{currentView === 'dashboard' ? (
|
{currentView === 'dashboard' ? (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ flexShrink: 0 }}>
|
<Box sx={{ flexShrink: 0 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
{(this.props.allowCreate !== false) && (
|
||||||
<MenuItem value="create_new" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
|
<MenuItem value="create_new" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
|
||||||
<AddIcon sx={{ mr: 1 }} />
|
<AddIcon sx={{ mr: 1 }} />
|
||||||
Neuen Kreditor erstellen
|
Neuen Kreditor erstellen
|
||||||
</MenuItem>
|
</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,13 +71,16 @@ 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,
|
||||||
formData: bu ? {
|
formData: bu ? {
|
||||||
bu: bu.bu,
|
bu: bu.bu,
|
||||||
name: bu.name,
|
name: bu.name,
|
||||||
vst: bu.vst || '',
|
vst: bu.vst !== null && bu.vst !== undefined ? bu.vst.toString() : '',
|
||||||
} : {
|
} : {
|
||||||
bu: '',
|
bu: '',
|
||||||
name: '',
|
name: '',
|
||||||
@@ -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) => {
|
||||||
@@ -102,13 +117,20 @@ class BUTable extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isFormValid = () => {
|
||||||
|
const { formData } = this.state;
|
||||||
|
return formData.bu.trim() !== '' &&
|
||||||
|
formData.name.trim() !== '' &&
|
||||||
|
formData.vst !== '';
|
||||||
|
};
|
||||||
|
|
||||||
handleSave = async () => {
|
handleSave = async () => {
|
||||||
const { editingBU, formData } = this.state;
|
const { editingBU, formData } = this.state;
|
||||||
|
|
||||||
// Convert vst to number or null
|
// Convert vst to number or null
|
||||||
const payload = {
|
const payload = {
|
||||||
...formData,
|
...formData,
|
||||||
vst: formData.vst ? parseFloat(formData.vst) : null,
|
vst: formData.vst !== '' ? parseFloat(formData.vst) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -138,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,
|
||||||
@@ -150,6 +175,13 @@ class BUTable extends Component {
|
|||||||
|
|
||||||
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}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -172,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() {
|
||||||
@@ -220,7 +259,7 @@ class BUTable extends Component {
|
|||||||
<TableCell>{bu.bu}</TableCell>
|
<TableCell>{bu.bu}</TableCell>
|
||||||
<TableCell>{bu.name}</TableCell>
|
<TableCell>{bu.name}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{bu.vst ? `${bu.vst}%` : '-'}
|
{bu.vst !== null && bu.vst !== undefined ? `${bu.vst}%` : '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -243,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"
|
||||||
@@ -287,7 +337,11 @@ class BUTable extends Component {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
|
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
|
||||||
<Button onClick={this.handleSave} variant="contained">
|
<Button
|
||||||
|
onClick={this.handleSave}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!this.isFormValid()}
|
||||||
|
>
|
||||||
Speichern
|
Speichern
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -299,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) => {
|
||||||
@@ -98,6 +113,12 @@ class KontoTable extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isFormValid = () => {
|
||||||
|
const { formData } = this.state;
|
||||||
|
return formData.konto.trim() !== '' &&
|
||||||
|
formData.name.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
handleSave = async () => {
|
handleSave = async () => {
|
||||||
const { editingKonto, formData } = this.state;
|
const { editingKonto, formData } = this.state;
|
||||||
|
|
||||||
@@ -128,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,
|
||||||
@@ -140,6 +164,13 @@ class KontoTable extends Component {
|
|||||||
|
|
||||||
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}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -162,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() {
|
||||||
@@ -229,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"
|
||||||
@@ -257,7 +306,11 @@ class KontoTable extends Component {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
|
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
|
||||||
<Button onClick={this.handleSave} variant="contained">
|
<Button
|
||||||
|
onClick={this.handleSave}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!this.isFormValid()}
|
||||||
|
>
|
||||||
Speichern
|
Speichern
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -269,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) => {
|
||||||
@@ -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 () => {
|
handleSave = async () => {
|
||||||
const { editingKreditor, formData } = this.state;
|
const { editingKreditor, formData } = this.state;
|
||||||
|
|
||||||
@@ -132,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,
|
||||||
@@ -144,6 +169,13 @@ class KreditorTable extends Component {
|
|||||||
|
|
||||||
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}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -166,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() {
|
||||||
@@ -235,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"
|
||||||
@@ -270,7 +320,11 @@ class KreditorTable extends Component {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
|
<Button onClick={this.handleCloseDialog}>Abbrechen</Button>
|
||||||
<Button onClick={this.handleSave} variant="contained">
|
<Button
|
||||||
|
onClick={this.handleSave}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!this.isFormValid()}
|
||||||
|
>
|
||||||
Speichern
|
Speichern
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -282,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() {
|
||||||
|
// Use setTimeout to avoid unmounting during render
|
||||||
|
setTimeout(() => {
|
||||||
if (this.reactRoot) {
|
if (this.reactRoot) {
|
||||||
this.reactRoot.unmount();
|
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
|
||||||
|
// Use setTimeout to avoid unmounting during render
|
||||||
|
setTimeout(() => {
|
||||||
if (this.reactRoot) {
|
if (this.reactRoot) {
|
||||||
this.reactRoot.unmount();
|
this.reactRoot.unmount();
|
||||||
}
|
}
|
||||||
this.reactRoot = createRoot(this.eGui);
|
this.reactRoot = createRoot(this.eGui);
|
||||||
this.reactRoot.render(<FilterComponent />);
|
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() {
|
||||||
|
// Use setTimeout to avoid unmounting during render
|
||||||
|
setTimeout(() => {
|
||||||
if (this.reactRoot) {
|
if (this.reactRoot) {
|
||||||
this.reactRoot.unmount();
|
this.reactRoot.unmount();
|
||||||
this.reactRoot = null;
|
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
|
||||||
@@ -188,8 +274,11 @@ export default class IbanSelectionFilter {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{this.state.selectedValues.length > 0 && (
|
<Box
|
||||||
<Box sx={{ mt: 1, textAlign: 'right' }}>
|
className="clear-button-container"
|
||||||
|
sx={{ mt: 1, textAlign: 'right' }}
|
||||||
|
style={{ display: (this.state.selectedValues.length > 0 || this.state.partialIban.trim() !== '') ? 'block' : 'none' }}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={this.clearFilter}
|
onClick={this.clearFilter}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -197,10 +286,9 @@ export default class IbanSelectionFilter {
|
|||||||
color="primary"
|
color="primary"
|
||||||
sx={{ fontSize: '0.75rem' }}
|
sx={{ fontSize: '0.75rem' }}
|
||||||
>
|
>
|
||||||
Filter löschen
|
Alle Filter löschen
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</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 () => {
|
||||||
|
// Check if grid API is still valid before removing listener
|
||||||
|
if (params.api && !params.api.isDestroyed()) {
|
||||||
params.api.removeEventListener('sortChanged', updateSortState);
|
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={{
|
||||||
@@ -290,21 +290,19 @@ export default class TextHeaderWithFilter {
|
|||||||
this.eGui.style.height = '100%';
|
this.eGui.style.height = '100%';
|
||||||
this.eGui.style.display = 'flex';
|
this.eGui.style.display = 'flex';
|
||||||
this.eGui.style.flexDirection = 'column';
|
this.eGui.style.flexDirection = 'column';
|
||||||
|
|
||||||
console.log('TextHeaderWithFilter constructor');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(params) {
|
init(params) {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
console.log('TextHeaderWithFilter init params:', params);
|
|
||||||
|
|
||||||
// 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
|
||||||
@@ -312,11 +310,15 @@ export default class TextHeaderWithFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGui() {
|
getGui() {
|
||||||
console.log('TextHeaderWithFilter getGui called');
|
|
||||||
return this.eGui;
|
return this.eGui;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export const getRowStyle = (params, selectedRows) => {
|
|||||||
const rowId = params.data?.id || params.rowIndex;
|
const rowId = params.data?.id || params.rowIndex;
|
||||||
const isSelected = selectedRows && selectedRows.has && selectedRows.has(rowId);
|
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) {
|
if (params.data.isJTLOnly) {
|
||||||
return {
|
return {
|
||||||
backgroundColor: isSelected ? '#e3f2fd' : '#ffebee',
|
backgroundColor: isSelected ? '#e3f2fd' : '#ffebee',
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -199,7 +199,7 @@ router.post('/buchungsschluessel', authenticateToken, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await executeQuery(
|
await executeQuery(
|
||||||
'INSERT INTO fibdash.BU (bu, name, vst) VALUES (@bu, @name, @vst)',
|
'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' });
|
res.json({ message: 'Buchungsschlüssel erfolgreich erstellt' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -224,7 +224,7 @@ router.put('/buchungsschluessel/:id', authenticateToken, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await executeQuery(
|
await executeQuery(
|
||||||
'UPDATE fibdash.BU SET bu = @bu, name = @name, vst = @vst WHERE id = @id',
|
'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' });
|
res.json({ message: 'Buchungsschlüssel erfolgreich aktualisiert' });
|
||||||
} catch (error) {
|
} 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 => {
|
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