Introduction to the Project
The Complaint Management System is a comprehensive, full-stack web application designed to streamline the process of submitting, tracking, and resolving complaints across various organizations. Whether for educational institutions, corporate offices, government departments, or customer service centers, this system provides an efficient platform for managing user grievances and ensuring timely resolution.
This application features role-based access control with four distinct user types: Admin, Department Heads, Support Staff, and Customers/Users. The system ensures transparency, accountability, and efficient handling of complaints through automated workflows, priority management, and real-time tracking.
Key Features
Admin Features
- Dashboard Overview: Comprehensive statistics on complaints, resolution times, and staff performance
- Department Management: Create and manage departments (IT, HR, Finance, etc.)
- Staff Management: Assign support staff to departments with role-based permissions
- Category Management: Create complaint categories and subcategories
- Escalation Matrix: Define escalation rules and timelines
- Analytics & Reports: Generate detailed reports on complaint trends and resolution metrics
- System Settings: Configure email templates, SLA rules, and notification preferences
- Audit Logs: Track all system activities and changes
Department Head Features
- Department Dashboard: View department-specific complaint statistics
- Complaint Assignment: Assign complaints to support staff
- Staff Performance: Monitor individual staff resolution times and ratings
- Escalation Management: Handle escalated complaints
- Report Generation: Generate department-wise reports
- Resource Allocation: Manage workload distribution
Support Staff Features
- Personal Dashboard: View assigned complaints and priorities
- Complaint Management: Update status, add comments, and resolve complaints
- Customer Communication: Send updates and communicate with complainants
- Knowledge Base: Access common solutions and FAQs
- Performance Tracking: View personal resolution metrics
- Workload Management: Prioritize tasks based on urgency
Customer/User Features
- Complaint Submission: File complaints with category, priority, and attachments
- Tracking System: Track complaint status with unique reference number
- History View: Access complete complaint history
- Feedback & Ratings: Rate resolution quality and provide feedback
- Communication: Receive updates and communicate with support staff
- Knowledge Base: Browse FAQs and common solutions
General Features
- Multi-channel Submission: Web portal, email, and mobile app support
- Priority Management: High, Medium, Low priority with SLA enforcement
- Escalation Workflow: Automatic escalation based on time and priority
- File Attachments: Upload images, documents, and screenshots
- Email Notifications: Automated updates at each stage
- SMS Integration: Optional SMS notifications
- Search & Filter: Advanced search with multiple filters
- Feedback System: Rate resolution and provide comments
- Knowledge Base: FAQs and common solutions repository
- Data Export: Export complaints and reports (PDF/Excel)
Technology Stack
- Frontend: HTML5, CSS3, JavaScript (ES6+), Bootstrap 5
- Backend: PHP 8.0+ (Core PHP with OOP MVC pattern)
- Database: MySQL 8.0+
- Additional Libraries:
- PHPMailer for email notifications
- Twilio SDK for SMS
- Bootstrap 5 for responsive UI
- Font Awesome for icons
- jQuery for AJAX operations
- DataTables for advanced tables
- Chart.js for analytics
- Select2 for enhanced dropdowns
- Moment.js for date handling
- TCPDF for PDF generation
- PhpSpreadsheet for Excel export
Project File Structure
complaint-system/ │ ├── assets/ │ ├── css/ │ │ ├── style.css │ │ ├── dashboard.css │ │ ├── responsive.css │ │ └── dark-mode.css │ ├── js/ │ │ ├── main.js │ │ ├── dashboard.js │ │ ├── complaints.js │ │ ├── validation.js │ │ ├── charts.js │ │ └── notifications.js │ ├── images/ │ │ ├── avatars/ │ │ ├── attachments/ │ │ └── icons/ │ └── plugins/ │ ├── datatables/ │ ├── select2/ │ └── chart.js/ │ ├── includes/ │ ├── config/ │ │ ├── database.php │ │ ├── app.php │ │ └── constants.php │ ├── core/ │ │ ├── Controller.php │ │ ├── Model.php │ │ ├── View.php │ │ ├── Database.php │ │ └── Auth.php │ ├── helpers/ │ │ ├── functions.php │ │ ├── ValidationHelper.php │ │ ├── DateHelper.php │ │ ├── FileHelper.php │ │ └── NotificationHelper.php │ ├── models/ │ │ ├── User.php │ │ ├── Complaint.php │ │ ├── Department.php │ │ ├── Category.php │ │ ├── Comment.php │ │ ├── Attachment.php │ │ ├── Escalation.php │ │ └── Feedback.php │ └── middleware/ │ ├── AuthMiddleware.php │ ├── RoleMiddleware.php │ └── CsrfMiddleware.php │ ├── controllers/ │ ├── AuthController.php │ ├── HomeController.php │ ├── ComplaintController.php │ ├── DashboardController.php │ ├── AdminController.php │ ├── DepartmentController.php │ ├── UserController.php │ ├── ReportController.php │ ├── ApiController.php │ └── FeedbackController.php │ ├── views/ │ ├── layouts/ │ │ ├── header.php │ │ ├── footer.php │ │ ├── sidebar.php │ │ └── navbar.php │ ├── auth/ │ │ ├── login.php │ │ ├── register.php │ │ ├── forgot-password.php │ │ └── reset-password.php │ ├── home/ │ │ └── index.php │ ├── complaints/ │ │ ├── create.php │ │ ├── view.php │ │ ├── edit.php │ │ ├── list.php │ │ └── track.php │ ├── admin/ │ │ ├── dashboard.php │ │ ├── users.php │ │ ├── departments.php │ │ ├── categories.php │ │ ├── reports.php │ │ ├── settings.php │ │ └── audit-logs.php │ ├── staff/ │ │ ├── dashboard.php │ │ ├── assigned.php │ │ ├── complaint-details.php │ │ └── performance.php │ ├── user/ │ │ ├── dashboard.php │ │ ├── my-complaints.php │ │ ├── profile.php │ │ └── feedback.php │ └── errors/ │ ├── 404.php │ ├── 403.php │ └── 500.php │ ├── api/ │ ├── complaints.php │ ├── status.php │ ├── comments.php │ └── stats.php │ ├── uploads/ │ ├── complaints/ │ └── profiles/ │ ├── storage/ │ ├── logs/ │ ├── cache/ │ └── exports/ │ ├── vendor/ │ ├── .env ├── .htaccess ├── .gitignore ├── index.php ├── composer.json └── README.md
Database Schema
File: database/schema.sql
-- Create Database
CREATE DATABASE IF NOT EXISTS `complaint_system`;
USE `complaint_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),
`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', 'department_head', 'staff', 'user') NOT NULL DEFAULT 'user',
`department_id` INT(11) NULL,
`is_active` BOOLEAN DEFAULT TRUE,
`email_verified` BOOLEAN DEFAULT FALSE,
`email_verified_at` DATETIME NULL,
`verification_token` VARCHAR(255),
`reset_token` VARCHAR(255),
`reset_expires` DATETIME,
`last_login` DATETIME,
`last_login_ip` VARCHAR(45),
`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` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Departments Table
-- =====================================================
CREATE TABLE `departments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`code` VARCHAR(20) UNIQUE NOT NULL,
`description` TEXT,
`head_id` INT(11) NULL,
`email` VARCHAR(100),
`phone` VARCHAR(20),
`sla_hours` INT DEFAULT 48,
`escalation_hours` INT DEFAULT 24,
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`head_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Complaint Categories Table
-- =====================================================
CREATE TABLE `categories` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`description` TEXT,
`department_id` INT(11) NOT NULL,
`parent_id` INT(11) NULL,
`sla_hours` INT DEFAULT 48,
`escalation_level` INT DEFAULT 1,
`icon` VARCHAR(50) DEFAULT 'fa-tag',
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`department_id`) REFERENCES `departments`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`parent_id`) REFERENCES `categories`(`id`) ON DELETE CASCADE,
INDEX `idx_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Complaints Main Table
-- =====================================================
CREATE TABLE `complaints` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`complaint_id` VARCHAR(20) UNIQUE NOT NULL,
`user_id` INT(11) NOT NULL,
`category_id` INT(11) NOT NULL,
`department_id` INT(11) NOT NULL,
`assigned_to` INT(11) NULL,
`title` VARCHAR(255) NOT NULL,
`description` TEXT NOT NULL,
`priority` ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
`status` ENUM('pending', 'assigned', 'in_progress', 'resolved', 'closed', 'reopened', 'escalated') DEFAULT 'pending',
`source` ENUM('web', 'email', 'mobile', 'phone') DEFAULT 'web',
`reference_no` VARCHAR(50) UNIQUE,
`submission_mode` ENUM('public', 'private') DEFAULT 'public',
`submitted_at` TIMESTAMP NULL,
`assigned_at` TIMESTAMP NULL,
`started_at` TIMESTAMP NULL,
`resolved_at` TIMESTAMP NULL,
`closed_at` TIMESTAMP NULL,
`due_date` DATETIME NULL,
`escalation_level` INT DEFAULT 0,
`escalated_at` TIMESTAMP NULL,
`resolution_time` INT NULL COMMENT 'Time taken to resolve in minutes',
`feedback_rating` INT NULL,
`feedback_comment` TEXT,
`feedback_submitted_at` TIMESTAMP NULL,
`ip_address` VARCHAR(45),
`user_agent` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`),
FOREIGN KEY (`department_id`) REFERENCES `departments`(`id`),
FOREIGN KEY (`assigned_to`) REFERENCES `users`(`id`) ON DELETE SET NULL,
INDEX `idx_complaint_id` (`complaint_id`),
INDEX `idx_status` (`status`),
INDEX `idx_priority` (`priority`),
INDEX `idx_department` (`department_id`),
INDEX `idx_assigned` (`assigned_to`),
INDEX `idx_created` (`created_at`),
INDEX `idx_due_date` (`due_date`),
FULLTEXT INDEX `idx_search` (`title`, `description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Complaint Comments/Updates Table
-- =====================================================
CREATE TABLE `complaint_comments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`complaint_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`comment` TEXT NOT NULL,
`is_staff` BOOLEAN DEFAULT FALSE,
`is_private` BOOLEAN DEFAULT FALSE COMMENT 'Internal notes only visible to staff',
`attachments` JSON,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`complaint_id`) REFERENCES `complaints`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_complaint` (`complaint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Complaint Attachments Table
-- =====================================================
CREATE TABLE `complaint_attachments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`complaint_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`file_name` VARCHAR(255) NOT NULL,
`file_path` VARCHAR(500) NOT NULL,
`file_size` INT,
`file_type` VARCHAR(100),
`mime_type` VARCHAR(100),
`is_image` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`complaint_id`) REFERENCES `complaints`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_complaint` (`complaint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Complaint History/Log Table
-- =====================================================
CREATE TABLE `complaint_history` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`complaint_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`action` VARCHAR(100) NOT NULL,
`old_value` TEXT,
`new_value` TEXT,
`description` TEXT,
`ip_address` VARCHAR(45),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`complaint_id`) REFERENCES `complaints`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_complaint` (`complaint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Escalation Rules Table
-- =====================================================
CREATE TABLE `escalation_rules` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`priority` ENUM('low', 'medium', 'high', 'urgent') NOT NULL,
`department_id` INT(11),
`category_id` INT(11),
`escalation_level` INT NOT NULL,
`escalate_after_hours` INT NOT NULL,
`escalate_to_role` ENUM('staff', 'department_head', 'admin') DEFAULT 'department_head',
`notify_users` JSON,
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`department_id`) REFERENCES `departments`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON DELETE CASCADE,
INDEX `idx_priority` (`priority`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Notifications Table
-- =====================================================
CREATE TABLE `notifications` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`type` VARCHAR(50) NOT NULL,
`title` VARCHAR(255) NOT NULL,
`message` TEXT NOT NULL,
`data` JSON,
`complaint_id` INT(11),
`is_read` BOOLEAN DEFAULT FALSE,
`read_at` DATETIME NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`complaint_id`) REFERENCES `complaints`(`id`) ON DELETE CASCADE,
INDEX `idx_user_read` (`user_id`, `is_read`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Email Templates Table
-- =====================================================
CREATE TABLE `email_templates` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`subject` VARCHAR(255) NOT NULL,
`body` TEXT NOT NULL,
`variables` JSON,
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Audit Logs Table
-- =====================================================
CREATE TABLE `audit_logs` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NULL,
`action` VARCHAR(100) NOT NULL,
`table_name` VARCHAR(100),
`record_id` INT(11),
`old_data` JSON,
`new_data` JSON,
`ip_address` VARCHAR(45),
`user_agent` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
INDEX `idx_user` (`user_id`),
INDEX `idx_action` (`action`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- System Settings Table
-- =====================================================
CREATE TABLE `settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`key` VARCHAR(100) UNIQUE NOT NULL,
`value` TEXT,
`type` ENUM('text', 'number', 'boolean', 'json', 'email') DEFAULT 'text',
`description` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- Default Data Inserts
-- =====================================================
-- Insert default admin user (password: Admin@123)
INSERT INTO `users` (`user_id`, `email`, `password`, `first_name`, `last_name`, `role`, `email_verified`)
VALUES ('ADMIN001', '[email protected]', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'System', 'Administrator', 'admin', TRUE);
-- Insert sample departments
INSERT INTO `departments` (`name`, `code`, `description`, `email`, `sla_hours`, `escalation_hours`) VALUES
('Information Technology', 'IT', 'Technical support, software issues, hardware problems', '[email protected]', 24, 12),
('Human Resources', 'HR', 'Employee relations, payroll, benefits', '[email protected]', 48, 24),
('Finance', 'FIN', 'Billing, payments, reimbursement', '[email protected]', 48, 24),
('Customer Service', 'CS', 'General inquiries, feedback, suggestions', '[email protected]', 72, 36),
('Facilities', 'FAC', 'Infrastructure, maintenance, security', '[email protected]', 72, 36);
-- Insert default categories
INSERT INTO `categories` (`name`, `department_id`, `description`, `sla_hours`, `icon`) VALUES
('Hardware Issue', 1, 'Computer, printer, network equipment issues', 24, 'fa-computer'),
('Software Issue', 1, 'Application, operating system problems', 24, 'fa-code'),
('Network Issue', 1, 'Internet, connectivity, VPN problems', 12, 'fa-wifi'),
('Payroll Issue', 3, 'Salary, tax, deduction inquiries', 48, 'fa-money-bill'),
('Reimbursement', 3, 'Expense claims, refunds', 72, 'fa-hand-holding-dollar'),
('Attendance Issue', 2, 'Leave, overtime, attendance records', 48, 'fa-clock'),
('Policy Inquiry', 2, 'HR policies, procedures', 72, 'fa-file-lines');
-- Insert email templates
INSERT INTO `email_templates` (`name`, `subject`, `body`, `variables`) VALUES
('complaint_registered', 'Complaint #{complaint_id} Registered Successfully',
'<h2>Dear {user_name},</h2><p>Your complaint has been registered successfully.</p><p><strong>Complaint ID:</strong> {complaint_id}</p><p><strong>Title:</strong> {title}</p><p><strong>Status:</strong> {status}</p><p>You can track your complaint at: {tracking_url}</p>',
'["user_name", "complaint_id", "title", "status", "tracking_url"]'),
('complaint_assigned', 'Complaint #{complaint_id} Assigned',
'<h2>Dear {user_name},</h2><p>Your complaint has been assigned to {staff_name}.</p><p><strong>Complaint ID:</strong> {complaint_id}</p><p><strong>Assigned To:</strong> {staff_name}</p><p><strong>Expected Resolution:</strong> {due_date}</p>',
'["user_name", "complaint_id", "staff_name", "due_date"]'),
('complaint_resolved', 'Complaint #{complaint_id} Resolved',
'<h2>Dear {user_name},</h2><p>Your complaint has been resolved.</p><p><strong>Complaint ID:</strong> {complaint_id}</p><p><strong>Resolution:</strong> {resolution}</p><p>Please provide your feedback: {feedback_url}</p>',
'["user_name", "complaint_id", "resolution", "feedback_url"]'),
('staff_assignment', 'New Complaint Assigned - #{complaint_id}',
'<h2>Dear {staff_name},</h2><p>A new complaint has been assigned to you.</p><p><strong>Complaint ID:</strong> {complaint_id}</p><p><strong>Priority:</strong> {priority}</p><p><strong>Due Date:</strong> {due_date}</p><p>View details: {complaint_url}</p>',
'["staff_name", "complaint_id", "priority", "due_date", "complaint_url"]'),
('escalation_notification', 'Complaint #{complaint_id} Escalated',
'<h2>Urgent: Complaint #{complaint_id} Escalated</h2><p>This complaint has been escalated to Level {level}.</p><p><strong>Priority:</strong> {priority}</p><p><strong>Current Status:</strong> {status}</p><p><strong>Pending Since:</strong> {pending_since}</p><p>Action required immediately.</p>',
'["complaint_id", "level", "priority", "status", "pending_since"]');
-- Insert system settings
INSERT INTO `settings` (`key`, `value`, `type`, `description`) VALUES
('site_name', 'Complaint Management System', 'text', 'Name of the site'),
('site_email', '[email protected]', 'email', 'System email address'),
('site_phone', '+1-555-123-4567', 'text', 'Contact phone number'),
('address', '123 Business Ave, City, State 12345', 'text', 'Office address'),
('timezone', 'America/New_York', 'text', 'System timezone'),
('date_format', 'Y-m-d', 'text', 'Date display format'),
('time_format', 'H:i:s', 'text', 'Time display format'),
('items_per_page', '20', 'number', 'Items per page in listings'),
('allow_attachments', '1', 'boolean', 'Allow file attachments'),
('max_file_size', '5', 'number', 'Maximum file size in MB'),
('allowed_file_types', 'jpg,jpeg,png,gif,pdf,doc,docx', 'text', 'Allowed file extensions'),
('auto_assign', '1', 'boolean', 'Auto-assign complaints to staff'),
('enable_notifications', '1', 'boolean', 'Enable email notifications'),
('enable_sms', '0', 'boolean', 'Enable SMS notifications'),
('enable_escalation', '1', 'boolean', 'Enable automatic escalation'),
('maintenance_mode', '0', 'boolean', 'Maintenance mode status'),
('company_logo', '/assets/images/logo.png', 'text', 'Company logo path'),
('favicon', '/assets/images/favicon.ico', 'text', 'Favicon path'),
('primary_color', '#0d6efd', 'text', 'Primary theme color'),
('secondary_color', '#6c757d', 'text', 'Secondary theme color');
-- Insert default escalation rules
INSERT INTO `escalation_rules` (`name`, `priority`, `escalation_level`, `escalate_after_hours`, `escalate_to_role`) VALUES
('Low Priority Escalation', 'low', 1, 72, 'department_head'),
('Medium Priority Escalation', 'medium', 1, 48, 'department_head'),
('High Priority Escalation', 'high', 1, 24, 'department_head'),
('Urgent Escalation Level 1', 'urgent', 1, 12, 'department_head'),
('Urgent Escalation Level 2', 'urgent', 2, 24, 'admin');
Core System Files
Database Configuration
File: includes/config/database.php
<?php
/**
* Database Configuration
* Handles database connection and PDO setup
*/
return [
'driver' => 'mysql',
'host' => getenv('DB_HOST') ?: 'localhost',
'database' => getenv('DB_NAME') ?: 'complaint_system',
'username' => getenv('DB_USER') ?: 'root',
'password' => getenv('DB_PASS') ?: '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
]
];
Application Configuration
File: includes/config/app.php
<?php
/**
* Application Configuration
* Main application settings and constants
*/
return [
'name' => getenv('APP_NAME') ?: 'Complaint Management System',
'env' => getenv('APP_ENV') ?: 'production',
'debug' => getenv('APP_DEBUG') === 'true',
'url' => getenv('APP_URL') ?: 'http://localhost/complaint-system',
'timezone' => getenv('APP_TIMEZONE') ?: 'UTC',
'locale' => getenv('APP_LOCALE') ?: 'en',
'key' => getenv('APP_KEY') ?: 'base64:'.base64_encode(random_bytes(32)),
'mail' => [
'driver' => getenv('MAIL_DRIVER') ?: 'smtp',
'host' => getenv('MAIL_HOST') ?: 'smtp.mailtrap.io',
'port' => getenv('MAIL_PORT') ?: 2525,
'username' => getenv('MAIL_USERNAME') ?: '',
'password' => getenv('MAIL_PASSWORD') ?: '',
'encryption' => getenv('MAIL_ENCRYPTION') ?: 'tls',
'from_address' => getenv('MAIL_FROM_ADDRESS') ?: '[email protected]',
'from_name' => getenv('MAIL_FROM_NAME') ?: 'Complaint System'
],
'sms' => [
'enabled' => getenv('SMS_ENABLED') === 'true',
'provider' => getenv('SMS_PROVIDER') ?: 'twilio',
'twilio_sid' => getenv('TWILIO_SID') ?: '',
'twilio_token' => getenv('TWILIO_TOKEN') ?: '',
'twilio_phone' => getenv('TWILIO_PHONE') ?: ''
],
'upload' => [
'max_size' => getenv('MAX_FILE_SIZE') ?: 5 * 1024 * 1024,
'allowed_types' => ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'],
'path' => __DIR__ . '/../../uploads/'
],
'pagination' => [
'per_page' => getenv('ITEMS_PER_PAGE') ?: 20
]
];
Constants Definition
File: includes/config/constants.php
<?php
/**
* System Constants
* Define all system-wide constants
*/
// File paths
define('ROOT_PATH', dirname(__DIR__, 2));
define('APP_PATH', ROOT_PATH . '/app');
define('VIEWS_PATH', ROOT_PATH . '/views');
define('CONTROLLERS_PATH', ROOT_PATH . '/controllers');
define('MODELS_PATH', ROOT_PATH . '/models');
define('UPLOAD_PATH', ROOT_PATH . '/uploads');
define('STORAGE_PATH', ROOT_PATH . '/storage');
// URLs
define('BASE_URL', rtrim(getenv('APP_URL') ?: 'http://localhost/complaint-system', '/'));
define('ASSETS_URL', BASE_URL . '/assets');
define('UPLOADS_URL', BASE_URL . '/uploads');
// Date/Time formats
define('DATE_FORMAT', 'Y-m-d');
define('TIME_FORMAT', 'H:i:s');
define('DATETIME_FORMAT', 'Y-m-d H:i:s');
define('DISPLAY_DATE_FORMAT', 'M d, Y');
define('DISPLAY_TIME_FORMAT', 'h:i A');
define('DISPLAY_DATETIME_FORMAT', 'M d, Y h:i A');
// Complaint statuses
define('COMPLAINT_STATUSES', [
'pending' => 'Pending',
'assigned' => 'Assigned',
'in_progress' => 'In Progress',
'resolved' => 'Resolved',
'closed' => 'Closed',
'reopened' => 'Reopened',
'escalated' => 'Escalated'
]);
// Complaint priorities
define('COMPLAINT_PRIORITIES', [
'low' => 'Low',
'medium' => 'Medium',
'high' => 'High',
'urgent' => 'Urgent'
]);
// User roles
define('USER_ROLES', [
'admin' => 'Administrator',
'department_head' => 'Department Head',
'staff' => 'Support Staff',
'user' => 'Regular User'
]);
// Priority colors for UI
define('PRIORITY_COLORS', [
'low' => 'info',
'medium' => 'warning',
'high' => 'danger',
'urgent' => 'dark'
]);
// Status colors for UI
define('STATUS_COLORS', [
'pending' => 'secondary',
'assigned' => 'primary',
'in_progress' => 'info',
'resolved' => 'success',
'closed' => 'dark',
'reopened' => 'warning',
'escalated' => 'danger'
]);
// Session keys
define('SESSION_USER_ID', 'user_id');
define('SESSION_USER_ROLE', 'user_role');
define('SESSION_USER_NAME', 'user_name');
define('SESSION_CSRF_TOKEN', 'csrf_token');
define('SESSION_FLASH_MESSAGE', 'flash_message');
// Cache keys
define('CACHE_DEPARTMENTS', 'departments_list');
define('CACHE_CATEGORIES', 'categories_list');
define('CACHE_SETTINGS', 'system_settings');
Core Database Class
File: includes/core/Database.php
<?php
/**
* Database Class
* Singleton PDO database connection with query builder
*/
class Database {
private static $instance = null;
private $connection;
private $statement;
private $config;
private $table;
private $where = [];
private $orderBy = [];
private $limit = null;
private $offset = null;
private $joins = [];
private $select = ['*'];
/**
* Private constructor
*/
private function __construct() {
$this->config = require __DIR__ . '/../config/database.php';
$this->connect();
}
/**
* Establish database connection
*/
private function connect() {
$dsn = sprintf(
"%s:host=%s;dbname=%s;charset=%s",
$this->config['driver'],
$this->config['host'],
$this->config['database'],
$this->config['charset']
);
try {
$this->connection = new PDO(
$dsn,
$this->config['username'],
$this->config['password'],
$this->config['options']
);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
/**
* Get database instance
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Set table for query
*/
public function table($table) {
$this->table = $table;
return $this;
}
/**
* Select columns
*/
public function select($columns = ['*']) {
$this->select = is_array($columns) ? $columns : func_get_args();
return $this;
}
/**
* Add where condition
*/
public function where($column, $operator = null, $value = null) {
if ($value === null) {
$value = $operator;
$operator = '=';
}
$this->where[] = [
'type' => 'AND',
'column' => $column,
'operator' => $operator,
'value' => $value
];
return $this;
}
/**
* Add or where condition
*/
public function orWhere($column, $operator = null, $value = null) {
if ($value === null) {
$value = $operator;
$operator = '=';
}
$this->where[] = [
'type' => 'OR',
'column' => $column,
'operator' => $operator,
'value' => $value
];
return $this;
}
/**
* Add where in condition
*/
public function whereIn($column, $values) {
$this->where[] = [
'type' => 'AND',
'column' => $column,
'operator' => 'IN',
'value' => $values
];
return $this;
}
/**
* Add where between condition
*/
public function whereBetween($column, $min, $max) {
$this->where[] = [
'type' => 'AND',
'column' => $column,
'operator' => 'BETWEEN',
'value' => [$min, $max]
];
return $this;
}
/**
* Add where null condition
*/
public function whereNull($column) {
$this->where[] = [
'type' => 'AND',
'column' => $column,
'operator' => 'IS NULL',
'value' => null
];
return $this;
}
/**
* Add where not null condition
*/
public function whereNotNull($column) {
$this->where[] = [
'type' => 'AND',
'column' => $column,
'operator' => 'IS NOT NULL',
'value' => null
];
return $this;
}
/**
* Add join
*/
public function join($table, $first, $operator = null, $second = null, $type = 'INNER') {
if ($second === null) {
$second = $operator;
$operator = '=';
}
$this->joins[] = [
'type' => $type,
'table' => $table,
'first' => $first,
'operator' => $operator,
'second' => $second
];
return $this;
}
/**
* Add left join
*/
public function leftJoin($table, $first, $operator = null, $second = null) {
return $this->join($table, $first, $operator, $second, 'LEFT');
}
/**
* Add order by
*/
public function orderBy($column, $direction = 'ASC') {
$this->orderBy[] = ['column' => $column, 'direction' => $direction];
return $this;
}
/**
* Set limit
*/
public function limit($limit) {
$this->limit = $limit;
return $this;
}
/**
* Set offset
*/
public function offset($offset) {
$this->offset = $offset;
return $this;
}
/**
* Build where clause
*/
private function buildWhere() {
if (empty($this->where)) {
return ['sql' => '', 'params' => []];
}
$sql = 'WHERE ';
$params = [];
foreach ($this->where as $index => $condition) {
if ($index > 0) {
$sql .= ' ' . $condition['type'] . ' ';
}
$column = $condition['column'];
$operator = $condition['operator'];
$value = $condition['value'];
if ($operator === 'IN') {
$placeholders = implode(',', array_fill(0, count($value), '?'));
$sql .= "{$column} IN ({$placeholders})";
$params = array_merge($params, $value);
} elseif ($operator === 'BETWEEN') {
$sql .= "{$column} BETWEEN ? AND ?";
$params = array_merge($params, $value);
} elseif ($operator === 'IS NULL' || $operator === 'IS NOT NULL') {
$sql .= "{$column} {$operator}";
} else {
$sql .= "{$column} {$operator} ?";
$params[] = $value;
}
}
return ['sql' => $sql, 'params' => $params];
}
/**
* Build query
*/
private function buildSelectQuery() {
$sql = "SELECT " . implode(', ', $this->select) . " FROM {$this->table}";
// Add joins
foreach ($this->joins as $join) {
$sql .= " {$join['type']} JOIN {$join['table']} ON {$join['first']} {$join['operator']} {$join['second']}";
}
// Add where
$where = $this->buildWhere();
if (!empty($where['sql'])) {
$sql .= " " . $where['sql'];
}
// Add order by
if (!empty($this->orderBy)) {
$orderParts = [];
foreach ($this->orderBy as $order) {
$orderParts[] = "{$order['column']} {$order['direction']}";
}
$sql .= " ORDER BY " . implode(', ', $orderParts);
}
// Add limit and offset
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
if ($this->offset !== null) {
$sql .= " OFFSET {$this->offset}";
}
return ['sql' => $sql, 'params' => $where['params']];
}
/**
* Execute query and get results
*/
public function get() {
$query = $this->buildSelectQuery();
$this->statement = $this->connection->prepare($query['sql']);
$this->statement->execute($query['params']);
$this->reset();
return $this->statement->fetchAll();
}
/**
* Get first record
*/
public function first() {
$this->limit(1);
$results = $this->get();
return !empty($results) ? $results[0] : null;
}
/**
* Find by ID
*/
public function find($id, $column = 'id') {
return $this->where($column, $id)->first();
}
/**
* Insert record
*/
public function insert($data) {
$columns = implode(', ', array_keys($data));
$placeholders = implode(', ', array_fill(0, count($data), '?'));
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
$this->statement = $this->connection->prepare($sql);
$result = $this->statement->execute(array_values($data));
$this->reset();
return $result ? $this->connection->lastInsertId() : false;
}
/**
* Update records
*/
public function update($data) {
$sets = [];
foreach (array_keys($data) as $column) {
$sets[] = "{$column} = ?";
}
$sql = "UPDATE {$this->table} SET " . implode(', ', $sets);
$where = $this->buildWhere();
if (!empty($where['sql'])) {
$sql .= " " . $where['sql'];
}
$params = array_merge(array_values($data), $where['params']);
$this->statement = $this->connection->prepare($sql);
$result = $this->statement->execute($params);
$this->reset();
return $result ? $this->statement->rowCount() : false;
}
/**
* Delete records
*/
public function delete() {
$sql = "DELETE FROM {$this->table}";
$where = $this->buildWhere();
if (!empty($where['sql'])) {
$sql .= " " . $where['sql'];
}
$this->statement = $this->connection->prepare($sql);
$result = $this->statement->execute($where['params']);
$this->reset();
return $result ? $this->statement->rowCount() : false;
}
/**
* Count records
*/
public function count() {
$this->select = ['COUNT(*) as count'];
$result = $this->first();
return $result ? $result['count'] : 0;
}
/**
* Execute raw query
*/
public function raw($sql, $params = []) {
$this->statement = $this->connection->prepare($sql);
$this->statement->execute($params);
return $this->statement;
}
/**
* 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();
}
/**
* Reset query builder state
*/
private function reset() {
$this->table = null;
$this->where = [];
$this->orderBy = [];
$this->limit = null;
$this->offset = null;
$this->joins = [];
$this->select = ['*'];
}
/**
* Prevent cloning
*/
private function __clone() {}
/**
* Prevent unserialize
*/
public function __wakeup() {}
}
Base Model Class
File: includes/core/Model.php
<?php
/**
* Base Model Class
* Provides common database operations for all models
*/
abstract class Model {
protected $db;
protected $table;
protected $primaryKey = 'id';
protected $fillable = [];
protected $guarded = ['id'];
protected $hidden = ['password'];
protected $casts = [];
protected $dates = ['created_at', 'updated_at'];
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Get table name
*/
protected function getTable() {
if ($this->table) {
return $this->table;
}
// Generate table name from class name
$class = get_class($this);
$class = basename(str_replace('\\', '/', $class));
return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $class)) . 's';
}
/**
* Find record by ID
*/
public function find($id) {
$result = $this->db->table($this->getTable())
->where($this->primaryKey, $id)
->first();
return $result ? $this->hydrate($result) : null;
}
/**
* Get all records
*/
public function all() {
$results = $this->db->table($this->getTable())->get();
return $this->hydrateAll($results);
}
/**
* Create new record
*/
public function create($data) {
$data = $this->filterFillable($data);
if (in_array('created_at', $this->dates)) {
$data['created_at'] = date('Y-m-d H:i:s');
}
$id = $this->db->table($this->getTable())->insert($data);
if ($id) {
return $this->find($id);
}
return null;
}
/**
* Update record
*/
public function update($id, $data) {
$data = $this->filterFillable($data);
if (in_array('updated_at', $this->dates)) {
$data['updated_at'] = date('Y-m-d H:i:s');
}
return $this->db->table($this->getTable())
->where($this->primaryKey, $id)
->update($data);
}
/**
* Delete record
*/
public function delete($id) {
return $this->db->table($this->getTable())
->where($this->primaryKey, $id)
->delete();
}
/**
* Filter fillable fields
*/
protected function filterFillable($data) {
if (!empty($this->fillable)) {
return array_intersect_key($data, array_flip($this->fillable));
}
if (!empty($this->guarded)) {
return array_diff_key($data, array_flip($this->guarded));
}
return $data;
}
/**
* Cast values
*/
protected function cast($key, $value) {
if (!isset($this->casts[$key])) {
return $value;
}
switch ($this->casts[$key]) {
case 'int':
case 'integer':
return (int)$value;
case 'float':
case 'double':
return (float)$value;
case 'bool':
case 'boolean':
return (bool)$value;
case 'array':
case 'json':
return json_decode($value, true);
case 'object':
return json_decode($value);
case 'date':
return new DateTime($value);
default:
return $value;
}
}
/**
* Hydrate single record
*/
protected function hydrate($data) {
if (!$data) {
return null;
}
// Cast values
foreach ($data as $key => $value) {
$data[$key] = $this->cast($key, $value);
}
// Hide sensitive fields
foreach ($this->hidden as $field) {
unset($data[$field]);
}
return $data;
}
/**
* Hydrate multiple records
*/
protected function hydrateAll($results) {
return array_map([$this, 'hydrate'], $results);
}
/**
* Get query builder instance
*/
public function query() {
return $this->db->table($this->getTable());
}
/**
* Dynamic where calls
*/
public function __call($method, $arguments) {
if (strpos($method, 'findBy') === 0) {
$field = strtolower(substr($method, 6));
return $this->db->table($this->getTable())
->where($field, $arguments[0])
->first();
}
if (strpos($method, 'findAllBy') === 0) {
$field = strtolower(substr($method, 9));
return $this->db->table($this->getTable())
->where($field, $arguments[0])
->get();
}
throw new BadMethodCallException("Method {$method} does not exist");
}
}
Authentication Class
File: includes/core/Auth.php
<?php
/**
* Authentication Class
* Handles user authentication, registration, and session management
*/
class Auth {
private $db;
private $user = null;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
$this->initSession();
}
/**
* Initialize session
*/
private function initSession() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Check for remember me cookie
if (!$this->check() && isset($_COOKIE['remember_token'])) {
$this->loginWithToken($_COOKIE['remember_token']);
}
}
/**
* Attempt login
*/
public function attempt($email, $password, $remember = false) {
$user = $this->db->table('users')
->where('email', $email)
->where('is_active', 1)
->first();
if ($user && password_verify($password, $user['password'])) {
$this->login($user, $remember);
return true;
}
return false;
}
/**
* Login user
*/
public function login($user, $remember = false) {
$_SESSION[SESSION_USER_ID] = $user['id'];
$_SESSION[SESSION_USER_ROLE] = $user['role'];
$_SESSION[SESSION_USER_NAME] = $user['first_name'] . ' ' . $user['last_name'];
// Update last login
$this->db->table('users')
->where('id', $user['id'])
->update([
'last_login' => date('Y-m-d H:i:s'),
'last_login_ip' => $_SERVER['REMOTE_ADDR'] ?? null
]);
if ($remember) {
$this->setRememberToken($user['id']);
}
$this->user = $user;
}
/**
* Login with remember token
*/
private function loginWithToken($token) {
// Implement remember token validation
// This would check a remember_tokens table
}
/**
* Set remember me token
*/
private function setRememberToken($userId) {
$token = bin2hex(random_bytes(32));
$expires = time() + (86400 * 30); // 30 days
setcookie('remember_token', $token, $expires, '/', '', false, true);
// Store token in database (you'd need a remember_tokens table)
// $this->db->table('remember_tokens')->insert([...]);
}
/**
* Logout user
*/
public function logout() {
$_SESSION = array();
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params['path'], $params['domain'],
$params['secure'], $params['httponly']
);
}
setcookie('remember_token', '', time() - 3600, '/');
session_destroy();
$this->user = null;
}
/**
* Check if user is logged in
*/
public function check() {
return isset($_SESSION[SESSION_USER_ID]);
}
/**
* Get current user ID
*/
public function id() {
return $_SESSION[SESSION_USER_ID] ?? null;
}
/**
* Get current user role
*/
public function role() {
return $_SESSION[SESSION_USER_ROLE] ?? null;
}
/**
* Get current user name
*/
public function name() {
return $_SESSION[SESSION_USER_NAME] ?? null;
}
/**
* Get current user data
*/
public function user() {
if ($this->user === null && $this->check()) {
$this->user = $this->db->table('users')
->find($_SESSION[SESSION_USER_ID]);
}
return $this->user;
}
/**
* Check if user has role
*/
public function hasRole($role) {
return $this->role() === $role;
}
/**
* Check if user has any of given roles
*/
public function hasAnyRole($roles) {
return in_array($this->role(), (array)$roles);
}
/**
* Require authentication
*/
public function requireAuth() {
if (!$this->check()) {
$_SESSION['redirect_url'] = $_SERVER['REQUEST_URI'];
header('Location: ' . BASE_URL . '/login');
exit();
}
}
/**
* Require specific role
*/
public function requireRole($role) {
$this->requireAuth();
if (!$this->hasRole($role)) {
header('HTTP/1.0 403 Forbidden');
include VIEWS_PATH . '/errors/403.php';
exit();
}
}
/**
* Register new user
*/
public function register($data) {
// Check if email exists
$exists = $this->db->table('users')
->where('email', $data['email'])
->first();
if ($exists) {
return ['success' => false, 'error' => 'Email already registered'];
}
// Hash password
$data['password'] = password_hash($data['password'], PASSWORD_BCRYPT);
$data['user_id'] = $this->generateUserId();
$data['verification_token'] = bin2hex(random_bytes(32));
// Insert user
$id = $this->db->table('users')->insert($data);
if ($id) {
// Send verification email
$this->sendVerificationEmail($data['email'], $data['verification_token']);
return ['success' => true, 'user_id' => $id];
}
return ['success' => false, 'error' => 'Registration failed'];
}
/**
* Generate unique user ID
*/
private function generateUserId() {
$prefix = 'USR';
$timestamp = date('Ymd');
$random = strtoupper(substr(uniqid(), -4));
return $prefix . $timestamp . $random;
}
/**
* Verify email
*/
public function verifyEmail($token) {
$user = $this->db->table('users')
->where('verification_token', $token)
->first();
if ($user) {
$this->db->table('users')
->where('id', $user['id'])
->update([
'email_verified' => 1,
'email_verified_at' => date('Y-m-d H:i:s'),
'verification_token' => null
]);
return true;
}
return false;
}
/**
* Send verification email
*/
private function sendVerificationEmail($email, $token) {
$subject = "Verify Your Email - " . APP_NAME;
$verificationLink = BASE_URL . "/verify-email?token=" . $token;
$message = "Click here to verify your email: " . $verificationLink;
mail($email, $subject, $message);
}
/**
* Send password reset link
*/
public function sendPasswordResetLink($email) {
$user = $this->db->table('users')
->where('email', $email)
->first();
if ($user) {
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
$this->db->table('users')
->where('id', $user['id'])
->update([
'reset_token' => $token,
'reset_expires' => $expires
]);
$resetLink = BASE_URL . "/reset-password?token=" . $token;
$subject = "Password Reset - " . APP_NAME;
$message = "Click here to reset your password: " . $resetLink;
return mail($email, $subject, $message);
}
return false;
}
/**
* Reset password
*/
public function resetPassword($token, $password) {
$user = $this->db->table('users')
->where('reset_token', $token)
->where('reset_expires', '>', date('Y-m-d H:i:s'))
->first();
if ($user) {
$this->db->table('users')
->where('id', $user['id'])
->update([
'password' => password_hash($password, PASSWORD_BCRYPT),
'reset_token' => null,
'reset_expires' => null
]);
return true;
}
return false;
}
/**
* Change password
*/
public function changePassword($userId, $currentPassword, $newPassword) {
$user = $this->db->table('users')
->find($userId);
if (!password_verify($currentPassword, $user['password'])) {
return ['success' => false, 'error' => 'Current password is incorrect'];
}
$this->db->table('users')
->where('id', $userId)
->update([
'password' => password_hash($newPassword, PASSWORD_BCRYPT)
]);
return ['success' => true];
}
}
// Initialize Auth
$auth = new Auth();
Base Controller Class
File: includes/core/Controller.php
<?php
/**
* Base Controller Class
* Provides common functionality for all controllers
*/
class Controller {
protected $db;
protected $auth;
protected $viewData = [];
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
$this->auth = $GLOBALS['auth'];
// Set common view data
$this->setViewData('app_name', APP_NAME);
$this->setViewData('base_url', BASE_URL);
$this->setViewData('assets_url', ASSETS_URL);
$this->setViewData('auth', $this->auth);
$this->setViewData('current_user', $this->auth->user());
// Set flash messages
if (isset($_SESSION[SESSION_FLASH_MESSAGE])) {
$this->setViewData('flash', $_SESSION[SESSION_FLASH_MESSAGE]);
unset($_SESSION[SESSION_FLASH_MESSAGE]);
}
}
/**
* Set view data
*/
protected function setViewData($key, $value) {
$this->viewData[$key] = $value;
}
/**
* Render view
*/
protected function view($view, $data = []) {
// Merge data with view data
$data = array_merge($this->viewData, $data);
// Extract variables for view
extract($data);
// Build view path
$viewPath = VIEWS_PATH . '/' . str_replace('.', '/', $view) . '.php';
if (!file_exists($viewPath)) {
die("View not found: {$view}");
}
// Include layout
include VIEWS_PATH . '/layouts/header.php';
include $viewPath;
include VIEWS_PATH . '/layouts/footer.php';
}
/**
* Render JSON response
*/
protected function json($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit();
}
/**
* Redirect to URL
*/
protected function redirect($url) {
header('Location: ' . BASE_URL . $url);
exit();
}
/**
* Redirect back
*/
protected function back() {
$referer = $_SERVER['HTTP_REFERER'] ?? BASE_URL;
header('Location: ' . $referer);
exit();
}
/**
* Set flash message
*/
protected function setFlash($type, $message) {
$_SESSION[SESSION_FLASH_MESSAGE] = [
'type' => $type,
'message' => $message
];
}
/**
* Validate CSRF token
*/
protected function validateCsrf() {
$token = $_POST[SESSION_CSRF_TOKEN] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
if (!$token || $token !== $_SESSION[SESSION_CSRF_TOKEN]) {
$this->json(['error' => 'Invalid CSRF token'], 403);
}
}
/**
* Generate CSRF token
*/
protected function csrfField() {
if (!isset($_SESSION[SESSION_CSRF_TOKEN])) {
$_SESSION[SESSION_CSRF_TOKEN] = bin2hex(random_bytes(32));
}
return '<input type="hidden" name="' . SESSION_CSRF_TOKEN . '" value="' . $_SESSION[SESSION_CSRF_TOKEN] . '">';
}
/**
* Get paginated data
*/
protected function paginate($query, $perPage = null) {
$perPage = $perPage ?: (getenv('ITEMS_PER_PAGE') ?: 20);
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$offset = ($page - 1) * $perPage;
$total = $query->count();
$items = $query->limit($perPage)->offset($offset)->get();
$totalPages = ceil($total / $perPage);
return [
'data' => $items,
'current_page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => $totalPages,
'has_previous' => $page > 1,
'has_next' => $page < $totalPages,
'previous_page' => $page - 1,
'next_page' => $page + 1
];
}
/**
* Validate request data
*/
protected function validate($data, $rules) {
$validator = new ValidationHelper();
return $validator->validate($data, $rules);
}
}
Model Classes
Complaint Model
File: includes/models/Complaint.php
<?php
/**
* Complaint Model
* Handles complaint-related database operations
*/
require_once MODELS_PATH . '/BaseModel.php';
class Complaint extends BaseModel {
protected $table = 'complaints';
protected $primaryKey = 'id';
protected $fillable = [
'complaint_id', 'user_id', 'category_id', 'department_id',
'assigned_to', 'title', 'description', 'priority', 'status',
'source', 'reference_no', 'submission_mode', 'submitted_at',
'assigned_at', 'started_at', 'resolved_at', 'closed_at',
'due_date', 'escalation_level', 'escalated_at', 'resolution_time',
'feedback_rating', 'feedback_comment', 'feedback_submitted_at',
'ip_address', 'user_agent'
];
protected $casts = [
'user_id' => 'int',
'category_id' => 'int',
'department_id' => 'int',
'assigned_to' => 'int',
'priority' => 'string',
'status' => 'string',
'escalation_level' => 'int',
'resolution_time' => 'int',
'feedback_rating' => 'int'
];
protected $dates = [
'submitted_at', 'assigned_at', 'started_at', 'resolved_at',
'closed_at', 'due_date', 'escalated_at', 'feedback_submitted_at'
];
/**
* Generate unique complaint ID
*/
public function generateComplaintId() {
$prefix = 'CMP';
$year = date('Y');
$month = date('m');
$random = strtoupper(substr(uniqid(), -6));
return $prefix . $year . $month . $random;
}
/**
* Create new complaint
*/
public function createComplaint($data) {
$data['complaint_id'] = $this->generateComplaintId();
$data['reference_no'] = $data['complaint_id'];
$data['submitted_at'] = date('Y-m-d H:i:s');
$data['ip_address'] = $_SERVER['REMOTE_ADDR'] ?? null;
$data['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? null;
// Get department from category
$category = $this->db->table('categories')->find($data['category_id']);
if ($category) {
$data['department_id'] = $category['department_id'];
}
// Set due date based on SLA
$slaHours = $this->getSlaHours($data['category_id'], $data['priority']);
if ($slaHours) {
$data['due_date'] = date('Y-m-d H:i:s', strtotime("+{$slaHours} hours"));
}
// Auto-assign if enabled
$autoAssign = $this->db->table('settings')
->where('key', 'auto_assign')
->first();
if ($autoAssign && $autoAssign['value'] == '1') {
$staff = $this->getAvailableStaff($data['department_id']);
if ($staff) {
$data['assigned_to'] = $staff['id'];
$data['assigned_at'] = date('Y-m-d H:i:s');
$data['status'] = 'assigned';
}
}
$id = $this->create($data);
if ($id) {
// Log creation
$this->logHistory($id, $data['user_id'], 'created', null, null, 'Complaint created');
// Send notifications
$this->sendNewComplaintNotifications($id);
return $this->find($id);
}
return null;
}
/**
* Get SLA hours for complaint
*/
private function getSlaHours($categoryId, $priority) {
// Check category-specific SLA
$category = $this->db->table('categories')->find($categoryId);
if ($category && $category['sla_hours']) {
return $category['sla_hours'];
}
// Use priority-based SLA
$slaMap = [
'urgent' => 12,
'high' => 24,
'medium' => 48,
'low' => 72
];
return $slaMap[$priority] ?? 48;
}
/**
* Get available staff for assignment
*/
private function getAvailableStaff($departmentId) {
// Find staff with least workload
return $this->db->raw("
SELECT u.*, COUNT(c.id) as active_complaints
FROM users u
LEFT JOIN complaints c ON u.id = c.assigned_to
AND c.status IN ('assigned', 'in_progress')
WHERE u.department_id = ? AND u.role = 'staff' AND u.is_active = 1
GROUP BY u.id
ORDER BY active_complaints ASC
LIMIT 1
", [$departmentId])->fetch();
}
/**
* Assign complaint to staff
*/
public function assign($complaintId, $staffId) {
$staff = $this->db->table('users')->find($staffId);
if (!$staff || $staff['role'] != 'staff') {
return false;
}
$data = [
'assigned_to' => $staffId,
'assigned_at' => date('Y-m-d H:i:s'),
'status' => 'assigned'
];
$updated = $this->update($complaintId, $data);
if ($updated) {
$this->logHistory($complaintId, $staffId, 'assigned', null, $staffId, "Assigned to {$staff['first_name']} {$staff['last_name']}");
$this->sendAssignmentNotification($complaintId, $staffId);
}
return $updated;
}
/**
* Update complaint status
*/
public function updateStatus($complaintId, $status, $comment = null, $userId = null) {
$complaint = $this->find($complaintId);
if (!$complaint) {
return false;
}
$userId = $userId ?: ($_SESSION[SESSION_USER_ID] ?? null);
$oldStatus = $complaint['status'];
$data = ['status' => $status];
// Set timestamps based on status
switch ($status) {
case 'in_progress':
$data['started_at'] = date('Y-m-d H:i:s');
break;
case 'resolved':
$data['resolved_at'] = date('Y-m-d H:i:s');
$data['resolution_time'] = $this->calculateResolutionTime($complaint);
break;
case 'closed':
$data['closed_at'] = date('Y-m-d H:i:s');
break;
case 'reopened':
$data['resolved_at'] = null;
$data['closed_at'] = null;
$data['resolution_time'] = null;
break;
}
$updated = $this->update($complaintId, $data);
if ($updated) {
$this->logHistory($complaintId, $userId, 'status_changed', $oldStatus, $status, $comment);
// Send status update notification
$this->sendStatusUpdateNotification($complaintId, $status, $comment);
}
return $updated;
}
/**
* Calculate resolution time in minutes
*/
private function calculateResolutionTime($complaint) {
$start = strtotime($complaint['started_at'] ?: $complaint['assigned_at'] ?: $complaint['submitted_at']);
$end = time();
return round(($end - $start) / 60);
}
/**
* Add comment to complaint
*/
public function addComment($complaintId, $userId, $comment, $isPrivate = false) {
$data = [
'complaint_id' => $complaintId,
'user_id' => $userId,
'comment' => $comment,
'is_staff' => $this->isStaff($userId),
'is_private' => $isPrivate
];
$id = $this->db->table('complaint_comments')->insert($data);
if ($id) {
$this->logHistory($complaintId, $userId, 'comment_added', null, null, $comment);
// Notify relevant parties
$this->sendCommentNotification($complaintId, $userId, $comment, $isPrivate);
}
return $id;
}
/**
* Check if user is staff
*/
private function isStaff($userId) {
$user = $this->db->table('users')->find($userId);
return $user && in_array($user['role'], ['staff', 'department_head', 'admin']);
}
/**
* Get complaint with all details
*/
public function getComplaintDetails($complaintId) {
$complaint = $this->db->table('complaints c')
->select([
'c.*',
'u.first_name as user_first_name',
'u.last_name as user_last_name',
'u.email as user_email',
'u.phone as user_phone',
'cat.name as category_name',
'd.name as department_name',
'a.first_name as assigned_first_name',
'a.last_name as assigned_last_name'
])
->leftJoin('users u', 'c.user_id', 'u.id')
->leftJoin('categories cat', 'c.category_id', 'cat.id')
->leftJoin('departments d', 'c.department_id', 'd.id')
->leftJoin('users a', 'c.assigned_to', 'a.id')
->where('c.id', $complaintId)
->orWhere('c.complaint_id', $complaintId)
->first();
if ($complaint) {
$complaint['comments'] = $this->getComments($complaintId);
$complaint['attachments'] = $this->getAttachments($complaintId);
$complaint['history'] = $this->getHistory($complaintId);
}
return $complaint;
}
/**
* Get complaint comments
*/
public function getComments($complaintId) {
$userId = $_SESSION[SESSION_USER_ID] ?? null;
$isStaff = $this->isStaff($userId);
$query = $this->db->table('complaint_comments cc')
->select([
'cc.*',
'u.first_name',
'u.last_name',
'u.role',
'u.profile_picture'
])
->join('users u', 'cc.user_id', 'u.id')
->where('cc.complaint_id', $complaintId)
->orderBy('cc.created_at', 'DESC');
// Hide private comments from non-staff
if (!$isStaff) {
$query->where('cc.is_private', 0);
}
return $query->get();
}
/**
* Get complaint attachments
*/
public function getAttachments($complaintId) {
return $this->db->table('complaint_attachments')
->where('complaint_id', $complaintId)
->orderBy('created_at', 'DESC')
->get();
}
/**
* Get complaint history
*/
public function getHistory($complaintId) {
return $this->db->table('complaint_history')
->where('complaint_id', $complaintId)
->orderBy('created_at', 'DESC')
->get();
}
/**
* Log complaint history
*/
private function logHistory($complaintId, $userId, $action, $oldValue, $newValue, $description = null) {
$data = [
'complaint_id' => $complaintId,
'user_id' => $userId,
'action' => $action,
'old_value' => $oldValue,
'new_value' => $newValue,
'description' => $description,
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? null
];
return $this->db->table('complaint_history')->insert($data);
}
/**
* Get user complaints
*/
public function getUserComplaints($userId, $status = null, $limit = null) {
$query = $this->db->table('complaints')
->where('user_id', $userId)
->orderBy('created_at', 'DESC');
if ($status) {
$query->where('status', $status);
}
if ($limit) {
$query->limit($limit);
}
return $query->get();
}
/**
* Get department complaints
*/
public function getDepartmentComplaints($departmentId, $status = null) {
$query = $this->db->table('complaints')
->where('department_id', $departmentId)
->orderBy('created_at', 'DESC');
if ($status) {
$query->where('status', $status);
}
return $query->get();
}
/**
* Get staff assigned complaints
*/
public function getStaffComplaints($staffId, $status = null) {
$query = $this->db->table('complaints')
->where('assigned_to', $staffId)
->orderBy('created_at', 'DESC');
if ($status) {
$query->where('status', $status);
}
return $query->get();
}
/**
* Get statistics
*/
public function getStatistics($filters = []) {
$query = $this->db->table('complaints');
if (isset($filters['department_id'])) {
$query->where('department_id', $filters['department_id']);
}
if (isset($filters['date_from'])) {
$query->where('created_at', '>=', $filters['date_from']);
}
if (isset($filters['date_to'])) {
$query->where('created_at', '<=', $filters['date_to']);
}
$total = $query->count();
$byStatus = $this->db->raw("
SELECT status, COUNT(*) as count
FROM complaints
WHERE 1=1
" . (isset($filters['department_id']) ? " AND department_id = {$filters['department_id']}" : "") . "
GROUP BY status
")->fetchAll();
$byPriority = $this->db->raw("
SELECT priority, COUNT(*) as count
FROM complaints
WHERE 1=1
" . (isset($filters['department_id']) ? " AND department_id = {$filters['department_id']}" : "") . "
GROUP BY priority
")->fetchAll();
$avgResolutionTime = $this->db->raw("
SELECT AVG(resolution_time) as avg_time
FROM complaints
WHERE status = 'resolved'
" . (isset($filters['department_id']) ? " AND department_id = {$filters['department_id']}" : "") . "
")->fetch()['avg_time'];
return [
'total' => $total,
'by_status' => $byStatus,
'by_priority' => $byPriority,
'avg_resolution_time' => round($avgResolutionTime, 2)
];
}
/**
* Check for escalations
*/
public function checkEscalations() {
$escalated = [];
// Get all pending complaints past due date
$complaints = $this->db->table('complaints')
->whereIn('status', ['pending', 'assigned', 'in_progress'])
->where('due_date', '<', date('Y-m-d H:i:s'))
->get();
foreach ($complaints as $complaint) {
$escalationLevel = $complaint['escalation_level'] + 1;
// Get escalation rule
$rule = $this->db->table('escalation_rules')
->where('priority', $complaint['priority'])
->where('escalation_level', $escalationLevel)
->first();
if ($rule) {
// Update complaint
$this->update($complaint['id'], [
'escalation_level' => $escalationLevel,
'escalated_at' => date('Y-m-d H:i:s'),
'status' => 'escalated'
]);
// Log escalation
$this->logHistory(
$complaint['id'],
null,
'escalated',
$complaint['escalation_level'],
$escalationLevel,
"Escalated to level {$escalationLevel}"
);
// Send notifications
$this->sendEscalationNotification($complaint['id'], $escalationLevel);
$escalated[] = $complaint['id'];
}
}
return $escalated;
}
/**
* Send notifications
*/
private function sendNewComplaintNotifications($complaintId) {
$complaint = $this->getComplaintDetails($complaintId);
// Notify department head
$departmentHead = $this->db->table('departments d')
->join('users u', 'd.head_id', 'u.id')
->where('d.id', $complaint['department_id'])
->first();
if ($departmentHead) {
NotificationHelper::send(
$departmentHead['id'],
'new_complaint',
'New Complaint Registered',
"A new complaint (#{$complaint['complaint_id']}) has been registered in your department.",
['complaint_id' => $complaintId]
);
}
// Send email to user
if (!empty($complaint['user_email'])) {
$this->sendEmail(
$complaint['user_email'],
'complaint_registered',
[
'user_name' => $complaint['user_first_name'] . ' ' . $complaint['user_last_name'],
'complaint_id' => $complaint['complaint_id'],
'title' => $complaint['title'],
'status' => $complaint['status'],
'tracking_url' => BASE_URL . '/complaints/track?id=' . $complaint['complaint_id']
]
);
}
}
private function sendAssignmentNotification($complaintId, $staffId) {
$complaint = $this->getComplaintDetails($complaintId);
$staff = $this->db->table('users')->find($staffId);
// Notify staff
NotificationHelper::send(
$staffId,
'complaint_assigned',
'New Complaint Assigned',
"Complaint #{$complaint['complaint_id']} has been assigned to you.",
['complaint_id' => $complaintId]
);
// Send email to staff
if (!empty($staff['email'])) {
$this->sendEmail(
$staff['email'],
'staff_assignment',
[
'staff_name' => $staff['first_name'],
'complaint_id' => $complaint['complaint_id'],
'priority' => $complaint['priority'],
'due_date' => date(DISPLAY_DATETIME_FORMAT, strtotime($complaint['due_date'])),
'complaint_url' => BASE_URL . '/staff/complaint/' . $complaint['id']
]
);
}
}
private function sendStatusUpdateNotification($complaintId, $status, $comment) {
$complaint = $this->getComplaintDetails($complaintId);
// Notify user
NotificationHelper::send(
$complaint['user_id'],
'status_update',
'Complaint Status Updated',
"Your complaint #{$complaint['complaint_id']} status has been updated to {$status}.",
['complaint_id' => $complaintId]
);
}
private function sendCommentNotification($complaintId, $userId, $comment, $isPrivate) {
$complaint = $this->getComplaintDetails($complaintId);
// Determine recipients based on privacy
if ($isPrivate) {
// Notify only staff
$staff = $this->db->table('users')
->where('department_id', $complaint['department_id'])
->whereIn('role', ['staff', 'department_head'])
->where('id', '!=', $userId)
->get();
foreach ($staff as $s) {
NotificationHelper::send(
$s['id'],
'private_comment',
'Private Comment Added',
"A private comment has been added to complaint #{$complaint['complaint_id']}.",
['complaint_id' => $complaintId]
);
}
} else {
// Notify user and assigned staff
if ($complaint['user_id'] != $userId) {
NotificationHelper::send(
$complaint['user_id'],
'new_comment',
'New Comment on Your Complaint',
"A new comment has been added to your complaint #{$complaint['complaint_id']}.",
['complaint_id' => $complaintId]
);
}
if ($complaint['assigned_to'] && $complaint['assigned_to'] != $userId) {
NotificationHelper::send(
$complaint['assigned_to'],
'new_comment',
'New Comment on Assigned Complaint',
"A new comment has been added to complaint #{$complaint['complaint_id']}.",
['complaint_id' => $complaintId]
);
}
}
}
private function sendEscalationNotification($complaintId, $level) {
$complaint = $this->getComplaintDetails($complaintId);
// Notify admins
$admins = $this->db->table('users')
->where('role', 'admin')
->get();
foreach ($admins as $admin) {
NotificationHelper::send(
$admin['id'],
'escalation',
'Complaint Escalated',
"Complaint #{$complaint['complaint_id']} has been escalated to level {$level}.",
['complaint_id' => $complaintId]
);
}
}
private function sendEmail($to, $template, $data) {
// Load email template
$templateData = $this->db->table('email_templates')
->where('name', $template)
->first();
if (!$templateData) {
return false;
}
// Replace variables in template
$subject = $templateData['subject'];
$body = $templateData['body'];
foreach ($data as $key => $value) {
$subject = str_replace('{' . $key . '}', $value, $subject);
$body = str_replace('{' . $key . '}', $value, $body);
}
// Send email
$headers = [
'MIME-Version: 1.0',
'Content-type: text/html; charset=utf-8',
'From: ' . APP_NAME . ' <' . getenv('MAIL_FROM_ADDRESS') . '>',
'Reply-To: ' . getenv('MAIL_FROM_ADDRESS')
];
return mail($to, $subject, $body, implode("\r\n", $headers));
}
}
Frontend Pages
Login Page
File: views/auth/login.php
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Login - <?php echo APP_NAME; ?></title> <!-- Bootstrap 5 CSS --> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- Font Awesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <!-- Custom CSS --> <link href="<?php echo ASSETS_URL; ?>/css/style.css" rel="stylesheet"> </head> <body class="bg-light"> <div class="container"> <div class="row justify-content-center align-items-center min-vh-100"> <div class="col-md-6 col-lg-5"> <div class="card shadow-lg border-0 rounded-lg"> <div class="card-header bg-primary text-white text-center py-4"> <h3 class="mb-0"> <i class="fas fa-ticket-alt me-2"></i> <?php echo APP_NAME; ?> </h3> <p class="mb-0 text-white-50">Sign in to your account</p> </div> <div class="card-body p-5"> <?php if (isset($flash)): ?> <div class="alert alert-<?php echo $flash['type']; ?> alert-dismissible fade show"> <i class="fas <?php echo $flash['type'] == 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'; ?> me-2"></i> <?php echo $flash['message']; ?> <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> <?php endif; ?> <form method="POST" action="<?php echo BASE_URL; ?>/login" id="loginForm"> <?php echo $this->csrfField(); ?> <div class="mb-4"> <label for="email" class="form-label"> <i class="fas fa-envelope me-2"></i>Email Address </label> <div class="input-group"> <span class="input-group-text"><i class="fas fa-envelope"></i></span> <input type="email" class="form-control" id="email" name="email" placeholder="Enter your email" required autofocus> </div> </div> <div class="mb-4"> <label for="password" class="form-label"> <i class="fas fa-lock me-2"></i>Password </label> <div class="input-group"> <span class="input-group-text"><i class="fas fa-lock"></i></span> <input type="password" class="form-control" id="password" name="password" placeholder="Enter your password" required> <button class="btn btn-outline-secondary" type="button" id="togglePassword"> <i class="fas fa-eye"></i> </button> </div> </div> <div class="mb-4 form-check"> <input type="checkbox" class="form-check-input" id="remember" name="remember"> <label class="form-check-label" for="remember">Remember me</label> <a href="<?php echo BASE_URL; ?>/forgot-password" class="float-end text-decoration-none"> Forgot Password? </a> </div> <button type="submit" class="btn btn-primary w-100 py-2 mb-3" id="loginBtn"> <i class="fas fa-sign-in-alt me-2"></i>Sign In </button> </form> <div class="text-center"> <p class="mb-0"> Don't have an account? <a href="<?php echo BASE_URL; ?>/register" class="text-decoration-none">Register here</a> </p> </div> <hr class="my-4"> <div class="text-center"> <a href="<?php echo BASE_URL; ?>" class="text-decoration-none"> <i class="fas fa-arrow-left me-1"></i>Back to Home </a> </div> </div> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> // Toggle password visibility document.getElementById('togglePassword').addEventListener('click', function() { const password = document.getElementById('password'); const icon = this.querySelector('i'); if (password.type === 'password') { password.type = 'text'; icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); } else { password.type = 'password'; icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); } }); // Form validation document.getElementById('loginForm').addEventListener('submit', function(e) { const email = document.getElementById('email').value.trim(); const password = document.getElementById('password').value; const btn = document.getElementById('loginBtn'); if (!email || !password) { e.preventDefault(); alert('Please enter both email and password'); return false; } // Disable button to prevent double submission btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Signing In...'; return true; }); </script> <style> body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .card { backdrop-filter: blur(10px); background: rgba(255, 255, 255, 0.95); } </style> </body> </html>
Complaint Submission Page
File: views/complaints/create.php
<?php $this->setViewData('title', 'Submit Complaint'); ?>
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="fas fa-plus-circle me-2 text-primary"></i>
Submit New Complaint
</h5>
</div>
<div class="card-body p-4">
<form method="POST" action="<?php echo BASE_URL; ?>/complaints/store" enctype="multipart/form-data" id="complaintForm">
<?php echo $this->csrfField(); ?>
<div class="row">
<!-- Complaint Details -->
<div class="col-md-8">
<div class="mb-4">
<label for="title" class="form-label fw-bold">
<i class="fas fa-heading me-2 text-primary"></i>
Complaint Title <span class="text-danger">*</span>
</label>
<input type="text" class="form-control form-control-lg" id="title" name="title"
placeholder="Brief summary of your complaint" required maxlength="255">
<small class="text-muted">Maximum 255 characters</small>
</div>
<div class="mb-4">
<label for="category_id" class="form-label fw-bold">
<i class="fas fa-tag me-2 text-primary"></i>
Category <span class="text-danger">*</span>
</label>
<select class="form-select form-select-lg" id="category_id" name="category_id" required>
<option value="">Select Category</option>
<?php foreach ($categories as $category): ?>
<option value="<?php echo $category['id']; ?>"
data-department="<?php echo $category['department_id']; ?>">
<?php echo htmlspecialchars($category['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-4">
<label for="priority" class="form-label fw-bold">
<i class="fas fa-exclamation-triangle me-2 text-primary"></i>
Priority Level <span class="text-danger">*</span>
</label>
<select class="form-select form-select-lg" id="priority" name="priority" required>
<option value="low">Low - Non-urgent, general inquiry</option>
<option value="medium" selected>Medium - Normal priority</option>
<option value="high">High - Urgent attention needed</option>
<option value="urgent">Urgent - Immediate action required</option>
</select>
</div>
<div class="mb-4">
<label for="description" class="form-label fw-bold">
<i class="fas fa-align-left me-2 text-primary"></i>
Detailed Description <span class="text-danger">*</span>
</label>
<textarea class="form-control" id="description" name="description" rows="6"
placeholder="Please provide detailed information about your complaint" required></textarea>
<small class="text-muted">Include relevant details, dates, and any supporting information</small>
</div>
</div>
<!-- Sidebar Information -->
<div class="col-md-4">
<!-- Expected SLA Card -->
<div class="card bg-light border-0 mb-4">
<div class="card-body">
<h6 class="fw-bold mb-3">
<i class="fas fa-clock me-2 text-primary"></i>
Expected Resolution Time
</h6>
<div id="slaInfo" class="text-muted">
Select a category to see expected resolution time
</div>
</div>
</div>
<!-- Attachments Card -->
<div class="card bg-light border-0 mb-4">
<div class="card-body">
<h6 class="fw-bold mb-3">
<i class="fas fa-paperclip me-2 text-primary"></i>
Attachments
</h6>
<div class="mb-3">
<label for="attachments" class="form-label">
Upload supporting documents
</label>
<input type="file" class="form-control" id="attachments" name="attachments[]"
multiple accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx">
<small class="text-muted">
Max file size: 5MB each<br>
Allowed: JPG, PNG, PDF, DOC
</small>
</div>
<div id="fileList" class="mt-2"></div>
</div>
</div>
<!-- Privacy Card -->
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="fw-bold mb-3">
<i class="fas fa-shield-alt me-2 text-primary"></i>
Privacy Settings
</h6>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="submission_mode"
id="modePublic" value="public" checked>
<label class="form-check-label" for="modePublic">
<i class="fas fa-globe me-1 text-info"></i>
Public - Visible to all staff
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="submission_mode"
id="modePrivate" value="private">
<label class="form-check-label" for="modePrivate">
<i class="fas fa-lock me-1 text-warning"></i>
Private - Only department head
</label>
</div>
</div>
</div>
</div>
</div>
<hr class="my-4">
<!-- Terms and Submit -->
<div class="row align-items-center">
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="terms" name="terms" required>
<label class="form-check-label" for="terms">
I confirm that the information provided is accurate and complete
</label>
</div>
</div>
<div class="col-md-6 text-md-end mt-3 mt-md-0">
<button type="button" class="btn btn-secondary me-2" onclick="history.back()">
<i class="fas fa-times me-2"></i>Cancel
</button>
<button type="submit" class="btn btn-primary px-4" id="submitBtn">
<i class="fas fa-paper-plane me-2"></i>Submit Complaint
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const categorySelect = document.getElementById('category_id');
const slaInfo = document.getElementById('slaInfo');
const fileInput = document.getElementById('attachments');
const fileList = document.getElementById('fileList');
// Update SLA info when category changes
categorySelect.addEventListener('change', function() {
const selected = this.options[this.selectedIndex];
const slaHours = selected.getAttribute('data-sla') || 48;
let priority = document.getElementById('priority').value;
let multiplier = 1;
switch(priority) {
case 'urgent': multiplier = 0.5; break;
case 'high': multiplier = 0.75; break;
case 'low': multiplier = 1.5; break;
default: multiplier = 1;
}
const hours = Math.round(slaHours * multiplier);
const dueDate = new Date();
dueDate.setHours(dueDate.getHours() + hours);
slaInfo.innerHTML = `
<p class="mb-1"><strong>Estimated time:</strong> ${hours} hours</p>
<p class="mb-0"><small>Expected by: ${dueDate.toLocaleString()}</small></p>
`;
});
// Display selected files
fileInput.addEventListener('change', function() {
fileList.innerHTML = '';
for (let file of this.files) {
let size = (file.size / 1024).toFixed(2);
let unit = 'KB';
if (size > 1024) {
size = (size / 1024).toFixed(2);
unit = 'MB';
}
fileList.innerHTML += `
<div class="alert alert-info py-1 px-2 mb-1">
<i class="fas fa-file me-2"></i>
${file.name} (${size} ${unit})
</div>
`;
}
});
// Form validation
document.getElementById('complaintForm').addEventListener('submit', function(e) {
const title = document.getElementById('title').value.trim();
const category = document.getElementById('category_id').value;
const description = document.getElementById('description').value.trim();
const terms = document.getElementById('terms').checked;
if (!title || !category || !description || !terms) {
e.preventDefault();
alert('Please fill in all required fields and agree to the terms');
return false;
}
// Disable submit button
document.getElementById('submitBtn').disabled = true;
document.getElementById('submitBtn').innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Submitting...';
return true;
});
});
</script>
Complaint Tracking Page
File: views/complaints/track.php
<?php $this->setViewData('title', 'Track Complaint'); ?>
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="fas fa-search me-2 text-primary"></i>
Track Your Complaint
</h5>
</div>
<div class="card-body p-4">
<?php if (isset($complaint)): ?>
<!-- Complaint Details -->
<div class="complaint-details">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="text-primary">#<?php echo $complaint['complaint_id']; ?></h4>
<span class="badge bg-<?php echo STATUS_COLORS[$complaint['status']]; ?> p-2">
<?php echo COMPLAINT_STATUSES[$complaint['status']]; ?>
</span>
</div>
<h5><?php echo htmlspecialchars($complaint['title']); ?></h5>
<p class="text-muted mb-4"><?php echo nl2br(htmlspecialchars($complaint['description'])); ?></p>
<!-- Progress Timeline -->
<div class="timeline mt-4">
<div class="timeline-item <?php echo $complaint['submitted_at'] ? 'completed' : ''; ?>">
<div class="timeline-marker"></div>
<div class="timeline-content">
<h6>Complaint Registered</h6>
<p class="text-muted small">
<?php echo date(DISPLAY_DATETIME_FORMAT, strtotime($complaint['submitted_at'])); ?>
</p>
</div>
</div>
<div class="timeline-item <?php echo $complaint['assigned_at'] ? 'completed' : ''; ?>">
<div class="timeline-marker"></div>
<div class="timeline-content">
<h6>Assigned to Staff</h6>
<?php if ($complaint['assigned_at']): ?>
<p class="text-muted small">
<?php echo date(DISPLAY_DATETIME_FORMAT, strtotime($complaint['assigned_at'])); ?>
<?php if ($complaint['assigned_first_name']): ?>
<br>Assigned to: <?php echo $complaint['assigned_first_name'] . ' ' . $complaint['assigned_last_name']; ?>
<?php endif; ?>
</p>
<?php endif; ?>
</div>
</div>
<div class="timeline-item <?php echo $complaint['started_at'] ? 'completed' : ''; ?>">
<div class="timeline-marker"></div>
<div class="timeline-content">
<h6>Work in Progress</h6>
<?php if ($complaint['started_at']): ?>
<p class="text-muted small">
<?php echo date(DISPLAY_DATETIME_FORMAT, strtotime($complaint['started_at'])); ?>
</p>
<?php endif; ?>
</div>
</div>
<div class="timeline-item <?php echo $complaint['resolved_at'] ? 'completed' : ''; ?>">
<div class="timeline-marker"></div>
<div class="timeline-content">
<h6>Resolved</h6>
<?php if ($complaint['resolved_at']): ?>
<p class="text-muted small">
<?php echo date(DISPLAY_DATETIME_FORMAT, strtotime($complaint['resolved_at'])); ?>
</p>
<?php endif; ?>
</div>
</div>
</div>
<!-- Comments Section -->
<?php if (!empty($complaint['comments'])): ?>
<div class="comments-section mt-4">
<h6 class="fw-bold mb-3">
<i class="fas fa-comments me-2 text-primary"></i>
Updates & Comments
</h6>
<?php foreach ($complaint['comments'] as $comment): ?>
<div class="comment-item <?php echo $comment['is_staff'] ? 'staff-comment' : 'user-comment'; ?> mb-3">
<div class="d-flex">
<img src="<?php echo UPLOADS_URL; ?>/avatars/<?php echo $comment['profile_picture']; ?>"
class="rounded-circle me-3" width="40" height="40" alt="User">
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong>
<?php echo $comment['first_name'] . ' ' . $comment['last_name']; ?>
<?php if ($comment['is_staff']): ?>
<span class="badge bg-primary ms-2">Staff</span>
<?php endif; ?>
<?php if ($comment['is_private']): ?>
<span class="badge bg-warning ms-2">Private</span>
<?php endif; ?>
</strong>
<small class="text-muted">
<?php echo timeAgo($comment['created_at']); ?>
</small>
</div>
<p class="mb-0"><?php echo nl2br(htmlspecialchars($comment['comment'])); ?></p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Add Comment Form -->
<?php if ($auth->check() && $auth->id() == $complaint['user_id'] && $complaint['status'] != 'closed'): ?>
<div class="add-comment mt-4">
<h6 class="fw-bold mb-3">
<i class="fas fa-reply me-2 text-primary"></i>
Add Update
</h6>
<form method="POST" action="<?php echo BASE_URL; ?>/complaints/comment" class="mb-3">
<?php echo $this->csrfField(); ?>
<input type="hidden" name="complaint_id" value="<?php echo $complaint['id']; ?>">
<div class="mb-3">
<textarea class="form-control" name="comment" rows="3"
placeholder="Type your message..." required></textarea>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane me-2"></i>Post Update
</button>
</form>
</div>
<?php endif; ?>
<!-- Feedback Form (for resolved complaints) -->
<?php if ($complaint['status'] == 'resolved' && !$complaint['feedback_rating'] && $auth->id() == $complaint['user_id']): ?>
<div class="feedback-section mt-4 p-4 bg-light rounded">
<h6 class="fw-bold mb-3">
<i class="fas fa-star me-2 text-warning"></i>
Rate Your Experience
</h6>
<form method="POST" action="<?php echo BASE_URL; ?>/complaints/feedback">
<?php echo $this->csrfField(); ?>
<input type="hidden" name="complaint_id" value="<?php echo $complaint['id']; ?>">
<div class="mb-3">
<label class="form-label">Rating</label>
<div class="rating-stars">
<?php for ($i = 1; $i <= 5; $i++): ?>
<i class="far fa-star star-rating" data-rating="<?php echo $i; ?>"></i>
<?php endfor; ?>
</div>
<input type="hidden" name="rating" id="rating" required>
</div>
<div class="mb-3">
<label for="feedback" class="form-label">Your Feedback (Optional)</label>
<textarea class="form-control" id="feedback" name="feedback" rows="3"
placeholder="Tell us about your experience..."></textarea>
</div>
<button type="submit" class="btn btn-warning">
<i class="fas fa-paper-plane me-2"></i>Submit Feedback
</button>
</form>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<!-- Search Form -->
<form method="GET" action="<?php echo BASE_URL; ?>/complaints/track" class="track-form">
<div class="mb-4 text-center">
<i class="fas fa-search fa-3x text-muted mb-3"></i>
<h5>Enter your complaint reference number</h5>
<p class="text-muted">You can find this in your confirmation email</p>
</div>
<div class="input-group input-group-lg">
<input type="text" class="form-control" name="id"
placeholder="e.g., CMP202312ABC123" required>
<button class="btn btn-primary" type="submit">
<i class="fas fa-search me-2"></i>Track
</button>
</div>
</form>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<style>
.timeline {
position: relative;
padding: 20px 0;
}
.timeline-item {
position: relative;
padding-left: 40px;
margin-bottom: 30px;
}
.timeline-marker {
position: absolute;
left: 0;
top: 0;
width: 20px;
height: 20px;
border-radius: 50%;
background: #e9ecef;
border: 3px solid #fff;
box-shadow: 0 0 0 2px #dee2e6;
}
.timeline-item.completed .timeline-marker {
background: #28a745;
box-shadow: 0 0 0 2px #28a745;
}
.timeline-item::before {
content: '';
position: absolute;
left: 9px;
top: 20px;
bottom: -30px;
width: 2px;
background: #dee2e6;
}
.timeline-item:last-child::before {
display: none;
}
.timeline-content {
padding-bottom: 20px;
}
.comment-item {
padding: 15px;
border-radius: 10px;
}
.staff-comment {
background: #e3f2fd;
border-left: 4px solid #2196f3;
}
.user-comment {
background: #f5f5f5;
border-left: 4px solid #9e9e9e;
}
.rating-stars {
font-size: 2rem;
color: #ffc107;
cursor: pointer;
}
.rating-stars i {
margin-right: 5px;
transition: all 0.2s;
}
.rating-stars i:hover,
.rating-stars i.active {
font-weight: 900;
}
</style>
<script>
// Star rating functionality
document.querySelectorAll('.star-rating').forEach(star => {
star.addEventListener('mouseenter', function() {
const rating = this.dataset.rating;
highlightStars(rating);
});
star.addEventListener('mouseleave', function() {
const currentRating = document.getElementById('rating').value;
highlightStars(currentRating);
});
star.addEventListener('click', function() {
const rating = this.dataset.rating;
document.getElementById('rating').value = rating;
highlightStars(rating);
});
});
function highlightStars(rating) {
document.querySelectorAll('.star-rating').forEach((star, index) => {
if (index < rating) {
star.classList.remove('far');
star.classList.add('fas');
} else {
star.classList.remove('fas');
star.classList.add('far');
}
});
}
</script>
Environment Configuration
File: .env
# Application Configuration APP_NAME="Complaint Management System" APP_ENV=development APP_DEBUG=true APP_URL=http://localhost/complaint-system APP_TIMEZONE=UTC APP_LOCALE=en APP_KEY=base64:randomstringhere # Database Configuration DB_HOST=localhost DB_NAME=complaint_system DB_USER=root DB_PASS= # Mail Configuration MAIL_DRIVER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME= MAIL_PASSWORD= MAIL_ENCRYPTION=tls [email protected] MAIL_FROM_NAME="Complaint System" # SMS Configuration (Twilio) SMS_ENABLED=false SMS_PROVIDER=twilio TWILIO_SID= TWILIO_TOKEN= TWILIO_PHONE= # Upload Configuration MAX_FILE_SIZE=5242880 # 5MB in bytes ITEMS_PER_PAGE=20 # Security SESSION_TIMEOUT=3600 BCRYPT_ROUNDS=12
File: .htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /complaint-system/
# Redirect Trailing Slashes
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1 [L,R=301]
# Handle Front Controller
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
</IfModule>
# Security Headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
</IfModule>
# File Upload Limits
php_value upload_max_filesize 5M
php_value post_max_size 6M
php_value max_execution_time 300
File: composer.json
{
"name": "complaint-system/application",
"description": "Comprehensive Complaint Management 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",
"phpoffice/phpspreadsheet": "^1.29",
"tecnickcom/tcpdf": "^6.6"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"ComplaintSystem\\": "src/"
}
},
"scripts": {
"post-install-cmd": [
"chmod -R 755 uploads/",
"chmod -R 755 storage/logs/",
"chmod -R 755 storage/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)
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
complaint-system - Create all 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
complaint_systemand selectutf8mb4_general_ci - Click on "Import" tab
- Click "Choose File" and select the
database.sqlfile from thedatabasefolder - Click "Go" to import the database structure and sample data
Step 5: Configure Environment
- Rename
.env.exampleto.envin the project root - Update database credentials:
DB_HOST=localhost DB_NAME=complaint_system DB_USER=root DB_PASS=
- Update application URL:
APP_URL=http://localhost/complaint-system
- Configure email settings if using email notifications
- Configure SMS settings if using SMS notifications
Step 6: Set Folder Permissions
Create the following folders and ensure they are writable:
uploads/complaints/uploads/avatars/storage/logs/storage/cache/storage/exports/
On Windows: Right-click folders → Properties → Security → give Write permission to Users
On Mac/Linux: Run chmod -R 755 uploads/ storage/
Step 7: Create Admin User
- Open browser and go to
http://localhost/complaint-system/register - Register a new user (e.g., email:
[email protected], password:Admin@123) - Open phpMyAdmin, go to
userstable - Find the user and change
roleto 'admin' - Set
email_verifiedto 1
Step 8: Test the Installation
- Open browser and go to
http://localhost/complaint-system/ - You should see the homepage
- Test different user roles: Admin Login:
- Email:
[email protected] - Password:
Admin@123User Registration: - Register as a regular user
- Submit test complaints
System Walkthrough
For Regular Users:
- Registration/Login - Create account and login
- Submit Complaint - Fill complaint form with details
- Track Complaint - Use reference number to track status
- View History - See all past complaints
- Add Comments - Communicate with support staff
- Provide Feedback - Rate resolution quality
- Profile Management - Update personal information
For Support Staff:
- Dashboard - View assigned complaints and statistics
- Manage Complaints - Update status, add comments
- Communication - Respond to user queries
- Performance Tracking - View personal metrics
- Knowledge Base - Access common solutions
For Department Heads:
- Department Dashboard - Monitor team performance
- Assign Complaints - Distribute workload
- Escalation Management - Handle escalated issues
- Reports - Generate department reports
- Staff Management - Manage team members
For Admins:
- System Dashboard - View overall statistics
- User Management - Manage all users
- Department Management - Create/edit departments
- Category Management - Manage complaint categories
- Settings - Configure system parameters
- Reports - Generate comprehensive reports
- Audit Logs - Track system activities
Key Features Explained
Complaint Lifecycle
- Submission - User submits complaint with details
- Auto-assignment - System assigns to appropriate staff
- Processing - Staff works on resolution
- Escalation - Automatic escalation if SLA breached
- Resolution - Staff marks as resolved
- Feedback - User rates experience
- Closure - Complaint closed after feedback
SLA Management
- Each category has defined SLA hours
- Priority affects resolution time (urgent: 50% faster)
- Automatic escalation when SLA breached
- Email notifications at each stage
Notification System
- Email: Registration, complaint updates, assignments
- SMS: Urgent notifications (if enabled)
- In-app: Dashboard notifications
- Escalation alerts: To department heads and admins
Troubleshooting
Common Issues and Solutions
- Database Connection Error
- Check if MySQL is running
- Verify credentials in
.env - Ensure database exists
- 404 Page Not Found
- Check
.htaccessconfiguration - Verify
APP_URLin.env - Ensure mod_rewrite is enabled
- File Upload Issues
- Check folder permissions
- Verify
upload_max_filesizein php.ini - Check allowed file types
- Email Not Sending
- Configure SMTP settings
- Check spam folder
- Verify mail server settings
- Session Issues
- Clear browser cache
- Check
SESSION_TIMEOUTsetting - Verify session save path
Security Best Practices
- Change default admin password immediately
- Use HTTPS in production
- Regular database backups
- Input validation on all forms
- SQL injection prevention with prepared statements
- XSS prevention with
htmlspecialchars() - CSRF tokens for all forms
- Password hashing with bcrypt
- Rate limiting for login attempts
- File upload validation and scanning
Performance Optimizations
- Database indexing on frequently queried columns
- Query caching for repeated requests
- Image optimization for uploads
- Lazy loading for images
- Pagination for large datasets
- Minified CSS/JavaScript for production
- CDN for static assets
Conclusion
The Complaint Management System is a comprehensive, feature-rich application designed to streamline complaint handling and resolution across organizations. With its modular architecture, role-based access control, and automated workflows, it provides an efficient solution for managing user grievances.
This application demonstrates:
- Secure authentication with role-based access
- Complaint lifecycle management from submission to resolution
- SLA tracking and automatic escalation
- Multi-channel notifications (email, SMS, in-app)
- File attachments for supporting documents
- Feedback system for quality measurement
- Reporting and analytics for decision making
- Audit logging for compliance
- Responsive design for all devices
The system is built following industry best practices:
- MVC architecture for clean separation of concerns
- PDO prepared statements for database security
- CSRF protection for form submissions
- XSS prevention throughout the application
- Modular code structure for easy maintenance
- Comprehensive error handling and logging
Whether for customer service departments, educational institutions, or corporate environments, this complaint management system provides all the essential features needed to handle complaints efficiently while maintaining transparency and accountability throughout the resolution process.