This commit is contained in:
sebseb7
2025-07-20 04:46:01 +02:00
parent 102a4ec9ff
commit 9a0c985bfa
13 changed files with 1473 additions and 155 deletions

View File

@@ -0,0 +1,331 @@
import React, { Component } from 'react';
import { createRoot } from 'react-dom/client';
import {
Box,
TextField,
Typography,
IconButton
} from '@mui/material';
import {
FilterList as FilterIcon,
ArrowUpward as SortUpIcon,
ArrowDownward as SortDownIcon
} from '@mui/icons-material';
class HeaderComponent extends Component {
constructor(props) {
super(props);
this.state = {
filterValue: '',
sortDirection: null,
menuOpen: false
};
}
componentDidMount() {
// Get initial sort direction
if (this.props.params && this.props.params.column) {
const sort = this.props.params.column.getSort();
this.setState({ sortDirection: sort });
}
// Listen for filter changes to update icon color
if (this.props.params && this.props.params.api) {
this.props.params.api.addEventListener('filterChanged', this.onFilterChanged);
}
}
componentWillUnmount() {
// Clean up event listener
if (this.props.params && this.props.params.api) {
this.props.params.api.removeEventListener('filterChanged', this.onFilterChanged);
}
}
onFilterChanged = () => {
// Force re-render to update filter icon color
this.forceUpdate();
};
isFilterActive = () => {
if (!this.props.params || !this.props.params.api || !this.props.params.column) {
return false;
}
const filterModel = this.props.params.api.getFilterModel();
const colId = this.props.params.column.colId;
// Check if this column has an active filter
return filterModel && filterModel[colId] &&
(filterModel[colId].filter !== '' || filterModel[colId].filterType === 'type');
};
onSortRequested = (order, event) => {
if (this.props.params && this.props.params.setSort) {
this.props.params.setSort(order, event.shiftKey);
this.setState({ sortDirection: order });
}
};
onFilterIconClick = (event) => {
const { params } = this.props;
try {
if (this.state.menuOpen) {
// Menu is open, try to close it
if (params && params.api) {
params.api.hidePopupMenu();
}
this.setState({ menuOpen: false });
} else {
// Menu is closed, open it
if (params && params.showColumnMenu) {
params.showColumnMenu(event.target);
this.setState({ menuOpen: true });
} else if (params && params.api) {
// Alternative method using grid API
params.api.showColumnMenuAfterButtonClick(params.column, event.target);
this.setState({ menuOpen: true });
} else {
console.warn('Filter menu not available - params:', params);
}
}
} catch (error) {
console.error('Error toggling filter menu:', error);
this.setState({ menuOpen: false });
}
};
handleFilterChange = (event) => {
const value = event.target.value;
this.setState({ filterValue: value });
// Apply filter to the column
if (this.props.params && this.props.params.api && this.props.params.column) {
const colId = this.props.params.column.colId;
if (value.trim() === '') {
// Clear the filter when input is empty
const currentFilterModel = this.props.params.api.getFilterModel();
delete currentFilterModel[colId];
this.props.params.api.setFilterModel(currentFilterModel);
} else {
// Apply filter when input has value
this.props.params.api.setFilterModel({
...this.props.params.api.getFilterModel(),
[colId]: {
type: 'contains',
filter: value
}
});
}
}
};
render() {
const { filterValue, sortDirection } = this.state;
const { params } = this.props;
if (!params) return null;
const { displayName, column } = params;
// Only show text input for text columns (Beschreibung, Empfänger/Zahler)
const isTextColumn = column.colDef.field === 'description' ||
column.colDef.field === 'Beguenstigter/Zahlungspflichtiger';
const showTextFilter = isTextColumn;
return (
<Box sx={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
padding: '0 8px',
backgroundColor: '#f8f9fa',
borderBottom: '1px solid #dee2e6',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
}}>
{/* Column Title with proper AG Grid styling */}
<Typography
variant="body2"
sx={{
fontWeight: 600,
fontSize: '13px',
color: '#212529',
cursor: 'pointer',
minWidth: 'fit-content',
marginRight: '6px',
userSelect: 'none',
'&:hover': {
color: '#0969da'
}
}}
onClick={(e) => this.onSortRequested(sortDirection === 'asc' ? 'desc' : 'asc', e)}
>
{displayName}
</Typography>
{/* Sort Icons - Always visible */}
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginRight: '6px',
gap: 0
}}>
<SortUpIcon sx={{
fontSize: '10px',
color: sortDirection === 'asc' ? '#0969da' : '#ccc',
opacity: sortDirection === 'asc' ? 1 : 0.5,
cursor: 'pointer'
}}
onClick={(e) => this.onSortRequested('asc', e)}
/>
<SortDownIcon sx={{
fontSize: '10px',
color: sortDirection === 'desc' ? '#0969da' : '#ccc',
opacity: sortDirection === 'desc' ? 1 : 0.5,
cursor: 'pointer'
}}
onClick={(e) => this.onSortRequested('desc', e)}
/>
</Box>
{/* Filter Input - only for text columns */}
{showTextFilter && (
<TextField
size="small"
variant="outlined"
placeholder="Filter..."
value={filterValue}
onChange={this.handleFilterChange}
sx={{
flex: 1,
marginRight: '6px',
'& .MuiOutlinedInput-root': {
height: '24px',
fontSize: '12px',
backgroundColor: '#fff',
'& fieldset': {
borderColor: '#ced4da',
borderWidth: '1px'
},
'&:hover fieldset': {
borderColor: '#adb5bd'
},
'&.Mui-focused fieldset': {
borderColor: '#0969da',
borderWidth: '2px'
}
},
'& .MuiOutlinedInput-input': {
padding: '4px 8px',
fontFamily: 'inherit'
}
}}
/>
)}
{/* Spacer for non-text columns */}
{!showTextFilter && <Box sx={{ flex: 1 }} />}
{/* Filter Menu Icon - Larger hit zone with active state */}
<div
className="ag-header-menu-button"
onClick={this.onFilterIconClick}
style={{
cursor: 'pointer',
padding: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: '24px',
minHeight: '24px',
borderRadius: '4px',
backgroundColor: this.isFilterActive() ? '#e3f2fd' : 'transparent'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = this.isFilterActive() ? '#bbdefb' : '#f0f0f0';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = this.isFilterActive() ? '#e3f2fd' : 'transparent';
}}
>
<span
className="ag-icon ag-icon-filter"
role="presentation"
unselectable="on"
style={{
color: this.isFilterActive() ? '#1976d2' : '#666'
}}
></span>
</div>
</Box>
);
}
}
export default class TextHeaderWithFilter {
constructor() {
// Create the DOM element that AG Grid expects
this.eGui = document.createElement('div');
this.eGui.style.width = '100%';
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) => {
if (!event.visible && this.headerComponent) {
this.headerComponent.setState({ menuOpen: false });
}
});
}
// Render React component into the DOM element
this.renderReactComponent();
}
getGui() {
console.log('TextHeaderWithFilter getGui called');
return this.eGui;
}
destroy() {
// Use setTimeout to avoid unmounting during render
setTimeout(() => {
if (this.reactRoot) {
this.reactRoot.unmount();
this.reactRoot = null;
}
}, 0);
}
renderReactComponent() {
// Create React root if it doesn't exist
if (!this.reactRoot) {
this.reactRoot = createRoot(this.eGui);
}
// Use setTimeout to avoid synchronous render during React render cycle
setTimeout(() => {
if (this.reactRoot) {
this.reactRoot.render(
<HeaderComponent
ref={(ref) => { this.headerComponent = ref; }}
params={this.params}
/>
);
}
}, 0);
}
}