aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore33
-rw-r--r--CashFlo.pro27
-rw-r--r--README.md147
-rw-r--r--cashflow.cpp907
-rw-r--r--cashflow.h72
-rw-r--r--cashflow.ui619
-rw-r--r--database.cpp364
-rw-r--r--database.h50
-rw-r--r--main.cpp11
-rw-r--r--transaction.h56
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