diff options
| author | Calvin Morrison <calvin@pobox.com> | 2025-12-27 14:19:21 -0500 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2025-12-27 14:19:21 -0500 |
| commit | 88b069141faafd1c5aefda1573b2285a38885ce4 (patch) | |
| tree | a99e069672be94edd087ef49e7a22d23a0eb0fd0 /cashflow.cpp | |
initial commit
Diffstat (limited to 'cashflow.cpp')
| -rw-r--r-- | cashflow.cpp | 907 |
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); +} + + |
