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