Online Appointment Booking System – Complete Project IN HTML CSS AND JAVASCRIPT WITH PHP AND MY SQL

Introduction to the Project

The Online Appointment Booking System is a comprehensive, full-stack web application designed to streamline the process of scheduling and managing appointments across various industries. Whether for healthcare clinics, beauty salons, consulting services, or educational institutions, this system provides a seamless platform for service providers and customers to manage appointments efficiently.

This application features role-based access control with three distinct user types: Admin, Service Providers, and Customers. The system eliminates scheduling conflicts, reduces no-shows through automated reminders, and provides detailed analytics for business intelligence.

Key Features

Admin Features

  • Dashboard Overview: Comprehensive statistics on appointments, revenue, and user activity
  • Provider Management: Add, edit, and manage service providers with their schedules
  • Service Management: Create and manage service categories, pricing, and durations
  • Customer Management: View and manage customer profiles and booking history
  • Analytics & Reports: Generate detailed reports on appointments, revenue, and performance
  • System Settings: Configure business hours, holidays, and notification settings
  • Payment Integration: Track payments and manage refunds

Service Provider Features

  • Personal Dashboard: View daily, weekly, and monthly schedules
  • Appointment Management: Accept, reschedule, or cancel appointments
  • Availability Management: Set working hours, breaks, and time off
  • Customer History: View customer profiles and appointment history
  • Performance Analytics: Track earnings, completion rates, and customer ratings
  • Notifications: Receive alerts for new bookings and reminders

Customer Features

  • Online Booking: Browse services and book appointments 24/7
  • Provider Selection: Choose preferred service providers based on availability
  • Appointment Management: View, reschedule, or cancel upcoming appointments
  • Booking History: Access complete appointment history
  • Reviews & Ratings: Rate and review services after appointments
  • Reminders: Receive email/SMS notifications for upcoming appointments
  • Profile Management: Manage personal information and preferences

General Features

  • Real-time Availability: Check available time slots in real-time
  • Automated Reminders: Email and SMS notifications for upcoming appointments
  • Calendar Integration: Sync with Google Calendar, Outlook, etc.
  • Multi-language Support: Interface available in multiple languages
  • Payment Processing: Online payment integration (Stripe/PayPal)
  • Reviews & Ratings: Rate services and providers
  • Search & Filter: Find services by category, price, location, or rating
  • Responsive Design: Works seamlessly on desktop, tablet, and mobile

Technology Stack

  • Frontend: HTML5, CSS3, JavaScript (ES6+), Bootstrap 5
  • Backend: PHP 8.0+ (Core PHP with OOP approach)
  • Database: MySQL 5.7+
  • Additional Libraries:
  • FullCalendar.js for calendar display
  • Chart.js for analytics
  • 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
  • PHPMailer for email notifications
  • Twilio SDK for SMS notifications
  • Stripe/PayPal SDK for payments

Project File Structure

appointment-system/
│
├── assets/
│   ├── css/
│   │   ├── style.css
│   │   ├── dashboard.css
│   │   ├── calendar.css
│   │   ├── responsive.css
│   │   └── dark-mode.css
│   ├── js/
│   │   ├── main.js
│   │   ├── dashboard.js
│   │   ├── calendar.js
│   │   ├── booking.js
│   │   ├── validation.js
│   │   └── charts.js
│   ├── images/
│   │   ├── providers/
│   │   ├── services/
│   │   └── avatars/
│   └── plugins/
│       ├── fullcalendar/
│       ├── datatables/
│       └── select2/
│
├── includes/
│   ├── config.php
│   ├── Database.php
│   ├── functions.php
│   ├── auth.php
│   ├── Appointment.php
│   ├── Service.php
│   ├── Provider.php
│   ├── User.php
│   ├── Notification.php
│   ├── Payment.php
│   ├── Review.php
│   └── helpers/
│       ├── DateHelper.php
│       ├── TimeHelper.php
│       └── ValidationHelper.php
│
├── admin/
│   ├── dashboard.php
│   ├── manage_providers.php
│   ├── add_provider.php
│   ├── edit_provider.php
│   ├── manage_services.php
│   ├── add_service.php
│   ├── edit_service.php
│   ├── manage_customers.php
│   ├── appointments.php
│   ├── payments.php
│   ├── reports.php
│   ├── analytics.php
│   ├── settings.php
│   └── holidays.php
│
├── provider/
│   ├── dashboard.php
│   ├── schedule.php
│   ├── appointments.php
│   ├── appointment_details.php
│   ├── availability.php
│   ├── set_availability.php
│   ├── breaks.php
│   ├── customers.php
│   ├── earnings.php
│   ├── profile.php
│   └── settings.php
│
├── customer/
│   ├── dashboard.php
│   ├── book.php
│   ├── select_provider.php
│   ├── select_time.php
│   ├── confirm_booking.php
│   ├── my_appointments.php
│   ├── appointment_details.php
│   ├── history.php
│   ├── reviews.php
│   ├── write_review.php
│   ├── profile.php
│   └── favorites.php
│
├── api/
│   ├── get_availability.php
│   ├── book_appointment.php
│   ├── cancel_appointment.php
│   ├── reschedule.php
│   ├── get_providers.php
│   ├── get_services.php
│   ├── process_payment.php
│   └── search.php
│
├── includes/
│   └── notifications/
│       ├── email_templates/
│       │   ├── booking_confirmation.php
│       │   ├── reminder.php
│       │   ├── cancellation.php
│       │   └── reschedule.php
│       └── sms_templates/
│
├── uploads/
│   ├── providers/
│   ├── services/
│   └── avatars/
│
├── 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 `appointment_system`;
USE `appointment_system`;
-- Users Table
CREATE TABLE `users` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(20) 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,
`phone` VARCHAR(20),
`date_of_birth` DATE,
`gender` ENUM('male', 'female', 'other') NULL,
`address` TEXT,
`city` VARCHAR(100),
`state` VARCHAR(50),
`country` VARCHAR(50),
`postal_code` VARCHAR(20),
`profile_picture` VARCHAR(255) DEFAULT 'default.png',
`role` ENUM('admin', 'provider', 'customer') NOT NULL DEFAULT 'customer',
`status` ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
`email_verified` BOOLEAN DEFAULT FALSE,
`phone_verified` BOOLEAN DEFAULT FALSE,
`verification_token` VARCHAR(255),
`reset_token` VARCHAR(255),
`reset_expires` DATETIME,
`last_login` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_email` (`email`),
INDEX `idx_role` (`role`),
INDEX `idx_status` (`status`)
);
-- Service Providers Table (extends users)
CREATE TABLE `providers` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`business_name` VARCHAR(255),
`bio` TEXT,
`specialization` VARCHAR(255),
`qualifications` TEXT,
`experience_years` INT,
`languages` TEXT,
`services_offered` TEXT,
`base_price` DECIMAL(10,2),
`price_unit` ENUM('per_hour', 'per_session', 'fixed') DEFAULT 'per_session',
`rating` DECIMAL(3,2) DEFAULT 0.00,
`total_reviews` INT DEFAULT 0,
`total_appointments` INT DEFAULT 0,
`completion_rate` DECIMAL(5,2) DEFAULT 0.00,
`availability_settings` TEXT,
`buffer_time` INT DEFAULT 15, -- minutes between appointments
`is_verified` BOOLEAN DEFAULT FALSE,
`featured` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_rating` (`rating`),
INDEX `idx_featured` (`featured`)
);
-- Service Categories
CREATE TABLE `service_categories` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`description` TEXT,
`icon` VARCHAR(50) DEFAULT 'fa-tag',
`image` VARCHAR(255),
`sort_order` INT DEFAULT 0,
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_active` (`is_active`)
);
-- Services Table
CREATE TABLE `services` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`provider_id` INT(11) NOT NULL,
`category_id` INT(11),
`name` VARCHAR(255) NOT NULL,
`description` TEXT,
`duration` INT NOT NULL, -- in minutes
`price` DECIMAL(10,2) NOT NULL,
`discounted_price` DECIMAL(10,2),
`currency` VARCHAR(3) DEFAULT 'USD',
`max_capacity` INT DEFAULT 1, -- max customers per session
`buffer_before` INT DEFAULT 0, -- buffer time before in minutes
`buffer_after` INT DEFAULT 0, -- buffer time after in minutes
`location_type` ENUM('onsite', 'online', 'both') DEFAULT 'onsite',
`location` TEXT,
`meeting_link` VARCHAR(255), -- for online appointments
`image` VARCHAR(255),
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `service_categories`(`id`),
INDEX `idx_active` (`is_active`),
INDEX `idx_price` (`price`)
);
-- Provider Schedule (working hours)
CREATE TABLE `provider_schedule` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`provider_id` INT(11) NOT NULL,
`day_of_week` TINYINT NOT NULL, -- 0=Sunday, 1=Monday, etc.
`is_working` BOOLEAN DEFAULT TRUE,
`start_time` TIME,
`end_time` TIME,
`break_start` TIME,
`break_end` TIME,
`max_appointments` INT DEFAULT NULL, -- max per day
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_provider_day` (`provider_id`, `day_of_week`)
);
-- Time Off / Holidays
CREATE TABLE `provider_time_off` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`provider_id` INT(11) NOT NULL,
`title` VARCHAR(255),
`start_date` DATE NOT NULL,
`end_date` DATE NOT NULL,
`start_time` TIME,
`end_time` TIME,
`is_full_day` BOOLEAN DEFAULT TRUE,
`reason` VARCHAR(255),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`) ON DELETE CASCADE,
INDEX `idx_dates` (`start_date`, `end_date`)
);
-- Appointments Table
CREATE TABLE `appointments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`appointment_id` VARCHAR(20) UNIQUE NOT NULL,
`customer_id` INT(11) NOT NULL,
`provider_id` INT(11) NOT NULL,
`service_id` INT(11) NOT NULL,
`date` DATE NOT NULL,
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`duration` INT NOT NULL, -- in minutes
`status` ENUM('pending', 'confirmed', 'completed', 'cancelled', 'no_show', 'rescheduled') DEFAULT 'pending',
`payment_status` ENUM('pending', 'paid', 'refunded', 'failed') DEFAULT 'pending',
`payment_method` VARCHAR(50),
`payment_id` VARCHAR(255),
`amount` DECIMAL(10,2),
`currency` VARCHAR(3) DEFAULT 'USD',
`notes` TEXT,
`customer_notes` TEXT,
`provider_notes` TEXT,
`reminder_sent` BOOLEAN DEFAULT FALSE,
`reminder_sent_at` DATETIME,
`cancellation_reason` TEXT,
`cancelled_by` ENUM('customer', 'provider', 'admin') NULL,
`cancelled_at` DATETIME,
`rescheduled_from` INT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`customer_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`),
FOREIGN KEY (`service_id`) REFERENCES `services`(`id`),
FOREIGN KEY (`rescheduled_from`) REFERENCES `appointments`(`id`),
INDEX `idx_date` (`date`),
INDEX `idx_status` (`status`),
INDEX `idx_payment` (`payment_status`),
UNIQUE KEY `unique_appointment` (`provider_id`, `date`, `start_time`)
);
-- Reviews Table
CREATE TABLE `reviews` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`appointment_id` INT(11) NOT NULL,
`customer_id` INT(11) NOT NULL,
`provider_id` INT(11) NOT NULL,
`service_id` INT(11) NOT NULL,
`rating` TINYINT NOT NULL CHECK (rating >= 1 AND rating <= 5),
`title` VARCHAR(255),
`comment` TEXT,
`provider_response` TEXT,
`response_date` DATETIME,
`is_verified` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`appointment_id`) REFERENCES `appointments`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`customer_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`),
FOREIGN KEY (`service_id`) REFERENCES `services`(`id`),
UNIQUE KEY `unique_review` (`appointment_id`),
INDEX `idx_rating` (`rating`)
);
-- Notifications Table
CREATE TABLE `notifications` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`type` ENUM('email', 'sms', 'push') NOT NULL,
`title` VARCHAR(255) NOT NULL,
`message` TEXT NOT NULL,
`data` JSON,
`is_read` BOOLEAN DEFAULT FALSE,
`read_at` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_user_read` (`user_id`, `is_read`)
);
-- Payments Table
CREATE TABLE `payments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`appointment_id` INT(11) NOT NULL,
`transaction_id` VARCHAR(255) UNIQUE NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`currency` VARCHAR(3) DEFAULT 'USD',
`payment_method` VARCHAR(50),
`payment_status` ENUM('pending', 'completed', 'failed', 'refunded') DEFAULT 'pending',
`payment_details` JSON,
`refund_amount` DECIMAL(10,2),
`refund_reason` TEXT,
`refunded_at` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`appointment_id`) REFERENCES `appointments`(`id`) ON DELETE CASCADE,
INDEX `idx_transaction` (`transaction_id`),
INDEX `idx_status` (`payment_status`)
);
-- Favorites (Wishlist)
CREATE TABLE `favorites` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`customer_id` INT(11) NOT NULL,
`provider_id` INT(11) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`customer_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_favorite` (`customer_id`, `provider_id`)
);
-- Business Settings
CREATE TABLE `business_settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`provider_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`),
FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_setting` (`provider_id`, `setting_key`)
);
-- System Settings (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 Admin
INSERT INTO `users` (`user_id`, `email`, `password`, `first_name`, `last_name`, `role`, `email_verified`) 
VALUES ('ADMIN001', '[email protected]', '$2y$10$YourHashedPasswordHere', 'System', 'Administrator', 'admin', TRUE);
-- Insert Default System Settings
INSERT INTO `system_settings` (`setting_key`, `setting_value`, `description`) VALUES
('business_name', 'Appointment System', 'Name of the business'),
('business_email', '[email protected]', 'Contact email'),
('business_phone', '+1-555-123-4567', 'Contact phone'),
('business_address', '123 Business St, City, State 12345', 'Business address'),
('timezone', 'America/New_York', 'Default timezone'),
('date_format', 'Y-m-d', 'Date format'),
('time_format', 'H:i', 'Time format'),
('currency', 'USD', 'Default currency'),
('currency_symbol', '$', 'Currency symbol'),
('enable_payments', '1', 'Enable online payments'),
('payment_gateway', 'stripe', 'Payment gateway (stripe/paypal)'),
('enable_reminders', '1', 'Enable appointment reminders'),
('reminder_time', '24', 'Hours before appointment to send reminder'),
('enable_sms', '0', 'Enable SMS notifications'),
('sms_provider', 'twilio', 'SMS provider'),
('max_advance_booking', '90', 'Maximum days in advance for booking'),
('min_advance_booking', '1', 'Minimum hours in advance for booking'),
('cancellation_policy', '24', 'Hours before appointment for free cancellation'),
('no_show_fee', '0', 'Fee for no-show appointments'),
('business_hours_start', '09:00', 'Default business hours start'),
('business_hours_end', '17:00', 'Default business hours end'),
('app_version', '1.0.0', 'Application version');

Core PHP Classes

Database Class

File: includes/Database.php

<?php
/**
* Database Class
* Handles all database connections and operations using PDO with singleton pattern
*/
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') ?: 'appointment_system');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASS') ?: '');
// Application Configuration
define('APP_NAME', getenv('APP_NAME') ?: 'Appointment Booking System');
define('APP_URL', getenv('APP_URL') ?: 'http://localhost/appointment-system');
define('APP_VERSION', getenv('APP_VERSION') ?: '1.0.0');
define('DEBUG_MODE', getenv('DEBUG_MODE') === 'true');
// Security Configuration
define('SESSION_TIMEOUT', getenv('SESSION_TIMEOUT') ?: 3600); // 1 hour
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']);
// 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');
define('DATETIME_FORMAT', 'Y-m-d H:i:s');
// Business Settings
define('BUSINESS_NAME', getenv('BUSINESS_NAME') ?: 'Appointment System');
define('BUSINESS_EMAIL', getenv('BUSINESS_EMAIL') ?: '[email protected]');
define('BUSINESS_PHONE', getenv('BUSINESS_PHONE') ?: '+1-555-123-4567');
define('CURRENCY', getenv('CURRENCY') ?: 'USD');
define('CURRENCY_SYMBOL', getenv('CURRENCY_SYMBOL') ?: '$');
// Booking Settings
define('MAX_ADVANCE_BOOKING', getenv('MAX_ADVANCE_BOOKING') ?: 90); // days
define('MIN_ADVANCE_BOOKING', getenv('MIN_ADVANCE_BOOKING') ?: 1); // hours
define('CANCELLATION_POLICY', getenv('CANCELLATION_POLICY') ?: 24); // hours
// Notification Settings
define('ENABLE_REMINDERS', getenv('ENABLE_REMINDERS') === 'true');
define('REMINDER_TIME', getenv('REMINDER_TIME') ?: 24); // hours before
define('ENABLE_SMS', getenv('ENABLE_SMS') === 'true');
// Payment Settings
define('ENABLE_PAYMENTS', getenv('ENABLE_PAYMENTS') === 'true');
define('PAYMENT_GATEWAY', getenv('PAYMENT_GATEWAY') ?: 'stripe');
define('STRIPE_KEY', getenv('STRIPE_KEY') ?: '');
define('STRIPE_SECRET', getenv('STRIPE_SECRET') ?: '');
define('PAYPAL_CLIENT_ID', getenv('PAYPAL_CLIENT_ID') ?: '');
define('PAYPAL_SECRET', getenv('PAYPAL_SECRET') ?: '');
// 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__ . '/Appointment.php';
require_once __DIR__ . '/Service.php';
require_once __DIR__ . '/Provider.php';
require_once __DIR__ . '/Notification.php';
require_once __DIR__ . '/Payment.php';
require_once __DIR__ . '/Review.php';
// Initialize database connection
$db = Database::getInstance();
// Load system settings
$settings = $db->getRows("SELECT setting_key, setting_value FROM system_settings");
foreach ($settings as $setting) {
define(strtoupper($setting['setting_key']), $setting['setting_value']);
}
// 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();
}
/**
* Format amount with currency
*/
function formatAmount($amount, $currency = null) {
if ($currency === null) {
$currency = CURRENCY;
}
$symbols = [
'USD' => '$',
'EUR' => '€',
'GBP' => '£',
'JPY' => '¥',
'INR' => '₹',
'CAD' => 'C$',
'AUD' => 'A$'
];
$symbol = $symbols[$currency] ?? CURRENCY_SYMBOL;
return $symbol . ' ' . number_format($amount, 2);
}
/**
* Format date
*/
function formatDate($date, $format = null) {
if ($format === null) {
$format = DATE_FORMAT;
}
if ($date instanceof DateTime) {
return $date->format($format);
}
return date($format, strtotime($date));
}
/**
* Format time
*/
function formatTime($time, $format = null) {
if ($format === null) {
$format = TIME_FORMAT;
}
return date($format, strtotime($time));
}
/**
* 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 day name from day number
*/
function getDayName($dayNumber) {
$days = [
0 => 'Sunday',
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday'
];
return $days[$dayNumber] ?? '';
}
/**
* Generate time slots between start and end time
*/
function generateTimeSlots($startTime, $endTime, $duration, $buffer = 0) {
$slots = [];
$start = strtotime($startTime);
$end = strtotime($endTime);
while ($start + ($duration * 60) <= $end) {
$slotEnd = $start + ($duration * 60);
$slots[] = [
'start' => date('H:i', $start),
'end' => date('H:i', $slotEnd)
];
$start = $slotEnd + ($buffer * 60);
}
return $slots;
}
/**
* Check if time slot is available
*/
function isTimeSlotAvailable($providerId, $date, $startTime, $endTime, $excludeAppointmentId = null) {
$db = Database::getInstance();
$sql = "SELECT COUNT(*) FROM appointments 
WHERE provider_id = :provider_id 
AND date = :date 
AND status NOT IN ('cancelled', 'no_show')
AND (
(start_time < :end_time AND end_time > :start_time)
)";
$params = [
'provider_id' => $providerId,
'date' => $date,
'start_time' => $startTime,
'end_time' => $endTime
];
if ($excludeAppointmentId) {
$sql .= " AND id != :exclude_id";
$params['exclude_id'] = $excludeAppointmentId;
}
$count = $db->getValue($sql, $params);
return $count == 0;
}
/**
* Generate unique appointment ID
*/
function generateAppointmentId() {
return 'APT' . date('Ymd') . strtoupper(uniqid());
}
/**
* Generate unique user ID
*/
function generateUserId($role) {
$prefix = strtoupper(substr($role, 0, 3));
return $prefix . date('Y') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
}
/**
* Calculate end time based on start time and duration
*/
function calculateEndTime($startTime, $duration) {
return date('H:i', strtotime($startTime) + ($duration * 60));
}
/**
* Get available time slots for provider on a specific date
*/
function getAvailableSlots($providerId, $date, $serviceId = null) {
$db = Database::getInstance();
// Get provider schedule for the day
$dayOfWeek = date('w', strtotime($date));
$schedule = $db->getRow(
"SELECT * FROM provider_schedule 
WHERE provider_id = ? AND day_of_week = ?",
[$providerId, $dayOfWeek]
);
if (!$schedule || !$schedule['is_working']) {
return [];
}
// Check if provider has time off
$timeOff = $db->getRow(
"SELECT * FROM provider_time_off 
WHERE provider_id = ? 
AND start_date <= ? 
AND end_date >= ?",
[$providerId, $date, $date]
);
if ($timeOff) {
if ($timeOff['is_full_day']) {
return [];
}
// Adjust schedule based on time off
}
// Get service duration
if ($serviceId) {
$service = $db->getRow("SELECT duration, buffer_before, buffer_after FROM services WHERE id = ?", [$serviceId]);
$duration = $service['duration'];
$bufferBefore = $service['buffer_before'];
$bufferAfter = $service['buffer_after'];
} else {
// Use default duration from provider settings
$provider = $db->getRow("SELECT buffer_time FROM providers WHERE id = ?", [$providerId]);
$duration = 60; // Default 60 minutes
$bufferBefore = $provider['buffer_time'] ?? 15;
$bufferAfter = $provider['buffer_time'] ?? 15;
}
// Get existing appointments for the day
$appointments = $db->getRows(
"SELECT start_time, end_time FROM appointments 
WHERE provider_id = ? AND date = ? 
AND status NOT IN ('cancelled', 'no_show')",
[$providerId, $date]
);
// Generate all possible slots
$allSlots = generateTimeSlots(
$schedule['start_time'],
$schedule['end_time'],
$duration,
$bufferAfter
);
// Filter out booked slots
$availableSlots = [];
foreach ($allSlots as $slot) {
$isAvailable = true;
foreach ($appointments as $apt) {
if (!($slot['end'] <= $apt['start_time'] || $slot['start'] >= $apt['end_time'])) {
$isAvailable = false;
break;
}
}
if ($isAvailable) {
$availableSlots[] = $slot;
}
}
return $availableSlots;
}
/**
* 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 = uniqid() . '_' . time() . '.' . $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'];
}
/**
* Send email notification
*/
function sendEmail($to, $subject, $template, $data = []) {
// Load email template
$templateFile = __DIR__ . "/notifications/email_templates/{$template}.php";
if (!file_exists($templateFile)) {
logError("Email template not found: {$template}");
return false;
}
// Extract data for template
extract($data);
ob_start();
include $templateFile;
$message = ob_get_clean();
// Headers
$headers = [
'MIME-Version: 1.0',
'Content-type: text/html; charset=utf-8',
'From: ' . BUSINESS_NAME . ' <' . BUSINESS_EMAIL . '>',
'Reply-To: ' . BUSINESS_EMAIL,
'X-Mailer: PHP/' . phpversion()
];
return mail($to, $subject, $message, implode("\r\n", $headers));
}
/**
* Send SMS notification
*/
function sendSMS($to, $message) {
if (!ENABLE_SMS) {
return false;
}
// Implement SMS sending logic (Twilio, etc.)
// This is a placeholder
return true;
}
/**
* 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 phone number
*/
function validatePhone($phone) {
return preg_match('/^[0-9\-\(\)\/\+\s]+$/', $phone);
}
/**
* Get time slots for display
*/
function getTimeSlotsForDisplay($providerId, $date) {
$slots = getAvailableSlots($providerId, $date);
$html = '';
foreach ($slots as $slot) {
$html .= '<button type="button" class="btn btn-outline-primary time-slot m-1" 
data-start="' . $slot['start'] . '" 
data-end="' . $slot['end'] . '">
' . formatTime($slot['start']) . ' - ' . formatTime($slot['end']) . '
</button>';
}
return $html;
}
/**
* Calculate average rating
*/
function calculateAverageRating($providerId) {
$db = Database::getInstance();
$result = $db->getRow(
"SELECT AVG(rating) as avg_rating, COUNT(*) as total_reviews 
FROM reviews WHERE provider_id = ?",
[$providerId]
);
return [
'average' => round($result['avg_rating'] ?? 0, 1),
'total' => $result['total_reviews'] ?? 0
];
}
/**
* 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;
}
?>

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 email already exists
$existing = $this->db->getRow(
"SELECT id FROM users WHERE email = ?",
[$data['email']]
);
if ($existing) {
return ['success' => false, 'error' => 'Email already registered'];
}
// Hash password
$hashedPassword = password_hash($data['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
// Generate user ID
$userId = generateUserId($data['role'] ?? 'customer');
// Generate verification token
$verificationToken = generateRandomString();
// Prepare user data
$userData = [
'user_id' => $userId,
'email' => $data['email'],
'password' => $hashedPassword,
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'phone' => $data['phone'] ?? null,
'role' => $data['role'] ?? 'customer',
'verification_token' => $verificationToken
];
// Insert user
$newUserId = $this->db->insert('users', $userData);
if ($newUserId) {
// If role is provider, create provider record
if ($data['role'] === 'provider') {
$this->createProviderRecord($newUserId, $data);
}
// Send verification email
$this->sendVerificationEmail($data['email'], $verificationToken);
return [
'success' => true,
'user_id' => $newUserId,
'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()];
}
}
/**
* Create provider record
*/
private function createProviderRecord($userId, $data) {
$providerData = [
'user_id' => $userId,
'business_name' => $data['business_name'] ?? null,
'bio' => $data['bio'] ?? null,
'specialization' => $data['specialization'] ?? null,
'experience_years' => $data['experience_years'] ?? 0,
'base_price' => $data['base_price'] ?? 0,
'buffer_time' => $data['buffer_time'] ?? 15
];
$this->db->insert('providers', $providerData);
// Create default schedule
$this->createDefaultSchedule($userId);
}
/**
* Create default schedule for provider
*/
private function createDefaultSchedule($providerId) {
// Monday to Friday, 9 AM to 5 PM
for ($day = 1; $day <= 5; $day++) {
$this->db->insert('provider_schedule', [
'provider_id' => $providerId,
'day_of_week' => $day,
'is_working' => true,
'start_time' => '09:00:00',
'end_time' => '17:00:00'
]);
}
// Saturday and Sunday off
for ($day = 0; $day <= 6; $day += 6) {
$this->db->insert('provider_schedule', [
'provider_id' => $providerId,
'day_of_week' => $day,
'is_working' => false
]);
}
}
/**
* Login user
*/
public function login($email, $password, $remember = false) {
try {
// Get user
$user = $this->db->getRow(
"SELECT * FROM users WHERE email = ? AND status = 'active'",
[$email]
);
if (!$user) {
return ['success' => false, 'error' => 'Invalid email 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 email 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['user_email'] = $user['email'];
$_SESSION['user_name'] = $user['first_name'] . ' ' . $user['last_name'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['user_id_display'] = $user['user_id'];
$_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']);
}
// If provider, get provider details
if ($user['role'] === 'provider') {
$provider = $this->db->getRow(
"SELECT id FROM providers WHERE user_id = ?",
[$user['id']]
);
$_SESSION['provider_id'] = $provider['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 based on role
if ($this->hasRole('admin')) {
redirect('/admin/dashboard.php');
} elseif ($this->hasRole('provider')) {
redirect('/provider/dashboard.php');
} else {
redirect('/customer/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;
$data = [
'verification_link' => APP_URL . "/verify.php?token=" . $token
];
return sendEmail($email, $subject, 'verification', $data);
}
/**
* 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;
$data = [
'reset_link' => APP_URL . "/reset_password.php?token=" . $token
];
return sendEmail($email, $subject, 'password_reset', $data);
}
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;
}
/**
* Update user profile
*/
public function updateProfile($userId, $data) {
try {
$updateData = [
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'phone' => $data['phone'] ?? null,
'address' => $data['address'] ?? null,
'city' => $data['city'] ?? null,
'state' => $data['state'] ?? null,
'country' => $data['country'] ?? null,
'postal_code' => $data['postal_code'] ?? null
];
// Handle profile picture upload
if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'avatars/';
$result = uploadFile($_FILES['profile_picture'], $uploadDir);
if ($result['success']) {
$updateData['profile_picture'] = $result['filename'];
}
}
$this->db->update('users', $updateData, 'id = :id', ['id' => $userId]);
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();
?>

Appointment Class

File: includes/Appointment.php

<?php
/**
* Appointment Class
* Handles all appointment-related operations
*/
class Appointment {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Book a new appointment
*/
public function book($customerId, $data) {
try {
$this->db->beginTransaction();
// Validate availability
if (!$this->checkAvailability($data['provider_id'], $data['date'], $data['start_time'], $data['end_time'])) {
return ['success' => false, 'error' => 'Selected time slot is no longer available'];
}
// Get service details
$service = $this->db->getRow(
"SELECT * FROM services WHERE id = ? AND provider_id = ?",
[$data['service_id'], $data['provider_id']]
);
if (!$service) {
return ['success' => false, 'error' => 'Invalid service selected'];
}
// Generate appointment ID
$appointmentId = generateAppointmentId();
// Prepare appointment data
$appointmentData = [
'appointment_id' => $appointmentId,
'customer_id' => $customerId,
'provider_id' => $data['provider_id'],
'service_id' => $data['service_id'],
'date' => $data['date'],
'start_time' => $data['start_time'],
'end_time' => $data['end_time'],
'duration' => $service['duration'],
'amount' => $service['price'],
'currency' => $service['currency'],
'customer_notes' => $data['notes'] ?? null,
'status' => 'pending',
'payment_status' => ENABLE_PAYMENTS ? 'pending' : 'paid'
];
// Insert appointment
$appointmentDbId = $this->db->insert('appointments', $appointmentData);
// Process payment if required
if (ENABLE_PAYMENTS && isset($data['payment_method'])) {
$paymentResult = $this->processPayment($appointmentDbId, $data);
if (!$paymentResult['success']) {
throw new Exception($paymentResult['error']);
}
}
// Send confirmation notifications
$this->sendBookingConfirmation($appointmentDbId);
$this->db->commit();
return [
'success' => true,
'appointment_id' => $appointmentId,
'message' => 'Appointment booked successfully'
];
} catch (Exception $e) {
$this->db->rollback();
logError('Booking error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Failed to book appointment: ' . $e->getMessage()];
}
}
/**
* Check if time slot is available
*/
public function checkAvailability($providerId, $date, $startTime, $endTime) {
// Check for conflicting appointments
$conflict = $this->db->getRow(
"SELECT id FROM appointments 
WHERE provider_id = :provider_id 
AND date = :date 
AND status NOT IN ('cancelled', 'no_show')
AND (
(start_time < :end_time AND end_time > :start_time)
)",
[
'provider_id' => $providerId,
'date' => $date,
'start_time' => $startTime,
'end_time' => $endTime
]
);
if ($conflict) {
return false;
}
// Check provider's working hours
$dayOfWeek = date('w', strtotime($date));
$schedule = $this->db->getRow(
"SELECT * FROM provider_schedule 
WHERE provider_id = ? AND day_of_week = ? AND is_working = 1",
[$providerId, $dayOfWeek]
);
if (!$schedule) {
return false;
}
if ($startTime < $schedule['start_time'] || $endTime > $schedule['end_time']) {
return false;
}
// Check for time off
$timeOff = $this->db->getRow(
"SELECT * FROM provider_time_off 
WHERE provider_id = ? 
AND start_date <= ? 
AND end_date >= ?",
[$providerId, $date, $date]
);
if ($timeOff) {
if ($timeOff['is_full_day']) {
return false;
}
if (!$timeOff['is_full_day'] && $startTime >= $timeOff['start_time'] && $endTime <= $timeOff['end_time']) {
return false;
}
}
return true;
}
/**
* Cancel appointment
*/
public function cancel($appointmentId, $userId, $role, $reason = null) {
try {
$appointment = $this->getAppointmentById($appointmentId);
if (!$appointment) {
return ['success' => false, 'error' => 'Appointment not found'];
}
// Check permissions
if ($role === 'customer' && $appointment['customer_id'] != $userId) {
return ['success' => false, 'error' => 'Unauthorized to cancel this appointment'];
}
if ($role === 'provider' && $appointment['provider_id'] != $userId) {
return ['success' => false, 'error' => 'Unauthorized to cancel this appointment'];
}
// Check if cancellation is allowed
if ($role === 'customer') {
$appointmentTime = strtotime($appointment['date'] . ' ' . $appointment['start_time']);
$hoursUntilAppointment = ($appointmentTime - time()) / 3600;
if ($hoursUntilAppointment < CANCELLATION_POLICY) {
return ['success' => false, 'error' => 'Appointments can only be cancelled ' . CANCELLATION_POLICY . ' hours in advance'];
}
}
// Update appointment status
$this->db->update(
'appointments',
[
'status' => 'cancelled',
'cancellation_reason' => $reason,
'cancelled_by' => $role,
'cancelled_at' => date('Y-m-d H:i:s')
],
'id = :id',
['id' => $appointment['id']]
);
// Process refund if paid
if ($appointment['payment_status'] === 'paid') {
$this->processRefund($appointment['id']);
}
// Send cancellation notifications
$this->sendCancellationNotification($appointment['id']);
return ['success' => true, 'message' => 'Appointment cancelled successfully'];
} catch (Exception $e) {
logError('Cancel appointment error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to cancel appointment'];
}
}
/**
* Reschedule appointment
*/
public function reschedule($appointmentId, $userId, $role, $newDate, $newStartTime, $newEndTime) {
try {
$appointment = $this->getAppointmentById($appointmentId);
if (!$appointment) {
return ['success' => false, 'error' => 'Appointment not found'];
}
// Check permissions
if ($role === 'customer' && $appointment['customer_id'] != $userId) {
return ['success' => false, 'error' => 'Unauthorized to reschedule this appointment'];
}
if ($role === 'provider' && $appointment['provider_id'] != $userId) {
return ['success' => false, 'error' => 'Unauthorized to reschedule this appointment'];
}
// Check new slot availability
if (!$this->checkAvailability($appointment['provider_id'], $newDate, $newStartTime, $newEndTime)) {
return ['success' => false, 'error' => 'Selected time slot is not available'];
}
$this->db->beginTransaction();
// Create rescheduled from reference
$this->db->update(
'appointments',
[
'status' => 'rescheduled',
'rescheduled_from' => $appointment['id']
],
'id = :id',
['id' => $appointment['id']]
);
// Create new appointment
$newAppointmentData = [
'appointment_id' => generateAppointmentId(),
'customer_id' => $appointment['customer_id'],
'provider_id' => $appointment['provider_id'],
'service_id' => $appointment['service_id'],
'date' => $newDate,
'start_time' => $newStartTime,
'end_time' => $newEndTime,
'duration' => $appointment['duration'],
'amount' => $appointment['amount'],
'currency' => $appointment['currency'],
'status' => 'confirmed',
'payment_status' => $appointment['payment_status'],
'rescheduled_from' => $appointment['id']
];
$newId = $this->db->insert('appointments', $newAppointmentData);
$this->db->commit();
// Send reschedule notifications
$this->sendRescheduleNotification($appointment['id'], $newId);
return ['success' => true, 'message' => 'Appointment rescheduled successfully'];
} catch (Exception $e) {
$this->db->rollback();
logError('Reschedule error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to reschedule appointment'];
}
}
/**
* Get appointment by ID
*/
public function getAppointmentById($appointmentId) {
return $this->db->getRow(
"SELECT a.*, 
c.first_name as customer_first_name, c.last_name as customer_last_name,
c.email as customer_email, c.phone as customer_phone,
p.business_name, p.user_id as provider_user_id,
s.name as service_name, s.duration as service_duration
FROM appointments a
JOIN users c ON a.customer_id = c.id
JOIN providers p ON a.provider_id = p.id
JOIN services s ON a.service_id = s.id
WHERE a.id = ? OR a.appointment_id = ?",
[$appointmentId, $appointmentId]
);
}
/**
* Get customer appointments
*/
public function getCustomerAppointments($customerId, $status = null, $limit = null) {
$sql = "SELECT a.*, 
p.business_name,
s.name as service_name,
u.first_name as provider_first_name,
u.last_name as provider_last_name
FROM appointments a
JOIN providers p ON a.provider_id = p.id
JOIN services s ON a.service_id = s.id
JOIN users u ON p.user_id = u.id
WHERE a.customer_id = :customer_id";
$params = ['customer_id' => $customerId];
if ($status) {
if (is_array($status)) {
$placeholders = implode(',', array_fill(0, count($status), '?'));
$sql .= " AND a.status IN ($placeholders)";
$params = array_merge([$customerId], $status);
} else {
$sql .= " AND a.status = :status";
$params['status'] = $status;
}
}
$sql .= " ORDER BY a.date DESC, a.start_time DESC";
if ($limit) {
$sql .= " LIMIT :limit";
$params['limit'] = $limit;
}
return $this->db->getRows($sql, $params);
}
/**
* Get provider appointments
*/
public function getProviderAppointments($providerId, $date = null, $status = null) {
$sql = "SELECT a.*, 
u.first_name, u.last_name, u.email, u.phone,
s.name as service_name
FROM appointments a
JOIN users u ON a.customer_id = u.id
JOIN services s ON a.service_id = s.id
WHERE a.provider_id = :provider_id";
$params = ['provider_id' => $providerId];
if ($date) {
$sql .= " AND a.date = :date";
$params['date'] = $date;
}
if ($status) {
$sql .= " AND a.status = :status";
$params['status'] = $status;
}
$sql .= " ORDER BY a.date ASC, a.start_time ASC";
return $this->db->getRows($sql, $params);
}
/**
* Get upcoming appointments
*/
public function getUpcomingAppointments($userId, $role) {
$today = date('Y-m-d');
$now = date('H:i:s');
if ($role === 'customer') {
return $this->getCustomerAppointments($userId, ['pending', 'confirmed']);
} elseif ($role === 'provider') {
return $this->getProviderAppointments($userId, null, ['pending', 'confirmed']);
}
return [];
}
/**
* Get appointment statistics
*/
public function getStatistics($providerId = null, $startDate = null, $endDate = null) {
if (!$startDate) {
$startDate = date('Y-m-d', strtotime('-30 days'));
}
if (!$endDate) {
$endDate = date('Y-m-d');
}
$sql = "SELECT 
COUNT(*) as total_appointments,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled,
SUM(CASE WHEN status = 'no_show' THEN 1 ELSE 0 END) as no_show,
SUM(amount) as total_revenue,
AVG(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) * 100 as completion_rate
FROM appointments
WHERE date BETWEEN :start_date AND :end_date";
$params = [
'start_date' => $startDate,
'end_date' => $endDate
];
if ($providerId) {
$sql .= " AND provider_id = :provider_id";
$params['provider_id'] = $providerId;
}
return $this->db->getRow($sql, $params);
}
/**
* Process payment
*/
private function processPayment($appointmentId, $data) {
// Implement payment processing logic (Stripe, PayPal, etc.)
// This is a placeholder
$payment = new Payment();
return $payment->process($appointmentId, $data);
}
/**
* Process refund
*/
private function processRefund($appointmentId) {
// Implement refund logic
$payment = new Payment();
return $payment->refund($appointmentId);
}
/**
* Send booking confirmation notifications
*/
private function sendBookingConfirmation($appointmentId) {
$appointment = $this->getAppointmentById($appointmentId);
// Send email to customer
$customerData = [
'appointment' => $appointment,
'customer_name' => $appointment['customer_first_name'] . ' ' . $appointment['customer_last_name'],
'provider_name' => $appointment['business_name'],
'service_name' => $appointment['service_name'],
'date' => formatDate($appointment['date']),
'time' => formatTime($appointment['start_time']) . ' - ' . formatTime($appointment['end_time'])
];
sendEmail(
$appointment['customer_email'],
'Appointment Confirmation - ' . APP_NAME,
'booking_confirmation',
$customerData
);
// Send SMS if enabled
if (ENABLE_SMS && $appointment['customer_phone']) {
$message = "Your appointment with {$appointment['business_name']} on " .
formatDate($appointment['date']) . " at " .
formatTime($appointment['start_time']) . " has been confirmed.";
sendSMS($appointment['customer_phone'], $message);
}
// Send notification to provider
$notification = new Notification();
$notification->create(
$appointment['provider_user_id'],
'New Appointment Booking',
"New appointment booked by {$appointment['customer_first_name']} {$appointment['customer_last_name']}",
['appointment_id' => $appointmentId]
);
}
/**
* Send cancellation notification
*/
private function sendCancellationNotification($appointmentId) {
$appointment = $this->getAppointmentById($appointmentId);
// Send email to customer
sendEmail(
$appointment['customer_email'],
'Appointment Cancelled - ' . APP_NAME,
'cancellation',
['appointment' => $appointment]
);
// Send notification to provider
$notification = new Notification();
$notification->create(
$appointment['provider_user_id'],
'Appointment Cancelled',
"Appointment with {$appointment['customer_first_name']} {$appointment['customer_last_name']} has been cancelled",
['appointment_id' => $appointmentId]
);
}
/**
* Send reschedule notification
*/
private function sendRescheduleNotification($oldAppointmentId, $newAppointmentId) {
$oldAppointment = $this->getAppointmentById($oldAppointmentId);
$newAppointment = $this->getAppointmentById($newAppointmentId);
// Send email to customer
$customerData = [
'old_appointment' => $oldAppointment,
'new_appointment' => $newAppointment,
'customer_name' => $newAppointment['customer_first_name'] . ' ' . $newAppointment['customer_last_name'],
'provider_name' => $newAppointment['business_name']
];
sendEmail(
$newAppointment['customer_email'],
'Appointment Rescheduled - ' . APP_NAME,
'reschedule',
$customerData
);
// Send notification to provider
$notification = new Notification();
$notification->create(
$newAppointment['provider_user_id'],
'Appointment Rescheduled',
"Appointment with {$newAppointment['customer_first_name']} {$newAppointment['customer_last_name']} has been rescheduled",
['old_appointment_id' => $oldAppointmentId, 'new_appointment_id' => $newAppointmentId]
);
}
/**
* Send reminders for upcoming appointments
*/
public function sendReminders() {
$reminderTime = REMINDER_TIME; // hours before
$targetTime = date('Y-m-d H:i:s', strtotime("+{$reminderTime} hours"));
$targetDate = date('Y-m-d', strtotime($targetTime));
$targetHour = date('H', strtotime($targetTime));
// Get appointments that need reminders
$appointments = $this->db->getRows(
"SELECT a.*, 
c.email as customer_email, c.phone as customer_phone,
c.first_name, c.last_name,
p.business_name
FROM appointments a
JOIN users c ON a.customer_id = c.id
JOIN providers p ON a.provider_id = p.id
WHERE a.date = :date 
AND HOUR(a.start_time) = :hour
AND a.status IN ('confirmed')
AND a.reminder_sent = 0",
[
'date' => $targetDate,
'hour' => $targetHour
]
);
foreach ($appointments as $appointment) {
// Send email reminder
$customerData = [
'appointment' => $appointment,
'customer_name' => $appointment['first_name'] . ' ' . $appointment['last_name'],
'provider_name' => $appointment['business_name'],
'date' => formatDate($appointment['date']),
'time' => formatTime($appointment['start_time']) . ' - ' . formatTime($appointment['end_time'])
];
sendEmail(
$appointment['customer_email'],
'Appointment Reminder - ' . APP_NAME,
'reminder',
$customerData
);
// Send SMS reminder
if (ENABLE_SMS && $appointment['customer_phone']) {
$message = "Reminder: You have an appointment with {$appointment['business_name']} on " .
formatDate($appointment['date']) . " at " .
formatTime($appointment['start_time']);
sendSMS($appointment['customer_phone'], $message);
}
// Mark reminder as sent
$this->db->update(
'appointments',
[
'reminder_sent' => true,
'reminder_sent_at' => date('Y-m-d H:i:s')
],
'id = :id',
['id' => $appointment['id']]
);
}
return count($appointments);
}
}
?>

Provider Class

File: includes/Provider.php

<?php
/**
* Provider Class
* Handles all service provider-related operations
*/
class Provider {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Get provider details
*/
public function getProvider($providerId) {
return $this->db->getRow(
"SELECT p.*, u.first_name, u.last_name, u.email, u.phone, u.profile_picture,
u.address, u.city, u.state, u.country
FROM providers p
JOIN users u ON p.user_id = u.id
WHERE p.id = ?",
[$providerId]
);
}
/**
* Get provider by user ID
*/
public function getProviderByUserId($userId) {
return $this->db->getRow(
"SELECT * FROM providers WHERE user_id = ?",
[$userId]
);
}
/**
* Get all providers with filters
*/
public function getProviders($filters = []) {
$sql = "SELECT p.*, u.first_name, u.last_name, u.profile_picture,
(SELECT AVG(rating) FROM reviews WHERE provider_id = p.id) as avg_rating,
(SELECT COUNT(*) FROM reviews WHERE provider_id = p.id) as review_count
FROM providers p
JOIN users u ON p.user_id = u.id
WHERE u.status = 'active'";
$params = [];
if (!empty($filters['service_id'])) {
$sql .= " AND p.id IN (SELECT provider_id FROM services WHERE id = :service_id)";
$params['service_id'] = $filters['service_id'];
}
if (!empty($filters['category_id'])) {
$sql .= " AND p.id IN (SELECT provider_id FROM services WHERE category_id = :category_id)";
$params['category_id'] = $filters['category_id'];
}
if (!empty($filters['search'])) {
$sql .= " AND (p.business_name LIKE :search OR u.first_name LIKE :search OR u.last_name LIKE :search)";
$params['search'] = '%' . $filters['search'] . '%';
}
if (!empty($filters['min_rating'])) {
$sql .= " HAVING avg_rating >= :min_rating";
$params['min_rating'] = $filters['min_rating'];
}
if (!empty($filters['featured'])) {
$sql .= " AND p.featured = 1";
}
$sql .= " ORDER BY p.featured DESC, avg_rating 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 provider services
*/
public function getServices($providerId) {
return $this->db->getRows(
"SELECT s.*, c.name as category_name
FROM services s
LEFT JOIN service_categories c ON s.category_id = c.id
WHERE s.provider_id = ? AND s.is_active = 1
ORDER BY s.name ASC",
[$providerId]
);
}
/**
* Get provider schedule
*/
public function getSchedule($providerId) {
return $this->db->getRows(
"SELECT * FROM provider_schedule 
WHERE provider_id = ? 
ORDER BY day_of_week ASC",
[$providerId]
);
}
/**
* Update provider schedule
*/
public function updateSchedule($providerId, $scheduleData) {
try {
$this->db->beginTransaction();
// Delete existing schedule
$this->db->delete('provider_schedule', 'provider_id = ?', [$providerId]);
// Insert new schedule
foreach ($scheduleData as $dayData) {
$dayData['provider_id'] = $providerId;
$this->db->insert('provider_schedule', $dayData);
}
$this->db->commit();
return ['success' => true, 'message' => 'Schedule updated successfully'];
} catch (Exception $e) {
$this->db->rollback();
logError('Update schedule error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to update schedule'];
}
}
/**
* Add time off
*/
public function addTimeOff($providerId, $data) {
$timeOffData = [
'provider_id' => $providerId,
'title' => $data['title'],
'start_date' => $data['start_date'],
'end_date' => $data['end_date'],
'start_time' => $data['start_time'] ?? null,
'end_time' => $data['end_time'] ?? null,
'is_full_day' => $data['is_full_day'] ?? true,
'reason' => $data['reason'] ?? null
];
$id = $this->db->insert('provider_time_off', $timeOffData);
if ($id) {
return ['success' => true, 'message' => 'Time off added successfully'];
}
return ['success' => false, 'error' => 'Failed to add time off'];
}
/**
* Get time off periods
*/
public function getTimeOff($providerId, $startDate = null, $endDate = null) {
$sql = "SELECT * FROM provider_time_off WHERE provider_id = :provider_id";
$params = ['provider_id' => $providerId];
if ($startDate) {
$sql .= " AND end_date >= :start_date";
$params['start_date'] = $startDate;
}
if ($endDate) {
$sql .= " AND start_date <= :end_date";
$params['end_date'] = $endDate;
}
$sql .= " ORDER BY start_date ASC";
return $this->db->getRows($sql, $params);
}
/**
* Delete time off
*/
public function deleteTimeOff($timeOffId, $providerId) {
$deleted = $this->db->delete(
'provider_time_off',
'id = :id AND provider_id = :provider_id',
['id' => $timeOffId, 'provider_id' => $providerId]
);
if ($deleted) {
return ['success' => true, 'message' => 'Time off deleted successfully'];
}
return ['success' => false, 'error' => 'Failed to delete time off'];
}
/**
* Get provider reviews
*/
public function getReviews($providerId) {
return $this->db->getRows(
"SELECT r.*, u.first_name, u.last_name, u.profile_picture,
s.name as service_name
FROM reviews r
JOIN users u ON r.customer_id = u.id
JOIN services s ON r.service_id = s.id
WHERE r.provider_id = ?
ORDER BY r.created_at DESC",
[$providerId]
);
}
/**
* Update provider profile
*/
public function updateProfile($providerId, $data) {
try {
$updateData = [
'business_name' => $data['business_name'],
'bio' => $data['bio'] ?? null,
'specialization' => $data['specialization'] ?? null,
'qualifications' => $data['qualifications'] ?? null,
'experience_years' => $data['experience_years'] ?? 0,
'languages' => $data['languages'] ?? null,
'base_price' => $data['base_price'] ?? 0,
'buffer_time' => $data['buffer_time'] ?? 15
];
$this->db->update('providers', $updateData, 'id = :id', ['id' => $providerId]);
return ['success' => true, 'message' => 'Profile updated successfully'];
} catch (Exception $e) {
logError('Update provider profile error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to update profile'];
}
}
/**
* Get provider statistics
*/
public function getStatistics($providerId) {
$appointment = new Appointment();
$stats = $appointment->getStatistics($providerId);
// Get total customers
$stats['total_customers'] = $this->db->getValue(
"SELECT COUNT(DISTINCT customer_id) FROM appointments WHERE provider_id = ?",
[$providerId]
);
// Get rating
$rating = $this->db->getRow(
"SELECT AVG(rating) as avg_rating, COUNT(*) as total_reviews 
FROM reviews WHERE provider_id = ?",
[$providerId]
);
$stats['avg_rating'] = round($rating['avg_rating'] ?? 0, 1);
$stats['total_reviews'] = $rating['total_reviews'] ?? 0;
return $stats;
}
/**
* Get available dates for provider
*/
public function getAvailableDates($providerId, $serviceId = null, $startDate = null, $endDate = null) {
if (!$startDate) {
$startDate = date('Y-m-d');
}
if (!$endDate) {
$endDate = date('Y-m-d', strtotime('+' . MAX_ADVANCE_BOOKING . ' days'));
}
$availableDates = [];
$currentDate = $startDate;
while ($currentDate <= $endDate) {
$slots = getAvailableSlots($providerId, $currentDate, $serviceId);
if (!empty($slots)) {
$availableDates[] = [
'date' => $currentDate,
'slots' => $slots
];
}
$currentDate = date('Y-m-d', strtotime($currentDate . ' +1 day'));
}
return $availableDates;
}
}
?>

Frontend Pages

Main Landing Page

File: index.php

<?php
require_once 'includes/config.php';
// Get featured providers
$provider = new Provider();
$featuredProviders = $provider->getProviders(['featured' => true, 'limit' => 6]);
// Get service categories
$categories = $db->getRows(
"SELECT * FROM service_categories WHERE is_active = 1 ORDER BY sort_order ASC"
);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo APP_NAME; ?> - Book Appointments Online</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>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm fixed-top">
<div class="container">
<a class="navbar-brand" href="index.php">
<i class="fas fa-calendar-check text-primary me-2"></i>
<?php echo APP_NAME; ?>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link active" href="index.php">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#how-it-works">How It Works</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#providers">Providers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#categories">Services</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#contact">Contact</a>
</li>
<?php if ($auth->isLoggedIn()): ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user-circle me-1"></i>
<?php echo htmlspecialchars($_SESSION['user_name']); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="<?php 
echo $_SESSION['user_role'] == 'admin' ? 'admin/dashboard.php' : 
($_SESSION['user_role'] == 'provider' ? 'provider/dashboard.php' : 'customer/dashboard.php'); 
?>">
<i class="fas fa-tachometer-alt me-2"></i>Dashboard
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item text-danger" href="logout.php">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</a>
</li>
</ul>
</li>
<?php else: ?>
<li class="nav-item">
<a class="nav-link" href="login.php">Login</a>
</li>
<li class="nav-item">
<a class="btn btn-primary ms-2" href="register.php">Sign Up</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="hero-section bg-primary text-white py-5 mt-5">
<div class="container py-5">
<div class="row align-items-center">
<div class="col-lg-6">
<h1 class="display-4 fw-bold mb-4">Book Appointments with Top Professionals</h1>
<p class="lead mb-4">Find and book appointments with trusted service providers in your area. Easy, fast, and secure.</p>
<div class="search-box bg-white p-2 rounded-pill">
<form action="customer/search.php" method="GET" class="d-flex">
<input type="text" class="form-control border-0" name="search" 
placeholder="Search for services or providers..." required>
<button type="submit" class="btn btn-primary rounded-pill px-4">
<i class="fas fa-search me-2"></i>Search
</button>
</form>
</div>
<div class="mt-4">
<p class="mb-2">Popular searches:</p>
<div class="d-flex flex-wrap gap-2">
<a href="customer/search.php?search=Doctor" class="badge bg-light text-dark text-decoration-none p-2">Doctor</a>
<a href="customer/search.php?search=Salon" class="badge bg-light text-dark text-decoration-none p-2">Salon</a>
<a href="customer/search.php?search=Consultant" class="badge bg-light text-dark text-decoration-none p-2">Consultant</a>
<a href="customer/search.php?search=Trainer" class="badge bg-light text-dark text-decoration-none p-2">Trainer</a>
<a href="customer/search.php?search=Therapist" class="badge bg-light text-dark text-decoration-none p-2">Therapist</a>
</div>
</div>
</div>
<div class="col-lg-6 text-center">
<img src="assets/images/hero-illustration.svg" alt="Appointment Booking" class="img-fluid">
</div>
</div>
</div>
</section>
<!-- How It Works -->
<section id="how-it-works" class="py-5">
<div class="container">
<h2 class="text-center fw-bold mb-5">How It Works</h2>
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="step-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-search"></i>
</div>
<h4>1. Find a Service</h4>
<p class="text-muted">Browse through our list of professional service providers or search for specific services.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="step-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-calendar-alt"></i>
</div>
<h4>2. Choose a Time</h4>
<p class="text-muted">Select your preferred provider and pick an available time slot that works for you.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="step-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-check-circle"></i>
</div>
<h4>3. Get It Done</h4>
<p class="text-muted">Confirm your booking and receive instant confirmation. Get reminders before your appointment.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Service Categories -->
<section id="categories" class="py-5 bg-light">
<div class="container">
<h2 class="text-center fw-bold mb-5">Browse by Category</h2>
<div class="row g-4">
<?php foreach ($categories as $category): ?>
<div class="col-lg-3 col-md-4 col-6">
<a href="customer/search.php?category=<?php echo $category['id']; ?>" class="text-decoration-none">
<div class="card h-100 border-0 shadow-sm text-center p-4 category-card">
<div class="category-icon mb-3">
<i class="fas <?php echo $category['icon']; ?> fa-3x text-primary"></i>
</div>
<h6 class="mb-0"><?php echo htmlspecialchars($category['name']); ?></h6>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<!-- Featured Providers -->
<section id="providers" class="py-5">
<div class="container">
<h2 class="text-center fw-bold mb-5">Featured Providers</h2>
<div class="row g-4">
<?php foreach ($featuredProviders as $provider): ?>
<div class="col-lg-4 col-md-6">
<div class="card h-100 border-0 shadow-sm provider-card">
<div class="card-img-top position-relative">
<img src="uploads/avatars/<?php echo $provider['profile_picture']; ?>" 
class="img-fluid" alt="<?php echo htmlspecialchars($provider['business_name']); ?>">
<?php if ($provider['featured']): ?>
<span class="badge bg-warning position-absolute top-0 end-0 m-3">
<i class="fas fa-star me-1"></i>Featured
</span>
<?php endif; ?>
</div>
<div class="card-body">
<h5 class="card-title"><?php echo htmlspecialchars($provider['business_name']); ?></h5>
<p class="card-text text-muted small">
<i class="fas fa-tag me-1"></i><?php echo htmlspecialchars($provider['specialization'] ?? 'Professional'); ?>
</p>
<div class="d-flex align-items-center mb-2">
<div class="rating me-2">
<?php for ($i = 1; $i <= 5; $i++): ?>
<i class="fas fa-star <?php echo $i <= $provider['avg_rating'] ? 'text-warning' : 'text-muted'; ?>"></i>
<?php endfor; ?>
</div>
<span class="text-muted small">(<?php echo $provider['review_count']; ?> reviews)</span>
</div>
<p class="card-text small text-muted">
<i class="fas fa-briefcase me-1"></i><?php echo $provider['experience_years']; ?> years experience
</p>
<p class="card-text">
<strong><?php echo formatAmount($provider['base_price']); ?></strong>
<small class="text-muted">/session</small>
</p>
</div>
<div class="card-footer bg-white border-0 pb-3">
<a href="customer/book.php?provider=<?php echo $provider['id']; ?>" class="btn btn-primary w-100">
Book Now
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="text-center mt-4">
<a href="customer/search.php" class="btn btn-outline-primary btn-lg">
View All Providers <i class="fas fa-arrow-right ms-2"></i>
</a>
</div>
</div>
</section>
<!-- Testimonials -->
<section class="py-5 bg-light">
<div class="container">
<h2 class="text-center fw-bold mb-5">What Our Customers Say</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3">
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
</div>
<p class="card-text">"Great platform! Found an excellent therapist and booked an appointment in minutes. The reminders were very helpful."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user1.jpg" class="rounded-circle me-3" width="50" alt="User">
<div>
<h6 class="mb-0">Sarah Johnson</h6>
<small class="text-muted">Wellness Client</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3">
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
</div>
<p class="card-text">"As a service provider, this system has streamlined my bookings. Easy to manage schedule and get new clients."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user2.jpg" class="rounded-circle me-3" width="50" alt="User">
<div>
<h6 class="mb-0">Dr. Michael Chen</h6>
<small class="text-muted">Healthcare Provider</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3">
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star-half-alt text-warning"></i>
</div>
<p class="card-text">"Very user-friendly interface. Love the real-time availability feature. Saves so much time!"</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user3.jpg" class="rounded-circle me-3" width="50" alt="User">
<div>
<h6 class="mb-0">Emily Rodriguez</h6>
<small class="text-muted">Beauty Client</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Call to Action -->
<section class="py-5 bg-primary text-white">
<div class="container text-center py-4">
<h2 class="fw-bold mb-4">Ready to Get Started?</h2>
<p class="lead mb-4">Join thousands of satisfied customers and professional service providers.</p>
<?php if (!$auth->isLoggedIn()): ?>
<a href="register.php" class="btn btn-light btn-lg px-5 me-2">Sign Up Now</a>
<a href="register.php?role=provider" class="btn btn-outline-light btn-lg px-5">Become a Provider</a>
<?php else: ?>
<a href="customer/book.php" class="btn btn-light btn-lg px-5">Book an Appointment</a>
<?php endif; ?>
</div>
</section>
<!-- Footer -->
<footer id="contact" class="bg-dark text-white py-5">
<div class="container">
<div class="row">
<div class="col-md-4 mb-4">
<h5><i class="fas fa-calendar-check me-2"></i><?php echo APP_NAME; ?></h5>
<p class="text-white-50">Your trusted platform for booking appointments with top professionals.</p>
<div class="social-links">
<a href="#" class="text-white me-2"><i class="fab fa-facebook fa-lg"></i></a>
<a href="#" class="text-white me-2"><i class="fab fa-twitter fa-lg"></i></a>
<a href="#" class="text-white me-2"><i class="fab fa-instagram fa-lg"></i></a>
<a href="#" class="text-white me-2"><i class="fab fa-linkedin fa-lg"></i></a>
</div>
</div>
<div class="col-md-2 mb-4">
<h6>Quick Links</h6>
<ul class="list-unstyled">
<li><a href="index.php" class="text-white-50">Home</a></li>
<li><a href="#how-it-works" class="text-white-50">How It Works</a></li>
<li><a href="#providers" class="text-white-50">Providers</a></li>
<li><a href="#categories" class="text-white-50">Services</a></li>
<li><a href="#contact" class="text-white-50">Contact</a></li>
</ul>
</div>
<div class="col-md-3 mb-4">
<h6>For Providers</h6>
<ul class="list-unstyled">
<li><a href="register.php?role=provider" class="text-white-50">Become a Provider</a></li>
<li><a href="provider/login.php" class="text-white-50">Provider Login</a></li>
<li><a href="#" class="text-white-50">Pricing</a></li>
<li><a href="#" class="text-white-50">FAQs</a></li>
</ul>
</div>
<div class="col-md-3 mb-4">
<h6>Contact Us</h6>
<ul class="list-unstyled text-white-50">
<li><i class="fas fa-map-marker-alt me-2"></i><?php echo BUSINESS_ADDRESS; ?></li>
<li><i class="fas fa-phone me-2"></i><?php echo BUSINESS_PHONE; ?></li>
<li><i class="fas fa-envelope me-2"></i><?php echo BUSINESS_EMAIL; ?></li>
</ul>
</div>
</div>
<hr class="border-secondary">
<div class="row">
<div class="col-12 text-center">
<p class="text-white-50 mb-0">&copy; <?php echo date('Y'); ?> <?php echo APP_NAME; ?>. All rights reserved.</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js"></script>
<style>
.step-icon {
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.category-card {
transition: transform 0.3s ease;
}
.category-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.1) !important;
}
.provider-card {
transition: transform 0.3s ease;
}
.provider-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0,0,0,0.15) !important;
}
.provider-card img {
height: 200px;
object-fit: cover;
border-top-left-radius: 0.375rem;
border-top-right-radius: 0.375rem;
}
.rating i {
font-size: 0.9rem;
}
.navbar {
padding: 1rem 0;
}
.hero-section {
margin-top: 76px;
}
.search-box {
max-width: 500px;
}
.search-box input {
height: 50px;
}
.search-box button {
height: 50px;
}
</style>
</body>
</html>

Environment Configuration

File: .env

# Database Configuration
DB_HOST=localhost
DB_NAME=appointment_system
DB_USER=root
DB_PASS=
# Application Configuration
APP_NAME=Appointment Booking System
APP_URL=http://localhost/appointment-system
APP_VERSION=1.0.0
DEBUG_MODE=true
# Security
SESSION_TIMEOUT=3600
BCRYPT_ROUNDS=12
# Upload Configuration
MAX_FILE_SIZE=5242880  # 5MB in bytes
# Pagination
ITEMS_PER_PAGE=20
# Date/Time
TIMEZONE=America/New_York
DATE_FORMAT=Y-m-d
TIME_FORMAT=H:i
# Business Settings
BUSINESS_NAME=Appointment System
[email protected]
BUSINESS_PHONE=+1-555-123-4567
BUSINESS_ADDRESS=123 Business St, City, State 12345
CURRENCY=USD
CURRENCY_SYMBOL=$
# Booking Settings
MAX_ADVANCE_BOOKING=90
MIN_ADVANCE_BOOKING=1
CANCELLATION_POLICY=24
# Notification Settings
ENABLE_REMINDERS=true
REMINDER_TIME=24
ENABLE_SMS=false
# Payment Settings
ENABLE_PAYMENTS=false
PAYMENT_GATEWAY=stripe
STRIPE_KEY=your_stripe_key
STRIPE_SECRET=your_stripe_secret
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_SECRET=your_paypal_secret
# SMS Settings (Twilio)
TWILIO_SID=your_twilio_sid
TWILIO_TOKEN=your_twilio_token
TWILIO_PHONE=your_twilio_phone

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": "appointment-system/application",
"description": "Online Appointment Booking System",
"type": "project",
"require": {
"php": ">=7.4",
"ext-pdo": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-curl": "*",
"ext-gd": "*",
"phpmailer/phpmailer": "^6.8",
"twilio/sdk": "^7.0",
"stripe/stripe-php": "^13.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"AppointmentSystem\\": "src/"
}
},
"scripts": {
"test": "phpunit tests",
"post-install-cmd": [
"chmod -R 755 uploads/",
"chmod -R 755 logs/",
"chmod -R 755 cache/"
]
}
}

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. Composer: For dependency management
  4. 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: Install Composer Dependencies

  1. Download and install Composer from https://getcomposer.org/
  2. Navigate to your project directory in terminal
  3. Run: composer install

Step 3: Create Project Folder

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

Step 4: 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 appointment_system 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 5: 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=appointment_system
DB_USER=root
DB_PASS=
  1. Update application URL:
   APP_URL=http://localhost/appointment-system
  1. Configure email settings if using email notifications
  2. Configure payment gateway if using online payments

Step 6: Set Folder Permissions

Create the following folders and ensure they are writable:

  • uploads/providers/
  • uploads/services/
  • uploads/avatars/
  • logs/
  • cache/

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

Step 7: Create Admin Password Hash

  1. Go to http://localhost/appointment-system/register.php
  2. Register a test user (e.g., email: [email protected], password: Admin@123)
  3. Open phpMyAdmin, go to the users table
  4. Find the user and change role to 'admin'
  5. Copy the password hash for reference

Step 8: Test the Installation

  1. Open browser and go to http://localhost/appointment-system/
  2. You should see the landing page
  3. Test different user types: Admin Login:
  • Email: [email protected]
  • Password: Admin@123 (or the password you set) Provider Registration:
  • Click "Become a Provider" and register
  • Wait for admin approval Customer Registration:
  • Register as a new customer
  • Browse and book appointments

System Walkthrough

For Customers:

  1. Browse Services - Search for providers by category or keyword
  2. View Provider - Check provider details, services, and reviews
  3. Select Service - Choose specific service and view pricing
  4. Pick Date & Time - Select from available time slots
  5. Confirm Booking - Review details and confirm appointment
  6. Manage Appointments - View upcoming appointments, reschedule or cancel
  7. Leave Review - Rate and review completed appointments
  8. Favorite Providers - Save favorite providers for quick booking

For Service Providers:

  1. Dashboard - View today's schedule, upcoming appointments
  2. Manage Services - Add/edit services with pricing and duration
  3. Set Availability - Define working hours, breaks, and time off
  4. View Appointments - See all appointments, confirm or cancel
  5. Customer Management - View customer history and details
  6. Earnings - Track revenue and payments
  7. Reviews - View and respond to customer reviews
  8. Profile - Update business information and photos

For Admins:

  1. Dashboard - View system-wide statistics
  2. Provider Management - Approve/reject provider applications
  3. Customer Management - View and manage customers
  4. Service Categories - Manage service categories
  5. Appointments - View all appointments across system
  6. Payments - Track payments and handle refunds
  7. Reports - Generate detailed reports
  8. Settings - Configure system settings

Key Features Explained

Online Booking Process

  1. Customer selects provider and service
  2. System shows real-time availability
  3. Customer chooses time slot
  4. Optional payment processing
  5. Instant confirmation and calendar integration
  6. Automated reminders before appointment

Availability Management

  1. Providers set weekly working hours
  2. Can set breaks and time off
  3. Buffer time between appointments
  4. Maximum appointments per day
  5. Holiday closures
  6. Real-time slot availability

Notification System

  • Email Confirmations: Booking, cancellation, reschedule
  • SMS Reminders: Optional text reminders
  • In-app Notifications: Dashboard alerts
  • Calendar Integration: Add to Google/Outlook calendar

Review System

  1. Customers can rate after completed appointments
  2. Star ratings (1-5)
  3. Written reviews with titles
  4. Provider responses
  5. Verified purchase badges
  6. Average rating display

Troubleshooting

Common Issues and Solutions

  1. Database Connection Error
  • Check if MySQL is running
  • Verify database credentials in .env
  • Ensure database appointment_system exists
  1. 404 Page Not Found
  • Check file paths and folder structure
  • Verify APP_URL in .env
  • Ensure .htaccess is properly configured (if using Apache)
  1. Email Not Sending
  • Configure SMTP settings
  • Check spam folder
  • Verify PHP mail() function is enabled
  1. Time Slot Availability Issues
  • Check provider schedule settings
  • Verify time zone configuration
  • Check for conflicting appointments
  1. Payment Gateway Errors
  • Verify API keys in .env
  • Check SSL certificate (for production)
  • Test in sandbox mode first

Security Best Practices

  1. Change default admin password immediately after installation
  2. Use HTTPS in production with SSL certificate
  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 all forms
  8. Password hashing using bcrypt
  9. Rate limiting for login attempts
  10. Session security with proper timeout
  11. File upload validation and malware scanning

Performance Optimizations

  1. Database indexing on frequently queried columns
  2. Query caching for repeated requests
  3. Image optimization for provider photos
  4. Lazy loading for images and content
  5. Pagination for large datasets
  6. Minified CSS and JavaScript for production
  7. CDN for static assets
  8. Browser caching headers

Deployment to Production

  1. Update .env with production settings
  2. Set DEBUG_MODE=false
  3. Configure proper error logging
  4. Set up SSL certificate
  5. Configure cron jobs for reminders:
   # Run every hour to send reminders
0 * * * * php /path/to/project/cron/send_reminders.php
  1. Set up database backups

Leave a Reply

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


Macro Nepal Helper