aboutsummaryrefslogtreecommitdiff
path: root/cashflow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'cashflow.cpp')
-rw-r--r--cashflow.cpp907
1 files changed, 907 insertions, 0 deletions
diff --git a/cashflow.cpp b/cashflow.cpp
new file mode 100644
index 0000000..8e48d27
--- /dev/null
+++ b/cashflow.cpp
@@ -0,0 +1,907 @@
+#include "cashflow.h"
+#include "ui_cashflow.h"
+#include <QMessageBox>
+#include <QDir>
+#include <QStandardPaths>
+#include <QFontDialog>
+#include <QLocale>
+
+CashFlow::CashFlow(QWidget *parent)
+ : QMainWindow(parent)
+ , ui(new Ui::CashFlow)
+ , currentTransactionId(-1)
+ , currentRecurringId(-1)
+ , startingBalance(0.0)
+ , currentAmountFont("Courier New", 10)
+{
+ ui->setupUi(this);
+
+ // Initialize database
+ database = new Database();
+ QString dbPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/cashflow.db";
+ QDir().mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
+
+ if (!database->open(dbPath)) {
+ QMessageBox::critical(this, "Database Error", "Failed to open database: " + database->lastError());
+ return;
+ }
+
+ setupConnections();
+ loadSettings();
+
+ // Set default date range (current month to 3 months out)
+ QDate today = QDate::currentDate();
+ ui->dateFromEdit->setDate(QDate(today.year(), today.month(), 1));
+ ui->dateToEdit->setDate(today.addMonths(3));
+
+ clearTransactionEntry();
+ clearRecurringEntry();
+ refreshView();
+}
+
+CashFlow::~CashFlow()
+{
+ delete database;
+ delete ui;
+}
+
+void CashFlow::setupConnections() {
+ // Transaction tab
+ 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->showAccountBalancesCheck, &QCheckBox::stateChanged, this, &CashFlow::onDateRangeChanged);
+
+ // Auto-save period and show balances settings
+ connect(ui->periodCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this]() {
+ database->setSetting("default_period", QString::number(ui->periodCombo->currentIndex()));
+ });
+ connect(ui->showAccountBalancesCheck, &QCheckBox::stateChanged, this, [this]() {
+ database->setSetting("show_account_balances", QString::number(ui->showAccountBalancesCheck->isChecked() ? 1 : 0));
+ });
+
+ connect(ui->transactionTable, &QTableWidget::itemSelectionChanged, this, &CashFlow::onTransactionSelected);
+ connect(ui->saveBtn, &QPushButton::clicked, this, &CashFlow::onSaveTransaction);
+ connect(ui->newBtn, &QPushButton::clicked, this, &CashFlow::onNewTransaction);
+ connect(ui->deleteBtn, &QPushButton::clicked, this, &CashFlow::onDeleteTransaction);
+
+ // Set up Delete key shortcut for transaction table
+ ui->deleteBtn->setShortcut(Qt::Key_Delete);
+
+ // Auto-save transaction fields on change
+ connect(ui->entryDateEdit, &QDateEdit::dateChanged, this, &CashFlow::onTransactionFieldChanged);
+ connect(ui->entryAmountSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &CashFlow::onTransactionFieldChanged);
+ connect(ui->entryAccountCombo, &QComboBox::currentTextChanged, this, &CashFlow::onTransactionFieldChanged);
+ connect(ui->entryCategoryCombo, &QComboBox::currentTextChanged, this, &CashFlow::onTransactionFieldChanged);
+ connect(ui->entryDescriptionEdit, &QLineEdit::textChanged, this, &CashFlow::onTransactionFieldChanged);
+ connect(ui->entryTypeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CashFlow::onTransactionFieldChanged);
+
+ // Color-code amount inputs
+ connect(ui->entryAmountSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &CashFlow::updateAmountColors);
+ connect(ui->recurringAmountSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &CashFlow::updateAmountColors);
+
+ // Recurring tab
+ connect(ui->recurringTable, &QTableWidget::itemSelectionChanged, this, &CashFlow::onRecurringSelected);
+ connect(ui->saveRecurringBtn, &QPushButton::clicked, this, &CashFlow::onSaveRecurring);
+ connect(ui->newRecurringBtn, &QPushButton::clicked, this, &CashFlow::onNewRecurring);
+ connect(ui->deleteRecurringBtn, &QPushButton::clicked, this, &CashFlow::onDeleteRecurring);
+
+ // Settings tab
+ connect(ui->amountFontBtn, &QPushButton::clicked, this, &CashFlow::onChooseAmountFont);
+ connect(ui->saveSettingsBtn, &QPushButton::clicked, this, &CashFlow::onSaveSettings);
+
+ // Set up Delete key shortcut for recurring table
+ ui->deleteRecurringBtn->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Delete));
+}
+
+void CashFlow::refreshView() {
+ // Populate account filter dropdown with unique accounts
+ // Block signals to prevent recursive refresh
+ ui->accountFilterCombo->blockSignals(true);
+
+ QString currentFilter = ui->accountFilterCombo->currentText();
+ ui->accountFilterCombo->clear();
+ ui->accountFilterCombo->addItem("All Accounts");
+
+ // Get unique account names and categories
+ QSet<QString> accounts;
+ QSet<QString> categories;
+ for (const Transaction &t : database->getAllTransactions()) {
+ if (!t.account.isEmpty()) accounts.insert(t.account);
+ if (!t.category.isEmpty()) categories.insert(t.category);
+ }
+ for (const RecurringRule &r : database->getAllRecurringRules()) {
+ if (!r.account.isEmpty()) accounts.insert(r.account);
+ if (!r.category.isEmpty()) categories.insert(r.category);
+ }
+
+ QStringList sortedAccounts = accounts.values();
+ sortedAccounts.sort();
+ ui->accountFilterCombo->addItems(sortedAccounts);
+
+ // Populate entry form account combo
+ ui->entryAccountCombo->blockSignals(true);
+ QString currentAccount = ui->entryAccountCombo->currentText();
+ ui->entryAccountCombo->clear();
+ ui->entryAccountCombo->addItems(sortedAccounts);
+ ui->entryAccountCombo->setCurrentText(currentAccount);
+ ui->entryAccountCombo->blockSignals(false);
+
+ // Populate entry form category combo
+ ui->entryCategoryCombo->blockSignals(true);
+ QString currentCategory = ui->entryCategoryCombo->currentText();
+ ui->entryCategoryCombo->clear();
+ QStringList sortedCategories = categories.values();
+ sortedCategories.sort();
+ ui->entryCategoryCombo->addItems(sortedCategories);
+ ui->entryCategoryCombo->setCurrentText(currentCategory);
+ ui->entryCategoryCombo->blockSignals(false);
+
+ // Populate recurring rule account and category combos
+ ui->recurringAccountCombo->blockSignals(true);
+ ui->recurringAccountCombo->clear();
+ ui->recurringAccountCombo->addItems(sortedAccounts);
+ ui->recurringAccountCombo->blockSignals(false);
+
+ ui->recurringCategoryCombo->blockSignals(true);
+ ui->recurringCategoryCombo->clear();
+ 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();
+ calculateAndDisplayBalance();
+}
+
+void CashFlow::refreshTransactionTable() {
+ QList<Transaction> allTransactions = getAllTransactionsInRange();
+
+ ui->transactionTable->setRowCount(0);
+ ui->transactionTable->setColumnCount(7);
+ ui->transactionTable->setHorizontalHeaderLabels({"Date", "Amount", "Balance", "Account", "Category", "Description", "Type"});
+
+ double runningBalance = startingBalance;
+ QMap<QString, double> accountBalances; // Track per-account balances
+ QDate currentPeriodEnd;
+ QString periodLabel;
+ int periodCount = 1;
+
+ // Determine period type
+ PeriodType periodType = static_cast<PeriodType>(ui->periodCombo->currentIndex());
+
+ // Get first period end date
+ if (!allTransactions.isEmpty()) {
+ currentPeriodEnd = getPeriodEnd(allTransactions.first().date, periodType);
+ periodLabel = getPeriodLabel(allTransactions.first().date, periodType, periodCount);
+ }
+
+ for (const Transaction &t : allTransactions) {
+ // Check if we've crossed into a new period
+ if (t.date > currentPeriodEnd) {
+ // Insert period end row
+ insertPeriodEndRow(periodLabel, runningBalance, accountBalances);
+
+ // Move to next period
+ periodCount++;
+ currentPeriodEnd = getPeriodEnd(t.date, periodType);
+ periodLabel = getPeriodLabel(t.date, periodType, periodCount);
+ }
+
+ // Update balances
+ runningBalance += t.amount;
+ accountBalances[t.account] += t.amount;
+
+ // Insert transaction row
+ int row = ui->transactionTable->rowCount();
+ ui->transactionTable->insertRow(row);
+
+ // Store ID in first column's data for retrieval later
+ QTableWidgetItem *dateItem = new QTableWidgetItem(t.date.toString("MM/dd/yy"));
+ dateItem->setData(Qt::UserRole, t.id);
+ dateItem->setFlags(dateItem->flags() & ~Qt::ItemIsEditable);
+ ui->transactionTable->setItem(row, 0, dateItem);
+
+ // Format amount with color, right-align, monospace
+ QTableWidgetItem *amountItem = new QTableWidgetItem(QString("$%1").arg(formatCurrency(t.amount)));
+ amountItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
+ amountItem->setFont(currentAmountFont);
+ amountItem->setFlags(amountItem->flags() & ~Qt::ItemIsEditable);
+ if (t.amount < 0) {
+ amountItem->setForeground(QColor(200, 0, 0));
+ } else {
+ amountItem->setForeground(QColor(0, 150, 0));
+ }
+ ui->transactionTable->setItem(row, 1, amountItem);
+
+ // Format balance with right-align, monospace
+ QTableWidgetItem *balanceItem = new QTableWidgetItem(QString("$%1").arg(formatCurrency(runningBalance)));
+ balanceItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
+ balanceItem->setFont(currentAmountFont);
+ balanceItem->setFlags(balanceItem->flags() & ~Qt::ItemIsEditable);
+ if (runningBalance < 0) {
+ balanceItem->setForeground(QColor(200, 0, 0));
+ }
+ ui->transactionTable->setItem(row, 2, balanceItem);
+
+ QTableWidgetItem *accountItem = new QTableWidgetItem(t.account);
+ accountItem->setFlags(accountItem->flags() & ~Qt::ItemIsEditable);
+ ui->transactionTable->setItem(row, 3, accountItem);
+
+ QTableWidgetItem *categoryItem = new QTableWidgetItem(t.category);
+ categoryItem->setFlags(categoryItem->flags() & ~Qt::ItemIsEditable);
+ ui->transactionTable->setItem(row, 4, categoryItem);
+
+ QTableWidgetItem *descItem = new QTableWidgetItem(t.description);
+ descItem->setFlags(descItem->flags() & ~Qt::ItemIsEditable);
+ ui->transactionTable->setItem(row, 5, descItem);
+
+ QTableWidgetItem *typeItem = new QTableWidgetItem(
+ t.type == TransactionType::Actual ? "Actual" : "Estimated");
+ typeItem->setFlags(typeItem->flags() & ~Qt::ItemIsEditable);
+ ui->transactionTable->setItem(row, 6, typeItem);
+
+ // Color code estimated vs actual
+ QColor rowColor = t.type == TransactionType::Actual ?
+ QColor(200, 255, 200) : QColor(255, 255, 200);
+ for (int col = 0; col < 7; col++) {
+ if (ui->transactionTable->item(row, col)) {
+ ui->transactionTable->item(row, col)->setBackground(rowColor);
+ }
+ }
+ }
+
+ // Insert final period end row
+ if (!allTransactions.isEmpty()) {
+ insertPeriodEndRow(periodLabel, runningBalance, accountBalances);
+ }
+
+ ui->transactionTable->resizeColumnsToContents();
+
+ // Set minimum and optimal widths for specific columns
+ ui->transactionTable->setColumnWidth(0, 100); // Date
+ ui->transactionTable->setColumnWidth(1, 100); // Amount
+ ui->transactionTable->setColumnWidth(2, 100); // Balance
+ ui->transactionTable->setColumnWidth(3, 120); // Account
+ ui->transactionTable->setColumnWidth(4, 120); // Category
+ ui->transactionTable->horizontalHeader()->setStretchLastSection(false);
+ ui->transactionTable->setColumnWidth(5, 250); // Description
+ ui->transactionTable->setColumnWidth(6, 80); // Type
+}
+
+QDate CashFlow::getPeriodEnd(const QDate &date, PeriodType periodType) {
+ switch (periodType) {
+ case Daily:
+ return date;
+ case Weekly: {
+ // End on Sunday (7)
+ int daysUntilSunday = 7 - date.dayOfWeek();
+ return date.addDays(daysUntilSunday);
+ }
+ case Monthly:
+ return QDate(date.year(), date.month(), date.daysInMonth());
+ case Quarterly: {
+ int quarter = (date.month() - 1) / 3;
+ int lastMonthOfQuarter = (quarter + 1) * 3;
+ QDate lastDayOfQuarter(date.year(), lastMonthOfQuarter, 1);
+ return QDate(date.year(), lastMonthOfQuarter, lastDayOfQuarter.daysInMonth());
+ }
+ }
+ return date;
+}
+
+QString CashFlow::getPeriodLabel(const QDate &date, PeriodType periodType, int count) {
+ QDate periodStart = getPeriodStart(date, periodType);
+ QDate periodEnd = getPeriodEnd(date, periodType);
+
+ QString dateRange = QString("%1 - %2").arg(periodStart.toString("MM/dd/yy")).arg(periodEnd.toString("MM/dd/yy"));
+
+ switch (periodType) {
+ case Daily:
+ return QString("DAY %1 (%2) END").arg(count).arg(date.toString("MM/dd/yy"));
+ case Weekly:
+ return QString("WEEK %1 END (%2)").arg(count).arg(dateRange);
+ case Monthly:
+ return QString("%1 END (%2)").arg(date.toString("MMMM yyyy").toUpper()).arg(dateRange);
+ case Quarterly:
+ return QString("Q%1 %2 END (%3)").arg((date.month() - 1) / 3 + 1).arg(date.year()).arg(dateRange);
+ }
+ return "";
+}
+
+QDate CashFlow::getPeriodStart(const QDate &date, PeriodType periodType) {
+ switch (periodType) {
+ case Daily:
+ return date;
+ case Weekly: {
+ // Start on Monday (1)
+ int daysFromMonday = date.dayOfWeek() - 1;
+ return date.addDays(-daysFromMonday);
+ }
+ case Monthly:
+ return QDate(date.year(), date.month(), 1);
+ case Quarterly: {
+ int quarter = (date.month() - 1) / 3;
+ int firstMonthOfQuarter = quarter * 3 + 1;
+ return QDate(date.year(), firstMonthOfQuarter, 1);
+ }
+ }
+ return date;
+}
+
+void CashFlow::insertPeriodEndRow(const QString &label, double balance, const QMap<QString, double> &accountBalances) {
+ int row = ui->transactionTable->rowCount();
+ ui->transactionTable->insertRow(row);
+
+ // Build display text with optional account balances
+ QString displayText = QString("%1 Balance: $%2").arg(label).arg(formatCurrency(balance));
+
+ if (ui->showAccountBalancesCheck->isChecked() && !accountBalances.isEmpty()) {
+ QStringList accountTexts;
+ QMapIterator<QString, double> it(accountBalances);
+ while (it.hasNext()) {
+ it.next();
+ accountTexts.append(QString("%1: $%2").arg(it.key()).arg(formatCurrency(it.value())));
+ }
+ displayText += " " + accountTexts.join(" ");
+ }
+
+ QTableWidgetItem *spanItem = new QTableWidgetItem(displayText);
+ spanItem->setFont(QFont("Arial", 11, QFont::Bold));
+ spanItem->setBackground(QColor(180, 180, 180));
+ spanItem->setForeground(balance < 0 ? QColor(200, 0, 0) : QColor(0, 100, 0));
+ spanItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ spanItem->setFlags(spanItem->flags() & ~Qt::ItemIsSelectable); // Make non-selectable
+
+ ui->transactionTable->setItem(row, 0, spanItem);
+
+ // Span across all columns
+ ui->transactionTable->setSpan(row, 0, 1, 7);
+
+ // Make the row taller
+ ui->transactionTable->setRowHeight(row, 30);
+}
+
+void CashFlow::refreshRecurringTable() {
+ QList<RecurringRule> rules = database->getAllRecurringRules();
+
+ ui->recurringTable->setRowCount(0);
+ ui->recurringTable->setColumnCount(7);
+ ui->recurringTable->setHorizontalHeaderLabels({"ID", "Name", "Frequency", "Amount", "Account", "Category", "Start Date"});
+ ui->recurringTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
+
+ for (const RecurringRule &r : rules) {
+ int row = ui->recurringTable->rowCount();
+ ui->recurringTable->insertRow(row);
+
+ ui->recurringTable->setItem(row, 0, new QTableWidgetItem(QString::number(r.id)));
+ ui->recurringTable->setItem(row, 1, new QTableWidgetItem(r.name));
+
+ QString freqStr;
+ switch (r.frequency) {
+ case RecurrenceFrequency::Daily: freqStr = "Daily"; break;
+ case RecurrenceFrequency::Weekly: freqStr = "Weekly"; break;
+ case RecurrenceFrequency::BiWeekly: freqStr = "Bi-Weekly"; break;
+ case RecurrenceFrequency::Monthly: freqStr = "Monthly"; break;
+ case RecurrenceFrequency::Yearly: freqStr = "Yearly"; break;
+ default: freqStr = "None"; break;
+ }
+ ui->recurringTable->setItem(row, 2, new QTableWidgetItem(freqStr));
+
+ // Amount with color coding, right-align, monospace
+ QTableWidgetItem *amountItem = new QTableWidgetItem(QString("$%1").arg(formatCurrency(r.amount)));
+ amountItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
+ amountItem->setFont(currentAmountFont);
+ if (r.amount < 0) {
+ amountItem->setForeground(QColor(200, 0, 0)); // Red for debits
+ } else {
+ amountItem->setForeground(QColor(0, 150, 0)); // Green for credits
+ }
+ ui->recurringTable->setItem(row, 3, amountItem);
+
+ ui->recurringTable->setItem(row, 4, new QTableWidgetItem(r.account));
+ ui->recurringTable->setItem(row, 5, new QTableWidgetItem(r.category));
+ ui->recurringTable->setItem(row, 6, new QTableWidgetItem(r.startDate.toString("yyyy-MM-dd")));
+ }
+
+ ui->recurringTable->resizeColumnsToContents();
+}
+
+void CashFlow::calculateAndDisplayBalance() {
+ QList<Transaction> allTransactions = getAllTransactionsInRange();
+
+ double endBalance = startingBalance;
+ for (const Transaction &t : allTransactions) {
+ endBalance += t.amount;
+ }
+
+ ui->startBalanceLabel->setText(QString("Starting Balance: $%1").arg(formatCurrency(startingBalance)));
+ ui->endBalanceLabel->setText(QString("Ending Balance: $%1").arg(formatCurrency(endBalance)));
+
+ // Color code the ending balance
+ if (endBalance < 0) {
+ ui->endBalanceLabel->setStyleSheet("font-weight: bold; font-size: 12pt; color: red;");
+ } else if (endBalance < 1000) {
+ ui->endBalanceLabel->setStyleSheet("font-weight: bold; font-size: 12pt; color: orange;");
+ } else {
+ ui->endBalanceLabel->setStyleSheet("font-weight: bold; font-size: 12pt; color: green;");
+ }
+}
+
+QList<Transaction> CashFlow::getAllTransactionsInRange() {
+ QDate startDate = ui->dateFromEdit->date();
+ QDate endDate = ui->dateToEdit->date();
+ QString accountFilter = ui->accountFilterCombo->currentText();
+
+ // Safety check
+ if (accountFilter.isEmpty()) {
+ accountFilter = "All Accounts";
+ }
+
+ // Get actual transactions from database
+ QList<Transaction> actualTransactions = database->getTransactions(startDate, endDate);
+
+ // Generate projected transactions from recurring rules
+ QList<Transaction> projectedTransactions = generateProjectedTransactions();
+
+ // Combine
+ QList<Transaction> allTransactions = actualTransactions + projectedTransactions;
+
+ // Filter by account if not "All Accounts"
+ if (accountFilter != "All Accounts") {
+ QList<Transaction> filtered;
+ for (const Transaction &t : allTransactions) {
+ if (t.account == accountFilter) {
+ filtered.append(t);
+ }
+ }
+ allTransactions = filtered;
+ }
+
+ // Sort by date, then by sort_order, then credits before debits
+ 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;
+ // Credits (positive amounts) before debits (negative amounts)
+ return a.amount > b.amount;
+ });
+
+ return allTransactions;
+}
+
+QList<Transaction> CashFlow::generateProjectedTransactions() {
+ QList<Transaction> projections;
+ QList<RecurringRule> rules = database->getAllRecurringRules();
+
+ QDate startDate = ui->dateFromEdit->date();
+ QDate endDate = ui->dateToEdit->date();
+
+ for (const RecurringRule &rule : rules) {
+ QDate currentDate = rule.startDate > startDate ? rule.startDate : startDate;
+
+ // Align to proper day based on frequency
+ if (rule.frequency == RecurrenceFrequency::Weekly || rule.frequency == RecurrenceFrequency::BiWeekly) {
+ // Find next occurrence of the specified day of week
+ while (currentDate <= endDate && currentDate.dayOfWeek() != rule.dayOfWeek) {
+ currentDate = currentDate.addDays(1);
+ }
+ } else if (rule.frequency == RecurrenceFrequency::Monthly) {
+ // Set to the specified day of month
+ int targetDay = qMin(rule.dayOfMonth, currentDate.daysInMonth());
+ currentDate = QDate(currentDate.year(), currentDate.month(), targetDay);
+ if (currentDate < startDate) {
+ currentDate = currentDate.addMonths(1);
+ targetDay = qMin(rule.dayOfMonth, currentDate.daysInMonth());
+ currentDate = QDate(currentDate.year(), currentDate.month(), targetDay);
+ }
+ }
+
+ int count = 0;
+ while (currentDate <= endDate) {
+ if (rule.occurrences != -1 && count >= rule.occurrences) {
+ break;
+ }
+
+ if (rule.endDate.isValid() && currentDate > rule.endDate) {
+ break;
+ }
+
+ Transaction t;
+ t.id = -1; // Projected transactions have no ID
+ t.date = currentDate;
+ t.amount = rule.amount;
+ t.account = rule.account;
+ t.category = rule.category;
+ t.description = rule.description + " (projected)";
+ t.type = TransactionType::Estimated;
+ t.recurringId = rule.id;
+
+ projections.append(t);
+ count++;
+
+ // Calculate next occurrence
+ switch (rule.frequency) {
+ case RecurrenceFrequency::Daily:
+ currentDate = currentDate.addDays(1);
+ break;
+ case RecurrenceFrequency::Weekly:
+ currentDate = currentDate.addDays(7);
+ break;
+ case RecurrenceFrequency::BiWeekly:
+ currentDate = currentDate.addDays(14);
+ break;
+ case RecurrenceFrequency::Monthly: {
+ currentDate = currentDate.addMonths(1);
+ int targetDay = qMin(rule.dayOfMonth, currentDate.daysInMonth());
+ currentDate = QDate(currentDate.year(), currentDate.month(), targetDay);
+ break;
+ }
+ case RecurrenceFrequency::Yearly:
+ currentDate = currentDate.addYears(1);
+ break;
+ default:
+ currentDate = endDate.addDays(1); // Exit loop
+ break;
+ }
+ }
+ }
+
+ return projections;
+}
+
+void CashFlow::onDateRangeChanged() {
+ refreshView();
+}
+
+void CashFlow::onPeriodChanged() {
+ refreshView();
+}
+
+void CashFlow::onTransactionSelected() {
+ QList<QTableWidgetItem*> selected = ui->transactionTable->selectedItems();
+ if (selected.isEmpty()) {
+ return;
+ }
+
+ int row = selected[0]->row();
+ int id = ui->transactionTable->item(row, 0)->data(Qt::UserRole).toInt();
+
+ // If it's a projected transaction (id = -1), don't load it for editing
+ if (id == -1) {
+ ui->entryStatusLabel->setText("(Projected - cannot edit)");
+ currentTransactionId = -1;
+ return;
+ }
+
+ // Load from database
+ QList<Transaction> allTrans = database->getAllTransactions();
+ for (const Transaction &t : allTrans) {
+ if (t.id == id) {
+ currentTransactionId = id;
+ loadTransactionToEntry(t);
+ ui->entryStatusLabel->setText(QString("Editing ID: %1").arg(id));
+ return;
+ }
+ }
+}
+
+void CashFlow::onSaveTransaction() {
+ Transaction t;
+ t.id = currentTransactionId;
+ t.date = ui->entryDateEdit->date();
+ t.amount = ui->entryAmountSpin->value();
+ t.account = ui->entryAccountCombo->currentText();
+ t.category = ui->entryCategoryCombo->currentText();
+ t.description = ui->entryDescriptionEdit->text();
+ t.type = ui->entryTypeCombo->currentText() == "Actual" ? TransactionType::Actual : TransactionType::Estimated;
+ t.recurringId = -1; // Manual entries don't have recurring ID
+
+ bool success;
+ if (currentTransactionId == -1) {
+ // New transaction
+ success = database->addTransaction(t);
+ if (success) {
+ // Get the new ID and update currentTransactionId
+ QList<Transaction> allTrans = database->getAllTransactions();
+ if (!allTrans.isEmpty()) {
+ currentTransactionId = allTrans.last().id;
+ }
+ }
+ } else {
+ // Update existing
+ success = database->updateTransaction(t);
+ }
+
+ if (success) {
+ ui->entryStatusLabel->setText("Saved!");
+ refreshView();
+ } else {
+ QMessageBox::critical(this, "Error", "Failed to save: " + database->lastError());
+ }
+}
+
+void CashFlow::onTransactionFieldChanged() {
+ // Auto-save if we're editing an existing transaction or have data entered
+ if (currentTransactionId != -1 ||
+ !ui->entryAccountCombo->currentText().isEmpty() ||
+ ui->entryAmountSpin->value() != 0.0) {
+ onSaveTransaction();
+ }
+}
+
+void CashFlow::onNewTransaction() {
+ clearTransactionEntry();
+ ui->entryDateEdit->setDate(QDate::currentDate());
+ ui->entryDateEdit->setFocus();
+}
+
+void CashFlow::onDeleteTransaction() {
+ if (currentTransactionId == -1) {
+ QMessageBox::warning(this, "No Selection", "Please select a transaction to delete.");
+ return;
+ }
+
+ if (QMessageBox::question(this, "Confirm Delete",
+ QString("Delete transaction ID %1?").arg(currentTransactionId)) == QMessageBox::Yes) {
+ if (database->deleteTransaction(currentTransactionId)) {
+ clearTransactionEntry();
+ refreshView();
+ } else {
+ QMessageBox::critical(this, "Error", "Failed to delete: " + database->lastError());
+ }
+ }
+}
+
+void CashFlow::onRecurringSelected() {
+ QList<QTableWidgetItem*> selected = ui->recurringTable->selectedItems();
+ if (selected.isEmpty()) {
+ return;
+ }
+
+ int row = selected[0]->row();
+ int id = ui->recurringTable->item(row, 0)->text().toInt();
+
+ QList<RecurringRule> rules = database->getAllRecurringRules();
+ for (const RecurringRule &r : rules) {
+ if (r.id == id) {
+ loadRecurringToEntry(r);
+ currentRecurringId = id;
+ return;
+ }
+ }
+}
+
+void CashFlow::onSaveRecurring() {
+ RecurringRule r;
+ r.id = currentRecurringId;
+ r.name = ui->recurringNameEdit->text();
+ r.startDate = ui->recurringStartDateEdit->date();
+ r.amount = ui->recurringAmountSpin->value();
+ r.account = ui->recurringAccountCombo->currentText();
+ r.category = ui->recurringCategoryCombo->currentText();
+ r.description = ui->recurringDescriptionEdit->text();
+ r.occurrences = -1; // Default to infinite
+
+ QString freqStr = ui->recurringFrequencyCombo->currentText();
+ if (freqStr == "Daily") {
+ r.frequency = RecurrenceFrequency::Daily;
+ r.dayOfWeek = -1;
+ r.dayOfMonth = -1;
+ }
+ else if (freqStr == "Weekly") {
+ r.frequency = RecurrenceFrequency::Weekly;
+ r.dayOfWeek = r.startDate.dayOfWeek(); // Use the day of week from start date
+ r.dayOfMonth = -1;
+ }
+ else if (freqStr == "Bi-Weekly") {
+ r.frequency = RecurrenceFrequency::BiWeekly;
+ r.dayOfWeek = r.startDate.dayOfWeek(); // Use the day of week from start date
+ r.dayOfMonth = -1;
+ }
+ else if (freqStr == "Monthly") {
+ r.frequency = RecurrenceFrequency::Monthly;
+ r.dayOfWeek = -1;
+ r.dayOfMonth = r.startDate.day(); // Use the day of month from start date
+ }
+ else if (freqStr == "Yearly") {
+ r.frequency = RecurrenceFrequency::Yearly;
+ r.dayOfWeek = -1;
+ r.dayOfMonth = r.startDate.day();
+ }
+
+ bool success;
+ if (currentRecurringId == -1) {
+ success = database->addRecurringRule(r);
+ } else {
+ success = database->updateRecurringRule(r);
+ }
+
+ if (success) {
+ refreshView();
+ QMessageBox::information(this, "Success", "Recurring rule saved. Projections updated automatically.");
+ } else {
+ QMessageBox::critical(this, "Error", "Failed to save: " + database->lastError());
+ }
+}
+
+void CashFlow::onNewRecurring() {
+ clearRecurringEntry();
+ ui->recurringStartDateEdit->setDate(QDate::currentDate());
+ ui->recurringNameEdit->setFocus();
+}
+
+void CashFlow::onDeleteRecurring() {
+ if (currentRecurringId == -1) {
+ QMessageBox::warning(this, "No Selection", "Please select a recurring rule to delete.");
+ return;
+ }
+
+ if (QMessageBox::question(this, "Confirm Delete",
+ "Delete this recurring rule?") == QMessageBox::Yes) {
+ if (database->deleteRecurringRule(currentRecurringId)) {
+ clearRecurringEntry();
+ refreshView();
+ } else {
+ QMessageBox::critical(this, "Error", "Failed to delete: " + database->lastError());
+ }
+ }
+}
+
+void CashFlow::clearTransactionEntry() {
+ currentTransactionId = -1;
+ ui->entryDateEdit->setDate(QDate::currentDate());
+ ui->entryAmountSpin->setValue(0.0);
+ ui->entryAccountCombo->setCurrentText("");
+ ui->entryCategoryCombo->setCurrentText("");
+ ui->entryDescriptionEdit->clear();
+ ui->entryTypeCombo->setCurrentIndex(0);
+ ui->entryStatusLabel->setText("(New transaction)");
+ updateAmountColors();
+}
+
+void CashFlow::loadTransactionToEntry(const Transaction &t) {
+ // Block signals to prevent auto-save while loading
+ ui->entryDateEdit->blockSignals(true);
+ ui->entryAmountSpin->blockSignals(true);
+ ui->entryAccountCombo->blockSignals(true);
+ ui->entryCategoryCombo->blockSignals(true);
+ ui->entryDescriptionEdit->blockSignals(true);
+ ui->entryTypeCombo->blockSignals(true);
+
+ ui->entryDateEdit->setDate(t.date);
+ ui->entryAmountSpin->setValue(t.amount);
+ ui->entryAccountCombo->setCurrentText(t.account);
+ ui->entryCategoryCombo->setCurrentText(t.category);
+ ui->entryDescriptionEdit->setText(t.description);
+ ui->entryTypeCombo->setCurrentIndex(t.type == TransactionType::Actual ? 1 : 0);
+
+ ui->entryDateEdit->blockSignals(false);
+ ui->entryAmountSpin->blockSignals(false);
+ ui->entryAccountCombo->blockSignals(false);
+ ui->entryCategoryCombo->blockSignals(false);
+ ui->entryDescriptionEdit->blockSignals(false);
+ ui->entryTypeCombo->blockSignals(false);
+
+ updateAmountColors();
+}
+
+void CashFlow::clearRecurringEntry() {
+ currentRecurringId = -1;
+ ui->recurringNameEdit->clear();
+ ui->recurringStartDateEdit->setDate(QDate::currentDate());
+ ui->recurringAmountSpin->setValue(0.0);
+ ui->recurringAccountCombo->setCurrentText("");
+ ui->recurringCategoryCombo->setCurrentText("");
+ ui->recurringDescriptionEdit->clear();
+ ui->recurringFrequencyCombo->setCurrentIndex(3); // Default to Monthly
+ updateAmountColors();
+}
+
+void CashFlow::loadRecurringToEntry(const RecurringRule &r) {
+ ui->recurringNameEdit->setText(r.name);
+ ui->recurringStartDateEdit->setDate(r.startDate);
+ ui->recurringAmountSpin->setValue(r.amount);
+ ui->recurringAccountCombo->setCurrentText(r.account);
+ ui->recurringCategoryCombo->setCurrentText(r.category);
+ ui->recurringDescriptionEdit->setText(r.description);
+
+ int freqIndex = 3; // Default monthly
+ switch (r.frequency) {
+ case RecurrenceFrequency::Daily: freqIndex = 0; break;
+ case RecurrenceFrequency::Weekly: freqIndex = 1; break;
+ case RecurrenceFrequency::BiWeekly: freqIndex = 2; break;
+ case RecurrenceFrequency::Monthly: freqIndex = 3; break;
+ case RecurrenceFrequency::Yearly: freqIndex = 4; break;
+ default: break;
+ }
+ ui->recurringFrequencyCombo->setCurrentIndex(freqIndex);
+ updateAmountColors();
+}
+
+void CashFlow::updateAmountColors() {
+ // Color code transaction amount
+ if (ui->entryAmountSpin->value() < 0) {
+ ui->entryAmountSpin->setStyleSheet("QDoubleSpinBox { color: rgb(200, 0, 0); font-weight: bold; }");
+ } else if (ui->entryAmountSpin->value() > 0) {
+ ui->entryAmountSpin->setStyleSheet("QDoubleSpinBox { color: rgb(0, 150, 0); font-weight: bold; }");
+ } else {
+ ui->entryAmountSpin->setStyleSheet("");
+ }
+
+ // Color code recurring amount
+ if (ui->recurringAmountSpin->value() < 0) {
+ ui->recurringAmountSpin->setStyleSheet("QDoubleSpinBox { color: rgb(200, 0, 0); font-weight: bold; }");
+ } else if (ui->recurringAmountSpin->value() > 0) {
+ ui->recurringAmountSpin->setStyleSheet("QDoubleSpinBox { color: rgb(0, 150, 0); font-weight: bold; }");
+ } else {
+ ui->recurringAmountSpin->setStyleSheet("");
+ }
+}
+
+void CashFlow::loadSettings() {
+ // Load settings from database
+ QString currency = database->getSetting("currency_symbol", "$");
+ QString fontFamily = database->getSetting("amount_font", "Courier New");
+ int fontSize = database->getSetting("amount_font_size", "10").toInt();
+ int defaultPeriod = database->getSetting("default_period", "2").toInt(); // 2 = Monthly
+ bool showAccountBalances = database->getSetting("show_account_balances", "0").toInt();
+
+ // Set settings UI
+ ui->currencyEdit->setText(currency);
+ currentAmountFont = QFont(fontFamily, fontSize);
+ ui->amountFontBtn->setText(QString("%1, %2pt").arg(fontFamily).arg(fontSize));
+ ui->defaultPeriodCombo->setCurrentIndex(defaultPeriod);
+ ui->defaultShowAccountBalancesCheck->setChecked(showAccountBalances);
+
+ // Apply defaults to main UI (only on initial load)
+ ui->periodCombo->setCurrentIndex(defaultPeriod);
+ ui->showAccountBalancesCheck->setChecked(showAccountBalances);
+}
+
+void CashFlow::applySettings() {
+ // Currency symbol is now part of the formatted text in line edits
+ // Font is already stored in currentAmountFont
+
+ // Refresh to apply font changes
+ refreshView();
+}
+
+void CashFlow::onSaveSettings() {
+ // Save settings to database
+ database->setSetting("currency_symbol", ui->currencyEdit->text());
+ database->setSetting("amount_font", currentAmountFont.family());
+ database->setSetting("amount_font_size", QString::number(currentAmountFont.pointSize()));
+ database->setSetting("default_period", QString::number(ui->defaultPeriodCombo->currentIndex()));
+ database->setSetting("show_account_balances", QString::number(ui->defaultShowAccountBalancesCheck->isChecked() ? 1 : 0));
+
+ // Apply settings
+ applySettings();
+
+ QMessageBox::information(this, "Settings Saved", "Settings have been saved successfully.");
+}
+
+void CashFlow::onChooseAmountFont() {
+ bool ok;
+ QFont selectedFont = QFontDialog::getFont(&ok, currentAmountFont, this, "Choose Amount Font", QFontDialog::MonospacedFonts);
+
+ if (ok) {
+ currentAmountFont = selectedFont;
+ ui->amountFontBtn->setText(QString("%1, %2pt").arg(selectedFont.family()).arg(selectedFont.pointSize()));
+ }
+}
+
+QString CashFlow::formatCurrency(double amount) const {
+ QLocale locale;
+ return locale.toString(amount, 'f', 2);
+}
+
+