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">© <?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
- Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server (PHP 7.4+)
- Database: MySQL 5.7+ or MariaDB
- Composer: For dependency management
- Browser: Modern web browser (Chrome, Firefox, Edge, etc.)
Installation Steps
Step 1: Set Up Local Server
- Download and install XAMPP from https://www.apachefriends.org/
- Launch XAMPP Control Panel
- Start Apache and MySQL services
Step 2: Install Composer Dependencies
- Download and install Composer from https://getcomposer.org/
- Navigate to your project directory in terminal
- Run:
composer install
Step 3: Create Project Folder
- Navigate to
C:\xampp\htdocs\(Windows) or/Applications/XAMPP/htdocs/(Mac) - Create a new folder named
appointment-system - Create all the folders and files as shown in the Project File Structure
Step 4: Set Up Database
- Open browser and go to
http://localhost/phpmyadmin - Click on "New" to create a new database
- Name the database
appointment_systemand selectutf8_general_ci - Click on "Import" tab
- Click "Choose File" and select the
database.sqlfile from thesqlfolder - Click "Go" to import the database structure and sample data
Step 5: Configure Environment
- Rename
.env.exampleto.envin the project root - Update database credentials if different from default:
DB_HOST=localhost DB_NAME=appointment_system DB_USER=root DB_PASS=
- Update application URL:
APP_URL=http://localhost/appointment-system
- Configure email settings if using email notifications
- 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
- Go to
http://localhost/appointment-system/register.php - Register a test user (e.g., email:
[email protected], password:Admin@123) - Open phpMyAdmin, go to the
userstable - Find the user and change role to 'admin'
- Copy the password hash for reference
Step 8: Test the Installation
- Open browser and go to
http://localhost/appointment-system/ - You should see the landing page
- 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:
- Browse Services - Search for providers by category or keyword
- View Provider - Check provider details, services, and reviews
- Select Service - Choose specific service and view pricing
- Pick Date & Time - Select from available time slots
- Confirm Booking - Review details and confirm appointment
- Manage Appointments - View upcoming appointments, reschedule or cancel
- Leave Review - Rate and review completed appointments
- Favorite Providers - Save favorite providers for quick booking
For Service Providers:
- Dashboard - View today's schedule, upcoming appointments
- Manage Services - Add/edit services with pricing and duration
- Set Availability - Define working hours, breaks, and time off
- View Appointments - See all appointments, confirm or cancel
- Customer Management - View customer history and details
- Earnings - Track revenue and payments
- Reviews - View and respond to customer reviews
- Profile - Update business information and photos
For Admins:
- Dashboard - View system-wide statistics
- Provider Management - Approve/reject provider applications
- Customer Management - View and manage customers
- Service Categories - Manage service categories
- Appointments - View all appointments across system
- Payments - Track payments and handle refunds
- Reports - Generate detailed reports
- Settings - Configure system settings
Key Features Explained
Online Booking Process
- Customer selects provider and service
- System shows real-time availability
- Customer chooses time slot
- Optional payment processing
- Instant confirmation and calendar integration
- Automated reminders before appointment
Availability Management
- Providers set weekly working hours
- Can set breaks and time off
- Buffer time between appointments
- Maximum appointments per day
- Holiday closures
- 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
- Customers can rate after completed appointments
- Star ratings (1-5)
- Written reviews with titles
- Provider responses
- Verified purchase badges
- Average rating display
Troubleshooting
Common Issues and Solutions
- Database Connection Error
- Check if MySQL is running
- Verify database credentials in
.env - Ensure database
appointment_systemexists
- 404 Page Not Found
- Check file paths and folder structure
- Verify
APP_URLin.env - Ensure
.htaccessis properly configured (if using Apache)
- Email Not Sending
- Configure SMTP settings
- Check spam folder
- Verify PHP mail() function is enabled
- Time Slot Availability Issues
- Check provider schedule settings
- Verify time zone configuration
- Check for conflicting appointments
- Payment Gateway Errors
- Verify API keys in
.env - Check SSL certificate (for production)
- Test in sandbox mode first
Security Best Practices
- Change default admin password immediately after installation
- Use HTTPS in production with SSL certificate
- Regular backups of database and uploads
- Input validation on both client and server side
- SQL injection prevention using prepared statements
- XSS prevention using
htmlspecialchars() - CSRF tokens for all forms
- Password hashing using bcrypt
- Rate limiting for login attempts
- Session security with proper timeout
- File upload validation and malware scanning
Performance Optimizations
- Database indexing on frequently queried columns
- Query caching for repeated requests
- Image optimization for provider photos
- Lazy loading for images and content
- Pagination for large datasets
- Minified CSS and JavaScript for production
- CDN for static assets
- Browser caching headers
Deployment to Production
- Update
.envwith production settings - Set DEBUG_MODE=false
- Configure proper error logging
- Set up SSL certificate
- Configure cron jobs for reminders:
# Run every hour to send reminders 0 * * * * php /path/to/project/cron/send_reminders.php
- Set up database backups