Expense Tracker – Complete Project IN HTML CSSS AND JAVASCRIPT WITH PHP AND MY SQL

Introduction to the Project

The Expense Tracker is a comprehensive, full-stack web application designed to help individuals and businesses manage their finances effectively. This system provides users with a powerful platform to track income, monitor expenses, analyze spending patterns, and achieve financial goals. With role-based access control, the application supports multiple user types: Admin, Individual Users, and Family Members (for shared accounts).

The application features intuitive dashboards, visual analytics, budget management, and detailed reporting. Whether you're managing personal finances, household budgets, or small business expenses, this expense tracker offers all the essential tools needed for effective financial management.

Key Features

Core Features

  • Income Tracking: Record and categorize various income sources
  • Expense Management: Log daily expenses with categories and payment methods
  • Budget Planning: Set monthly budgets for different categories
  • Financial Dashboard: Visual overview of financial health with charts and graphs
  • Transaction History: Complete history with search and filter capabilities
  • Multi-Currency Support: Handle transactions in different currencies
  • Receipt Upload: Attach receipts to transactions for documentation

Advanced Features

  • Recurring Transactions: Set up automatic recurring income/expenses
  • Budget Alerts: Get notifications when approaching budget limits
  • Savings Goals: Set and track progress towards savings targets
  • Debt Management: Track loans, credit cards, and other liabilities
  • Investment Tracking: Monitor investments and portfolio performance
  • Tax Categories: Categorize expenses for tax purposes
  • Export Reports: Generate and export financial reports (PDF/Excel)
  • Data Visualization: Interactive charts for spending analysis
  • Shared Accounts: Family or team accounts with permission levels
  • Financial Insights: AI-powered spending insights and recommendations

User Roles

Admin:

  • Manage all users and accounts
  • View system-wide statistics
  • Configure system settings
  • Monitor platform usage
  • Generate global reports

Individual User:

  • Personal dashboard
  • Add/edit/delete transactions
  • Set budgets and goals
  • View analytics and reports
  • Export personal data

Family Member (Shared Account):

  • Limited access to shared accounts
  • Add expenses to shared categories
  • View shared budgets and goals

Technology Stack

  • Frontend: HTML5, CSS3, JavaScript (ES6+), Chart.js for analytics
  • Backend: PHP 8.0+ (Core PHP with OOP approach)
  • Database: MySQL
  • Additional Libraries:
  • Chart.js for data visualization
  • Bootstrap 5 for responsive UI
  • Font Awesome for icons
  • jQuery for AJAX operations
  • Select2 for enhanced dropdowns
  • DataTables for advanced tables
  • Moment.js for date handling
  • TCPDF for PDF generation
  • PhpSpreadsheet for Excel export

Project File Structure

expense-tracker/
│
├── assets/
│   ├── css/
│   │   ├── style.css
│   │   ├── dashboard.css
│   │   ├── responsive.css
│   │   └── dark-mode.css
│   ├── js/
│   │   ├── main.js
│   │   ├── dashboard.js
│   │   ├── charts.js
│   │   ├── transactions.js
│   │   ├── budget.js
│   │   └── validation.js
│   ├── images/
│   │   ├── receipts/
│   │   ├── avatars/
│   │   └── icons/
│   └── plugins/
│       ├── chart.js/
│       ├── datatables/
│       └── select2/
│
├── includes/
│   ├── config.php
│   ├── Database.php
│   ├── functions.php
│   ├── auth.php
│   ├── Transaction.php
│   ├── Category.php
│   ├── Budget.php
│   ├── User.php
│   ├── Report.php
│   └── helpers/
│       ├── CurrencyHelper.php
│       ├── DateHelper.php
│       └── ValidationHelper.php
│
├── admin/
│   ├── dashboard.php
│   ├── manage_users.php
│   ├── user_details.php
│   ├── system_settings.php
│   ├── categories.php
│   ├── reports.php
│   └── logs.php
│
├── user/
│   ├── dashboard.php
│   ├── transactions.php
│   ├── add_transaction.php
│   ├── edit_transaction.php
│   ├── categories.php
│   ├── budgets.php
│   ├── add_budget.php
│   ├── reports.php
│   ├── analytics.php
│   ├── goals.php
│   ├── profile.php
│   ├── settings.php
│   └── export.php
│
├── api/
│   ├── transactions.php
│   ├── categories.php
│   ├── budgets.php
│   ├── charts.php
│   └── search.php
│
├── uploads/
│   └── receipts/
│
├── vendor/
│
├── index.php
├── login.php
├── register.php
├── forgot_password.php
├── reset_password.php
├── logout.php
├── .env
├── .gitignore
├── composer.json
└── sql/
└── database.sql

Database Schema

File: sql/database.sql

-- Create Database
CREATE DATABASE IF NOT EXISTS `expense_tracker`;
USE `expense_tracker`;
-- Users Table
CREATE TABLE `users` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) UNIQUE NOT NULL,
`email` VARCHAR(100) UNIQUE NOT NULL,
`password` VARCHAR(255) NOT NULL,
`first_name` VARCHAR(50) NOT NULL,
`last_name` VARCHAR(50) NOT NULL,
`profile_picture` VARCHAR(255) DEFAULT 'default.png',
`role` ENUM('admin', 'user', 'family') DEFAULT 'user',
`account_type` ENUM('personal', 'family', 'business') DEFAULT 'personal',
`family_id` INT(11),
`currency` VARCHAR(3) DEFAULT 'USD',
`timezone` VARCHAR(50) DEFAULT 'America/New_York',
`monthly_budget` DECIMAL(10,2) DEFAULT 0.00,
`total_balance` DECIMAL(10,2) DEFAULT 0.00,
`email_verified` BOOLEAN DEFAULT FALSE,
`verification_token` VARCHAR(255),
`reset_token` VARCHAR(255),
`reset_expires` DATETIME,
`is_active` BOOLEAN DEFAULT TRUE,
`last_login` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`family_id`) REFERENCES `users`(`id`)
);
-- Categories Table
CREATE TABLE `categories` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`type` ENUM('income', 'expense', 'both') DEFAULT 'both',
`icon` VARCHAR(50) DEFAULT 'fa-tag',
`color` VARCHAR(7) DEFAULT '#6c757d',
`parent_id` INT(11),
`is_system` BOOLEAN DEFAULT FALSE,
`is_active` BOOLEAN DEFAULT TRUE,
`budget_amount` DECIMAL(10,2) DEFAULT 0.00,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`parent_id`) REFERENCES `categories`(`id`)
);
-- Default System Categories (will be inserted per user)
-- Income: Salary, Freelance, Investment, Gift, Refund, Other Income
-- Expenses: Food & Dining, Shopping, Transport, Entertainment, Bills & Utilities, 
--           Healthcare, Education, Travel, Personal Care, Home, Insurance, Taxes, Other Expenses
-- Payment Methods Table
CREATE TABLE `payment_methods` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`name` VARCHAR(50) NOT NULL,
`type` ENUM('cash', 'bank', 'credit_card', 'debit_card', 'digital_wallet', 'other') DEFAULT 'cash',
`account_number` VARCHAR(50),
`bank_name` VARCHAR(100),
`card_last_four` VARCHAR(4),
`initial_balance` DECIMAL(10,2) DEFAULT 0.00,
`current_balance` DECIMAL(10,2) DEFAULT 0.00,
`credit_limit` DECIMAL(10,2),
`billing_date` INT(2),
`due_date` INT(2),
`icon` VARCHAR(50) DEFAULT 'fa-credit-card',
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
-- Default Payment Methods: Cash, Bank Account, Credit Card, Debit Card, PayPal, etc.
-- Transactions Table
CREATE TABLE `transactions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`category_id` INT(11) NOT NULL,
`payment_method_id` INT(11),
`type` ENUM('income', 'expense', 'transfer') NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`description` VARCHAR(255),
`date` DATE NOT NULL,
`time` TIME,
`location` VARCHAR(255),
`receipt_path` VARCHAR(255),
`notes` TEXT,
`is_recurring` BOOLEAN DEFAULT FALSE,
`recurring_id` INT(11),
`is_tax_deductible` BOOLEAN DEFAULT FALSE,
`tax_category` VARCHAR(50),
`tags` VARCHAR(255),
`status` ENUM('completed', 'pending', 'cancelled') DEFAULT 'completed',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`),
FOREIGN KEY (`payment_method_id`) REFERENCES `payment_methods`(`id`)
);
-- Recurring Transactions Table
CREATE TABLE `recurring_transactions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`category_id` INT(11) NOT NULL,
`payment_method_id` INT(11),
`type` ENUM('income', 'expense') NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`description` VARCHAR(255),
`frequency` ENUM('daily', 'weekly', 'monthly', 'yearly') NOT NULL,
`interval_count` INT DEFAULT 1,
`start_date` DATE NOT NULL,
`end_date` DATE,
`next_date` DATE NOT NULL,
`last_processed` DATE,
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`),
FOREIGN KEY (`payment_method_id`) REFERENCES `payment_methods`(`id`)
);
-- Budgets Table
CREATE TABLE `budgets` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`category_id` INT(11) NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`period` ENUM('monthly', 'yearly') DEFAULT 'monthly',
`month` INT(2),
`year` INT(4),
`spent` DECIMAL(10,2) DEFAULT 0.00,
`remaining` DECIMAL(10,2) DEFAULT 0.00,
`alert_threshold` INT DEFAULT 80,
`alert_sent` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_budget` (`user_id`, `category_id`, `period`, `month`, `year`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON DELETE CASCADE
);
-- Savings Goals Table
CREATE TABLE `savings_goals` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`target_amount` DECIMAL(10,2) NOT NULL,
`current_amount` DECIMAL(10,2) DEFAULT 0.00,
`start_date` DATE NOT NULL,
`target_date` DATE,
`category_id` INT(11),
`icon` VARCHAR(50) DEFAULT 'fa-flag',
`color` VARCHAR(7) DEFAULT '#28a745',
`notes` TEXT,
`status` ENUM('active', 'completed', 'cancelled') DEFAULT 'active',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`)
);
-- Debts/Loans Table
CREATE TABLE `debts` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`type` ENUM('credit_card', 'loan', 'mortgage', 'personal', 'other') NOT NULL,
`total_amount` DECIMAL(10,2) NOT NULL,
`paid_amount` DECIMAL(10,2) DEFAULT 0.00,
`interest_rate` DECIMAL(5,2),
`minimum_payment` DECIMAL(10,2),
`due_date` INT(2),
`start_date` DATE,
`end_date` DATE,
`creditor` VARCHAR(100),
`notes` TEXT,
`status` ENUM('active', 'paid', 'defaulted') DEFAULT 'active',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
-- Investments Table
CREATE TABLE `investments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`type` ENUM('stock', 'bond', 'mutual_fund', 'etf', 'crypto', 'real_estate', 'other') NOT NULL,
`symbol` VARCHAR(20),
`quantity` DECIMAL(10,2),
`purchase_price` DECIMAL(10,2),
`current_price` DECIMAL(10,2),
`purchase_date` DATE,
`notes` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
-- Notifications Table
CREATE TABLE `notifications` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`type` ENUM('budget_alert', 'bill_reminder', 'goal_achieved', 'system', 'tip') NOT NULL,
`title` VARCHAR(255) NOT NULL,
`message` TEXT NOT NULL,
`is_read` BOOLEAN DEFAULT FALSE,
`action_url` VARCHAR(255),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
-- Attachments Table
CREATE TABLE `attachments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`transaction_id` INT(11),
`filename` VARCHAR(255) NOT NULL,
`original_name` VARCHAR(255) NOT NULL,
`file_path` VARCHAR(255) NOT NULL,
`file_size` INT,
`file_type` VARCHAR(50),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`transaction_id`) REFERENCES `transactions`(`id`) ON DELETE CASCADE
);
-- Settings Table
CREATE TABLE `settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`setting_key` VARCHAR(100) NOT NULL,
`setting_value` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_setting` (`user_id`, `setting_key`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
-- System Settings Table (Admin only)
CREATE TABLE `system_settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`setting_key` VARCHAR(100) UNIQUE NOT NULL,
`setting_value` TEXT,
`description` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
-- Insert Default System Settings
INSERT INTO `system_settings` (`setting_key`, `setting_value`, `description`) VALUES
('app_name', 'Expense Tracker', 'Application name'),
('app_version', '1.0.0', 'Application version'),
('default_currency', 'USD', 'Default currency'),
('date_format', 'Y-m-d', 'Date format'),
('time_format', 'H:i:s', 'Time format'),
('enable_receipt_upload', '1', 'Enable receipt upload feature'),
('max_file_size', '5', 'Maximum file size in MB'),
('allowed_file_types', 'jpg,jpeg,png,pdf', 'Allowed file types'),
('session_timeout', '30', 'Session timeout in minutes'),
('items_per_page', '20', 'Items per page in tables');
-- Insert Default Admin
INSERT INTO `users` (`username`, `email`, `password`, `first_name`, `last_name`, `role`, `email_verified`) 
VALUES ('admin', '[email protected]', '$2y$10$YourHashedPasswordHere', 'System', 'Administrator', 'admin', TRUE);
-- Insert Sample Payment Methods
INSERT INTO `payment_methods` (`user_id`, `name`, `type`, `icon`) VALUES
(1, 'Cash', 'cash', 'fa-money-bill-wave'),
(1, 'Bank Account', 'bank', 'fa-university'),
(1, 'Credit Card', 'credit_card', 'fa-credit-card');
-- Insert Sample Categories for Admin
INSERT INTO `categories` (`user_id`, `name`, `type`, `icon`, `color`, `is_system`) VALUES
(1, 'Salary', 'income', 'fa-briefcase', '#28a745', TRUE),
(1, 'Freelance', 'income', 'fa-laptop', '#17a2b8', TRUE),
(1, 'Investment', 'income', 'fa-chart-line', '#ffc107', TRUE),
(1, 'Gift', 'income', 'fa-gift', '#6f42c1', TRUE),
(1, 'Other Income', 'income', 'fa-plus-circle', '#6c757d', TRUE),
(1, 'Food & Dining', 'expense', 'fa-utensils', '#dc3545', TRUE),
(1, 'Shopping', 'expense', 'fa-shopping-bag', '#fd7e14', TRUE),
(1, 'Transport', 'expense', 'fa-car', '#007bff', TRUE),
(1, 'Entertainment', 'expense', 'fa-film', '#e83e8c', TRUE),
(1, 'Bills & Utilities', 'expense', 'fa-file-invoice', '#20c997', TRUE),
(1, 'Healthcare', 'expense', 'fa-hospital', '#6c757d', TRUE),
(1, 'Education', 'expense', 'fa-graduation-cap', '#6610f2', TRUE),
(1, 'Travel', 'expense', 'fa-plane', '#d63384', TRUE),
(1, 'Personal Care', 'expense', 'fa-smile', '#ff851b', TRUE),
(1, 'Home', 'expense', 'fa-home', '#7c4dff', TRUE),
(1, 'Insurance', 'expense', 'fa-shield-alt', '#39cccc', TRUE),
(1, 'Taxes', 'expense', 'fa-file-invoice-dollar', '#ff4136', TRUE),
(1, 'Other Expenses', 'expense', 'fa-ellipsis-h', '#aaaaaa', TRUE);

Core PHP Classes

Database Class

File: includes/Database.php

<?php
/**
* Database Class
* Handles all database connections and operations using PDO
*/
class Database {
private static $instance = null;
private $connection;
private $statement;
private $host;
private $dbname;
private $username;
private $password;
/**
* Private constructor for singleton pattern
*/
private function __construct() {
$this->host = DB_HOST;
$this->dbname = DB_NAME;
$this->username = DB_USER;
$this->password = DB_PASS;
try {
$this->connection = new PDO(
"mysql:host={$this->host};dbname={$this->dbname};charset=utf8mb4",
$this->username,
$this->password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
/**
* Get database instance (Singleton)
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Prepare and execute query with parameters
*/
public function query($sql, $params = []) {
try {
$this->statement = $this->connection->prepare($sql);
$this->statement->execute($params);
return $this->statement;
} catch (PDOException $e) {
$this->logError($e->getMessage(), $sql, $params);
throw new Exception("Database query failed: " . $e->getMessage());
}
}
/**
* Get single row
*/
public function getRow($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetch();
}
/**
* Get multiple rows
*/
public function getRows($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchAll();
}
/**
* Get single value
*/
public function getValue($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchColumn();
}
/**
* Insert data and return last insert ID
*/
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$this->query($sql, $data);
return $this->connection->lastInsertId();
}
/**
* Update data
*/
public function update($table, $data, $where, $whereParams = []) {
$set = [];
foreach (array_keys($data) as $column) {
$set[] = "{$column} = :{$column}";
}
$sql = "UPDATE {$table} SET " . implode(', ', $set) . " WHERE {$where}";
$params = array_merge($data, $whereParams);
return $this->query($sql, $params)->rowCount();
}
/**
* Delete data
*/
public function delete($table, $where, $params = []) {
$sql = "DELETE FROM {$table} WHERE {$where}";
return $this->query($sql, $params)->rowCount();
}
/**
* Begin transaction
*/
public function beginTransaction() {
return $this->connection->beginTransaction();
}
/**
* Commit transaction
*/
public function commit() {
return $this->connection->commit();
}
/**
* Rollback transaction
*/
public function rollback() {
return $this->connection->rollBack();
}
/**
* Get last insert ID
*/
public function lastInsertId() {
return $this->connection->lastInsertId();
}
/**
* Log database errors
*/
private function logError($message, $sql, $params) {
$logFile = __DIR__ . '/../logs/database.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] Error: {$message}\n";
$logMessage .= "SQL: {$sql}\n";
$logMessage .= "Params: " . json_encode($params) . "\n";
$logMessage .= "------------------------\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Prevent cloning of the instance
*/
private function __clone() {}
/**
* Prevent unserializing of the instance
*/
public function __wakeup() {}
}
?>

Configuration File

File: includes/config.php

<?php
/**
* Configuration File
* Loads environment variables and sets up constants
*/
// Start session if not started
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Load environment variables from .env file
function loadEnv($path) {
if (!file_exists($path)) {
return false;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!array_key_exists($name, $_ENV)) {
$_ENV[$name] = $value;
putenv(sprintf('%s=%s', $name, $value));
}
}
return true;
}
// Load environment variables
loadEnv(__DIR__ . '/../.env');
// Database Configuration
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_NAME', getenv('DB_NAME') ?: 'expense_tracker');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASS') ?: '');
// Application Configuration
define('APP_NAME', getenv('APP_NAME') ?: 'Expense Tracker');
define('APP_URL', getenv('APP_URL') ?: 'http://localhost/expense-tracker');
define('APP_VERSION', getenv('APP_VERSION') ?: '1.0.0');
define('DEBUG_MODE', getenv('DEBUG_MODE') === 'true');
// Security Configuration
define('SESSION_TIMEOUT', getenv('SESSION_TIMEOUT') ?: 1800); // 30 minutes
define('BCRYPT_ROUNDS', 12);
define('CSRF_TOKEN_NAME', 'csrf_token');
// Upload Configuration
define('UPLOAD_DIR', __DIR__ . '/../uploads/');
define('MAX_FILE_SIZE', getenv('MAX_FILE_SIZE') ?: 5 * 1024 * 1024); // 5MB
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'pdf']);
// Pagination
define('ITEMS_PER_PAGE', getenv('ITEMS_PER_PAGE') ?: 20);
// Date/Time Configuration
date_default_timezone_set(getenv('TIMEZONE') ?: 'America/New_York');
define('DATE_FORMAT', 'Y-m-d');
define('TIME_FORMAT', 'H:i:s');
define('DATETIME_FORMAT', 'Y-m-d H:i:s');
// Currency Configuration
define('DEFAULT_CURRENCY', getenv('DEFAULT_CURRENCY') ?: 'USD');
define('CURRENCY_SYMBOL', getenv('CURRENCY_SYMBOL') ?: '$');
// Error Reporting
if (DEBUG_MODE) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
// Include required files
require_once __DIR__ . '/Database.php';
require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/Transaction.php';
require_once __DIR__ . '/Category.php';
require_once __DIR__ . '/Budget.php';
require_once __DIR__ . '/User.php';
require_once __DIR__ . '/Report.php';
// Initialize database connection
$db = Database::getInstance();
// Set timezone for MySQL
$db->query("SET time_zone = ?", [date('P')]);
?>

Helper Functions

File: includes/functions.php

<?php
/**
* Helper Functions
* Common utility functions used throughout the application
*/
/**
* Sanitize input data
*/
function sanitize($input) {
if (is_array($input)) {
return array_map('sanitize', $input);
}
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
/**
* Generate CSRF token
*/
function generateCSRFToken() {
if (!isset($_SESSION[CSRF_TOKEN_NAME])) {
$_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
}
return $_SESSION[CSRF_TOKEN_NAME];
}
/**
* Verify CSRF token
*/
function verifyCSRFToken($token) {
if (!isset($_SESSION[CSRF_TOKEN_NAME]) || $token !== $_SESSION[CSRF_TOKEN_NAME]) {
return false;
}
return true;
}
/**
* Redirect to URL
*/
function redirect($url) {
header("Location: " . APP_URL . $url);
exit();
}
/**
* Display formatted amount
*/
function formatAmount($amount, $currency = null) {
if ($currency === null) {
$currency = $_SESSION['currency'] ?? DEFAULT_CURRENCY;
}
$symbols = [
'USD' => '$',
'EUR' => '€',
'GBP' => '£',
'JPY' => '¥',
'INR' => '₹'
];
$symbol = $symbols[$currency] ?? CURRENCY_SYMBOL;
return $symbol . ' ' . number_format($amount, 2);
}
/**
* Format date
*/
function formatDate($date, $format = null) {
if ($format === null) {
$format = $_SESSION['date_format'] ?? DATE_FORMAT;
}
if ($date instanceof DateTime) {
return $date->format($format);
}
return date($format, strtotime($date));
}
/**
* Get time ago string
*/
function timeAgo($datetime) {
$time = strtotime($datetime);
$now = time();
$diff = $now - $time;
if ($diff < 60) {
return $diff . ' seconds ago';
} elseif ($diff < 3600) {
return floor($diff / 60) . ' minutes ago';
} elseif ($diff < 86400) {
return floor($diff / 3600) . ' hours ago';
} elseif ($diff < 2592000) {
return floor($diff / 86400) . ' days ago';
} elseif ($diff < 31536000) {
return floor($diff / 2592000) . ' months ago';
} else {
return floor($diff / 31536000) . ' years ago';
}
}
/**
* Get percentage
*/
function getPercentage($value, $total) {
if ($total == 0) {
return 0;
}
return round(($value / $total) * 100, 2);
}
/**
* Get random color
*/
function getRandomColor($index = null) {
$colors = [
'#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b',
'#858796', '#5a5c69', '#2c3e50', '#fd7e14', '#6f42c1',
'#e83e8c', '#20c9a6', '#17a2b8', '#6610f2', '#d63384'
];
if ($index !== null) {
return $colors[$index % count($colors)];
}
return $colors[array_rand($colors)];
}
/**
* Generate unique filename
*/
function generateUniqueFilename($extension) {
return uniqid() . '_' . time() . '.' . $extension;
}
/**
* Upload file
*/
function uploadFile($file, $targetDir, $allowedTypes = null) {
if ($allowedTypes === null) {
$allowedTypes = ALLOWED_EXTENSIONS;
}
// Check for errors
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => 'Upload failed with error code: ' . $file['error']];
}
// Check file size
if ($file['size'] > MAX_FILE_SIZE) {
return ['success' => false, 'error' => 'File size exceeds limit'];
}
// Check file type
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedTypes)) {
return ['success' => false, 'error' => 'File type not allowed'];
}
// Generate unique filename
$filename = generateUniqueFilename($extension);
$targetPath = $targetDir . '/' . $filename;
// Create directory if not exists
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
// Upload file
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
return [
'success' => true,
'filename' => $filename,
'original_name' => $file['name'],
'path' => $targetPath
];
}
return ['success' => false, 'error' => 'Failed to move uploaded file'];
}
/**
* Delete file
*/
function deleteFile($path) {
if (file_exists($path)) {
return unlink($path);
}
return false;
}
/**
* Get month name
*/
function getMonthName($month) {
$months = [
1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April',
5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August',
9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
];
return $months[$month] ?? '';
}
/**
* Get months between dates
*/
function getMonthsBetween($startDate, $endDate) {
$start = new DateTime($startDate);
$end = new DateTime($endDate);
$interval = DateInterval::createFromDateString('1 month');
$period = new DatePeriod($start, $interval, $end);
$months = [];
foreach ($period as $dt) {
$months[] = $dt->format('Y-m');
}
return $months;
}
/**
* Send email
*/
function sendEmail($to, $subject, $message, $from = null) {
if ($from === null) {
$from = 'noreply@' . $_SERVER['HTTP_HOST'];
}
$headers = [
'MIME-Version: 1.0',
'Content-type: text/html; charset=utf-8',
'From: ' . $from,
'Reply-To: ' . $from,
'X-Mailer: PHP/' . phpversion()
];
return mail($to, $subject, $message, implode("\r\n", $headers));
}
/**
* Generate pagination
*/
function paginate($currentPage, $totalPages, $url) {
if ($totalPages <= 1) {
return '';
}
$html = '<nav aria-label="Page navigation"><ul class="pagination">';
// Previous button
if ($currentPage > 1) {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '?page=' . ($currentPage - 1) . '">Previous</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Previous</span></li>';
}
// Page numbers
for ($i = 1; $i <= $totalPages; $i++) {
if ($i == $currentPage) {
$html .= '<li class="page-item active"><span class="page-link">' . $i . '</span></li>';
} else {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '?page=' . $i . '">' . $i . '</a></li>';
}
}
// Next button
if ($currentPage < $totalPages) {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '?page=' . ($currentPage + 1) . '">Next</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Next</span></li>';
}
$html .= '</ul></nav>';
return $html;
}
/**
* Log error
*/
function logError($message, $context = []) {
$logFile = __DIR__ . '/../logs/error.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? ' ' . json_encode($context) : '';
$logMessage = "[{$timestamp}] {$message}{$contextStr}\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Get user IP address
*/
function getUserIP() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
return $_SERVER['REMOTE_ADDR'];
}
}
/**
* Generate random string
*/
function generateRandomString($length = 32) {
return bin2hex(random_bytes($length / 2));
}
/**
* Validate email
*/
function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Validate date
*/
function validateDate($date, $format = 'Y-m-d') {
$d = DateTime::createFromFormat($format, $date);
return $d && $d->format($format) === $date;
}
/**
* Get file size in human readable format
*/
function humanFileSize($bytes, $decimals = 2) {
$size = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . $size[$factor];
}
?>

Authentication Class

File: includes/auth.php

<?php
/**
* Authentication Class
* Handles user authentication, registration, and session management
*/
class Auth {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Register new user
*/
public function register($data) {
try {
// Check if username or email already exists
$existing = $this->db->getRow(
"SELECT id FROM users WHERE username = ? OR email = ?",
[$data['username'], $data['email']]
);
if ($existing) {
return ['success' => false, 'error' => 'Username or email already exists'];
}
// Hash password
$hashedPassword = password_hash($data['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
// Generate verification token
$verificationToken = generateRandomString();
// Insert user
$userId = $this->db->insert('users', [
'username' => $data['username'],
'email' => $data['email'],
'password' => $hashedPassword,
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'currency' => $data['currency'] ?? DEFAULT_CURRENCY,
'verification_token' => $verificationToken
]);
if ($userId) {
// Create default categories for user
$this->createDefaultCategories($userId);
// Create default payment methods
$this->createDefaultPaymentMethods($userId);
// Send verification email
$this->sendVerificationEmail($data['email'], $verificationToken);
return [
'success' => true,
'user_id' => $userId,
'message' => 'Registration successful. Please check your email to verify your account.'
];
}
return ['success' => false, 'error' => 'Registration failed'];
} catch (Exception $e) {
logError('Registration error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Registration failed: ' . $e->getMessage()];
}
}
/**
* Login user
*/
public function login($username, $password, $remember = false) {
try {
// Get user
$user = $this->db->getRow(
"SELECT * FROM users WHERE (username = ? OR email = ?) AND is_active = 1",
[$username, $username]
);
if (!$user) {
return ['success' => false, 'error' => 'Invalid username or password'];
}
// Check if email verified
if (!$user['email_verified']) {
return ['success' => false, 'error' => 'Please verify your email before logging in'];
}
// Verify password
if (!password_verify($password, $user['password'])) {
return ['success' => false, 'error' => 'Invalid username or password'];
}
// Check if password needs rehash
if (password_needs_rehash($user['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS])) {
$newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $newHash], 'id = :id', ['id' => $user['id']]);
}
// Set session
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['user_name'] = $user['first_name'] . ' ' . $user['last_name'];
$_SESSION['currency'] = $user['currency'];
$_SESSION['logged_in'] = true;
$_SESSION['login_time'] = time();
// Update last login
$this->db->update(
'users',
['last_login' => date('Y-m-d H:i:s')],
'id = :id',
['id' => $user['id']]
);
// Set remember me cookie
if ($remember) {
$this->setRememberMe($user['id']);
}
return ['success' => true, 'user' => $user];
} catch (Exception $e) {
logError('Login error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Login failed'];
}
}
/**
* Set remember me cookie
*/
private function setRememberMe($userId) {
$token = generateRandomString(64);
$expires = time() + (86400 * 30); // 30 days
// Store token in database (you would need a remember_tokens table)
// For now, just set cookie
setcookie('remember_token', $token, $expires, '/', '', false, true);
}
/**
* Logout user
*/
public function logout() {
// Clear session
$_SESSION = array();
// Clear session cookie
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
// Clear remember me cookie
setcookie('remember_token', '', time() - 3600, '/');
// Destroy session
session_destroy();
}
/**
* Check if user is logged in
*/
public function isLoggedIn() {
return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
}
/**
* Get current user
*/
public function getCurrentUser() {
if (!$this->isLoggedIn()) {
return null;
}
return $this->db->getRow(
"SELECT * FROM users WHERE id = ?",
[$_SESSION['user_id']]
);
}
/**
* Check if user has role
*/
public function hasRole($role) {
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === $role;
}
/**
* Require login
*/
public function requireLogin() {
if (!$this->isLoggedIn()) {
$_SESSION['error'] = 'Please login to access this page';
redirect('/login.php');
}
}
/**
* Require role
*/
public function requireRole($role) {
$this->requireLogin();
if (!$this->hasRole($role)) {
$_SESSION['error'] = 'You do not have permission to access this page';
redirect('/user/dashboard.php');
}
}
/**
* Verify email
*/
public function verifyEmail($token) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE verification_token = ?",
[$token]
);
if ($user) {
$this->db->update(
'users',
['email_verified' => true, 'verification_token' => null],
'id = :id',
['id' => $user['id']]
);
return true;
}
return false;
}
/**
* Send verification email
*/
private function sendVerificationEmail($email, $token) {
$subject = "Verify your email - " . APP_NAME;
$message = "Click the link to verify your email: " . APP_URL . "/verify.php?token=" . $token;
return sendEmail($email, $subject, $message);
}
/**
* Forgot password
*/
public function forgotPassword($email) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE email = ?",
[$email]
);
if ($user) {
$token = generateRandomString();
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
$this->db->update(
'users',
['reset_token' => $token, 'reset_expires' => $expires],
'id = :id',
['id' => $user['id']]
);
// Send reset email
$subject = "Password Reset - " . APP_NAME;
$message = "Click the link to reset your password: " . APP_URL . "/reset_password.php?token=" . $token;
return sendEmail($email, $subject, $message);
}
return false;
}
/**
* Reset password
*/
public function resetPassword($token, $password) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE reset_token = ? AND reset_expires > NOW()",
[$token]
);
if ($user) {
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update(
'users',
['password' => $hashedPassword, 'reset_token' => null, 'reset_expires' => null],
'id = :id',
['id' => $user['id']]
);
return true;
}
return false;
}
/**
* Create default categories for new user
*/
private function createDefaultCategories($userId) {
$incomeCategories = [
['name' => 'Salary', 'icon' => 'fa-briefcase', 'color' => '#28a745', 'type' => 'income'],
['name' => 'Freelance', 'icon' => 'fa-laptop', 'color' => '#17a2b8', 'type' => 'income'],
['name' => 'Investment', 'icon' => 'fa-chart-line', 'color' => '#ffc107', 'type' => 'income'],
['name' => 'Gift', 'icon' => 'fa-gift', 'color' => '#6f42c1', 'type' => 'income'],
['name' => 'Other Income', 'icon' => 'fa-plus-circle', 'color' => '#6c757d', 'type' => 'income']
];
$expenseCategories = [
['name' => 'Food & Dining', 'icon' => 'fa-utensils', 'color' => '#dc3545', 'type' => 'expense'],
['name' => 'Shopping', 'icon' => 'fa-shopping-bag', 'color' => '#fd7e14', 'type' => 'expense'],
['name' => 'Transport', 'icon' => 'fa-car', 'color' => '#007bff', 'type' => 'expense'],
['name' => 'Entertainment', 'icon' => 'fa-film', 'color' => '#e83e8c', 'type' => 'expense'],
['name' => 'Bills & Utilities', 'icon' => 'fa-file-invoice', 'color' => '#20c997', 'type' => 'expense'],
['name' => 'Healthcare', 'icon' => 'fa-hospital', 'color' => '#6c757d', 'type' => 'expense'],
['name' => 'Education', 'icon' => 'fa-graduation-cap', 'color' => '#6610f2', 'type' => 'expense'],
['name' => 'Travel', 'icon' => 'fa-plane', 'color' => '#d63384', 'type' => 'expense'],
['name' => 'Personal Care', 'icon' => 'fa-smile', 'color' => '#ff851b', 'type' => 'expense'],
['name' => 'Home', 'icon' => 'fa-home', 'color' => '#7c4dff', 'type' => 'expense'],
['name' => 'Insurance', 'icon' => 'fa-shield-alt', 'color' => '#39cccc', 'type' => 'expense'],
['name' => 'Taxes', 'icon' => 'fa-file-invoice-dollar', 'color' => '#ff4136', 'type' => 'expense'],
['name' => 'Other Expenses', 'icon' => 'fa-ellipsis-h', 'color' => '#aaaaaa', 'type' => 'expense']
];
foreach ($incomeCategories as $category) {
$category['user_id'] = $userId;
$category['is_system'] = true;
$this->db->insert('categories', $category);
}
foreach ($expenseCategories as $category) {
$category['user_id'] = $userId;
$category['is_system'] = true;
$this->db->insert('categories', $category);
}
}
/**
* Create default payment methods for new user
*/
private function createDefaultPaymentMethods($userId) {
$methods = [
['name' => 'Cash', 'type' => 'cash', 'icon' => 'fa-money-bill-wave'],
['name' => 'Bank Account', 'type' => 'bank', 'icon' => 'fa-university'],
['name' => 'Credit Card', 'type' => 'credit_card', 'icon' => 'fa-credit-card']
];
foreach ($methods as $method) {
$method['user_id'] = $userId;
$this->db->insert('payment_methods', $method);
}
}
/**
* Update user profile
*/
public function updateProfile($userId, $data) {
try {
$updateData = [
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'currency' => $data['currency']
];
$this->db->update('users', $updateData, 'id = :id', ['id' => $userId]);
$_SESSION['currency'] = $data['currency'];
return ['success' => true, 'message' => 'Profile updated successfully'];
} catch (Exception $e) {
logError('Profile update error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to update profile'];
}
}
/**
* Change password
*/
public function changePassword($userId, $currentPassword, $newPassword) {
$user = $this->db->getRow("SELECT password FROM users WHERE id = ?", [$userId]);
if (!password_verify($currentPassword, $user['password'])) {
return ['success' => false, 'error' => 'Current password is incorrect'];
}
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $hashedPassword], 'id = :id', ['id' => $userId]);
return ['success' => true, 'message' => 'Password changed successfully'];
}
}
// Initialize Auth
$auth = new Auth();
?>

Transaction Class

File: includes/Transaction.php

<?php
/**
* Transaction Class
* Handles all transaction-related operations
*/
class Transaction {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Add transaction
*/
public function add($userId, $data) {
try {
$this->db->beginTransaction();
// Prepare transaction data
$transactionData = [
'user_id' => $userId,
'category_id' => $data['category_id'],
'payment_method_id' => $data['payment_method_id'] ?? null,
'type' => $data['type'],
'amount' => $data['amount'],
'description' => $data['description'] ?? null,
'date' => $data['date'],
'time' => $data['time'] ?? date('H:i:s'),
'location' => $data['location'] ?? null,
'notes' => $data['notes'] ?? null,
'is_tax_deductible' => $data['is_tax_deductible'] ?? false,
'tags' => $data['tags'] ?? null
];
// Handle receipt upload
if (isset($_FILES['receipt']) && $_FILES['receipt']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'receipts/';
$result = uploadFile($_FILES['receipt'], $uploadDir);
if ($result['success']) {
$transactionData['receipt_path'] = $result['filename'];
}
}
// Insert transaction
$transactionId = $this->db->insert('transactions', $transactionData);
// Update budget spent amount
$this->updateBudgetSpent($userId, $data['category_id'], $data['amount'], $data['date']);
// Update payment method balance
if (isset($data['payment_method_id'])) {
$this->updatePaymentMethodBalance(
$data['payment_method_id'],
$data['amount'],
$data['type']
);
}
// Update user total balance
$this->updateUserBalance($userId, $data['amount'], $data['type']);
$this->db->commit();
return [
'success' => true,
'transaction_id' => $transactionId,
'message' => 'Transaction added successfully'
];
} catch (Exception $e) {
$this->db->rollback();
logError('Add transaction error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Failed to add transaction'];
}
}
/**
* Update transaction
*/
public function update($transactionId, $userId, $data) {
try {
$this->db->beginTransaction();
// Get old transaction to adjust budgets and balances
$oldTransaction = $this->getById($transactionId, $userId);
// Prepare update data
$updateData = [
'category_id' => $data['category_id'],
'payment_method_id' => $data['payment_method_id'] ?? null,
'amount' => $data['amount'],
'description' => $data['description'] ?? null,
'date' => $data['date'],
'location' => $data['location'] ?? null,
'notes' => $data['notes'] ?? null,
'is_tax_deductible' => $data['is_tax_deductible'] ?? false,
'tags' => $data['tags'] ?? null
];
// Handle receipt upload
if (isset($_FILES['receipt']) && $_FILES['receipt']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'receipts/';
$result = uploadFile($_FILES['receipt'], $uploadDir);
if ($result['success']) {
// Delete old receipt if exists
if ($oldTransaction['receipt_path']) {
deleteFile(UPLOAD_DIR . 'receipts/' . $oldTransaction['receipt_path']);
}
$updateData['receipt_path'] = $result['filename'];
}
}
// Update transaction
$this->db->update(
'transactions',
$updateData,
'id = :id AND user_id = :user_id',
['id' => $transactionId, 'user_id' => $userId]
);
// Reverse old budget update
$this->updateBudgetSpent(
$userId,
$oldTransaction['category_id'],
-$oldTransaction['amount'],
$oldTransaction['date']
);
// Apply new budget update
$this->updateBudgetSpent($userId, $data['category_id'], $data['amount'], $data['date']);
// Update payment method balances
if ($oldTransaction['payment_method_id'] != $data['payment_method_id']) {
// Reverse old payment method
if ($oldTransaction['payment_method_id']) {
$this->updatePaymentMethodBalance(
$oldTransaction['payment_method_id'],
-$oldTransaction['amount'],
$oldTransaction['type']
);
}
// Apply new payment method
if (isset($data['payment_method_id'])) {
$this->updatePaymentMethodBalance(
$data['payment_method_id'],
$data['amount'],
$data['type']
);
}
} else if ($oldTransaction['payment_method_id']) {
// Adjust amount difference
$difference = $data['amount'] - $oldTransaction['amount'];
if ($difference != 0) {
$this->updatePaymentMethodBalance(
$data['payment_method_id'],
$difference,
$data['type']
);
}
}
// Update user balance
$balanceDifference = $data['amount'] - $oldTransaction['amount'];
if ($balanceDifference != 0) {
$this->updateUserBalance(
$userId,
$balanceDifference,
$data['type']
);
}
$this->db->commit();
return [
'success' => true,
'message' => 'Transaction updated successfully'
];
} catch (Exception $e) {
$this->db->rollback();
logError('Update transaction error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Failed to update transaction'];
}
}
/**
* Delete transaction
*/
public function delete($transactionId, $userId) {
try {
$this->db->beginTransaction();
// Get transaction
$transaction = $this->getById($transactionId, $userId);
if (!$transaction) {
return ['success' => false, 'error' => 'Transaction not found'];
}
// Reverse budget update
$this->updateBudgetSpent(
$userId,
$transaction['category_id'],
-$transaction['amount'],
$transaction['date']
);
// Reverse payment method balance
if ($transaction['payment_method_id']) {
$this->updatePaymentMethodBalance(
$transaction['payment_method_id'],
-$transaction['amount'],
$transaction['type']
);
}
// Reverse user balance
$this->updateUserBalance(
$userId,
-$transaction['amount'],
$transaction['type']
);
// Delete receipt if exists
if ($transaction['receipt_path']) {
deleteFile(UPLOAD_DIR . 'receipts/' . $transaction['receipt_path']);
}
// Delete transaction
$this->db->delete(
'transactions',
'id = :id AND user_id = :user_id',
['id' => $transactionId, 'user_id' => $userId]
);
$this->db->commit();
return [
'success' => true,
'message' => 'Transaction deleted successfully'
];
} catch (Exception $e) {
$this->db->rollback();
logError('Delete transaction error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to delete transaction'];
}
}
/**
* Get transaction by ID
*/
public function getById($transactionId, $userId) {
return $this->db->getRow(
"SELECT t.*, c.name as category_name, c.color as category_color,
pm.name as payment_method_name
FROM transactions t
LEFT JOIN categories c ON t.category_id = c.id
LEFT JOIN payment_methods pm ON t.payment_method_id = pm.id
WHERE t.id = ? AND t.user_id = ?",
[$transactionId, $userId]
);
}
/**
* Get user transactions with filters
*/
public function getUserTransactions($userId, $filters = []) {
$sql = "SELECT t.*, c.name as category_name, c.color as category_color,
pm.name as payment_method_name
FROM transactions t
LEFT JOIN categories c ON t.category_id = c.id
LEFT JOIN payment_methods pm ON t.payment_method_id = pm.id
WHERE t.user_id = :user_id";
$params = ['user_id' => $userId];
// Apply filters
if (!empty($filters['type'])) {
$sql .= " AND t.type = :type";
$params['type'] = $filters['type'];
}
if (!empty($filters['category_id'])) {
$sql .= " AND t.category_id = :category_id";
$params['category_id'] = $filters['category_id'];
}
if (!empty($filters['payment_method_id'])) {
$sql .= " AND t.payment_method_id = :payment_method_id";
$params['payment_method_id'] = $filters['payment_method_id'];
}
if (!empty($filters['start_date'])) {
$sql .= " AND t.date >= :start_date";
$params['start_date'] = $filters['start_date'];
}
if (!empty($filters['end_date'])) {
$sql .= " AND t.date <= :end_date";
$params['end_date'] = $filters['end_date'];
}
if (!empty($filters['search'])) {
$sql .= " AND (t.description LIKE :search OR t.notes LIKE :search)";
$params['search'] = '%' . $filters['search'] . '%';
}
if (!empty($filters['min_amount'])) {
$sql .= " AND t.amount >= :min_amount";
$params['min_amount'] = $filters['min_amount'];
}
if (!empty($filters['max_amount'])) {
$sql .= " AND t.amount <= :max_amount";
$params['max_amount'] = $filters['max_amount'];
}
// Order by
$sql .= " ORDER BY t.date DESC, t.time DESC";
// Pagination
$page = $filters['page'] ?? 1;
$limit = $filters['limit'] ?? ITEMS_PER_PAGE;
$offset = ($page - 1) * $limit;
$sql .= " LIMIT :limit OFFSET :offset";
$params['limit'] = $limit;
$params['offset'] = $offset;
return $this->db->getRows($sql, $params);
}
/**
* Get transaction count
*/
public function getTransactionCount($userId, $filters = []) {
$sql = "SELECT COUNT(*) as count FROM transactions WHERE user_id = :user_id";
$params = ['user_id' => $userId];
// Apply same filters as above
if (!empty($filters['type'])) {
$sql .= " AND type = :type";
$params['type'] = $filters['type'];
}
if (!empty($filters['start_date'])) {
$sql .= " AND date >= :start_date";
$params['start_date'] = $filters['start_date'];
}
if (!empty($filters['end_date'])) {
$sql .= " AND date <= :end_date";
$params['end_date'] = $filters['end_date'];
}
$result = $this->db->getRow($sql, $params);
return $result['count'];
}
/**
* Get monthly summary
*/
public function getMonthlySummary($userId, $year, $month) {
$startDate = "{$year}-{$month}-01";
$endDate = date('Y-m-t', strtotime($startDate));
$sql = "SELECT 
SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) as total_income,
SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as total_expense,
COUNT(*) as transaction_count
FROM transactions
WHERE user_id = :user_id
AND date BETWEEN :start_date AND :end_date";
return $this->db->getRow($sql, [
'user_id' => $userId,
'start_date' => $startDate,
'end_date' => $endDate
]);
}
/**
* Get category breakdown
*/
public function getCategoryBreakdown($userId, $type, $startDate, $endDate) {
$sql = "SELECT 
c.id,
c.name,
c.color,
SUM(t.amount) as total,
COUNT(*) as count
FROM transactions t
JOIN categories c ON t.category_id = c.id
WHERE t.user_id = :user_id
AND t.type = :type
AND t.date BETWEEN :start_date AND :end_date
GROUP BY t.category_id
ORDER BY total DESC";
return $this->db->getRows($sql, [
'user_id' => $userId,
'type' => $type,
'start_date' => $startDate,
'end_date' => $endDate
]);
}
/**
* Get daily totals for chart
*/
public function getDailyTotals($userId, $type, $year, $month) {
$startDate = "{$year}-{$month}-01";
$endDate = date('Y-m-t', strtotime($startDate));
$sql = "SELECT 
DATE(date) as day,
SUM(amount) as total
FROM transactions
WHERE user_id = :user_id
AND type = :type
AND date BETWEEN :start_date AND :end_date
GROUP BY DATE(date)
ORDER BY day ASC";
return $this->db->getRows($sql, [
'user_id' => $userId,
'type' => $type,
'start_date' => $startDate,
'end_date' => $endDate
]);
}
/**
* Update budget spent amount
*/
private function updateBudgetSpent($userId, $categoryId, $amount, $date) {
$year = date('Y', strtotime($date));
$month = date('m', strtotime($date));
// Get budget for this category and month
$budget = $this->db->getRow(
"SELECT id, spent FROM budgets 
WHERE user_id = :user_id 
AND category_id = :category_id 
AND period = 'monthly'
AND month = :month 
AND year = :year",
[
'user_id' => $userId,
'category_id' => $categoryId,
'month' => $month,
'year' => $year
]
);
if ($budget) {
$newSpent = $budget['spent'] + $amount;
$remaining = $this->db->getValue(
"SELECT amount FROM budgets WHERE id = ?",
[$budget['id']]
) - $newSpent;
$this->db->update(
'budgets',
['spent' => $newSpent, 'remaining' => $remaining],
'id = :id',
['id' => $budget['id']]
);
// Check alert threshold
$this->checkBudgetAlert($userId, $budget['id']);
}
}
/**
* Check budget alert
*/
private function checkBudgetAlert($userId, $budgetId) {
$budget = $this->db->getRow(
"SELECT * FROM budgets WHERE id = ?",
[$budgetId]
);
if ($budget && !$budget['alert_sent']) {
$percentage = ($budget['spent'] / $budget['amount']) * 100;
if ($percentage >= $budget['alert_threshold']) {
$category = $this->db->getRow(
"SELECT name FROM categories WHERE id = ?",
[$budget['category_id']]
);
// Create notification
$this->db->insert('notifications', [
'user_id' => $userId,
'type' => 'budget_alert',
'title' => 'Budget Alert',
'message' => "You've used {$percentage}% of your {$category['name']} budget",
'action_url' => '/user/budgets.php'
]);
// Update alert sent
$this->db->update(
'budgets',
['alert_sent' => true],
'id = :id',
['id' => $budgetId]
);
}
}
}
/**
* Update payment method balance
*/
private function updatePaymentMethodBalance($paymentMethodId, $amount, $type) {
$method = $this->db->getRow(
"SELECT * FROM payment_methods WHERE id = ?",
[$paymentMethodId]
);
if ($method) {
if ($type == 'income') {
$newBalance = $method['current_balance'] + $amount;
} else {
$newBalance = $method['current_balance'] - $amount;
}
$this->db->update(
'payment_methods',
['current_balance' => $newBalance],
'id = :id',
['id' => $paymentMethodId]
);
}
}
/**
* Update user total balance
*/
private function updateUserBalance($userId, $amount, $type) {
$user = $this->db->getRow(
"SELECT total_balance FROM users WHERE id = ?",
[$userId]
);
if ($user) {
if ($type == 'income') {
$newBalance = $user['total_balance'] + $amount;
} else {
$newBalance = $user['total_balance'] - $amount;
}
$this->db->update(
'users',
['total_balance' => $newBalance],
'id = :id',
['id' => $userId]
);
}
}
/**
* Get recurring transactions
*/
public function getRecurringTransactions($userId) {
return $this->db->getRows(
"SELECT r.*, c.name as category_name, pm.name as payment_method_name
FROM recurring_transactions r
LEFT JOIN categories c ON r.category_id = c.id
LEFT JOIN payment_methods pm ON r.payment_method_id = pm.id
WHERE r.user_id = ? AND r.is_active = 1
ORDER BY r.next_date ASC",
[$userId]
);
}
/**
* Process recurring transactions
*/
public function processRecurringTransactions() {
$today = date('Y-m-d');
$recurring = $this->db->getRows(
"SELECT * FROM recurring_transactions 
WHERE is_active = 1 AND next_date <= ?",
[$today]
);
foreach ($recurring as $item) {
// Create transaction
$this->add($item['user_id'], [
'category_id' => $item['category_id'],
'payment_method_id' => $item['payment_method_id'],
'type' => $item['type'],
'amount' => $item['amount'],
'description' => $item['description'] . ' (Recurring)',
'date' => $today,
'is_recurring' => true,
'recurring_id' => $item['id']
]);
// Calculate next date
$nextDate = $this->calculateNextDate(
$item['next_date'],
$item['frequency'],
$item['interval_count']
);
if ($nextDate && (!$item['end_date'] || $nextDate <= $item['end_date'])) {
// Update next date
$this->db->update(
'recurring_transactions',
[
'next_date' => $nextDate,
'last_processed' => $today
],
'id = :id',
['id' => $item['id']]
);
} else {
// Deactivate recurring transaction
$this->db->update(
'recurring_transactions',
['is_active' => false],
'id = :id',
['id' => $item['id']]
);
}
}
}
/**
* Calculate next date for recurring transaction
*/
private function calculateNextDate($currentDate, $frequency, $interval) {
$date = new DateTime($currentDate);
switch ($frequency) {
case 'daily':
$date->modify("+{$interval} days");
break;
case 'weekly':
$date->modify("+{$interval} weeks");
break;
case 'monthly':
$date->modify("+{$interval} months");
break;
case 'yearly':
$date->modify("+{$interval} years");
break;
default:
return null;
}
return $date->format('Y-m-d');
}
}
?>

Frontend Pages

Login Page

File: login.php

<?php
require_once 'includes/config.php';
// Redirect if already logged in
if ($auth->isLoggedIn()) {
redirect('/user/dashboard.php');
}
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = sanitize($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$remember = isset($_POST['remember']);
if (empty($username) || empty($password)) {
$error = 'Please enter username/email and password';
} else {
$result = $auth->login($username, $password, $remember);
if ($result['success']) {
$user = $result['user'];
// Redirect based on role
if ($user['role'] === 'admin') {
redirect('/admin/dashboard.php');
} else {
redirect('/user/dashboard.php');
}
} else {
$error = $result['error'];
}
}
}
// Check for session messages
if (isset($_SESSION['success'])) {
$success = $_SESSION['success'];
unset($_SESSION['success']);
}
if (isset($_SESSION['error'])) {
$error = $_SESSION['error'];
unset($_SESSION['error']);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - <?php echo APP_NAME; ?></title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="bg-light">
<div class="container">
<div class="row justify-content-center align-items-center min-vh-100">
<div class="col-md-6 col-lg-5">
<div class="card shadow-lg border-0 rounded-lg">
<div class="card-header bg-primary text-white text-center py-4">
<h3 class="mb-0">
<i class="fas fa-chart-pie me-2"></i>
<?php echo APP_NAME; ?>
</h3>
<p class="mb-0 text-white-50">Track your expenses, achieve your goals</p>
</div>
<div class="card-body p-5">
<h4 class="text-center mb-4">Welcome Back!</h4>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show">
<i class="fas fa-exclamation-circle me-2"></i>
<?php echo $error; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success alert-dismissible fade show">
<i class="fas fa-check-circle me-2"></i>
<?php echo $success; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form method="POST" action="" onsubmit="return validateLogin()">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<div class="mb-4">
<label for="username" class="form-label">
<i class="fas fa-user me-2"></i>Username or Email
</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" class="form-control" id="username" name="username" 
placeholder="Enter username or email" required>
</div>
</div>
<div class="mb-4">
<label for="password" class="form-label">
<i class="fas fa-lock me-2"></i>Password
</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" id="password" name="password" 
placeholder="Enter password" required>
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword()">
<i class="fas fa-eye" id="togglePasswordIcon"></i>
</button>
</div>
</div>
<div class="mb-4 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">Remember me</label>
<a href="forgot_password.php" class="float-end text-decoration-none">
Forgot Password?
</a>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 mb-3">
<i class="fas fa-sign-in-alt me-2"></i>Login
</button>
</form>
<div class="text-center">
<p class="mb-0">
Don't have an account? 
<a href="register.php" class="text-decoration-none">Register here</a>
</p>
</div>
<hr class="my-4">
<div class="text-center">
<a href="index.php" class="text-decoration-none">
<i class="fas fa-arrow-left me-1"></i>Back to Home
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
function togglePassword() {
const password = document.getElementById('password');
const icon = document.getElementById('togglePasswordIcon');
if (password.type === 'password') {
password.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
password.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
function validateLogin() {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (username === '') {
alert('Please enter username or email');
return false;
}
if (password === '') {
alert('Please enter password');
return false;
}
return true;
}
</script>
</body>
</html>

Registration Page

File: register.php

<?php
require_once 'includes/config.php';
// Redirect if already logged in
if ($auth->isLoggedIn()) {
redirect('/user/dashboard.php');
}
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Sanitize inputs
$data = [
'username' => sanitize($_POST['username'] ?? ''),
'email' => sanitize($_POST['email'] ?? ''),
'password' => $_POST['password'] ?? '',
'confirm_password' => $_POST['confirm_password'] ?? '',
'first_name' => sanitize($_POST['first_name'] ?? ''),
'last_name' => sanitize($_POST['last_name'] ?? ''),
'currency' => sanitize($_POST['currency'] ?? 'USD')
];
// Validate inputs
$errors = [];
if (empty($data['username'])) {
$errors[] = 'Username is required';
} elseif (strlen($data['username']) < 3 || strlen($data['username']) > 20) {
$errors[] = 'Username must be between 3 and 20 characters';
} elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $data['username'])) {
$errors[] = 'Username can only contain letters, numbers, and underscores';
}
if (empty($data['email'])) {
$errors[] = 'Email is required';
} elseif (!validateEmail($data['email'])) {
$errors[] = 'Please enter a valid email address';
}
if (empty($data['password'])) {
$errors[] = 'Password is required';
} elseif (strlen($data['password']) < 8) {
$errors[] = 'Password must be at least 8 characters';
} elseif (!preg_match('/[A-Z]/', $data['password'])) {
$errors[] = 'Password must contain at least one uppercase letter';
} elseif (!preg_match('/[a-z]/', $data['password'])) {
$errors[] = 'Password must contain at least one lowercase letter';
} elseif (!preg_match('/[0-9]/', $data['password'])) {
$errors[] = 'Password must contain at least one number';
}
if ($data['password'] !== $data['confirm_password']) {
$errors[] = 'Passwords do not match';
}
if (empty($data['first_name'])) {
$errors[] = 'First name is required';
}
if (empty($data['last_name'])) {
$errors[] = 'Last name is required';
}
// If no errors, attempt registration
if (empty($errors)) {
$result = $auth->register($data);
if ($result['success']) {
$_SESSION['success'] = $result['message'];
redirect('/login.php');
} else {
$error = $result['error'];
}
} else {
$error = implode('<br>', $errors);
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - <?php echo APP_NAME; ?></title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="bg-light">
<div class="container">
<div class="row justify-content-center align-items-center min-vh-100 py-4">
<div class="col-md-8 col-lg-6">
<div class="card shadow-lg border-0 rounded-lg">
<div class="card-header bg-primary text-white text-center py-4">
<h3 class="mb-0">
<i class="fas fa-user-plus me-2"></i>
Create Account
</h3>
<p class="mb-0 text-white-50">Start tracking your expenses today</p>
</div>
<div class="card-body p-5">
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show">
<i class="fas fa-exclamation-circle me-2"></i>
<?php echo $error; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form method="POST" action="" onsubmit="return validateRegistration()">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<div class="row">
<div class="col-md-6 mb-3">
<label for="first_name" class="form-label">
<i class="fas fa-user me-2"></i>First Name *
</label>
<input type="text" class="form-control" id="first_name" name="first_name" 
value="<?php echo htmlspecialchars($_POST['first_name'] ?? ''); ?>" required>
</div>
<div class="col-md-6 mb-3">
<label for="last_name" class="form-label">
<i class="fas fa-user me-2"></i>Last Name *
</label>
<input type="text" class="form-control" id="last_name" name="last_name" 
value="<?php echo htmlspecialchars($_POST['last_name'] ?? ''); ?>" required>
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">
<i class="fas fa-at me-2"></i>Username *
</label>
<input type="text" class="form-control" id="username" name="username" 
value="<?php echo htmlspecialchars($_POST['username'] ?? ''); ?>" 
placeholder="Choose a username" required>
<small class="text-muted">3-20 characters, letters, numbers, and underscores only</small>
</div>
<div class="mb-3">
<label for="email" class="form-label">
<i class="fas fa-envelope me-2"></i>Email Address *
</label>
<input type="email" class="form-control" id="email" name="email" 
value="<?php echo htmlspecialchars($_POST['email'] ?? ''); ?>" 
placeholder="Enter your email" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="password" class="form-label">
<i class="fas fa-lock me-2"></i>Password *
</label>
<div class="input-group">
<input type="password" class="form-control" id="password" name="password" 
placeholder="Create password" required>
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('password')">
<i class="fas fa-eye" id="togglePasswordIcon1"></i>
</button>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="confirm_password" class="form-label">
<i class="fas fa-lock me-2"></i>Confirm Password *
</label>
<div class="input-group">
<input type="password" class="form-control" id="confirm_password" name="confirm_password" 
placeholder="Confirm password" required>
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('confirm_password')">
<i class="fas fa-eye" id="togglePasswordIcon2"></i>
</button>
</div>
</div>
</div>
<div class="mb-4">
<label for="currency" class="form-label">
<i class="fas fa-dollar-sign me-2"></i>Preferred Currency
</label>
<select class="form-select" id="currency" name="currency">
<option value="USD" selected>USD - US Dollar</option>
<option value="EUR">EUR - Euro</option>
<option value="GBP">GBP - British Pound</option>
<option value="JPY">JPY - Japanese Yen</option>
<option value="INR">INR - Indian Rupee</option>
<option value="CAD">CAD - Canadian Dollar</option>
<option value="AUD">AUD - Australian Dollar</option>
</select>
</div>
<div class="mb-4 form-check">
<input type="checkbox" class="form-check-input" id="terms" name="terms" required>
<label class="form-check-label" for="terms">
I agree to the <a href="#" target="_blank">Terms of Service</a> and 
<a href="#" target="_blank">Privacy Policy</a>
</label>
</div>
<div class="mb-3">
<div class="password-requirements">
<small class="text-muted">
Password must contain:
<span id="req-length" class="req-badge">✓ 8+ characters</span>
<span id="req-uppercase" class="req-badge">✓ Uppercase</span>
<span id="req-lowercase" class="req-badge">✓ Lowercase</span>
<span id="req-number" class="req-badge">✓ Number</span>
</small>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 mb-3">
<i class="fas fa-user-plus me-2"></i>Create Account
</button>
</form>
<div class="text-center">
<p class="mb-0">
Already have an account? 
<a href="login.php" class="text-decoration-none">Sign In</a>
</p>
</div>
<hr class="my-4">
<div class="text-center">
<a href="index.php" class="text-decoration-none">
<i class="fas fa-arrow-left me-1"></i>Back to Home
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
$('#currency').select2({
theme: 'bootstrap-5',
width: '100%'
});
});
function togglePassword(fieldId) {
const password = document.getElementById(fieldId);
const icon = document.getElementById('togglePasswordIcon' + (fieldId === 'password' ? '1' : '2'));
if (password.type === 'password') {
password.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
password.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
function validateRegistration() {
const password = document.getElementById('password').value;
const confirm = document.getElementById('confirm_password').value;
const terms = document.getElementById('terms').checked;
// Check password requirements
const hasLength = password.length >= 8;
const hasUppercase = /[A-Z]/.test(password);
const hasLowercase = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
if (!hasLength || !hasUppercase || !hasLowercase || !hasNumber) {
alert('Please meet all password requirements');
return false;
}
if (password !== confirm) {
alert('Passwords do not match');
return false;
}
if (!terms) {
alert('You must agree to the Terms of Service');
return false;
}
return true;
}
// Real-time password validation
document.getElementById('password').addEventListener('input', function() {
const password = this.value;
// Length check
document.getElementById('req-length').style.color = password.length >= 8 ? 'green' : 'red';
// Uppercase check
document.getElementById('req-uppercase').style.color = /[A-Z]/.test(password) ? 'green' : 'red';
// Lowercase check
document.getElementById('req-lowercase').style.color = /[a-z]/.test(password) ? 'green' : 'red';
// Number check
document.getElementById('req-number').style.color = /[0-9]/.test(password) ? 'green' : 'red';
});
</script>
<style>
.password-requirements {
background: #f8f9fa;
padding: 10px;
border-radius: 5px;
}
.req-badge {
display: inline-block;
margin-right: 10px;
font-size: 0.85rem;
}
.req-badge.good {
color: green;
}
.req-badge.bad {
color: red;
}
</style>
</body>
</html>

User Dashboard

File: user/dashboard.php

<?php
require_once '../includes/config.php';
require_once '../includes/auth.php';
// Require login
$auth->requireLogin();
// Get current user
$user = $auth->getCurrentUser();
// Initialize classes
$transaction = new Transaction();
$budget = new Budget();
// Get current month and year
$currentMonth = date('m');
$currentYear = date('Y');
// Get monthly summary
$summary = $transaction->getMonthlySummary($user['id'], $currentYear, $currentMonth);
// Get recent transactions
$recentTransactions = $transaction->getUserTransactions($user['id'], ['limit' => 5]);
// Get category breakdown for current month
$startDate = "{$currentYear}-{$currentMonth}-01";
$endDate = date('Y-m-t', strtotime($startDate));
$expenseBreakdown = $transaction->getCategoryBreakdown($user['id'], 'expense', $startDate, $endDate);
// Get budget status
$budgets = $budget->getMonthlyBudgets($user['id'], $currentMonth, $currentYear);
// Get notifications
$notifications = $budget->getUnreadNotifications($user['id']);
// Process recurring transactions
$transaction->processRecurringTransactions();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - <?php echo APP_NAME; ?></title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Custom CSS -->
<link rel="stylesheet" href="../assets/css/dashboard.css">
</head>
<body>
<div class="d-flex">
<!-- Sidebar -->
<div class="sidebar bg-dark text-white">
<div class="sidebar-header p-4">
<a href="dashboard.php" class="text-white text-decoration-none">
<i class="fas fa-chart-pie fa-2x mb-3"></i>
<h5 class="mb-0"><?php echo APP_NAME; ?></h5>
<small>Track your finances</small>
</a>
</div>
<div class="user-info p-4 border-top border-secondary">
<div class="d-flex align-items-center">
<div class="avatar bg-primary rounded-circle d-flex align-items-center justify-content-center me-3" 
style="width: 50px; height: 50px; font-size: 1.5rem;">
<?php echo strtoupper(substr($user['first_name'], 0, 1) . substr($user['last_name'], 0, 1)); ?>
</div>
<div>
<h6 class="mb-0"><?php echo htmlspecialchars($user['first_name'] . ' ' . $user['last_name']); ?></h6>
<small class="text-secondary"><?php echo ucfirst($user['role']); ?></small>
</div>
</div>
</div>
<nav class="nav flex-column p-3">
<a href="dashboard.php" class="nav-link text-white active">
<i class="fas fa-tachometer-alt me-3"></i>Dashboard
</a>
<a href="transactions.php" class="nav-link text-white">
<i class="fas fa-exchange-alt me-3"></i>Transactions
</a>
<a href="add_transaction.php" class="nav-link text-white">
<i class="fas fa-plus-circle me-3"></i>Add Transaction
</a>
<a href="categories.php" class="nav-link text-white">
<i class="fas fa-tags me-3"></i>Categories
</a>
<a href="budgets.php" class="nav-link text-white">
<i class="fas fa-chart-line me-3"></i>Budgets
</a>
<a href="reports.php" class="nav-link text-white">
<i class="fas fa-file-alt me-3"></i>Reports
</a>
<a href="analytics.php" class="nav-link text-white">
<i class="fas fa-chart-bar me-3"></i>Analytics
</a>
<a href="goals.php" class="nav-link text-white">
<i class="fas fa-flag-checkered me-3"></i>Savings Goals
</a>
<a href="profile.php" class="nav-link text-white">
<i class="fas fa-user-circle me-3"></i>Profile
</a>
<a href="../logout.php" class="nav-link text-white mt-4">
<i class="fas fa-sign-out-alt me-3"></i>Logout
</a>
</nav>
</div>
<!-- Main Content -->
<div class="main-content flex-grow-1 bg-light">
<header class="bg-white shadow-sm p-3">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-tachometer-alt me-2 text-primary"></i>
Dashboard
</h5>
<div class="d-flex align-items-center">
<!-- Notifications -->
<div class="dropdown me-3">
<button class="btn btn-light position-relative" type="button" data-bs-toggle="dropdown">
<i class="fas fa-bell"></i>
<?php if (count($notifications) > 0): ?>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
<?php echo count($notifications); ?>
</span>
<?php endif; ?>
</button>
<div class="dropdown-menu dropdown-menu-end notification-dropdown" style="width: 300px;">
<?php if (count($notifications) > 0): ?>
<?php foreach ($notifications as $notification): ?>
<a class="dropdown-item" href="<?php echo $notification['action_url']; ?>">
<small class="text-muted d-block"><?php echo timeAgo($notification['created_at']); ?></small>
<strong><?php echo htmlspecialchars($notification['title']); ?></strong>
<p class="mb-0 small"><?php echo htmlspecialchars($notification['message']); ?></p>
</a>
<div class="dropdown-divider"></div>
<?php endforeach; ?>
<a class="dropdown-item text-center" href="notifications.php">
View all notifications
</a>
<?php else: ?>
<span class="dropdown-item text-center text-muted">
No new notifications
</span>
<?php endif; ?>
</div>
</div>
<!-- Date -->
<span class="text-muted">
<i class="fas fa-calendar me-1"></i>
<?php echo date('F j, Y'); ?>
</span>
</div>
</div>
</header>
<div class="p-4">
<!-- Welcome Banner -->
<div class="row mb-4">
<div class="col-12">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h4>Welcome back, <?php echo htmlspecialchars($user['first_name']); ?>!</h4>
<p class="mb-0">
Here's your financial summary for 
<?php echo date('F Y'); ?>
</p>
</div>
<div class="col-md-4 text-md-end">
<span class="badge bg-light text-primary p-2">
<i class="fas fa-dollar-sign me-1"></i>
Balance: <?php echo formatAmount($user['total_balance']); ?>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Total Income</h6>
<h3 class="mb-0 text-success">
<?php echo formatAmount($summary['total_income'] ?? 0); ?>
</h3>
</div>
<div class="icon-circle bg-success bg-opacity-10 text-success">
<i class="fas fa-arrow-up"></i>
</div>
</div>
<small class="text-muted">This month</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Total Expenses</h6>
<h3 class="mb-0 text-danger">
<?php echo formatAmount($summary['total_expense'] ?? 0); ?>
</h3>
</div>
<div class="icon-circle bg-danger bg-opacity-10 text-danger">
<i class="fas fa-arrow-down"></i>
</div>
</div>
<small class="text-muted">This month</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Net Savings</h6>
<h3 class="mb-0 text-primary">
<?php 
$savings = ($summary['total_income'] ?? 0) - ($summary['total_expense'] ?? 0);
echo formatAmount($savings);
?>
</h3>
</div>
<div class="icon-circle bg-primary bg-opacity-10 text-primary">
<i class="fas fa-piggy-bank"></i>
</div>
</div>
<small class="text-muted">
<?php 
$total = ($summary['total_income'] ?? 0) + ($summary['total_expense'] ?? 0);
if ($total > 0) {
$savingsRate = ($savings / ($summary['total_income'] ?? 1)) * 100;
echo 'Savings rate: ' . number_format($savingsRate, 1) . '%';
}
?>
</small>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Expense Breakdown Chart -->
<div class="col-lg-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-chart-pie me-2 text-primary"></i>
Expense Breakdown
</h6>
</div>
<div class="card-body">
<canvas id="expenseChart" style="height: 300px;"></canvas>
</div>
</div>
</div>
<!-- Recent Transactions -->
<div class="col-lg-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-clock me-2 text-primary"></i>
Recent Transactions
</h6>
<a href="transactions.php" class="btn btn-sm btn-outline-primary">
View All
</a>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
<?php if (count($recentTransactions) > 0): ?>
<?php foreach ($recentTransactions as $trans): ?>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="d-flex align-items-center">
<div class="me-3">
<span class="badge" style="background-color: <?php echo $trans['category_color']; ?>; width: 12px; height: 12px; border-radius: 50%;">&nbsp;</span>
</div>
<div>
<h6 class="mb-1"><?php echo htmlspecialchars($trans['description'] ?: $trans['category_name']); ?></h6>
<small class="text-muted">
<?php echo formatDate($trans['date']); ?> • 
<?php echo htmlspecialchars($trans['category_name']); ?>
</small>
</div>
</div>
</div>
<div class="text-end">
<span class="<?php echo $trans['type'] == 'income' ? 'text-success' : 'text-danger'; ?> fw-bold">
<?php echo $trans['type'] == 'income' ? '+' : '-'; ?>
<?php echo formatAmount($trans['amount']); ?>
</span>
</div>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="text-center py-4">
<i class="fas fa-receipt fa-3x text-muted mb-3"></i>
<p class="text-muted">No transactions yet</p>
<a href="add_transaction.php" class="btn btn-primary btn-sm">
Add Your First Transaction
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<!-- Budget Status -->
<?php if (count($budgets) > 0): ?>
<div class="row mt-2">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-chart-line me-2 text-primary"></i>
Monthly Budget Status
</h6>
<a href="budgets.php" class="btn btn-sm btn-outline-primary">
Manage Budgets
</a>
</div>
<div class="card-body">
<?php foreach ($budgets as $budget): ?>
<?php 
$percentage = ($budget['spent'] / $budget['amount']) * 100;
$progressClass = $percentage >= 100 ? 'bg-danger' : ($percentage >= 80 ? 'bg-warning' : 'bg-success');
?>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span><?php echo htmlspecialchars($budget['category_name']); ?></span>
<span class="fw-bold">
<?php echo formatAmount($budget['spent']); ?> / 
<?php echo formatAmount($budget['amount']); ?>
(<?php echo number_format($percentage, 1); ?>%)
</span>
</div>
<div class="progress" style="height: 10px;">
<div class="progress-bar <?php echo $progressClass; ?>" 
style="width: <?php echo min($percentage, 100); ?>%"></div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<!-- Quick Actions -->
<div class="row mt-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-bolt me-2 text-primary"></i>
Quick Actions
</h6>
</div>
<div class="card-body">
<a href="add_transaction.php" class="btn btn-primary me-2 mb-2">
<i class="fas fa-plus-circle me-2"></i>Add Transaction
</a>
<a href="add_transaction.php?type=income" class="btn btn-success me-2 mb-2">
<i class="fas fa-arrow-up me-2"></i>Add Income
</a>
<a href="add_transaction.php?type=expense" class="btn btn-danger me-2 mb-2">
<i class="fas fa-arrow-down me-2"></i>Add Expense
</a>
<a href="reports.php" class="btn btn-info me-2 mb-2">
<i class="fas fa-file-pdf me-2"></i>Generate Report
</a>
<a href="export.php" class="btn btn-secondary me-2 mb-2">
<i class="fas fa-download me-2"></i>Export Data
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Expense Chart
const ctx = document.getElementById('expenseChart').getContext('2d');
<?php
$chartLabels = [];
$chartData = [];
$chartColors = [];
foreach ($expenseBreakdown as $item) {
$chartLabels[] = $item['name'];
$chartData[] = $item['total'];
$chartColors[] = $item['color'] ?? getRandomColor();
}
?>
new Chart(ctx, {
type: 'doughnut',
data: {
labels: <?php echo json_encode($chartLabels); ?>,
datasets: [{
data: <?php echo json_encode($chartData); ?>,
backgroundColor: <?php echo json_encode($chartColors); ?>,
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
},
cutout: '70%'
}
});
</script>
<style>
.icon-circle {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.notification-dropdown {
max-height: 400px;
overflow-y: auto;
}
.sidebar {
width: 280px;
min-height: 100vh;
}
.main-content {
margin-left: 280px;
}
.nav-link {
color: rgba(255,255,255,0.8) !important;
padding: 12px 20px;
border-radius: 5px;
margin: 2px 10px;
transition: all 0.3s;
}
.nav-link:hover,
.nav-link.active {
background: rgba(255,255,255,0.1) !important;
color: white !important;
}
.nav-link i {
width: 20px;
}
.avatar {
width: 50px;
height: 50px;
font-weight: bold;
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
position: fixed;
z-index: 1000;
}
.main-content {
margin-left: 0;
}
}
</style>
</body>
</html>

Environment Configuration

File: .env

# Database Configuration
DB_HOST=localhost
DB_NAME=expense_tracker
DB_USER=root
DB_PASS=
# Application Configuration
APP_NAME=Expense Tracker
APP_URL=http://localhost/expense-tracker
APP_VERSION=1.0.0
DEBUG_MODE=true
# Security
SESSION_TIMEOUT=1800
BCRYPT_ROUNDS=12
# Upload Configuration
MAX_FILE_SIZE=5242880  # 5MB in bytes
# Pagination
ITEMS_PER_PAGE=20
# Date/Time
TIMEZONE=America/New_York
# Currency
DEFAULT_CURRENCY=USD
CURRENCY_SYMBOL=$
# Email Configuration (if using)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
[email protected]
SMTP_PASS=your_password
SMTP_ENCRYPTION=tls

File: .gitignore

# Environment variables
.env
# Dependencies
/vendor/
node_modules/
# IDE files
.vscode/
.idea/
*.sublime-*
# OS files
.DS_Store
Thumbs.db
# Logs
/logs/
*.log
# Uploads
/uploads/
!/uploads/.gitkeep
# Cache
/cache/
!/cache/.gitkeep
# Composer
composer.lock
# Temp files
*.tmp
*.temp

File: composer.json

{
"name": "expense-tracker/application",
"description": "Personal Expense Tracker Application",
"type": "project",
"require": {
"php": ">=7.4",
"ext-pdo": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-curl": "*",
"ext-gd": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"ExpenseTracker\\": "src/"
}
},
"scripts": {
"test": "phpunit tests"
}
}

How to Use the Project (Step-by-Step Guide)

Prerequisites

  1. Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server (PHP 7.4+)
  2. Database: MySQL 5.7+ or MariaDB
  3. Browser: Modern web browser (Chrome, Firefox, Edge, etc.)

Installation Steps

Step 1: Set Up Local Server

  1. Download and install XAMPP from https://www.apachefriends.org/
  2. Launch XAMPP Control Panel
  3. Start Apache and MySQL services

Step 2: Create Project Folder

  1. Navigate to C:\xampp\htdocs\ (Windows) or /Applications/XAMPP/htdocs/ (Mac)
  2. Create a new folder named expense-tracker
  3. Create all the folders and files as shown in the Project File Structure

Step 3: Set Up Database

  1. Open browser and go to http://localhost/phpmyadmin
  2. Click on "New" to create a new database
  3. Name the database expense_tracker and select utf8_general_ci
  4. Click on "Import" tab
  5. Click "Choose File" and select the database.sql file from the sql folder
  6. Click "Go" to import the database structure and sample data

Step 4: Configure Environment

  1. Rename .env.example to .env in the project root
  2. Update database credentials if different from default:
   DB_HOST=localhost
DB_NAME=expense_tracker
DB_USER=root
DB_PASS=
  1. Update application URL:
   APP_URL=http://localhost/expense-tracker

Step 5: Set Folder Permissions

Create the following folders and ensure they are writable:

  • uploads/receipts/
  • logs/
  • cache/

On Windows, right-click folders → Properties → Security → give Write permission to Users
On Mac/Linux, run: chmod 777 uploads/ logs/ cache/

Step 6: Create Admin Password Hash

  1. Go to http://localhost/expense-tracker/register.php
  2. Register a test user (e.g., username: admin, password: Admin@123)
  3. Open phpMyAdmin, go to the users table
  4. Copy the password hash for the admin user
  5. Update the admin insert query in database.sql or manually update the admin password in the database

Step 7: Test the Installation

  1. Open browser and go to http://localhost/expense-tracker/
  2. You should see the login page
  3. Test different user types: Admin Login:
  • Username: admin
  • Password: Admin@123 (or the password you set) Regular User:
  • Register a new user account
  • Login with the new credentials

System Walkthrough

For Regular Users:

  1. Dashboard - View financial summary, recent transactions, budget status
  2. Transactions - View, add, edit, and delete transactions
  3. Add Transaction - Record income or expenses with categories
  4. Categories - Manage custom categories for transactions
  5. Budgets - Set monthly budgets for different categories
  6. Reports - Generate financial reports
  7. Analytics - Visual analysis of spending patterns
  8. Savings Goals - Set and track savings targets
  9. Profile - Update personal information and preferences

For Admins:

  1. Admin Dashboard - View system-wide statistics
  2. Manage Users - View, edit, and manage user accounts
  3. System Settings - Configure application settings
  4. Global Reports - Generate reports across all users

Key Features Explained

Adding Transactions

  1. Click "Add Transaction" from dashboard
  2. Select type (Income/Expense)
  3. Choose category and payment method
  4. Enter amount, date, and description
  5. Optionally add receipt or notes
  6. Click "Save Transaction"

Setting Budgets

  1. Go to "Budgets" section
  2. Click "Add Budget"
  3. Select category
  4. Enter budget amount
  5. Choose period (monthly/yearly)
  6. Set alert threshold (e.g., 80%)
  7. Budget tracking will automatically update

Generating Reports

  1. Go to "Reports" section
  2. Select date range
  3. Choose report type (Summary, Category Breakdown, etc.)
  4. Click "Generate Report"
  5. Export as PDF or Excel

Managing Categories

  1. Go to "Categories" section
  2. View default categories
  3. Add custom categories with icons and colors
  4. Edit or delete categories (except system categories)

Troubleshooting

Common Issues and Solutions

  1. Database Connection Error
  • Check if MySQL is running
  • Verify database credentials in .env file
  • Ensure database expense_tracker exists
  1. 404 Page Not Found
  • Check file paths and folder structure
  • Verify APP_URL in .env file
  • Ensure .htaccess is properly configured (if using Apache)
  1. File Upload Issues
  • Check folder permissions (uploads/receipts/ must be writable)
  • Verify MAX_FILE_SIZE in .env
  • Check allowed file extensions
  1. Session/Login Issues
  • Clear browser cookies and cache
  • Check SESSION_TIMEOUT in .env
  • Verify session save path is writable
  1. Chart Not Displaying
  • Check browser console for JavaScript errors
  • Ensure Chart.js is properly loaded
  • Verify data is being passed correctly

Security Best Practices

  1. Change default admin password immediately after installation
  2. Use HTTPS in production
  3. Regular backups of database and uploads
  4. Input validation on both client and server side
  5. SQL injection prevention using prepared statements
  6. XSS prevention using htmlspecialchars()
  7. CSRF tokens for forms
  8. Password hashing using bcrypt
  9. Rate limiting for login attempts
  10. Regular security updates

Performance Optimizations

  1. Database indexing on frequently queried columns
  2. Caching of API responses and database queries
  3. Image optimization for receipts
  4. Lazy loading of charts and graphs
  5. Pagination for large datasets
  6. Minified CSS and JavaScript for production

Future Enhancements

  1. Mobile App - React Native or Flutter app
  2. API Integration - Bank account integration for automatic transactions
  3. Machine Learning - Spending predictions and insights
  4. Multi-currency - Automatic currency conversion
  5. Investment Tracking - Portfolio management
  6. Bill Reminders - SMS and email notifications
  7. OCR Integration - Scan receipts automatically
  8. Tax Preparation - Export data for tax software
  9. Family Accounts - Shared budgets and permissions
  10. Export Formats - More export options (CSV, QuickBooks, etc.)

Conclusion

The Expense Tracker is a comprehensive, feature-rich application for personal financial management. With its intuitive interface, powerful analytics, and robust features, it provides everything needed to track income, manage expenses, set budgets, and achieve financial goals.

This application demonstrates:

  • Secure user authentication with password hashing
  • CRUD operations for transactions, categories, and budgets
  • Data visualization with Chart.js
  • File upload for receipts
  • Responsive design for all devices
  • Modular code structure following OOP principles
  • Database design with proper relationships and indexing
  • Error handling and logging
  • Security best practices

The system is built to be extensible, allowing easy addition of new features and integration with other services. Whether you're managing personal finances or building a commercial application, this expense tracker provides a solid foundation that can be customized to meet specific needs.

With proper deployment, regular backups, and security updates, this application can serve as a reliable tool for long-term financial management and planning.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper