#include "cashflow.h" #include "ui_cashflow.h" #include #include #include #include #include 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::of(&QComboBox::currentIndexChanged), this, &CashFlow::onPeriodChanged); connect(ui->accountFilterCombo, QOverload::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::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::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::of(&QComboBox::currentIndexChanged), this, &CashFlow::onTransactionFieldChanged); // Color-code amount inputs connect(ui->entryAmountSpin, QOverload::of(&QDoubleSpinBox::valueChanged), this, &CashFlow::updateAmountColors); connect(ui->recurringAmountSpin, QOverload::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 accounts; QSet 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 allTransactions = getAllTransactionsInRange(); ui->transactionTable->setRowCount(0); ui->transactionTable->setColumnCount(7); ui->transactionTable->setHorizontalHeaderLabels({"Date", "Amount", "Balance", "Account", "Category", "Description", "Type"}); double runningBalance = startingBalance; QMap accountBalances; // Track per-account balances QDate currentPeriodEnd; QString periodLabel; int periodCount = 1; // Determine period type PeriodType periodType = static_cast(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 &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 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 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 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 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 actualTransactions = database->getTransactions(startDate, endDate); // Generate projected transactions from recurring rules QList projectedTransactions = generateProjectedTransactions(); // Combine QList allTransactions = actualTransactions + projectedTransactions; // Filter by account if not "All Accounts" if (accountFilter != "All Accounts") { QList 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 CashFlow::generateProjectedTransactions() { QList projections; QList 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 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 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 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 selected = ui->recurringTable->selectedItems(); if (selected.isEmpty()) { return; } int row = selected[0]->row(); int id = ui->recurringTable->item(row, 0)->text().toInt(); QList 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); }