diff --git a/client/src/components/TransactionsTable.js b/client/src/components/TransactionsTable.js
index 442a2bb..7b04cac 100644
--- a/client/src/components/TransactionsTable.js
+++ b/client/src/components/TransactionsTable.js
@@ -10,275 +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',
- cellRenderer: this.DescriptionRenderer
- },
- {
- headerName: 'Empfänger/Zahler',
- field: 'Beguenstigter/Zahlungspflichtiger',
- width: 200,
- sortable: true,
- headerComponent: TextHeaderWithFilter,
- tooltipField: 'Beguenstigter/Zahlungspflichtiger',
- cellRenderer: this.RecipientRenderer
- },
- {
- 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: 26,
- 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
- DescriptionRenderer = (params) => {
- return (
-
- {params.value}
-
- );
- };
-
- RecipientRenderer = (params) => {
- return (
-
- {params.value}
-
- );
- };
-
- AmountRenderer = (params) => {
- const amount = params.value;
- const color = amount >= 0 ? '#388e3c' : '#d32f2f';
- return (
-
- {this.formatAmount(amount)}
-
- );
- };
-
- TypeRenderer = (params) => {
- const amount = params.data.numericAmount;
- const color = amount >= 0 ? '#388e3c' : '#d32f2f';
- return (
-
- );
- };
-
- JtlRenderer = (params) => {
- const hasJTL = params.value;
- const backgroundColor = hasJTL ? '#388e3c' : '#f5f5f5';
- const border = hasJTL ? 'none' : '1px solid #ccc';
-
- return (
-
- );
- };
-
- // 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);
}
@@ -300,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);
}
@@ -311,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;
@@ -336,7 +78,7 @@ class TransactionsTable extends Component {
}}>
- Transaktionen für {this.getSelectedDisplayName(selectedMonth)}
+ Transaktionen für {getSelectedDisplayName(selectedMonth)}
@@ -367,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}
diff --git a/client/src/components/cellRenderers/AmountRenderer.js b/client/src/components/cellRenderers/AmountRenderer.js
new file mode 100644
index 0000000..95a0800
--- /dev/null
+++ b/client/src/components/cellRenderers/AmountRenderer.js
@@ -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 (
+
+ {formatAmount(amount)}
+
+ );
+};
+
+export default AmountRenderer;
\ No newline at end of file
diff --git a/client/src/components/cellRenderers/DescriptionRenderer.js b/client/src/components/cellRenderers/DescriptionRenderer.js
new file mode 100644
index 0000000..2bd86b8
--- /dev/null
+++ b/client/src/components/cellRenderers/DescriptionRenderer.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+const DescriptionRenderer = (params) => {
+ return (
+
+ {params.value}
+
+ );
+};
+
+export default DescriptionRenderer;
\ No newline at end of file
diff --git a/client/src/components/cellRenderers/JtlRenderer.js b/client/src/components/cellRenderers/JtlRenderer.js
new file mode 100644
index 0000000..5d12bf7
--- /dev/null
+++ b/client/src/components/cellRenderers/JtlRenderer.js
@@ -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 (
+
+ );
+};
+
+export default JtlRenderer;
\ No newline at end of file
diff --git a/client/src/components/cellRenderers/RecipientRenderer.js b/client/src/components/cellRenderers/RecipientRenderer.js
new file mode 100644
index 0000000..f7e319c
--- /dev/null
+++ b/client/src/components/cellRenderers/RecipientRenderer.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+const RecipientRenderer = (params) => {
+ return (
+
+ {params.value}
+
+ );
+};
+
+export default RecipientRenderer;
\ No newline at end of file
diff --git a/client/src/components/cellRenderers/TypeRenderer.js b/client/src/components/cellRenderers/TypeRenderer.js
new file mode 100644
index 0000000..18c2591
--- /dev/null
+++ b/client/src/components/cellRenderers/TypeRenderer.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const TypeRenderer = (params) => {
+ const amount = params.data.numericAmount;
+ const color = amount >= 0 ? '#388e3c' : '#d32f2f';
+
+ return (
+
+ );
+};
+
+export default TypeRenderer;
\ No newline at end of file
diff --git a/client/src/components/config/gridConfig.js b/client/src/components/config/gridConfig.js
new file mode 100644
index 0000000..aa1f46a
--- /dev/null
+++ b/client/src/components/config/gridConfig.js
@@ -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
+};
\ No newline at end of file
diff --git a/client/src/components/filters/SelectionFilter.js b/client/src/components/filters/SelectionFilter.js
deleted file mode 100644
index 5458b23..0000000
--- a/client/src/components/filters/SelectionFilter.js
+++ /dev/null
@@ -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 (
- this.eGui = el}
- sx={{ minWidth: 200 }}
- className="ag-filter-custom"
- >
-
- Filter {colDef.headerName}
-
-
-
- {selectedValues.length > 0 && (
-
-
-
- )}
-
- );
- }
-}
-
-export default SelectionFilter;
\ No newline at end of file
diff --git a/client/src/components/utils/dataUtils.js b/client/src/components/utils/dataUtils.js
new file mode 100644
index 0000000..129d5a8
--- /dev/null
+++ b/client/src/components/utils/dataUtils.js
@@ -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);
+ }
+};
\ No newline at end of file
diff --git a/debug-login.html b/debug-login.html
deleted file mode 100644
index 1fa0565..0000000
--- a/debug-login.html
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
- FibDash Login Debug
-
-
-
- FibDash Login Debug
-
- Initializing...
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index e4af9af..6a93b08 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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') {