diff options
| -rw-r--r-- | .gitignore | 33 | ||||
| -rw-r--r-- | CashFlo.pro | 27 | ||||
| -rw-r--r-- | README.md | 147 | ||||
| -rw-r--r-- | cashflow.cpp | 907 | ||||
| -rw-r--r-- | cashflow.h | 72 | ||||
| -rw-r--r-- | cashflow.ui | 619 | ||||
| -rw-r--r-- | database.cpp | 364 | ||||
| -rw-r--r-- | database.h | 50 | ||||
| -rw-r--r-- | main.cpp | 11 | ||||
| -rw-r--r-- | transaction.h | 56 |
10 files changed, 2286 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..611ddb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Qt build artifacts +*.o +moc_*.cpp +moc_predefs.h +ui_*.h +qrc_*.cpp + +# Compiled binary +CashFlo + +# Qt Creator user files +*.pro.user +*.pro.user.* + +# Database files (user data) +*.db +*.db-shm +*.db-wal + +# OS specific +.DS_Store +Thumbs.db +*~ +*.swp +*.swo + +# IDE +.vscode/ +.idea/ + +# Makefiles (generated by qmake) +Makefile +.qmake.stash diff --git a/CashFlo.pro b/CashFlo.pro new file mode 100644 index 0000000..70333c2 --- /dev/null +++ b/CashFlo.pro @@ -0,0 +1,27 @@ +QT += core gui sql + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++17 + +# You can make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp \ + cashflow.cpp \ + database.cpp + +HEADERS += \ + cashflow.h \ + database.h \ + transaction.h + +FORMS += \ + cashflow.ui + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/README.md b/README.md new file mode 100644 index 0000000..3845713 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# CashFlo - Cash Flow Projection & Reconciliation App + +A Qt-based cash flow projection application with SQLite backend for managing financial transactions and forecasting future cash flow. + +## Features + +### Core Functionality +- **Transaction Management**: Add, edit, and delete transactions with date, amount, account, and description +- **Transaction Types**: + - **Estimated**: Future projections based on recurring rules + - **Actual**: Confirmed transactions after reconciliation +- **Recurring Rules**: Define repeating transactions with various schedules: + - Daily + - Weekly (specify day of week) + - Bi-Weekly (every 2 weeks) + - Monthly (specify day of month) + - Yearly +- **Projection Generation**: Automatically create estimated future transactions from recurring rules +- **Reconciliation**: Convert estimated transactions to actuals with updated amounts +- **Date Range Filtering**: View transactions within specific date ranges +- **Running Balance**: See projected balance based on selected date range + +## Building the Application + +### Prerequisites +- Qt 6 (with Widgets and SQL modules) +- C++17 compiler +- SQLite (included with Qt) + +### Build Instructions +```bash +cd /home/calvin/CashFlo +qmake6 +make +./CashFlo +``` + +## Usage Guide + +### 1. Setting Up Recurring Rules +1. Click the **"Recurring Rules"** tab +2. Click **"Add Recurring Rule"** +3. Enter details: + - Name (e.g., "Rent", "Salary") + - Frequency (Daily, Weekly, Monthly, etc.) + - Start Date + - Number of occurrences (-1 for indefinite) + - Amount (negative for expenses, positive for income) + - Account name + - Description + +**Examples:** +- **Monthly Rent**: Frequency: Monthly, Day: 1, Amount: -2350.00, Occurrences: 12 +- **Bi-weekly Salary**: Frequency: Bi-Weekly, Day: Friday (5), Amount: 3500.00 +- **Weekly Grocery**: Frequency: Weekly, Day: Saturday (6), Amount: -250.00 + +### 2. Generating Projections +1. Set your desired date range (From/To dates) +2. Click **"Generate Projection"** +3. The system will create estimated transactions based on your recurring rules +4. View all projected transactions in the Transactions tab + - Yellow rows = Estimated transactions + - Green rows = Actual transactions + +### 3. Adding Manual Transactions +1. Go to the **"Transactions"** tab +2. Click **"Add Transaction"** +3. Enter date, amount, account, description, and type (Estimated/Actual) + +### 4. Reconciliation (Closing the Books) +When an estimated transaction occurs and you want to record the actual amount: +1. Select the estimated transaction in the table +2. Click **"Reconcile (Est → Actual)"** +3. Enter the actual amount (can be different from estimate) +4. The transaction is now marked as "Actual" and the amount is updated + +### 5. Viewing Cash Flow +- The **"Projected Balance"** shows the cumulative balance for the selected date range +- Sort transactions by clicking column headers +- Adjust date range to see different projection periods + +## Database Schema + +### Transactions Table +```sql +id INTEGER PRIMARY KEY +date TEXT (ISO format) +amount REAL (negative=expense, positive=income) +account TEXT +description TEXT +type TEXT ('estimated' or 'actual') +recurring_id INTEGER (links to recurring rule, -1 if manual) +``` + +### Recurring Rules Table +```sql +id INTEGER PRIMARY KEY +name TEXT +frequency TEXT ('daily', 'weekly', 'biweekly', 'monthly', 'yearly') +start_date TEXT +end_date TEXT (optional) +amount REAL +account TEXT +description TEXT +day_of_week INTEGER (1=Mon, 7=Sun, -1 if not applicable) +day_of_month INTEGER (1-31, -1 if not applicable) +occurrences INTEGER (-1 for indefinite) +``` + +## Database Location +The SQLite database is stored at: +- Linux: `~/.local/share/CashFlo/cashflow.db` +- Created automatically on first run + +## Use Cases + +### Example: 3-Month Budget Planning +1. Add recurring rules for: + - Rent (-$2,350/month on 1st) + - Salary (+$7,000/month, bi-weekly) + - Utilities (-$150/month on 15th) + - Groceries (-$250/week on Saturdays) +2. Set date range: Jan 1 - Mar 31 +3. Generate projection +4. View projected balance to see cash flow over 3 months + +### Example: Reconciling Actual Expenses +1. At end of month, review estimated transactions +2. Select each estimate (e.g., "Grocery estimated $250") +3. Reconcile with actual amount (e.g., actual was $267.43) +4. Transaction converts to "Actual" with correct amount +5. Projected balance updates to reflect reality + +## Tips +- Use negative amounts for expenses (outflows) +- Use positive amounts for income (inflows) +- Generate projections after adding/updating recurring rules +- Reconcile regularly to keep projections accurate +- Use descriptive account names (e.g., "Checking", "Savings", "Credit Card") + +## Future Enhancements (Potential) +- Multiple accounts with transfers +- Categories and reporting +- CSV import/export +- Charts and visualizations +- Budget vs actual comparison +- Search and filtering 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); +} + + diff --git a/cashflow.h b/cashflow.h new file mode 100644 index 0000000..9de01ae --- /dev/null +++ b/cashflow.h @@ -0,0 +1,72 @@ +#ifndef CASHFLOW_H +#define CASHFLOW_H + +#include <QMainWindow> +#include <QTableWidget> +#include <QDate> +#include "database.h" +#include "transaction.h" + +QT_BEGIN_NAMESPACE +namespace Ui { class CashFlow; } +QT_END_NAMESPACE + +class CashFlow : public QMainWindow +{ + Q_OBJECT + +public: + CashFlow(QWidget *parent = nullptr); + ~CashFlow(); + +private slots: + void onDateRangeChanged(); + void onTransactionSelected(); + void onSaveTransaction(); + void onTransactionFieldChanged(); + void onNewTransaction(); + void onDeleteTransaction(); + void onRecurringSelected(); + void onSaveRecurring(); + void onNewRecurring(); + void onDeleteRecurring(); + void onPeriodChanged(); + +private: + Ui::CashFlow *ui; + Database *database; + int currentTransactionId; + int currentRecurringId; + double startingBalance; + QFont currentAmountFont; + + enum PeriodType { + Daily, + Weekly, + Monthly, + Quarterly + }; + + void setupConnections(); + void refreshView(); + void refreshTransactionTable(); + void refreshRecurringTable(); + void calculateAndDisplayBalance(); + QList<Transaction> getAllTransactionsInRange(); + QList<Transaction> generateProjectedTransactions(); + void clearTransactionEntry(); + void loadTransactionToEntry(const Transaction &t); + void clearRecurringEntry(); + void loadRecurringToEntry(const RecurringRule &r); + QDate getPeriodEnd(const QDate &date, PeriodType periodType); + QDate getPeriodStart(const QDate &date, PeriodType periodType); + QString getPeriodLabel(const QDate &date, PeriodType periodType, int count); + void insertPeriodEndRow(const QString &label, double balance, const QMap<QString, double> &accountBalances); + void updateAmountColors(); + void loadSettings(); + void applySettings(); + void onSaveSettings(); + void onChooseAmountFont(); + QString formatCurrency(double amount) const; +}; +#endif // CASHFLOW_H diff --git a/cashflow.ui b/cashflow.ui new file mode 100644 index 0000000..6592c0d --- /dev/null +++ b/cashflow.ui @@ -0,0 +1,619 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CashFlow</class> + <widget class="QMainWindow" name="CashFlow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1200</width> + <height>700</height> + </rect> + </property> + <property name="windowTitle"> + <string>CashFlo - Cash Flow Projection</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="dateRangeBox"> + <property name="title"> + <string>Date Range</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>From:</string> + </property> + </widget> + </item> + <item> + <widget class="QDateEdit" name="dateFromEdit"> + <property name="calendarPopup"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>To:</string> + </property> + </widget> + </item> + <item> + <widget class="QDateEdit" name="dateToEdit"> + <property name="calendarPopup"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_14"> + <property name="text"> + <string>Period:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="periodCombo"> + <item> + <property name="text"> + <string>Daily</string> + </property> + </item> + <item> + <property name="text"> + <string>Weekly</string> + </property> + </item> + <item> + <property name="text"> + <string>Monthly</string> + </property> + </item> + <item> + <property name="text"> + <string>Quarterly</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string>Account:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="accountFilterCombo"> + <item> + <property name="text"> + <string>All Accounts</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QCheckBox" name="showAccountBalancesCheck"> + <property name="text"> + <string>Show Account Balances</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="startBalanceLabel"> + <property name="text"> + <string>Starting Balance: $0.00</string> + </property> + <property name="styleSheet"> + <string notr="true">font-weight: bold; font-size: 12pt;</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="endBalanceLabel"> + <property name="text"> + <string>Ending Balance: $0.00</string> + </property> + <property name="styleSheet"> + <string notr="true">font-weight: bold; font-size: 12pt; color: blue;</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="transactionsTab"> + <attribute name="title"> + <string>Transactions</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTableWidget" name="transactionTable"> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="sortingEnabled"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="entryBox"> + <property name="title"> + <string>Transaction Entry</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Date:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QDateEdit" name="entryDateEdit"> + <property name="calendarPopup"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Amount:</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QDoubleSpinBox" name="entryAmountSpin"> + <property name="buttonSymbols"> + <enum>QAbstractSpinBox::NoButtons</enum> + </property> + <property name="prefix"> + <string>$ </string> + </property> + <property name="decimals"> + <number>2</number> + </property> + <property name="minimum"> + <double>-999999.990000000000000</double> + </property> + <property name="maximum"> + <double>999999.990000000000000</double> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Account:</string> + </property> + </widget> + </item> + <item row="0" column="5"> + <widget class="QComboBox" name="entryAccountCombo"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_16"> + <property name="text"> + <string>Category:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="entryCategoryCombo"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="2" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Description:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="entryDescriptionEdit"/> + </item> + </layout> + </item> + <item row="1" column="4"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Type:</string> + </property> + </widget> + </item> + <item row="1" column="5"> + <widget class="QComboBox" name="entryTypeCombo"> + <item> + <property name="text"> + <string>Estimated</string> + </property> + </item> + <item> + <property name="text"> + <string>Actual</string> + </property> + </item> + </widget> + </item> + <item row="2" column="0" colspan="3"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="saveBtn"> + <property name="text"> + <string>Save</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="newBtn"> + <property name="text"> + <string>New</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="deleteBtn"> + <property name="text"> + <string>Delete</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="4" colspan="2"> + <widget class="QLabel" name="entryStatusLabel"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="recurringTab"> + <attribute name="title"> + <string>Recurring Rules</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTableWidget" name="recurringTable"> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="recurringEntryBox"> + <property name="title"> + <string>Recurring Rule Entry</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Name:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="recurringNameEdit"/> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Frequency:</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QComboBox" name="recurringFrequencyCombo"> + <item> + <property name="text"> + <string>Daily</string> + </property> + </item> + <item> + <property name="text"> + <string>Weekly</string> + </property> + </item> + <item> + <property name="text"> + <string>Bi-Weekly</string> + </property> + </item> + <item> + <property name="text"> + <string>Monthly</string> + </property> + </item> + <item> + <property name="text"> + <string>Yearly</string> + </property> + </item> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Start Date:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QDateEdit" name="recurringStartDateEdit"> + <property name="calendarPopup"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Amount:</string> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QDoubleSpinBox" name="recurringAmountSpin"> + <property name="buttonSymbols"> + <enum>QAbstractSpinBox::NoButtons</enum> + </property> + <property name="prefix"> + <string>$ </string> + </property> + <property name="decimals"> + <number>2</number> + </property> + <property name="minimum"> + <double>-999999.990000000000000</double> + </property> + <property name="maximum"> + <double>999999.990000000000000</double> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_12"> + <property name="text"> + <string>Account:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QComboBox" name="recurringAccountCombo"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QLabel" name="label_17"> + <property name="text"> + <string>Category:</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QComboBox" name="recurringCategoryCombo"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_13"> + <property name="text"> + <string>Description:</string> + </property> + </widget> + </item> + <item row="3" column="1" colspan="3"> + <widget class="QLineEdit" name="recurringDescriptionEdit"/> + </item> + <item row="4" column="0" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="saveRecurringBtn"> + <property name="text"> + <string>Save</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="newRecurringBtn"> + <property name="text"> + <string>New</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="deleteRecurringBtn"> + <property name="text"> + <string>Delete</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QLabel" name="infoLabel"> + <property name="text"> + <string>Recurring rules automatically generate estimated transactions in the date range. Edit dates to see projections update.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="settingsTab"> + <attribute name="title"> + <string>Settings</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QGroupBox" name="settingsGroupBox"> + <property name="title"> + <string>Application Settings</string> + </property> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_18"> + <property name="text"> + <string>Currency Symbol:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="currencyEdit"> + <property name="maxLength"> + <number>3</number> + </property> + <property name="placeholderText"> + <string>$</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_19"> + <property name="text"> + <string>Amount Font:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="amountFontBtn"> + <property name="text"> + <string>Choose Font...</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_20"> + <property name="text"> + <string>Default Period:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QComboBox" name="defaultPeriodCombo"> + <item> + <property name="text"> + <string>Daily</string> + </property> + </item> + <item> + <property name="text"> + <string>Weekly</string> + </property> + </item> + <item> + <property name="text"> + <string>Monthly</string> + </property> + </item> + <item> + <property name="text"> + <string>Quarterly</string> + </property> + </item> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <widget class="QCheckBox" name="defaultShowAccountBalancesCheck"> + <property name="text"> + <string>Show Account Balances by Default</string> + </property> + </widget> + </item> + <item row="4" column="0" colspan="2"> + <widget class="QPushButton" name="saveSettingsBtn"> + <property name="text"> + <string>Save Settings</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1200</width> + <height>22</height> + </rect> + </property> + </widget> + <widget class="QStatusBar" name="statusbar"/> + </widget> + <resources/> + <connections/> +</ui> diff --git a/database.cpp b/database.cpp new file mode 100644 index 0000000..5f9d99c --- /dev/null +++ b/database.cpp @@ -0,0 +1,364 @@ +#include "database.h" +#include <QSqlDatabase> +#include <QSqlQuery> +#include <QSqlError> +#include <QDebug> + +Database::Database() { + db = QSqlDatabase::addDatabase("QSQLITE"); +} + +Database::~Database() { + if (db.isOpen()) { + db.close(); + } +} + +bool Database::open(const QString &path) { + db.setDatabaseName(path); + if (!db.open()) { + errorMsg = db.lastError().text(); + return false; + } + return createTables(); +} + +bool Database::createTables() { + QSqlQuery query; + + // Create transactions table + QString createTransactions = R"( + CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + amount REAL NOT NULL, + account TEXT NOT NULL, + category TEXT, + description TEXT, + type TEXT NOT NULL, + recurring_id INTEGER DEFAULT -1, + sort_order INTEGER DEFAULT 0 + ) + )"; + + if (!query.exec(createTransactions)) { + errorMsg = query.lastError().text(); + return false; + } + + // Create recurring_rules table + QString createRecurring = R"( + CREATE TABLE IF NOT EXISTS recurring_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + frequency TEXT NOT NULL, + start_date TEXT NOT NULL, + end_date TEXT, + amount REAL NOT NULL, + account TEXT NOT NULL, + category TEXT, + description TEXT, + day_of_week INTEGER DEFAULT -1, + day_of_month INTEGER DEFAULT -1, + occurrences INTEGER DEFAULT -1, + sort_order INTEGER DEFAULT 0 + ) + )"; + + if (!query.exec(createRecurring)) { + errorMsg = query.lastError().text(); + return false; + } + + // Create settings table + QString createSettings = R"( + CREATE TABLE IF NOT EXISTS settings ( + setting_name TEXT PRIMARY KEY, + setting_value TEXT + ) + )"; + + if (!query.exec(createSettings)) { + errorMsg = query.lastError().text(); + return false; + } + + // Migrate existing tables - add category column if missing + query.exec("ALTER TABLE transactions ADD COLUMN category TEXT"); + query.exec("ALTER TABLE recurring_rules ADD COLUMN category TEXT"); + + // Migrate existing tables - add sort_order column if missing + query.exec("ALTER TABLE transactions ADD COLUMN sort_order INTEGER DEFAULT 0"); + query.exec("ALTER TABLE recurring_rules ADD COLUMN sort_order INTEGER DEFAULT 0"); + + return true; +} + +bool Database::addTransaction(const Transaction &transaction) { + QSqlQuery query; + query.prepare("INSERT INTO transactions (date, amount, account, category, description, type, recurring_id, sort_order) " + "VALUES (:date, :amount, :account, :category, :description, :type, :recurring_id, :sort_order)"); + query.bindValue(":date", transaction.date.toString(Qt::ISODate)); + query.bindValue(":amount", transaction.amount); + query.bindValue(":account", transaction.account); + query.bindValue(":category", transaction.category); + query.bindValue(":description", transaction.description); + query.bindValue(":type", transaction.type == TransactionType::Actual ? "actual" : "estimated"); + query.bindValue(":recurring_id", transaction.recurringId); + query.bindValue(":sort_order", transaction.sortOrder); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return false; + } + return true; +} + +bool Database::updateTransaction(const Transaction &transaction) { + QSqlQuery query; + query.prepare("UPDATE transactions SET date=:date, amount=:amount, account=:account, category=:category, " + "description=:description, type=:type, recurring_id=:recurring_id, sort_order=:sort_order WHERE id=:id"); + query.bindValue(":id", transaction.id); + query.bindValue(":date", transaction.date.toString(Qt::ISODate)); + query.bindValue(":amount", transaction.amount); + query.bindValue(":account", transaction.account); + query.bindValue(":category", transaction.category); + query.bindValue(":description", transaction.description); + query.bindValue(":type", transaction.type == TransactionType::Actual ? "actual" : "estimated"); + query.bindValue(":recurring_id", transaction.recurringId); + query.bindValue(":sort_order", transaction.sortOrder); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return false; + } + return true; +} + +bool Database::deleteTransaction(int id) { + QSqlQuery query; + query.prepare("DELETE FROM transactions WHERE id=:id"); + query.bindValue(":id", id); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return false; + } + return true; +} + +QList<Transaction> Database::getTransactions(const QDate &startDate, const QDate &endDate) { + QList<Transaction> transactions; + QSqlQuery query; + query.prepare("SELECT * FROM transactions WHERE date >= :start AND date <= :end ORDER BY date"); + query.bindValue(":start", startDate.toString(Qt::ISODate)); + query.bindValue(":end", endDate.toString(Qt::ISODate)); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return transactions; + } + + while (query.next()) { + transactions.append(queryToTransaction(query)); + } + return transactions; +} + +QList<Transaction> Database::getAllTransactions() { + QList<Transaction> transactions; + QSqlQuery query("SELECT * FROM transactions ORDER BY date"); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return transactions; + } + + while (query.next()) { + transactions.append(queryToTransaction(query)); + } + return transactions; +} + +bool Database::addRecurringRule(const RecurringRule &rule) { + QSqlQuery query; + query.prepare("INSERT INTO recurring_rules (name, frequency, start_date, end_date, amount, " + "account, category, description, day_of_week, day_of_month, occurrences, sort_order) " + "VALUES (:name, :frequency, :start_date, :end_date, :amount, :account, " + ":category, :description, :day_of_week, :day_of_month, :occurrences, :sort_order)"); + query.bindValue(":name", rule.name); + + QString freq; + switch (rule.frequency) { + case RecurrenceFrequency::Daily: freq = "daily"; break; + case RecurrenceFrequency::Weekly: freq = "weekly"; break; + case RecurrenceFrequency::BiWeekly: freq = "biweekly"; break; + case RecurrenceFrequency::Monthly: freq = "monthly"; break; + case RecurrenceFrequency::Yearly: freq = "yearly"; break; + default: freq = "none"; break; + } + query.bindValue(":frequency", freq); + query.bindValue(":start_date", rule.startDate.toString(Qt::ISODate)); + query.bindValue(":end_date", rule.endDate.isValid() ? rule.endDate.toString(Qt::ISODate) : QVariant()); + query.bindValue(":amount", rule.amount); + query.bindValue(":account", rule.account); + query.bindValue(":category", rule.category); + query.bindValue(":description", rule.description); + query.bindValue(":day_of_week", rule.dayOfWeek); + query.bindValue(":day_of_month", rule.dayOfMonth); + query.bindValue(":occurrences", rule.occurrences); + query.bindValue(":sort_order", rule.sortOrder); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return false; + } + return true; +} + +bool Database::updateRecurringRule(const RecurringRule &rule) { + QSqlQuery query; + query.prepare("UPDATE recurring_rules SET name=:name, frequency=:frequency, start_date=:start_date, " + "end_date=:end_date, amount=:amount, account=:account, category=:category, description=:description, " + "day_of_week=:day_of_week, day_of_month=:day_of_month, occurrences=:occurrences, sort_order=:sort_order WHERE id=:id"); + query.bindValue(":id", rule.id); + query.bindValue(":name", rule.name); + + QString freq; + switch (rule.frequency) { + case RecurrenceFrequency::Daily: freq = "daily"; break; + case RecurrenceFrequency::Weekly: freq = "weekly"; break; + case RecurrenceFrequency::BiWeekly: freq = "biweekly"; break; + case RecurrenceFrequency::Monthly: freq = "monthly"; break; + case RecurrenceFrequency::Yearly: freq = "yearly"; break; + default: freq = "none"; break; + } + query.bindValue(":frequency", freq); + query.bindValue(":start_date", rule.startDate.toString(Qt::ISODate)); + query.bindValue(":end_date", rule.endDate.isValid() ? rule.endDate.toString(Qt::ISODate) : QVariant()); + query.bindValue(":amount", rule.amount); + query.bindValue(":account", rule.account); + query.bindValue(":category", rule.category); + query.bindValue(":description", rule.description); + query.bindValue(":day_of_week", rule.dayOfWeek); + query.bindValue(":day_of_month", rule.dayOfMonth); + query.bindValue(":occurrences", rule.occurrences); + query.bindValue(":sort_order", rule.sortOrder); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return false; + } + return true; +} + +bool Database::deleteRecurringRule(int id) { + QSqlQuery query; + query.prepare("DELETE FROM recurring_rules WHERE id=:id"); + query.bindValue(":id", id); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return false; + } + return true; +} + +QList<RecurringRule> Database::getAllRecurringRules() { + QList<RecurringRule> rules; + QSqlQuery query("SELECT * FROM recurring_rules"); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return rules; + } + + while (query.next()) { + rules.append(queryToRecurringRule(query)); + } + return rules; +} + +bool Database::convertToActual(int transactionId, double actualAmount) { + QSqlQuery query; + query.prepare("UPDATE transactions SET type='actual', amount=:amount WHERE id=:id"); + query.bindValue(":amount", actualAmount); + query.bindValue(":id", transactionId); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return false; + } + return true; +} + +QString Database::lastError() const { + return errorMsg; +} + +Transaction Database::queryToTransaction(QSqlQuery &query) { + Transaction t; + t.id = query.value("id").toInt(); + t.date = QDate::fromString(query.value("date").toString(), Qt::ISODate); + t.amount = query.value("amount").toDouble(); + t.account = query.value("account").toString(); + t.category = query.value("category").toString(); + t.description = query.value("description").toString(); + t.type = query.value("type").toString() == "actual" ? TransactionType::Actual : TransactionType::Estimated; + t.recurringId = query.value("recurring_id").toInt(); + t.sortOrder = query.value("sort_order").toInt(); + return t; +} + +RecurringRule Database::queryToRecurringRule(QSqlQuery &query) { + RecurringRule r; + r.id = query.value("id").toInt(); + r.name = query.value("name").toString(); + + QString freq = query.value("frequency").toString(); + if (freq == "daily") r.frequency = RecurrenceFrequency::Daily; + else if (freq == "weekly") r.frequency = RecurrenceFrequency::Weekly; + else if (freq == "biweekly") r.frequency = RecurrenceFrequency::BiWeekly; + else if (freq == "monthly") r.frequency = RecurrenceFrequency::Monthly; + else if (freq == "yearly") r.frequency = RecurrenceFrequency::Yearly; + else r.frequency = RecurrenceFrequency::None; + + r.startDate = QDate::fromString(query.value("start_date").toString(), Qt::ISODate); + QString endDateStr = query.value("end_date").toString(); + if (!endDateStr.isEmpty()) { + r.endDate = QDate::fromString(endDateStr, Qt::ISODate); + } + r.amount = query.value("amount").toDouble(); + r.account = query.value("account").toString(); + r.category = query.value("category").toString(); + r.description = query.value("description").toString(); + r.dayOfWeek = query.value("day_of_week").toInt(); + r.dayOfMonth = query.value("day_of_month").toInt(); + r.occurrences = query.value("occurrences").toInt(); + r.sortOrder = query.value("sort_order").toInt(); + return r; +} + +QString Database::getSetting(const QString &name, const QString &defaultValue) { + QSqlQuery query; + query.prepare("SELECT setting_value FROM settings WHERE setting_name = :name"); + query.bindValue(":name", name); + + if (query.exec() && query.next()) { + return query.value(0).toString(); + } + return defaultValue; +} + +bool Database::setSetting(const QString &name, const QString &value) { + QSqlQuery query; + query.prepare("INSERT OR REPLACE INTO settings (setting_name, setting_value) VALUES (:name, :value)"); + query.bindValue(":name", name); + query.bindValue(":value", value); + + if (!query.exec()) { + errorMsg = query.lastError().text(); + return false; + } + return true; +} diff --git a/database.h b/database.h new file mode 100644 index 0000000..cdde765 --- /dev/null +++ b/database.h @@ -0,0 +1,50 @@ +#ifndef DATABASE_H +#define DATABASE_H + +#include <QSqlDatabase> +#include <QSqlQuery> +#include <QSqlError> +#include <QString> +#include <QList> +#include <QDate> +#include "transaction.h" + +class Database { +public: + Database(); + ~Database(); + + bool open(const QString &path); + bool createTables(); + + // Transaction operations + bool addTransaction(const Transaction &transaction); + bool updateTransaction(const Transaction &transaction); + bool deleteTransaction(int id); + QList<Transaction> getTransactions(const QDate &startDate, const QDate &endDate); + QList<Transaction> getAllTransactions(); + + // Recurring rule operations + bool addRecurringRule(const RecurringRule &rule); + bool updateRecurringRule(const RecurringRule &rule); + bool deleteRecurringRule(int id); + QList<RecurringRule> getAllRecurringRules(); + + // Reconciliation + bool convertToActual(int transactionId, double actualAmount); + + // Settings + QString getSetting(const QString &name, const QString &defaultValue = QString()); + bool setSetting(const QString &name, const QString &value); + + QString lastError() const; + +private: + QSqlDatabase db; + QString errorMsg; + + Transaction queryToTransaction(QSqlQuery &query); + RecurringRule queryToRecurringRule(QSqlQuery &query); +}; + +#endif // DATABASE_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..9553f70 --- /dev/null +++ b/main.cpp @@ -0,0 +1,11 @@ +#include "cashflow.h" + +#include <QApplication> + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + CashFlow w; + w.show(); + return a.exec(); +} diff --git a/transaction.h b/transaction.h new file mode 100644 index 0000000..8aa0a06 --- /dev/null +++ b/transaction.h @@ -0,0 +1,56 @@ +#ifndef TRANSACTION_H +#define TRANSACTION_H + +#include <QString> +#include <QDate> + +enum class TransactionType { + Estimated, + Actual +}; + +enum class RecurrenceFrequency { + None, + Daily, + Weekly, + BiWeekly, + Monthly, + Yearly +}; + +struct Transaction { + int id; + QDate date; + double amount; + QString account; + QString category; + QString description; + TransactionType type; + int recurringId; // -1 if not part of recurring series + int sortOrder; // For ordering on same date + + Transaction() : id(-1), amount(0.0), type(TransactionType::Estimated), recurringId(-1), sortOrder(0) {} + + double getBalance() const { return amount; } +}; + +struct RecurringRule { + int id; + QString name; + RecurrenceFrequency frequency; + QDate startDate; + QDate endDate; // Can be null for indefinite + double amount; + QString account; + QString category; + QString description; + int dayOfWeek; // 1-7 for weekly (1=Monday), -1 if not applicable + int dayOfMonth; // 1-31 for monthly, -1 if not applicable + int occurrences; // Number of times to repeat, -1 for indefinite + int sortOrder; // For ordering transactions on same date + + RecurringRule() : id(-1), frequency(RecurrenceFrequency::None), + amount(0.0), dayOfWeek(-1), dayOfMonth(-1), occurrences(-1), sortOrder(0) {} +}; + +#endif // TRANSACTION_H |
