diff options
Diffstat (limited to 'cashflow.cpp')
| -rw-r--r-- | cashflow.cpp | 369 |
1 files changed, 344 insertions, 25 deletions
diff --git a/cashflow.cpp b/cashflow.cpp index 6c5ea7d..a8b6a5a 100644 --- a/cashflow.cpp +++ b/cashflow.cpp @@ -1,6 +1,7 @@ #include "cashflow.h" #include "ui_cashflow.h" #include "settingsdialog.h" +#include "multiselectcombobox.h" #include <QMessageBox> #include <QDebug> #include <QDir> @@ -23,6 +24,8 @@ CashFlow::CashFlow(QWidget *parent) , startingBalance(0.0) , currentAmountFont("Courier New", 10) , weekStartDay(1) // Default to Monday + , chartView(nullptr) + , chart(nullptr) { ui->setupUi(this); @@ -36,6 +39,26 @@ CashFlow::CashFlow(QWidget *parent) setupConnections(); + // Initialize chart view + chart = new QChart(); + chart->setTitle("Balance Over Time"); + chart->setAnimationOptions(QChart::SeriesAnimations); + + chartView = new QChartView(chart); + chartView->setRenderHint(QPainter::Antialiasing); + chartView->setRubberBand(QChartView::RectangleRubberBand); // Enable zoom with mouse drag + chartView->installEventFilter(this); // Install event filter for mouse wheel zoom + + // Add chart view to the chart tab layout + QVBoxLayout *chartLayout = ui->chartsTab->findChild<QVBoxLayout*>("chartViewLayout"); + if (chartLayout) { + chartLayout->addWidget(chartView); + } + + // Initialize chart date range to match main date range + ui->chartDateFromEdit->setDate(QDate::currentDate().addMonths(-1)); + ui->chartDateToEdit->setDate(QDate::currentDate().addMonths(3)); + if (!openDatabase(defaultPath)) { QMessageBox::critical(this, "Database Error", "Failed to open default database: " + database->lastError()); return; @@ -132,6 +155,27 @@ void CashFlow::keyPressEvent(QKeyEvent *event) QMainWindow::keyPressEvent(event); } +bool CashFlow::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == chartView && event->type() == QEvent::Wheel) { + QWheelEvent *wheelEvent = static_cast<QWheelEvent*>(event); + + if (chart->axes().isEmpty()) { + return QMainWindow::eventFilter(watched, event); + } + + // Zoom factor based on wheel delta + double factor = wheelEvent->angleDelta().y() > 0 ? 0.9 : 1.1; + + // Zoom on both axes + chart->zoom(factor); + + return true; + } + + return QMainWindow::eventFilter(watched, event); +} + void CashFlow::setupConnections() { // File menu connect(ui->actionNew, &QAction::triggered, this, &CashFlow::onNewFile); @@ -147,7 +191,7 @@ void CashFlow::setupConnections() { connect(ui->dateFromEdit, &QDateEdit::dateChanged, this, &CashFlow::onDateRangeChanged); connect(ui->dateToEdit, &QDateEdit::dateChanged, this, &CashFlow::onDateRangeChanged); connect(ui->periodCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CashFlow::onPeriodChanged); - connect(ui->accountFilterCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CashFlow::onDateRangeChanged); + connect(ui->accountFilterCombo, &MultiSelectComboBox::selectionChanged, this, &CashFlow::onDateRangeChanged); connect(ui->recurringFilterCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CashFlow::onDateRangeChanged); connect(ui->showAccountBalancesCheck, &QCheckBox::stateChanged, this, &CashFlow::onDateRangeChanged); connect(ui->searchEdit, &QLineEdit::textChanged, this, &CashFlow::onSearchTextChanged); @@ -197,6 +241,10 @@ void CashFlow::setupConnections() { // Set up Delete key shortcut for recurring table ui->deleteRecurringBtn->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Delete)); + + // Charts tab + connect(ui->chartDateFromEdit, &QDateEdit::dateChanged, this, &CashFlow::refreshCharts); + connect(ui->chartDateToEdit, &QDateEdit::dateChanged, this, &CashFlow::refreshCharts); } void CashFlow::refreshView() { @@ -204,9 +252,8 @@ void CashFlow::refreshView() { // Block signals to prevent recursive refresh ui->accountFilterCombo->blockSignals(true); - QString currentFilter = ui->accountFilterCombo->currentText(); + QStringList previouslySelected = ui->accountFilterCombo->getSelectedItems(); ui->accountFilterCombo->clear(); - ui->accountFilterCombo->addItem("All Accounts"); // Get unique account names and categories QSet<QString> accounts; @@ -224,7 +271,20 @@ void CashFlow::refreshView() { sortedAccounts.sort(); ui->accountFilterCombo->addItems(sortedAccounts); - // Populate entry form account combo + // Restore previous selection if possible, otherwise select all + if (!previouslySelected.isEmpty()) { + QStringList validSelection; + for (const QString &account : previouslySelected) { + if (sortedAccounts.contains(account)) { + validSelection.append(account); + } + } + if (!validSelection.isEmpty()) { + ui->accountFilterCombo->setSelectedItems(validSelection); + } + } + + ui->accountFilterCombo->blockSignals(false); ui->entryAccountCombo->blockSignals(true); QString currentAccount = ui->entryAccountCombo->currentText(); ui->entryAccountCombo->clear(); @@ -256,16 +316,9 @@ void CashFlow::refreshView() { ui->recurringCategoryCombo->addItems(sortedCategories); ui->recurringCategoryCombo->blockSignals(false); - // Restore previous selection if possible - int index = ui->accountFilterCombo->findText(currentFilter); - if (index >= 0) { - ui->accountFilterCombo->setCurrentIndex(index); - } - - ui->accountFilterCombo->blockSignals(false); - refreshTransactionTable(); refreshRecurringTable(); + refreshCharts(); calculateAndDisplayBalance(); } @@ -274,6 +327,34 @@ void CashFlow::refreshTransactionTable() { ui->transactionTable->setRowCount(0); + // Calculate starting balance from all transactions before the date range + QDate startDate = ui->dateFromEdit->date(); + QStringList selectedAccounts = ui->accountFilterCombo->getSelectedItems(); + bool showAllAccounts = ui->accountFilterCombo->allSelected(); + + // Get transactions before the visible range + QList<Transaction> priorTransactions = database->getTransactions(QDate(1900, 1, 1), startDate.addDays(-1)); + + // Filter by selected accounts if applicable + if (!showAllAccounts && !selectedAccounts.isEmpty()) { + QList<Transaction> filtered; + for (const Transaction &t : priorTransactions) { + if (selectedAccounts.contains(t.account)) { + filtered.append(t); + } + } + priorTransactions = filtered; + } + + // Calculate starting balance and per-account balances from prior transactions + double calculatedStartingBalance = 0.0; + QMap<QString, double> accountBalances; // Track per-account balances + + for (const Transaction &t : priorTransactions) { + calculatedStartingBalance += t.amount; + accountBalances[t.account] += t.amount; + } + // Determine if we're showing per-account columns bool showAccountColumns = ui->showAccountBalancesCheck->isChecked(); @@ -288,6 +369,13 @@ void CashFlow::refreshTransactionTable() { } accountList = uniqueAccounts.values(); std::sort(accountList.begin(), accountList.end()); + + // Ensure all accounts in accountList are initialized in accountBalances map + for (const QString &account : accountList) { + if (!accountBalances.contains(account)) { + accountBalances[account] = 0.0; + } + } } // Set up columns: Date | Amount | Balance | [Per-account columns] | Account | Category | Description | Type @@ -307,8 +395,7 @@ void CashFlow::refreshTransactionTable() { // Enable column reordering by dragging headers ui->transactionTable->horizontalHeader()->setSectionsMovable(true); - double runningBalance = startingBalance; - QMap<QString, double> accountBalances; // Track per-account balances + double runningBalance = calculatedStartingBalance; QDate currentPeriodEnd; QString periodLabel; int periodCount = 1; @@ -769,15 +856,252 @@ void CashFlow::refreshRecurringTable() { ui->recurringTable->resizeColumnsToContents(); } +void CashFlow::refreshCharts() { + if (!chart || !chartView) return; + + // Clear existing series and axes + chart->removeAllSeries(); + + // Remove all existing axes + for (QAbstractAxis *axis : chart->axes()) { + chart->removeAxis(axis); + delete axis; + } + + // Get chart-specific date range + QDate chartStartDate = ui->chartDateFromEdit->date(); + QDate chartEndDate = ui->chartDateToEdit->date(); + + // Get all transactions (not filtered by main date range) + QList<Transaction> allTransactions = database->getTransactions(chartStartDate, chartEndDate); + + // Apply account filter from main view + QStringList selectedAccounts = ui->accountFilterCombo->getSelectedItems(); + bool showAllAccounts = ui->accountFilterCombo->allSelected(); + + // Filter transactions by selected accounts + if (!showAllAccounts && !selectedAccounts.isEmpty()) { + QList<Transaction> filtered; + for (const Transaction &t : allTransactions) { + if (selectedAccounts.contains(t.account)) { + filtered.append(t); + } + } + allTransactions = filtered; + } + + // Sort by date + std::sort(allTransactions.begin(), allTransactions.end(), + [](const Transaction &a, const Transaction &b) { + if (a.date != b.date) return a.date < b.date; + if (a.sortOrder != b.sortOrder) return a.sortOrder < b.sortOrder; + return a.amount > b.amount; + }); + + if (allTransactions.isEmpty()) { + chart->setTitle("Balance Over Time (No Data)"); + return; + } + + // Get unique accounts + QSet<QString> accountSet; + for (const Transaction &t : allTransactions) { + if (!t.account.isEmpty()) { + accountSet.insert(t.account); + } + } + QStringList accounts = accountSet.values(); + std::sort(accounts.begin(), accounts.end()); + + // Filter to only selected accounts + if (!showAllAccounts && !selectedAccounts.isEmpty()) { + accounts = selectedAccounts; + } + + // Create a series for each account + QMap<QString, QLineSeries*> seriesMap; + QMap<QString, double> accountBalances; + + for (const QString &account : accounts) { + QLineSeries *series = new QLineSeries(); + series->setName(account); + seriesMap[account] = series; + accountBalances[account] = 0.0; + + // Add starting point + series->append(QDateTime(chartStartDate, QTime(0, 0)).toMSecsSinceEpoch(), 0.0); + } + + // Also create total balance series if showing multiple accounts + QLineSeries *totalSeries = nullptr; + double totalBalance = startingBalance; + if (showAllAccounts && accounts.size() > 1) { + totalSeries = new QLineSeries(); + totalSeries->setName("Total"); + totalSeries->append(QDateTime(chartStartDate, QTime(0, 0)).toMSecsSinceEpoch(), totalBalance); + } + + // Process transactions and update balances + for (const Transaction &t : allTransactions) { + if (t.type == TransactionType::Reconciliation) continue; + + // Update account balance + if (seriesMap.contains(t.account)) { + accountBalances[t.account] += t.amount; + seriesMap[t.account]->append(QDateTime(t.date, QTime(0, 0)).toMSecsSinceEpoch(), + accountBalances[t.account]); + } + + // Update total balance + if (totalSeries) { + totalBalance += t.amount; + totalSeries->append(QDateTime(t.date, QTime(0, 0)).toMSecsSinceEpoch(), totalBalance); + } + } + + // Add all series to chart + for (QLineSeries *series : seriesMap.values()) { + chart->addSeries(series); + } + if (totalSeries) { + chart->addSeries(totalSeries); + QPen pen = totalSeries->pen(); + pen.setWidth(3); + totalSeries->setPen(pen); + } + + // Create axes + QDateTimeAxis *axisX = new QDateTimeAxis; + axisX->setFormat("MMM dd"); + axisX->setTitleText("Date"); + axisX->setTickCount(10); + axisX->setRange(QDateTime(chartStartDate, QTime(0, 0)), QDateTime(chartEndDate, QTime(23, 59))); + chart->addAxis(axisX, Qt::AlignBottom); + + // Find min/max values across all series + double minBalance = 0.0; + double maxBalance = 0.0; + bool firstValue = true; + + for (QLineSeries *series : seriesMap.values()) { + for (const QPointF &point : series->points()) { + if (firstValue) { + minBalance = maxBalance = point.y(); + firstValue = false; + } else { + minBalance = qMin(minBalance, point.y()); + maxBalance = qMax(maxBalance, point.y()); + } + } + } + if (totalSeries) { + for (const QPointF &point : totalSeries->points()) { + if (firstValue) { + minBalance = maxBalance = point.y(); + firstValue = false; + } else { + minBalance = qMin(minBalance, point.y()); + maxBalance = qMax(maxBalance, point.y()); + } + } + } + + // Add 10% padding to the range + double range = maxBalance - minBalance; + double padding = range * 0.1; + if (range < 0.01) padding = 100; // Minimum padding if range is tiny + + QValueAxis *axisY = new QValueAxis; + axisY->setTitleText("Balance ($)"); + axisY->setLabelFormat("$%.2f"); + axisY->setRange(minBalance - padding, maxBalance + padding); + chart->addAxis(axisY, Qt::AlignLeft); + + // Attach axes to all series + for (QLineSeries *series : seriesMap.values()) { + series->attachAxis(axisX); + series->attachAxis(axisY); + + // Enable hover events for tooltips + series->setPointsVisible(false); + connect(series, &QLineSeries::hovered, this, [this, series](const QPointF &point, bool state) { + if (state) { + QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(point.x()); + QString tooltip = QString("%1\n%2\n$%3") + .arg(series->name()) + .arg(dateTime.date().toString("MMM dd, yyyy")) + .arg(formatCurrency(point.y())); + chart->setToolTip(tooltip); + } + }); + } + if (totalSeries) { + totalSeries->attachAxis(axisX); + totalSeries->attachAxis(axisY); + + // Enable hover events for total series + totalSeries->setPointsVisible(false); + connect(totalSeries, &QLineSeries::hovered, this, [this, totalSeries](const QPointF &point, bool state) { + if (state) { + QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(point.x()); + QString tooltip = QString("%1\n%2\n$%3") + .arg(totalSeries->name()) + .arg(dateTime.date().toString("MMM dd, yyyy")) + .arg(formatCurrency(point.y())); + chart->setToolTip(tooltip); + } + }); + } + + // Set chart title + QString title; + if (showAllAccounts) { + title = "Balance Over Time (All Accounts)"; + } else if (selectedAccounts.size() == 1) { + title = QString("Balance Over Time (%1)").arg(selectedAccounts.first()); + } else { + title = QString("Balance Over Time (%1 Accounts)").arg(selectedAccounts.size()); + } + chart->setTitle(title); + + chart->legend()->setVisible(true); + chart->legend()->setAlignment(Qt::AlignBottom); +} + void CashFlow::calculateAndDisplayBalance() { QList<Transaction> allTransactions = getAllTransactionsInRange(); - double endBalance = startingBalance; + // Calculate starting balance from all transactions before the date range + QDate startDate = ui->dateFromEdit->date(); + QStringList selectedAccounts = ui->accountFilterCombo->getSelectedItems(); + bool showAllAccounts = ui->accountFilterCombo->allSelected(); + + // Get transactions before the visible range + QList<Transaction> priorTransactions = database->getTransactions(QDate(1900, 1, 1), startDate.addDays(-1)); + + // Filter by selected accounts if applicable + if (!showAllAccounts && !selectedAccounts.isEmpty()) { + QList<Transaction> filtered; + for (const Transaction &t : priorTransactions) { + if (selectedAccounts.contains(t.account)) { + filtered.append(t); + } + } + priorTransactions = filtered; + } + + // Calculate starting balance from prior transactions + double calculatedStartingBalance = 0.0; + for (const Transaction &t : priorTransactions) { + calculatedStartingBalance += t.amount; + } + + double endBalance = calculatedStartingBalance; for (const Transaction &t : allTransactions) { endBalance += t.amount; } - ui->startBalanceLabel->setText(QString("Starting Balance: $%1").arg(formatCurrency(startingBalance))); + ui->startBalanceLabel->setText(QString("Starting Balance: $%1").arg(formatCurrency(calculatedStartingBalance))); ui->endBalanceLabel->setText(QString("Ending Balance: $%1").arg(formatCurrency(endBalance))); // Color code the ending balance @@ -793,23 +1117,18 @@ void CashFlow::calculateAndDisplayBalance() { QList<Transaction> CashFlow::getAllTransactionsInRange() { QDate startDate = ui->dateFromEdit->date(); QDate endDate = ui->dateToEdit->date(); - QString accountFilter = ui->accountFilterCombo->currentText(); + QStringList selectedAccounts = ui->accountFilterCombo->getSelectedItems(); int recurringFilter = ui->recurringFilterCombo->currentData().toInt(); QString searchText = ui->searchEdit->text().trimmed().toLower(); - // Safety check - if (accountFilter.isEmpty()) { - accountFilter = "All Accounts"; - } - // Get all transactions from database (includes both actual and projected) QList<Transaction> allTransactions = database->getTransactions(startDate, endDate); - // Filter by account if not "All Accounts" - if (accountFilter != "All Accounts") { + // Filter by selected accounts + if (!selectedAccounts.isEmpty() && !ui->accountFilterCombo->allSelected()) { QList<Transaction> filtered; for (const Transaction &t : allTransactions) { - if (t.account == accountFilter) { + if (selectedAccounts.contains(t.account)) { filtered.append(t); } } |
