Compare commits
2 Commits
9a0c985bfa
...
b9af7694a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9af7694a0 | ||
|
|
992adc7bcf |
@@ -10,257 +10,28 @@ import {
|
||||
Typography,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import CheckboxFilter from './filters/CheckboxFilter';
|
||||
import TextHeaderWithFilter from './headers/TextHeaderWithFilter';
|
||||
import { getColumnDefs, defaultColDef, gridOptions } from './config/gridConfig';
|
||||
import { processTransactionData, getRowStyle, getSelectedDisplayName } from './utils/dataUtils';
|
||||
|
||||
class TransactionsTable extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
columnDefs: [
|
||||
{
|
||||
headerName: 'Datum',
|
||||
field: 'Buchungstag',
|
||||
width: 100,
|
||||
valueFormatter: (params) => this.formatDate(params.value),
|
||||
pinned: 'left',
|
||||
sortable: true,
|
||||
filter: 'agDateColumnFilter',
|
||||
floatingFilter: false,
|
||||
headerComponent: TextHeaderWithFilter
|
||||
},
|
||||
{
|
||||
headerName: 'Beschreibung',
|
||||
field: 'description',
|
||||
width: 350,
|
||||
sortable: true,
|
||||
headerComponent: TextHeaderWithFilter,
|
||||
tooltipField: 'description'
|
||||
},
|
||||
{
|
||||
headerName: 'Empfänger/Zahler',
|
||||
field: 'Beguenstigter/Zahlungspflichtiger',
|
||||
width: 200,
|
||||
sortable: true,
|
||||
headerComponent: TextHeaderWithFilter,
|
||||
tooltipField: 'Beguenstigter/Zahlungspflichtiger'
|
||||
},
|
||||
{
|
||||
headerName: 'Betrag',
|
||||
field: 'numericAmount',
|
||||
width: 120,
|
||||
cellRenderer: this.AmountRenderer,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
floatingFilter: false,
|
||||
type: 'rightAligned',
|
||||
headerComponent: TextHeaderWithFilter
|
||||
},
|
||||
{
|
||||
headerName: 'Typ',
|
||||
field: 'typeText',
|
||||
width: 70,
|
||||
cellRenderer: this.TypeRenderer,
|
||||
sortable: true,
|
||||
filter: CheckboxFilter,
|
||||
filterParams: {
|
||||
filterOptions: [
|
||||
{
|
||||
value: 'income',
|
||||
label: 'Einnahme',
|
||||
color: 'success',
|
||||
dotStyle: {
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
backgroundColor: '#388e3c'
|
||||
},
|
||||
condition: (fieldValue) => fieldValue === 'Einnahme'
|
||||
},
|
||||
{
|
||||
value: 'expense',
|
||||
label: 'Ausgabe',
|
||||
color: 'error',
|
||||
dotStyle: {
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
backgroundColor: '#d32f2f'
|
||||
},
|
||||
condition: (fieldValue) => fieldValue === 'Ausgabe'
|
||||
}
|
||||
]
|
||||
},
|
||||
floatingFilter: false,
|
||||
headerComponent: TextHeaderWithFilter
|
||||
},
|
||||
{
|
||||
headerName: 'JTL',
|
||||
field: 'hasJTL',
|
||||
width: 70,
|
||||
cellRenderer: this.JtlRenderer,
|
||||
sortable: true,
|
||||
filter: CheckboxFilter,
|
||||
filterParams: {
|
||||
filterOptions: [
|
||||
{
|
||||
value: 'present',
|
||||
label: 'Vorhanden',
|
||||
color: 'success',
|
||||
dotStyle: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
backgroundColor: '#388e3c',
|
||||
border: 'none',
|
||||
fontSize: '8px',
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
content: '✓'
|
||||
},
|
||||
condition: (fieldValue) => fieldValue === true
|
||||
},
|
||||
{
|
||||
value: 'missing',
|
||||
label: 'Fehlend',
|
||||
color: 'error',
|
||||
dotStyle: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
border: '1px solid #ccc'
|
||||
},
|
||||
condition: (fieldValue) => fieldValue === false
|
||||
}
|
||||
]
|
||||
},
|
||||
floatingFilter: false,
|
||||
headerComponent: TextHeaderWithFilter
|
||||
}
|
||||
],
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
floatingFilter: false,
|
||||
suppressHeaderMenuButton: false
|
||||
},
|
||||
gridOptions: {
|
||||
animateRows: true,
|
||||
rowSelection: {
|
||||
mode: 'singleRow',
|
||||
enableClickSelection: true
|
||||
},
|
||||
rowBuffer: 10,
|
||||
// Enable virtualization (default behavior)
|
||||
suppressRowVirtualisation: false,
|
||||
suppressColumnVirtualisation: false,
|
||||
// Performance optimizations
|
||||
suppressChangeDetection: false,
|
||||
// Row height
|
||||
rowHeight: 35,
|
||||
headerHeight: 40,
|
||||
// Pagination (optional - can be removed for infinite scrolling)
|
||||
pagination: false,
|
||||
paginationPageSize: 100
|
||||
}
|
||||
columnDefs: getColumnDefs(),
|
||||
defaultColDef: defaultColDef,
|
||||
gridOptions: gridOptions
|
||||
};
|
||||
}
|
||||
|
||||
formatAmount = (amount) => {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
formatDate = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const parts = dateString.split('.');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[0]}.${parts[1]}.20${parts[2]}`;
|
||||
}
|
||||
return dateString;
|
||||
};
|
||||
|
||||
getMonthName = (monthYear) => {
|
||||
if (!monthYear) return '';
|
||||
const [year, month] = monthYear.split('-');
|
||||
const date = new Date(year, month - 1);
|
||||
return date.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
|
||||
};
|
||||
|
||||
// Custom cell renderers as React components
|
||||
AmountRenderer = (params) => {
|
||||
const amount = params.value;
|
||||
const color = amount >= 0 ? '#388e3c' : '#d32f2f';
|
||||
return (
|
||||
<span style={{ color: color, fontWeight: '600' }}>
|
||||
{this.formatAmount(amount)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
TypeRenderer = (params) => {
|
||||
const amount = params.data.numericAmount;
|
||||
const color = amount >= 0 ? '#388e3c' : '#d32f2f';
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: color }}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
JtlRenderer = (params) => {
|
||||
const hasJTL = params.value;
|
||||
const backgroundColor = hasJTL ? '#388e3c' : '#f5f5f5';
|
||||
const border = hasJTL ? 'none' : '1px solid #ccc';
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: backgroundColor,
|
||||
border: border,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '8px',
|
||||
color: 'white',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{hasJTL && '✓'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Row styling based on JTL status
|
||||
getRowStyle = (params) => {
|
||||
if (params.data.isJTLOnly) {
|
||||
return {
|
||||
backgroundColor: '#ffebee',
|
||||
borderLeft: '4px solid #f44336'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Process data for AG Grid
|
||||
processTransactionData = (transactions) => {
|
||||
return transactions.map(transaction => ({
|
||||
...transaction,
|
||||
description: transaction['Verwendungszweck'] || transaction['Buchungstext'],
|
||||
type: transaction.numericAmount >= 0 ? 'Income' : 'Expense',
|
||||
isIncome: transaction.numericAmount >= 0,
|
||||
typeText: transaction.numericAmount >= 0 ? 'Einnahme' : 'Ausgabe'
|
||||
}));
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Update data without recreating grid when transactions change
|
||||
if (prevProps.transactions !== this.props.transactions && this.gridApi) {
|
||||
const processedTransactions = this.props.transactions ? this.processTransactionData(this.props.transactions) : [];
|
||||
const processedTransactions = this.props.transactions ? processTransactionData(this.props.transactions) : [];
|
||||
// Use setGridOption to update data while preserving grid state
|
||||
this.gridApi.setGridOption('rowData', processedTransactions);
|
||||
}
|
||||
@@ -282,7 +53,7 @@ class TransactionsTable extends Component {
|
||||
|
||||
// Set initial data if available
|
||||
if (this.props.transactions) {
|
||||
const processedTransactions = this.processTransactionData(this.props.transactions);
|
||||
const processedTransactions = processTransactionData(this.props.transactions);
|
||||
this.gridApi.setGridOption('rowData', processedTransactions);
|
||||
}
|
||||
|
||||
@@ -293,18 +64,7 @@ class TransactionsTable extends Component {
|
||||
this.gridInitialized = true;
|
||||
};
|
||||
|
||||
getSelectedDisplayName = (selectedValue) => {
|
||||
if (!selectedValue) return '';
|
||||
|
||||
if (selectedValue.includes('-Q')) {
|
||||
const [year, quarterPart] = selectedValue.split('-Q');
|
||||
return `Q${quarterPart} ${year}`;
|
||||
} else if (selectedValue.length === 4) {
|
||||
return `Jahr ${selectedValue}`;
|
||||
} else {
|
||||
return this.getMonthName(selectedValue);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { selectedMonth, loading } = this.props;
|
||||
@@ -318,7 +78,7 @@ class TransactionsTable extends Component {
|
||||
}}>
|
||||
<Box sx={{ p: 1, flexShrink: 0 }}>
|
||||
<Typography variant="h6" component="h2" gutterBottom sx={{ m: 0 }}>
|
||||
Transaktionen für {this.getSelectedDisplayName(selectedMonth)}
|
||||
Transaktionen für {getSelectedDisplayName(selectedMonth)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -349,7 +109,7 @@ class TransactionsTable extends Component {
|
||||
defaultColDef={this.state.defaultColDef}
|
||||
gridOptions={this.state.gridOptions}
|
||||
onGridReady={this.onGridReady}
|
||||
getRowStyle={this.getRowStyle}
|
||||
getRowStyle={getRowStyle}
|
||||
suppressRowTransform={true}
|
||||
// Use new theming system
|
||||
theme={themeQuartz}
|
||||
|
||||
21
client/src/components/cellRenderers/AmountRenderer.js
Normal file
21
client/src/components/cellRenderers/AmountRenderer.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
const AmountRenderer = (params) => {
|
||||
const formatAmount = (amount) => {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const amount = params.value;
|
||||
const color = amount >= 0 ? '#388e3c' : '#d32f2f';
|
||||
|
||||
return (
|
||||
<span style={{ color: color, fontWeight: '600' }}>
|
||||
{formatAmount(amount)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default AmountRenderer;
|
||||
11
client/src/components/cellRenderers/DescriptionRenderer.js
Normal file
11
client/src/components/cellRenderers/DescriptionRenderer.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const DescriptionRenderer = (params) => {
|
||||
return (
|
||||
<span style={{ fontSize: '0.7rem', lineHeight: '1.2' }}>
|
||||
{params.value}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default DescriptionRenderer;
|
||||
29
client/src/components/cellRenderers/JtlRenderer.js
Normal file
29
client/src/components/cellRenderers/JtlRenderer.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
const JtlRenderer = (params) => {
|
||||
const hasJTL = params.value;
|
||||
const backgroundColor = hasJTL ? '#388e3c' : '#f5f5f5';
|
||||
const border = hasJTL ? 'none' : '1px solid #ccc';
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: backgroundColor,
|
||||
border: border,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '8px',
|
||||
color: 'white',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{hasJTL && '✓'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JtlRenderer;
|
||||
11
client/src/components/cellRenderers/RecipientRenderer.js
Normal file
11
client/src/components/cellRenderers/RecipientRenderer.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const RecipientRenderer = (params) => {
|
||||
return (
|
||||
<span style={{ fontSize: '0.7rem', lineHeight: '1.2' }}>
|
||||
{params.value}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecipientRenderer;
|
||||
14
client/src/components/cellRenderers/TypeRenderer.js
Normal file
14
client/src/components/cellRenderers/TypeRenderer.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
const TypeRenderer = (params) => {
|
||||
const amount = params.data.numericAmount;
|
||||
const color = amount >= 0 ? '#388e3c' : '#d32f2f';
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: color }}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TypeRenderer;
|
||||
165
client/src/components/config/gridConfig.js
Normal file
165
client/src/components/config/gridConfig.js
Normal file
@@ -0,0 +1,165 @@
|
||||
import CheckboxFilter from '../filters/CheckboxFilter';
|
||||
import TextHeaderWithFilter from '../headers/TextHeaderWithFilter';
|
||||
import AmountRenderer from '../cellRenderers/AmountRenderer';
|
||||
import TypeRenderer from '../cellRenderers/TypeRenderer';
|
||||
import JtlRenderer from '../cellRenderers/JtlRenderer';
|
||||
import DescriptionRenderer from '../cellRenderers/DescriptionRenderer';
|
||||
import RecipientRenderer from '../cellRenderers/RecipientRenderer';
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const parts = dateString.split('.');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[0]}.${parts[1]}.20${parts[2]}`;
|
||||
}
|
||||
return dateString;
|
||||
};
|
||||
|
||||
export const getColumnDefs = () => [
|
||||
{
|
||||
headerName: 'Datum',
|
||||
field: 'Buchungstag',
|
||||
width: 100,
|
||||
valueFormatter: (params) => formatDate(params.value),
|
||||
pinned: 'left',
|
||||
sortable: true,
|
||||
filter: 'agDateColumnFilter',
|
||||
floatingFilter: false,
|
||||
headerComponent: TextHeaderWithFilter
|
||||
},
|
||||
{
|
||||
headerName: 'Beschreibung',
|
||||
field: 'description',
|
||||
width: 350,
|
||||
sortable: true,
|
||||
headerComponent: TextHeaderWithFilter,
|
||||
tooltipField: 'description',
|
||||
cellRenderer: DescriptionRenderer
|
||||
},
|
||||
{
|
||||
headerName: 'Empfänger/Zahler',
|
||||
field: 'Beguenstigter/Zahlungspflichtiger',
|
||||
width: 200,
|
||||
sortable: true,
|
||||
headerComponent: TextHeaderWithFilter,
|
||||
tooltipField: 'Beguenstigter/Zahlungspflichtiger',
|
||||
cellRenderer: RecipientRenderer
|
||||
},
|
||||
{
|
||||
headerName: 'Betrag',
|
||||
field: 'numericAmount',
|
||||
width: 120,
|
||||
cellRenderer: AmountRenderer,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
floatingFilter: false,
|
||||
type: 'rightAligned',
|
||||
headerComponent: TextHeaderWithFilter
|
||||
},
|
||||
{
|
||||
headerName: 'Typ',
|
||||
field: 'typeText',
|
||||
width: 70,
|
||||
cellRenderer: TypeRenderer,
|
||||
sortable: true,
|
||||
filter: CheckboxFilter,
|
||||
filterParams: {
|
||||
filterOptions: [
|
||||
{
|
||||
value: 'income',
|
||||
label: 'Einnahme',
|
||||
color: 'success',
|
||||
dotStyle: {
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
backgroundColor: '#388e3c'
|
||||
},
|
||||
condition: (fieldValue) => fieldValue === 'Einnahme'
|
||||
},
|
||||
{
|
||||
value: 'expense',
|
||||
label: 'Ausgabe',
|
||||
color: 'error',
|
||||
dotStyle: {
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
backgroundColor: '#d32f2f'
|
||||
},
|
||||
condition: (fieldValue) => fieldValue === 'Ausgabe'
|
||||
}
|
||||
]
|
||||
},
|
||||
floatingFilter: false,
|
||||
headerComponent: TextHeaderWithFilter
|
||||
},
|
||||
{
|
||||
headerName: 'JTL',
|
||||
field: 'hasJTL',
|
||||
width: 70,
|
||||
cellRenderer: JtlRenderer,
|
||||
sortable: true,
|
||||
filter: CheckboxFilter,
|
||||
filterParams: {
|
||||
filterOptions: [
|
||||
{
|
||||
value: 'present',
|
||||
label: 'Vorhanden',
|
||||
color: 'success',
|
||||
dotStyle: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
backgroundColor: '#388e3c',
|
||||
border: 'none',
|
||||
fontSize: '8px',
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
content: '✓'
|
||||
},
|
||||
condition: (fieldValue) => fieldValue === true
|
||||
},
|
||||
{
|
||||
value: 'missing',
|
||||
label: 'Fehlend',
|
||||
color: 'error',
|
||||
dotStyle: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
border: '1px solid #ccc'
|
||||
},
|
||||
condition: (fieldValue) => fieldValue === false
|
||||
}
|
||||
]
|
||||
},
|
||||
floatingFilter: false,
|
||||
headerComponent: TextHeaderWithFilter
|
||||
}
|
||||
];
|
||||
|
||||
export const defaultColDef = {
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
floatingFilter: false,
|
||||
suppressHeaderMenuButton: false
|
||||
};
|
||||
|
||||
export const gridOptions = {
|
||||
animateRows: true,
|
||||
rowSelection: {
|
||||
mode: 'singleRow',
|
||||
enableClickSelection: true
|
||||
},
|
||||
rowBuffer: 10,
|
||||
// Enable virtualization (default behavior)
|
||||
suppressRowVirtualisation: false,
|
||||
suppressColumnVirtualisation: false,
|
||||
// Performance optimizations
|
||||
suppressChangeDetection: false,
|
||||
// Row height
|
||||
rowHeight: 26,
|
||||
headerHeight: 40,
|
||||
// Pagination (optional - can be removed for infinite scrolling)
|
||||
pagination: false,
|
||||
paginationPageSize: 100
|
||||
};
|
||||
@@ -1,216 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Checkbox,
|
||||
ListItemText,
|
||||
Box,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
|
||||
class SelectionFilter extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedValues: [],
|
||||
isActive: false
|
||||
};
|
||||
}
|
||||
|
||||
// AG Grid filter interface methods
|
||||
init = (params) => {
|
||||
this.params = params;
|
||||
this.updateAvailableValues();
|
||||
};
|
||||
|
||||
getGui = () => {
|
||||
return this.eGui;
|
||||
};
|
||||
|
||||
destroy = () => {
|
||||
// Cleanup if needed
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// Get unique values from the column data
|
||||
this.updateAvailableValues();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.api !== this.props.api) {
|
||||
this.updateAvailableValues();
|
||||
}
|
||||
}
|
||||
|
||||
updateAvailableValues = () => {
|
||||
const api = (this.params && this.params.api) || this.props.api;
|
||||
const colDef = (this.params && this.params.colDef) || this.props.colDef;
|
||||
|
||||
if (!api || !colDef) return;
|
||||
|
||||
// Get all row data
|
||||
const allRowData = [];
|
||||
api.forEachNode(node => {
|
||||
if (node.data) {
|
||||
allRowData.push(node.data);
|
||||
}
|
||||
});
|
||||
|
||||
// Extract unique values from the column
|
||||
const values = [...new Set(allRowData.map(row => row[colDef.field]))];
|
||||
this.availableValues = values.filter(val => val != null);
|
||||
};
|
||||
|
||||
isFilterActive = () => {
|
||||
return this.state.selectedValues.length > 0;
|
||||
};
|
||||
|
||||
doesFilterPass = (params) => {
|
||||
const { selectedValues } = this.state;
|
||||
if (selectedValues.length === 0) return true;
|
||||
|
||||
const value = params.data[this.props.colDef.field];
|
||||
return selectedValues.includes(value);
|
||||
};
|
||||
|
||||
getModel = () => {
|
||||
if (!this.isFilterActive()) return null;
|
||||
|
||||
return {
|
||||
filterType: 'selection',
|
||||
values: this.state.selectedValues
|
||||
};
|
||||
};
|
||||
|
||||
setModel = (model) => {
|
||||
if (!model) {
|
||||
this.setState({ selectedValues: [], isActive: false });
|
||||
} else {
|
||||
this.setState({
|
||||
selectedValues: model.values || [],
|
||||
isActive: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
notifyFilterChanged = () => {
|
||||
// Use the params callback if available, otherwise try props
|
||||
const callback = (this.params && this.params.filterChangedCallback) ||
|
||||
this.props.filterChangedCallback ||
|
||||
this.props.onFilterChanged;
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
handleSelectionChange = (event) => {
|
||||
const values = event.target.value;
|
||||
this.setState({
|
||||
selectedValues: values,
|
||||
isActive: values.length > 0
|
||||
}, () => {
|
||||
this.notifyFilterChanged();
|
||||
});
|
||||
};
|
||||
|
||||
clearFilter = () => {
|
||||
this.setState({ selectedValues: [], isActive: false }, () => {
|
||||
this.notifyFilterChanged();
|
||||
});
|
||||
};
|
||||
|
||||
getDisplayValue = (value) => {
|
||||
const colDef = (this.params && this.params.colDef) || this.props.colDef;
|
||||
|
||||
// Use custom display formatter if provided
|
||||
if (colDef && colDef.filterParams && colDef.filterParams.valueFormatter) {
|
||||
return colDef.filterParams.valueFormatter(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { selectedValues } = this.state;
|
||||
const colDef = (this.params && this.params.colDef) || this.props.colDef;
|
||||
|
||||
if (!this.availableValues || !colDef) return null;
|
||||
|
||||
const displayValues = this.availableValues.map(val => ({
|
||||
value: val,
|
||||
display: this.getDisplayValue(val)
|
||||
}));
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={(el) => this.eGui = el}
|
||||
sx={{ minWidth: 200 }}
|
||||
className="ag-filter-custom"
|
||||
>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Filter {colDef.headerName}</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={selectedValues}
|
||||
onChange={this.handleSelectionChange}
|
||||
label={`Filter ${colDef.headerName}`}
|
||||
renderValue={(selected) => (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{selected.map((value) => (
|
||||
<Chip
|
||||
key={value}
|
||||
label={this.getDisplayValue(value)}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 300,
|
||||
width: 250,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{displayValues.map((item) => (
|
||||
<MenuItem key={item.value} value={item.value}>
|
||||
<Checkbox
|
||||
checked={selectedValues.indexOf(item.value) > -1}
|
||||
size="small"
|
||||
/>
|
||||
<ListItemText primary={item.display} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{selectedValues.length > 0 && (
|
||||
<Box sx={{ mt: 1, textAlign: 'right' }}>
|
||||
<button
|
||||
onClick={this.clearFilter}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#1976d2',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
>
|
||||
Clear Filter
|
||||
</button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectionFilter;
|
||||
39
client/src/components/utils/dataUtils.js
Normal file
39
client/src/components/utils/dataUtils.js
Normal file
@@ -0,0 +1,39 @@
|
||||
export const processTransactionData = (transactions) => {
|
||||
return transactions.map(transaction => ({
|
||||
...transaction,
|
||||
description: transaction['Verwendungszweck'] || transaction['Buchungstext'],
|
||||
type: transaction.numericAmount >= 0 ? 'Income' : 'Expense',
|
||||
isIncome: transaction.numericAmount >= 0,
|
||||
typeText: transaction.numericAmount >= 0 ? 'Einnahme' : 'Ausgabe'
|
||||
}));
|
||||
};
|
||||
|
||||
export const getRowStyle = (params) => {
|
||||
if (params.data.isJTLOnly) {
|
||||
return {
|
||||
backgroundColor: '#ffebee',
|
||||
borderLeft: '4px solid #f44336'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getMonthName = (monthYear) => {
|
||||
if (!monthYear) return '';
|
||||
const [year, month] = monthYear.split('-');
|
||||
const date = new Date(year, month - 1);
|
||||
return date.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
|
||||
};
|
||||
|
||||
export const getSelectedDisplayName = (selectedValue) => {
|
||||
if (!selectedValue) return '';
|
||||
|
||||
if (selectedValue.includes('-Q')) {
|
||||
const [year, quarterPart] = selectedValue.split('-Q');
|
||||
return `Q${quarterPart} ${year}`;
|
||||
} else if (selectedValue.length === 4) {
|
||||
return `Jahr ${selectedValue}`;
|
||||
} else {
|
||||
return getMonthName(selectedValue);
|
||||
}
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>FibDash Login Debug</title>
|
||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>FibDash Login Debug</h1>
|
||||
|
||||
<div id="status">Initializing...</div>
|
||||
|
||||
<div id="g_id_onload"
|
||||
data-client_id="928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com"
|
||||
data-callback="handleCredentialResponse">
|
||||
</div>
|
||||
|
||||
<div class="g_id_signin" data-type="standard"></div>
|
||||
|
||||
<script>
|
||||
function handleCredentialResponse(response) {
|
||||
console.log('Google credential response:', response);
|
||||
document.getElementById('status').innerHTML = 'Got Google token, sending to server...';
|
||||
|
||||
// Send to our backend
|
||||
fetch('/api/auth/google', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: response.credential
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Server response status:', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Server response data:', data);
|
||||
if (data.success) {
|
||||
document.getElementById('status').innerHTML = '✅ Login successful! User: ' + data.user.email;
|
||||
} else {
|
||||
document.getElementById('status').innerHTML = '❌ Login failed: ' + (data.message || data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Login error:', error);
|
||||
document.getElementById('status').innerHTML = '❌ Network error: ' + error.message;
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
document.getElementById('status').innerHTML = 'Ready - Click "Sign in with Google"';
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -27,12 +27,7 @@ app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Debug login page (development only)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
app.get('/debug-login', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../debug-login.html'));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Serve static files in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
Reference in New Issue
Block a user