aboutsummaryrefslogtreecommitdiff
path: root/cashflow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'cashflow.cpp')
-rw-r--r--cashflow.cpp369
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);
}
}