Compare commits
2 Commits
9a0c985bfa
...
b9af7694a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9af7694a0 | ||
|
|
992adc7bcf |
@@ -10,257 +10,28 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import CheckboxFilter from './filters/CheckboxFilter';
|
import { getColumnDefs, defaultColDef, gridOptions } from './config/gridConfig';
|
||||||
import TextHeaderWithFilter from './headers/TextHeaderWithFilter';
|
import { processTransactionData, getRowStyle, getSelectedDisplayName } from './utils/dataUtils';
|
||||||
|
|
||||||
class TransactionsTable extends Component {
|
class TransactionsTable extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
columnDefs: [
|
columnDefs: getColumnDefs(),
|
||||||
{
|
defaultColDef: defaultColDef,
|
||||||
headerName: 'Datum',
|
gridOptions: gridOptions
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
componentDidUpdate(prevProps) {
|
||||||
// Update data without recreating grid when transactions change
|
// Update data without recreating grid when transactions change
|
||||||
if (prevProps.transactions !== this.props.transactions && this.gridApi) {
|
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
|
// Use setGridOption to update data while preserving grid state
|
||||||
this.gridApi.setGridOption('rowData', processedTransactions);
|
this.gridApi.setGridOption('rowData', processedTransactions);
|
||||||
}
|
}
|
||||||
@@ -282,7 +53,7 @@ class TransactionsTable extends Component {
|
|||||||
|
|
||||||
// Set initial data if available
|
// Set initial data if available
|
||||||
if (this.props.transactions) {
|
if (this.props.transactions) {
|
||||||
const processedTransactions = this.processTransactionData(this.props.transactions);
|
const processedTransactions = processTransactionData(this.props.transactions);
|
||||||
this.gridApi.setGridOption('rowData', processedTransactions);
|
this.gridApi.setGridOption('rowData', processedTransactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,18 +64,7 @@ class TransactionsTable extends Component {
|
|||||||
this.gridInitialized = true;
|
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() {
|
render() {
|
||||||
const { selectedMonth, loading } = this.props;
|
const { selectedMonth, loading } = this.props;
|
||||||
@@ -318,7 +78,7 @@ class TransactionsTable extends Component {
|
|||||||
}}>
|
}}>
|
||||||
<Box sx={{ p: 1, flexShrink: 0 }}>
|
<Box sx={{ p: 1, flexShrink: 0 }}>
|
||||||
<Typography variant="h6" component="h2" gutterBottom sx={{ m: 0 }}>
|
<Typography variant="h6" component="h2" gutterBottom sx={{ m: 0 }}>
|
||||||
Transaktionen für {this.getSelectedDisplayName(selectedMonth)}
|
Transaktionen für {getSelectedDisplayName(selectedMonth)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -349,7 +109,7 @@ class TransactionsTable extends Component {
|
|||||||
defaultColDef={this.state.defaultColDef}
|
defaultColDef={this.state.defaultColDef}
|
||||||
gridOptions={this.state.gridOptions}
|
gridOptions={this.state.gridOptions}
|
||||||
onGridReady={this.onGridReady}
|
onGridReady={this.onGridReady}
|
||||||
getRowStyle={this.getRowStyle}
|
getRowStyle={getRowStyle}
|
||||||
suppressRowTransform={true}
|
suppressRowTransform={true}
|
||||||
// Use new theming system
|
// Use new theming system
|
||||||
theme={themeQuartz}
|
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() });
|
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
|
// Serve static files in production
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
|||||||
Reference in New Issue
Block a user