From b9af7694a09a6d797b07dd78279cb8e153de311d Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Sun, 20 Jul 2025 05:06:28 +0200 Subject: [PATCH] Remove debug login page and refactor TransactionsTable to utilize new utility functions and cell renderers for improved code organization and maintainability. --- client/src/components/TransactionsTable.js | 278 +----------------- .../cellRenderers/AmountRenderer.js | 21 ++ .../cellRenderers/DescriptionRenderer.js | 11 + .../components/cellRenderers/JtlRenderer.js | 29 ++ .../cellRenderers/RecipientRenderer.js | 11 + .../components/cellRenderers/TypeRenderer.js | 14 + client/src/components/config/gridConfig.js | 165 +++++++++++ .../src/components/filters/SelectionFilter.js | 216 -------------- client/src/components/utils/dataUtils.js | 39 +++ debug-login.html | 57 ---- src/index.js | 7 +- 11 files changed, 301 insertions(+), 547 deletions(-) create mode 100644 client/src/components/cellRenderers/AmountRenderer.js create mode 100644 client/src/components/cellRenderers/DescriptionRenderer.js create mode 100644 client/src/components/cellRenderers/JtlRenderer.js create mode 100644 client/src/components/cellRenderers/RecipientRenderer.js create mode 100644 client/src/components/cellRenderers/TypeRenderer.js create mode 100644 client/src/components/config/gridConfig.js delete mode 100644 client/src/components/filters/SelectionFilter.js create mode 100644 client/src/components/utils/dataUtils.js delete mode 100644 debug-login.html 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 ( -
-
- {hasJTL && '✓'} -
-
- ); - }; - - // 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 ( +
+
+ {hasJTL && '✓'} +
+
+ ); +}; + +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') {