Complaint Management System IN HTML CSS AND JAVASCRIPT WITH PHP AND MY SQL

Project Introduction

A comprehensive complaint management system that allows users to submit, track, and resolve complaints efficiently. This system provides a structured workflow for handling customer grievances, internal issues, or any type of complaints within an organization. Perfect for businesses, educational institutions, or service providers who need to manage and resolve user concerns systematically.


✨ Features

User/Customer Features

  • Submit Complaints: Easy-to-use form for submitting complaints with category selection
  • Track Status: Real-time tracking of complaint progress
  • File Attachments: Upload supporting documents/images
  • Communication: Chat with support team about complaints
  • Feedback & Rating: Rate resolution quality after closure
  • History View: Access all past complaints

Support Team Features

  • Dashboard: Overview of pending, in-progress, and resolved complaints
  • Priority Management: Assign priority levels (Low, Medium, High, Critical)
  • Assignment: Assign complaints to specific team members
  • Internal Notes: Add private notes for team collaboration
  • Status Updates: Update complaint status throughout workflow
  • Escalation: Escalate unresolved complaints to higher management

Admin Features

  • User Management: Manage support staff and their roles
  • Category Management: Create and manage complaint categories
  • SLAs: Define Service Level Agreements for response/resolution times
  • Reports & Analytics: Generate reports on complaint trends
  • Performance Metrics: Track team performance and resolution times
  • Audit Logs: View all actions taken on complaints

Advanced Features

  • Email Notifications: Automated emails for status changes
  • SMS Alerts: Optional SMS notifications for critical complaints
  • Knowledge Base: FAQ section for common issues
  • Auto-Assignment: Automatic assignment based on workload
  • SLA Monitoring: Alert for approaching/reached SLA deadlines
  • Export Data: Export complaints to CSV/PDF for reporting

📁 File Structure

blog-website/
│
├── complaints/                         # Main complaints directory
│   ├── index.php                         # Complaints dashboard
│   ├── submit.php                           # Submit new complaint
│   ├── view.php                                 # View single complaint
│   ├── track.php                                  # Track complaint status
│   │
│   ├── user/                                        # User area
│   │   ├── dashboard.php                               # User complaints dashboard
│   │   ├── history.php                                    # Complaint history
│   │   └── feedback.php                                    # Give feedback
│   │
│   ├── staff/                                       # Support staff area
│   │   ├── dashboard.php                               # Staff dashboard
│   │   ├── assigned.php                                  # My assigned complaints
│   │   ├── manage.php                                      # Manage complaint
│   │   └── search.php                                        # Search complaints
│   │
│   ├── admin/                                       # Admin area
│   │   ├── dashboard.php                               # Admin dashboard
│   │   ├── users.php                                      # Manage staff
│   │   ├── categories.php                                  # Manage categories
│   │   ├── sla.php                                            # SLA management
│   │   ├── reports.php                                          # Reports & analytics
│   │   └── settings.php                                           # System settings
│   │
│   ├── includes/                                    # Core includes
│   │   ├── complaint-config.php                        # Configuration
│   │   ├── complaint-functions.php                        # Core functions
│   │   ├── auth.php                                           # Authentication
│   │   ├── notifications.php                                     # Email/SMS notifications
│   │   ├── sla-monitor.php                                         # SLA monitoring
│   │   └── export.php                                                # Export functions
│   │
│   ├── api/                                          # API endpoints
│   │   ├── get-complaint.php                            # Get complaint details
│   │   ├── update-status.php                               # Update status
│   │   ├── add-note.php                                       # Add internal note
│   │   ├── assign.php                                            # Assign complaint
│   │   └── upload.php                                               # File upload
│   │
│   ├── css/                                          # Stylesheets
│   │   └── complaints.css                               # Complaint system styles
│   │
│   ├── js/                                           # JavaScript
│   │   ├── complaints.js                                 # Main JS
│   │   ├── dashboard.js                                     # Dashboard interactions
│   │   └── file-upload.js                                      # File upload handling
│   │
│   └── uploads/                                       # File uploads
│       └── complaints/                                     # Complaint attachments
│
├── .env                                                  # Environment variables
├── composer.json                                            # Composer dependencies
│
└── database/
└── complaints.sql                                          # Database schema

🗄️ Database Schema (database/complaints.sql)

-- Create complaints database
CREATE DATABASE IF NOT EXISTS complaints_db;
USE complaints_db;
-- Users table (extends main users or standalone)
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
full_name VARCHAR(255) NOT NULL,
phone VARCHAR(20),
user_type ENUM('customer', 'staff', 'admin') DEFAULT 'customer',
department VARCHAR(100),
position VARCHAR(100),
profile_image VARCHAR(500),
is_active BOOLEAN DEFAULT TRUE,
email_verified BOOLEAN DEFAULT FALSE,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_type (user_type),
INDEX idx_email (email),
INDEX idx_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Categories table
CREATE TABLE IF NOT EXISTS categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
parent_id INT NULL,
department VARCHAR(100),
default_priority ENUM('low', 'medium', 'high', 'critical') DEFAULT 'medium',
estimated_resolution_hours INT,
is_active BOOLEAN DEFAULT TRUE,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_parent (parent_id),
INDEX idx_active (is_active),
FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Complaints table
CREATE TABLE IF NOT EXISTS complaints (
id INT AUTO_INCREMENT PRIMARY KEY,
complaint_number VARCHAR(50) UNIQUE NOT NULL,
user_id INT NOT NULL,
category_id INT NOT NULL,
subject VARCHAR(500) NOT NULL,
description TEXT NOT NULL,
priority ENUM('low', 'medium', 'high', 'critical') DEFAULT 'medium',
status ENUM('pending', 'in_progress', 'resolved', 'closed', 'reopened', 'escalated') DEFAULT 'pending',
source ENUM('web', 'email', 'phone', 'mobile_app') DEFAULT 'web',
attachments JSON,
assigned_to INT NULL,
assigned_at TIMESTAMP NULL,
escalated_to INT NULL,
escalated_at TIMESTAMP NULL,
escalation_reason TEXT,
resolution_notes TEXT,
resolved_at TIMESTAMP NULL,
closed_at TIMESTAMP NULL,
feedback_rating TINYINT NULL,
feedback_comment TEXT,
ip_address VARCHAR(45),
user_agent TEXT,
is_deleted BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_category (category_id),
INDEX idx_assigned (assigned_to),
INDEX idx_status (status),
INDEX idx_priority (priority),
INDEX idx_created (created_at),
INDEX idx_number (complaint_number),
FULLTEXT INDEX idx_search (subject, description, resolution_notes),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id),
FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (escalated_to) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Complaint timeline/updates
CREATE TABLE IF NOT EXISTS complaint_updates (
id INT AUTO_INCREMENT PRIMARY KEY,
complaint_id INT NOT NULL,
user_id INT NOT NULL,
update_type ENUM('status_change', 'assignment', 'note', 'attachment', 'escalation', 'resolution') NOT NULL,
old_value TEXT,
new_value TEXT,
note TEXT,
is_public BOOLEAN DEFAULT TRUE, -- False for internal notes
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_complaint (complaint_id),
INDEX idx_user (user_id),
FOREIGN KEY (complaint_id) REFERENCES complaints(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Internal notes (private between staff)
CREATE TABLE IF NOT EXISTS internal_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
complaint_id INT NOT NULL,
user_id INT NOT NULL,
note TEXT NOT NULL,
attachments JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_complaint (complaint_id),
INDEX idx_user (user_id),
FOREIGN KEY (complaint_id) REFERENCES complaints(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- SLA definitions
CREATE TABLE IF NOT EXISTS sla_definitions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
category_id INT NULL,
priority ENUM('low', 'medium', 'high', 'critical') NOT NULL,
response_time_hours INT NOT NULL, -- Time to first response
resolution_time_hours INT NOT NULL, -- Time to resolution
escalation_time_hours INT NULL, -- Time after which to escalate
escalate_to_role VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_category (category_id),
INDEX idx_priority (priority),
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- SLA tracking
CREATE TABLE IF NOT EXISTS sla_tracking (
id INT AUTO_INCREMENT PRIMARY KEY,
complaint_id INT NOT NULL UNIQUE,
sla_definition_id INT NOT NULL,
response_deadline TIMESTAMP NOT NULL,
resolution_deadline TIMESTAMP NOT NULL,
first_response_at TIMESTAMP NULL,
resolved_at TIMESTAMP NULL,
response_breached BOOLEAN DEFAULT FALSE,
resolution_breached BOOLEAN DEFAULT FALSE,
escalation_sent BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_complaint (complaint_id),
INDEX idx_response_breached (response_breached),
INDEX idx_resolution_breached (resolution_breached),
FOREIGN KEY (complaint_id) REFERENCES complaints(id) ON DELETE CASCADE,
FOREIGN KEY (sla_definition_id) REFERENCES sla_definitions(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Messages between user and support
CREATE TABLE IF NOT EXISTS messages (
id INT AUTO_INCREMENT PRIMARY KEY,
complaint_id INT NOT NULL,
sender_id INT NOT NULL,
message TEXT NOT NULL,
attachments JSON,
is_staff_reply BOOLEAN DEFAULT FALSE,
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_complaint (complaint_id),
INDEX idx_sender (sender_id),
INDEX idx_read (is_read),
FOREIGN KEY (complaint_id) REFERENCES complaints(id) ON DELETE CASCADE,
FOREIGN KEY (sender_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- FAQ/Knowledge base
CREATE TABLE IF NOT EXISTS faq (
id INT AUTO_INCREMENT PRIMARY KEY,
category_id INT NULL,
question TEXT NOT NULL,
answer TEXT NOT NULL,
views INT DEFAULT 0,
helpful_count INT DEFAULT 0,
not_helpful_count INT DEFAULT 0,
is_published BOOLEAN DEFAULT TRUE,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_category (category_id),
INDEX idx_published (is_published),
FULLTEXT INDEX idx_search (question, answer),
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Escalation rules
CREATE TABLE IF NOT EXISTS escalation_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
trigger_event ENUM('no_response', 'no_resolution', 'customer_escalation', 'high_priority') NOT NULL,
category_id INT NULL,
priority ENUM('low', 'medium', 'high', 'critical') NULL,
wait_hours INT NOT NULL,
escalate_to_role VARCHAR(100) NOT NULL,
notify_users JSON, -- Additional users to notify
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Audit log
CREATE TABLE IF NOT EXISTS audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
action VARCHAR(100) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id INT NULL,
old_data JSON,
new_data JSON,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_entity (entity_type, entity_id),
INDEX idx_created (created_at),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Notifications
CREATE TABLE IF NOT EXISTS notifications (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
type VARCHAR(50) NOT NULL,
title VARCHAR(500) NOT NULL,
message TEXT,
data JSON,
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_read (is_read),
INDEX idx_created (created_at),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Holiday calendar (for SLA calculations)
CREATE TABLE IF NOT EXISTS holidays (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
date DATE NOT NULL,
recurring BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_date (date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Insert default categories
INSERT INTO categories (name, slug, description, default_priority, estimated_resolution_hours) VALUES
('Technical Issue', 'technical-issue', 'Hardware, software, or technical problems', 'high', 24),
('Billing Problem', 'billing-problem', 'Invoicing, payment, or subscription issues', 'medium', 48),
('Account Access', 'account-access', 'Login, password, or account-related issues', 'high', 12),
('Product Feedback', 'product-feedback', 'Suggestions or feedback about products', 'low', 72),
('Service Complaint', 'service-complaint', 'Issues with service quality', 'medium', 48),
('Data Privacy', 'data-privacy', 'Concerns about data handling and privacy', 'critical', 8);
-- Insert default admin user (password: admin123 - change in production)
INSERT INTO users (email, password, full_name, user_type, is_active, email_verified) VALUES
('[email protected]', '$2y$10$YourHashedPasswordHere', 'System Admin', 'admin', TRUE, TRUE);
-- Insert sample staff
INSERT INTO users (email, password, full_name, user_type, department, position) VALUES
('[email protected]', '$2y$10$YourHashedPasswordHere', 'Support Team', 'staff', 'Customer Support', 'Support Agent'),
('[email protected]', '$2y$10$YourHashedPasswordHere', 'Tech Support', 'staff', 'Technical Support', 'Senior Technician');
-- Insert sample customer
INSERT INTO users (email, password, full_name, phone, user_type) VALUES
('[email protected]', '$2y$10$YourHashedPasswordHere', 'John Doe', '+1234567890', 'customer');
-- Insert SLA definitions
INSERT INTO sla_definitions (name, priority, response_time_hours, resolution_time_hours, escalation_time_hours, escalate_to_role) VALUES
('Critical SLA', 'critical', 1, 4, 2, 'manager'),
('High Priority SLA', 'high', 4, 24, 8, 'supervisor'),
('Medium Priority SLA', 'medium', 8, 48, 24, 'team_lead'),
('Low Priority SLA', 'low', 24, 72, 48, 'team_lead');
-- Insert escalation rules
INSERT INTO escalation_rules (name, trigger_event, wait_hours, escalate_to_role) VALUES
('No Response - High Priority', 'no_response', 2, 'supervisor'),
('No Response - Medium Priority', 'no_response', 4, 'team_lead'),
('No Resolution - Critical', 'no_resolution', 6, 'manager'),
('Customer Request Escalation', 'customer_escalation', 0, 'supervisor');

🔧 Core Functions

1. includes/complaint-config.php

<?php
/**
* Complaint Management System Configuration
*/
require_once __DIR__ . '/../../vendor/autoload.php';
use Dotenv\Dotenv;
// Load environment variables
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../');
$dotenv->load();
// Database configuration
define('DB_HOST', $_ENV['DB_HOST']);
define('DB_USER', $_ENV['DB_USER']);
define('DB_PASS', $_ENV['DB_PASS']);
define('DB_NAME', 'complaints_db');
// Application settings
define('APP_NAME', $_ENV['APP_NAME'] ?? 'Complaint Management System');
define('APP_URL', $_ENV['APP_URL'] ?? 'http://localhost/blog-website');
// Complaint settings
define('COMPLAINT_PREFIX', 'CMP');
define('ALLOW_REOPEN_DAYS', 7); // Days after closing to allow reopening
define('MAX_ATTACHMENTS', 5);
define('MAX_FILE_SIZE', 10 * 1024 * 1024); // 10MB
define('ALLOWED_FILE_TYPES', 'jpg,jpeg,png,gif,pdf,doc,docx,txt');
// Notification settings
define('ENABLE_EMAIL_NOTIFICATIONS', true);
define('ENABLE_SMS_NOTIFICATIONS', false);
define('FROM_EMAIL', '[email protected]');
define('FROM_NAME', 'Complaint System');
// Working hours (for SLA calculations)
define('WORKING_HOURS_START', 9); // 9 AM
define('WORKING_HOURS_END', 17); // 5 PM
define('WORKING_DAYS', '1,2,3,4,5'); // Monday to Friday
// Start session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Database connection
function getDB() {
static $pdo = null;
if ($pdo === null) {
try {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4";
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]);
} catch (PDOException $e) {
error_log("Database connection failed: " . $e->getMessage());
die("Database connection failed. Please try again later.");
}
}
return $pdo;
}
// Get current user
function getCurrentUser() {
if (isset($_SESSION['user_id'])) {
$pdo = getDB();
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
return $stmt->fetch();
}
return null;
}
// Check if user is logged in
function isLoggedIn() {
return isset($_SESSION['user_id']);
}
// Require login
function requireLogin() {
if (!isLoggedIn()) {
header('Location: /blog-website/login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
}
// Require specific user type
function requireUserType($types) {
$user = getCurrentUser();
$types = is_array($types) ? $types : [$types];
if (!$user || !in_array($user['user_type'], $types)) {
header('Location: /blog-website/complaints/index.php');
exit;
}
}
// Generate unique complaint number
function generateComplaintNumber() {
$pdo = getDB();
$year = date('Y');
$month = date('m');
// Get last complaint number for this month
$stmt = $pdo->prepare("
SELECT complaint_number FROM complaints 
WHERE complaint_number LIKE ? 
ORDER BY id DESC LIMIT 1
");
$stmt->execute([COMPLAINT_PREFIX . $year . $month . '%']);
$last = $stmt->fetch();
if ($last) {
$lastNum = intval(substr($last['complaint_number'], -4));
$newNum = $lastNum + 1;
} else {
$newNum = 1;
}
return COMPLAINT_PREFIX . $year . $month . str_pad($newNum, 4, '0', STR_PAD_LEFT);
}
// Format date for display
function formatDate($date, $format = 'M d, Y h:i A') {
if (!$date) return 'N/A';
return date($format, strtotime($date));
}
// Get status badge class
function getStatusBadge($status) {
$badges = [
'pending' => 'badge-warning',
'in_progress' => 'badge-info',
'resolved' => 'badge-success',
'closed' => 'badge-secondary',
'reopened' => 'badge-danger',
'escalated' => 'badge-dark'
];
return $badges[$status] ?? 'badge-secondary';
}
// Get priority badge class
function getPriorityBadge($priority) {
$badges = [
'low' => 'badge-success',
'medium' => 'badge-warning',
'high' => 'badge-danger',
'critical' => 'badge-dark'
];
return $badges[$priority] ?? 'badge-secondary';
}
// Calculate working hours between two dates (for SLA)
function calculateWorkingHours($start, $end) {
$startTime = strtotime($start);
$endTime = strtotime($end);
if ($endTime <= $startTime) return 0;
$hours = 0;
$current = $startTime;
while ($current < $endTime) {
$dayOfWeek = date('N', $current);
$hour = date('G', $current);
// Check if it's a working day and working hour
if (in_array($dayOfWeek, explode(',', WORKING_DAYS)) && 
$hour >= WORKING_HOURS_START && $hour < WORKING_HOURS_END) {
$hours++;
}
$current = strtotime('+1 hour', $current);
}
return $hours;
}
// Add working hours to a date
function addWorkingHours($start, $hours) {
$current = strtotime($start);
$added = 0;
while ($added < $hours) {
$dayOfWeek = date('N', $current);
$hour = date('G', $current);
// Check if current time is within working hours
if (in_array($dayOfWeek, explode(',', WORKING_DAYS)) && 
$hour >= WORKING_HOURS_START && $hour < WORKING_HOURS_END) {
$added++;
$current = strtotime('+1 hour', $current);
} else {
// Skip to next working hour
if ($hour >= WORKING_HOURS_END) {
// Move to next day 9 AM
$current = strtotime('tomorrow 9:00', $current);
} elseif ($hour < WORKING_HOURS_START) {
// Move to today 9 AM
$current = strtotime(date('Y-m-d 9:00', $current));
} elseif (!in_array($dayOfWeek, explode(',', WORKING_DAYS))) {
// Move to next working day 9 AM
$nextDay = $current;
do {
$nextDay = strtotime('+1 day', $nextDay);
$nextDayOfWeek = date('N', $nextDay);
} while (!in_array($nextDayOfWeek, explode(',', WORKING_DAYS)));
$current = strtotime(date('Y-m-d 9:00', $nextDay));
}
}
}
return date('Y-m-d H:i:s', $current);
}
// Log audit action
function logAudit($action, $entityType, $entityId = null, $oldData = null, $newData = null) {
$pdo = getDB();
$stmt = $pdo->prepare("
INSERT INTO audit_log (user_id, action, entity_type, entity_id, old_data, new_data, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$_SESSION['user_id'] ?? null,
$action,
$entityType,
$entityId,
$oldData ? json_encode($oldData) : null,
$newData ? json_encode($newData) : null,
$_SERVER['REMOTE_ADDR'] ?? null,
$_SERVER['HTTP_USER_AGENT'] ?? null
]);
}
// Send notification
function sendNotification($userId, $type, $title, $message, $data = []) {
$pdo = getDB();
$stmt = $pdo->prepare("
INSERT INTO notifications (user_id, type, title, message, data)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([$userId, $type, $title, $message, json_encode($data)]);
// Also send email if enabled
if (ENABLE_EMAIL_NOTIFICATIONS) {
// Get user email
$userStmt = $pdo->prepare("SELECT email FROM users WHERE id = ?");
$userStmt->execute([$userId]);
$user = $userStmt->fetch();
if ($user) {
// Send email (implement email sending here)
sendEmail($user['email'], $title, $message);
}
}
}
// Placeholder for email sending
function sendEmail($to, $subject, $message) {
// Implement actual email sending (PHPMailer, etc.)
error_log("Email to: $to, Subject: $subject");
}
// Check SLA deadlines
function checkSLAComplaint($complaintId) {
$pdo = getDB();
$stmt = $pdo->prepare("
SELECT st.*, c.status, c.resolved_at
FROM sla_tracking st
JOIN complaints c ON st.complaint_id = c.id
WHERE st.complaint_id = ?
");
$stmt->execute([$complaintId]);
$sla = $stmt->fetch();
if (!$sla) return;
$now = date('Y-m-d H:i:s');
// Check response breach
if (!$sla['first_response_at'] && $sla['response_deadline'] < $now) {
$pdo->prepare("UPDATE sla_tracking SET response_breached = TRUE WHERE id = ?")
->execute([$sla['id']]);
// Send breach notification
sendNotification(
$sla['assigned_to'] ?? null,
'sla_breach',
'SLA Response Time Breached',
"Response deadline exceeded for complaint #{$complaintId}"
);
}
// Check resolution breach
if ($sla['status'] != 'resolved' && $sla['status'] != 'closed' && 
$sla['resolution_deadline'] < $now) {
$pdo->prepare("UPDATE sla_tracking SET resolution_breached = TRUE WHERE id = ?")
->execute([$sla['id']]);
sendNotification(
$sla['assigned_to'] ?? null,
'sla_breach',
'SLA Resolution Time Breached',
"Resolution deadline exceeded for complaint #{$complaintId}"
);
}
}

2. complaints/index.php (Main Dashboard)

<?php
require_once '../includes/config.php';
require_once 'includes/complaint-config.php';
require_once 'includes/complaint-functions.php';
$user = getCurrentUser();
$page_title = 'Complaint Management System';
// Get statistics based on user type
$pdo = getDB();
if ($user) {
if ($user['user_type'] == 'admin') {
// Admin stats
$stats = [
'total' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE is_deleted = FALSE")->fetchColumn(),
'pending' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE status = 'pending' AND is_deleted = FALSE")->fetchColumn(),
'in_progress' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE status = 'in_progress' AND is_deleted = FALSE")->fetchColumn(),
'resolved' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE status = 'resolved' AND is_deleted = FALSE")->fetchColumn(),
'escalated' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE status = 'escalated' AND is_deleted = FALSE")->fetchColumn(),
'avg_response' => $pdo->query("
SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, 
(SELECT created_at FROM complaint_updates WHERE complaint_id = complaints.id AND update_type = 'assignment' LIMIT 1)
)) FROM complaints WHERE status != 'pending'
")->fetchColumn()
];
} elseif ($user['user_type'] == 'staff') {
// Staff stats - their assigned complaints
$stats = [
'assigned' => $pdo->prepare("SELECT COUNT(*) FROM complaints WHERE assigned_to = ? AND status IN ('pending', 'in_progress')")->execute([$user['id']])->fetchColumn(),
'resolved' => $pdo->prepare("SELECT COUNT(*) FROM complaints WHERE assigned_to = ? AND status = 'resolved'")->execute([$user['id']])->fetchColumn(),
'overdue' => $pdo->prepare("
SELECT COUNT(*) FROM complaints c
JOIN sla_tracking s ON c.id = s.complaint_id
WHERE c.assigned_to = ? AND c.status NOT IN ('resolved', 'closed')
AND s.resolution_deadline < NOW()
")->execute([$user['id']])->fetchColumn()
];
} else {
// Customer stats - their complaints
$stats = [
'total' => $pdo->prepare("SELECT COUNT(*) FROM complaints WHERE user_id = ?")->execute([$user['id']])->fetchColumn(),
'open' => $pdo->prepare("SELECT COUNT(*) FROM complaints WHERE user_id = ? AND status IN ('pending', 'in_progress', 'reopened')")->execute([$user['id']])->fetchColumn(),
'resolved' => $pdo->prepare("SELECT COUNT(*) FROM complaints WHERE user_id = ? AND status IN ('resolved', 'closed')")->execute([$user['id']])->fetchColumn()
];
}
}
// Get recent complaints
if ($user) {
if ($user['user_type'] == 'admin') {
$recent = $pdo->query("
SELECT c.*, u.full_name as user_name, cat.name as category_name
FROM complaints c
JOIN users u ON c.user_id = u.id
JOIN categories cat ON c.category_id = cat.id
WHERE c.is_deleted = FALSE
ORDER BY c.created_at DESC
LIMIT 10
")->fetchAll();
} elseif ($user['user_type'] == 'staff') {
$stmt = $pdo->prepare("
SELECT c.*, u.full_name as user_name, cat.name as category_name
FROM complaints c
JOIN users u ON c.user_id = u.id
JOIN categories cat ON c.category_id = cat.id
WHERE c.assigned_to = ? OR c.assigned_to IS NULL
ORDER BY 
CASE c.priority 
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
c.created_at ASC
LIMIT 10
");
$stmt->execute([$user['id']]);
$recent = $stmt->fetchAll();
} else {
$stmt = $pdo->prepare("
SELECT c.*, cat.name as category_name
FROM complaints c
JOIN categories cat ON c.category_id = cat.id
WHERE c.user_id = ?
ORDER BY c.created_at DESC
LIMIT 10
");
$stmt->execute([$user['id']]);
$recent = $stmt->fetchAll();
}
}
include '../includes/header.php';
?>
<link rel="stylesheet" href="css/complaints.css">
<div class="complaints-container">
<!-- Header -->
<div class="complaints-header">
<h1>Complaint Management System</h1>
<?php if (!$user): ?>
<div class="header-actions">
<a href="submit.php" class="btn-primary">Submit Complaint</a>
<a href="track.php" class="btn-secondary">Track Complaint</a>
</div>
<?php elseif ($user['user_type'] == 'customer'): ?>
<div class="header-actions">
<a href="submit.php" class="btn-primary">New Complaint</a>
<a href="user/history.php" class="btn-secondary">My Complaints</a>
</div>
<?php endif; ?>
</div>
<?php if (!$user): ?>
<!-- Public landing page -->
<div class="public-landing">
<div class="hero-section">
<h2>We're Here to Help</h2>
<p>Have an issue? Submit a complaint and we'll get back to you within 24 hours.</p>
<div class="hero-buttons">
<a href="submit.php" class="btn-primary btn-large">Submit Complaint</a>
<a href="track.php" class="btn-secondary btn-large">Track Existing</a>
</div>
</div>
<div class="features-section">
<h3>How It Works</h3>
<div class="features-grid">
<div class="feature">
<div class="feature-icon">📝</div>
<h4>Submit Complaint</h4>
<p>Fill out the form with details about your issue</p>
</div>
<div class="feature">
<div class="feature-icon">🔍</div>
<h4>Track Progress</h4>
<p>Get real-time updates on your complaint status</p>
</div>
<div class="feature">
<div class="feature-icon">✅</div>
<h4>Get Resolution</h4>
<p>Our team works to resolve your issue promptly</p>
</div>
<div class="feature">
<div class="feature-icon">📊</div>
<h4>Provide Feedback</h4>
<p>Rate your experience and help us improve</p>
</div>
</div>
</div>
<div class="faq-preview">
<h3>Frequently Asked Questions</h3>
<?php
$faqs = $pdo->query("SELECT * FROM faq WHERE is_published = TRUE ORDER BY views DESC LIMIT 3")->fetchAll();
foreach ($faqs as $faq):
?>
<div class="faq-item">
<h4><?php echo htmlspecialchars($faq['question']); ?></h4>
<p><?php echo htmlspecialchars(substr($faq['answer'], 0, 200)) . '...'; ?></p>
<a href="faq.php?id=<?php echo $faq['id']; ?>">Read More →</a>
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<!-- Dashboard for logged in users -->
<div class="dashboard">
<!-- Stats Cards -->
<div class="stats-grid">
<?php if ($user['user_type'] == 'admin'): ?>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['total']; ?></div>
<div class="stat-label">Total Complaints</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['pending']; ?></div>
<div class="stat-label">Pending</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['in_progress']; ?></div>
<div class="stat-label">In Progress</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['resolved']; ?></div>
<div class="stat-label">Resolved</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo round($stats['avg_response']); ?>h</div>
<div class="stat-label">Avg Response</div>
</div>
<?php elseif ($user['user_type'] == 'staff'): ?>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['assigned']; ?></div>
<div class="stat-label">My Assigned</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['resolved']; ?></div>
<div class="stat-label">Resolved</div>
</div>
<div class="stat-card">
<div class="stat-value <?php echo $stats['overdue'] > 0 ? 'text-danger' : ''; ?>">
<?php echo $stats['overdue']; ?>
</div>
<div class="stat-label">Overdue</div>
</div>
<?php else: ?>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['total']; ?></div>
<div class="stat-label">Total Complaints</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['open']; ?></div>
<div class="stat-label">Open</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['resolved']; ?></div>
<div class="stat-label">Resolved</div>
</div>
<?php endif; ?>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<h3>Quick Actions</h3>
<div class="action-buttons">
<?php if ($user['user_type'] == 'customer'): ?>
<a href="submit.php" class="action-btn">📝 New Complaint</a>
<a href="user/history.php" class="action-btn">📋 My History</a>
<?php elseif ($user['user_type'] == 'staff'): ?>
<a href="staff/assigned.php" class="action-btn">📋 My Complaints</a>
<a href="staff/search.php" class="action-btn">🔍 Search</a>
<?php elseif ($user['user_type'] == 'admin'): ?>
<a href="admin/dashboard.php" class="action-btn">📊 Admin Panel</a>
<a href="admin/reports.php" class="action-btn">📈 Reports</a>
<a href="admin/users.php" class="action-btn">👥 Users</a>
<?php endif; ?>
</div>
</div>
<!-- Recent Complaints -->
<div class="recent-complaints">
<div class="section-header">
<h3>Recent Complaints</h3>
<?php if ($user['user_type'] == 'customer'): ?>
<a href="user/history.php" class="view-all">View All →</a>
<?php else: ?>
<a href="<?php echo $user['user_type']; ?>/dashboard.php" class="view-all">View All →</a>
<?php endif; ?>
</div>
<table class="complaints-table">
<thead>
<tr>
<th>ID</th>
<th>Subject</th>
<th>Category</th>
<th>Priority</th>
<th>Status</th>
<th>Date</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent as $complaint): ?>
<tr>
<td><?php echo htmlspecialchars($complaint['complaint_number']); ?></td>
<td><?php echo htmlspecialchars(substr($complaint['subject'], 0, 50)); ?></td>
<td><?php echo htmlspecialchars($complaint['category_name']); ?></td>
<td>
<span class="badge <?php echo getPriorityBadge($complaint['priority']); ?>">
<?php echo ucfirst($complaint['priority']); ?>
</span>
</td>
<td>
<span class="badge <?php echo getStatusBadge($complaint['status']); ?>">
<?php echo ucfirst(str_replace('_', ' ', $complaint['status'])); ?>
</span>
</td>
<td><?php echo formatDate($complaint['created_at']); ?></td>
<td>
<a href="view.php?id=<?php echo $complaint['id']; ?>" class="btn-view">View</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
<script src="js/complaints.js"></script>
<style>
.complaints-container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
.complaints-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.complaints-header h1 {
font-size: 2rem;
color: #333;
}
.header-actions {
display: flex;
gap: 1rem;
}
.btn-primary, .btn-secondary {
padding: 0.8rem 1.5rem;
border-radius: 5px;
text-decoration: none;
font-weight: 500;
transition: opacity 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-large {
padding: 1rem 2rem;
font-size: 1.1rem;
}
.btn-primary:hover, .btn-secondary:hover {
opacity: 0.9;
}
/* Public Landing Page */
.public-landing {
animation: fadeIn 0.5s;
}
.hero-section {
text-align: center;
padding: 4rem 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin-bottom: 3rem;
}
.hero-section h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.hero-section p {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.hero-buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
.hero-buttons .btn-secondary {
background: rgba(255,255,255,0.2);
border: 1px solid white;
}
.features-section {
margin-bottom: 3rem;
}
.features-section h3 {
text-align: center;
font-size: 2rem;
color: #333;
margin-bottom: 2rem;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.feature {
text-align: center;
padding: 2rem;
background: white;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transition: transform 0.3s;
}
.feature:hover {
transform: translateY(-5px);
}
.feature-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.feature h4 {
margin-bottom: 0.5rem;
color: #333;
}
.feature p {
color: #666;
line-height: 1.6;
}
.faq-preview {
background: white;
border-radius: 10px;
padding: 2rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.faq-preview h3 {
margin-bottom: 1.5rem;
color: #333;
}
.faq-item {
padding: 1rem;
border-bottom: 1px solid #eee;
}
.faq-item:last-child {
border-bottom: none;
}
.faq-item h4 {
color: #333;
margin-bottom: 0.5rem;
}
.faq-item p {
color: #666;
margin-bottom: 0.5rem;
}
.faq-item a {
color: #667eea;
text-decoration: none;
}
/* Dashboard */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #333;
margin-bottom: 0.5rem;
}
.stat-label {
color: #666;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.text-danger {
color: #dc3545;
}
.quick-actions {
background: white;
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.quick-actions h3 {
margin-bottom: 1rem;
color: #333;
}
.action-buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.action-btn {
padding: 0.8rem 1.5rem;
background: #f8f9fa;
color: #333;
text-decoration: none;
border-radius: 5px;
transition: all 0.3s;
}
.action-btn:hover {
background: #667eea;
color: white;
}
.recent-complaints {
background: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.section-header h3 {
color: #333;
}
.view-all {
color: #667eea;
text-decoration: none;
}
.complaints-table {
width: 100%;
border-collapse: collapse;
}
.complaints-table th {
text-align: left;
padding: 1rem;
background: #f8f9fa;
color: #555;
font-weight: 500;
}
.complaints-table td {
padding: 1rem;
border-bottom: 1px solid #e9ecef;
}
.complaints-table tr:hover td {
background: #f8f9fa;
}
.badge {
display: inline-block;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.badge-warning { background: #fff3cd; color: #856404; }
.badge-info { background: #d1ecf1; color: #0c5460; }
.badge-success { background: #d4edda; color: #155724; }
.badge-secondary { background: #e2e3e5; color: #383d41; }
.badge-danger { background: #f8d7da; color: #721c24; }
.badge-dark { background: #d6d8d9; color: #1b1e21; }
.btn-view {
padding: 0.3rem 0.8rem;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 3px;
font-size: 0.9rem;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.complaints-header {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.hero-section {
padding: 2rem 1rem;
}
.hero-section h2 {
font-size: 2rem;
}
.hero-buttons {
flex-direction: column;
}
.features-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
.complaints-table {
display: block;
overflow-x: auto;
}
}
</style>
<?php include '../includes/footer.php'; ?>

3. complaints/submit.php (Submit New Complaint)

<?php
require_once '../includes/config.php';
require_once 'includes/complaint-config.php';
require_once 'includes/complaint-functions.php';
$page_title = 'Submit Complaint';
// Get categories for dropdown
$pdo = getDB();
$categories = $pdo->query("SELECT * FROM categories WHERE is_active = TRUE ORDER BY name")->fetchAll();
// Handle form submission
$errors = [];
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Validate input
$category = $_POST['category'] ?? '';
$subject = trim($_POST['subject'] ?? '');
$description = trim($_POST['description'] ?? '');
$priority = $_POST['priority'] ?? 'medium';
if (empty($category)) {
$errors['category'] = 'Please select a category';
}
if (empty($subject)) {
$errors['subject'] = 'Subject is required';
} elseif (strlen($subject) < 10) {
$errors['subject'] = 'Subject must be at least 10 characters';
}
if (empty($description)) {
$errors['description'] = 'Description is required';
} elseif (strlen($description) < 50) {
$errors['description'] = 'Please provide more details (at least 50 characters)';
}
// Handle file uploads
$attachments = [];
if (!empty($_FILES['attachments']['name'][0])) {
$uploadDir = __DIR__ . '/uploads/complaints/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$fileCount = count($_FILES['attachments']['name']);
if ($fileCount > MAX_ATTACHMENTS) {
$errors['attachments'] = 'Maximum ' . MAX_ATTACHMENTS . ' files allowed';
} else {
foreach ($_FILES['attachments']['tmp_name'] as $key => $tmpName) {
if ($_FILES['attachments']['error'][$key] === UPLOAD_ERR_OK) {
// Validate file type
$ext = strtolower(pathinfo($_FILES['attachments']['name'][$key], PATHINFO_EXTENSION));
$allowed = explode(',', ALLOWED_FILE_TYPES);
if (!in_array($ext, $allowed)) {
$errors['attachments'] = 'File type not allowed. Allowed: ' . ALLOWED_FILE_TYPES;
break;
}
// Validate file size
if ($_FILES['attachments']['size'][$key] > MAX_FILE_SIZE) {
$errors['attachments'] = 'File size exceeds ' . (MAX_FILE_SIZE / 1048576) . 'MB';
break;
}
$fileName = time() . '_' . uniqid() . '.' . $ext;
$filePath = $uploadDir . $fileName;
if (move_uploaded_file($tmpName, $filePath)) {
$attachments[] = [
'name' => $_FILES['attachments']['name'][$key],
'path' => 'uploads/complaints/' . $fileName,
'size' => $_FILES['attachments']['size'][$key],
'type' => $ext
];
}
}
}
}
}
if (empty($errors)) {
// Create complaint
$complaintNumber = generateComplaintNumber();
$userId = $_SESSION['user_id'] ?? null;
if (!$userId) {
// For non-logged in users, create temporary user or store email
$email = $_POST['email'] ?? '';
$name = $_POST['name'] ?? '';
if (empty($email)) {
$errors['email'] = 'Email is required for tracking';
} else {
// Check if user exists, create if not
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
$stmt->execute([$email]);
$existingUser = $stmt->fetch();
if ($existingUser) {
$userId = $existingUser['id'];
} else {
// Create new user
$stmt = $pdo->prepare("INSERT INTO users (email, full_name, password, user_type) VALUES (?, ?, ?, 'customer')");
$tempPassword = bin2hex(random_bytes(8));
$hashedPassword = password_hash($tempPassword, PASSWORD_DEFAULT);
$stmt->execute([$email, $name, $hashedPassword]);
$userId = $pdo->lastInsertId();
// Send email with login details
// sendWelcomeEmail($email, $tempPassword);
}
}
}
if (empty($errors)) {
$sql = "INSERT INTO complaints (complaint_number, user_id, category_id, subject, description, priority, attachments, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
$success = $stmt->execute([
$complaintNumber,
$userId,
$category,
$subject,
$description,
$priority,
json_encode($attachments),
$_SERVER['REMOTE_ADDR'],
$_SERVER['HTTP_USER_AGENT']
]);
if ($success) {
$complaintId = $pdo->lastInsertId();
// Add to timeline
$timelineStmt = $pdo->prepare("
INSERT INTO complaint_updates (complaint_id, user_id, update_type, note, is_public)
VALUES (?, ?, 'note', ?, TRUE)
");
$timelineStmt->execute([$complaintId, $userId, 'Complaint submitted']);
// Set up SLA tracking
setupSLA($complaintId, $priority, $category);
// Send notifications
sendNotification($userId, 'complaint_submitted', 'Complaint Submitted', 
"Your complaint #{$complaintNumber} has been submitted successfully.");
// Notify staff
notifyStaffNewComplaint($complaintId);
// Log audit
logAudit('create', 'complaint', $complaintId, null, [
'complaint_number' => $complaintNumber,
'category' => $category,
'priority' => $priority
]);
// Redirect to tracking page
$_SESSION['complaint_success'] = $complaintNumber;
header('Location: track.php?ref=' . $complaintNumber);
exit;
}
}
}
}
include '../includes/header.php';
?>
<link rel="stylesheet" href="css/complaints.css">
<div class="submit-container">
<div class="submit-header">
<h1>Submit a Complaint</h1>
<p>Please provide detailed information about your issue. We'll respond within 24 hours.</p>
</div>
<?php if (!empty($errors)): ?>
<div class="alert error">
<h4>Please fix the following errors:</h4>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo $error; ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="POST" enctype="multipart/form-data" class="submit-form" id="complaintForm">
<?php if (!isset($_SESSION['user_id'])): ?>
<!-- Guest user fields -->
<div class="form-section">
<h2>Your Information</h2>
<div class="form-row">
<div class="form-group">
<label for="name">Full Name *</label>
<input type="text" 
id="name" 
name="name" 
class="form-control <?php echo isset($errors['name']) ? 'error' : ''; ?>"
value="<?php echo htmlspecialchars($_POST['name'] ?? ''); ?>"
required>
</div>
<div class="form-group">
<label for="email">Email Address *</label>
<input type="email" 
id="email" 
name="email" 
class="form-control <?php echo isset($errors['email']) ? 'error' : ''; ?>"
value="<?php echo htmlspecialchars($_POST['email'] ?? ''); ?>"
required>
<small class="help-text">You'll use this email to track your complaint</small>
</div>
</div>
</div>
<?php endif; ?>
<!-- Complaint Details -->
<div class="form-section">
<h2>Complaint Details</h2>
<div class="form-row">
<div class="form-group">
<label for="category">Category *</label>
<select id="category" 
name="category" 
class="form-control <?php echo isset($errors['category']) ? 'error' : ''; ?>"
required>
<option value="">Select a category</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat['id']; ?>" 
<?php echo ($_POST['category'] ?? '') == $cat['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($cat['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="priority">Priority</label>
<select id="priority" name="priority" class="form-control">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
<small class="help-text">Select based on urgency</small>
</div>
</div>
<div class="form-group">
<label for="subject">Subject *</label>
<input type="text" 
id="subject" 
name="subject" 
class="form-control <?php echo isset($errors['subject']) ? 'error' : ''; ?>"
value="<?php echo htmlspecialchars($_POST['subject'] ?? ''); ?>"
placeholder="Brief summary of your issue"
required>
</div>
<div class="form-group">
<label for="description">Detailed Description *</label>
<textarea id="description" 
name="description" 
class="form-control <?php echo isset($errors['description']) ? 'error' : ''; ?>"
rows="8"
placeholder="Please provide as much detail as possible..."
required><?php echo htmlspecialchars($_POST['description'] ?? ''); ?></textarea>
<div class="character-counter">
<span id="descLength"><?php echo strlen($_POST['description'] ?? ''); ?></span>/5000
</div>
</div>
<!-- Attachments -->
<div class="form-group">
<label>Attachments (Optional)</label>
<p class="help-text">Upload relevant files (Max <?php echo MAX_ATTACHMENTS; ?> files, <?php echo MAX_FILE_SIZE / 1048576; ?>MB each)</p>
<div class="file-upload-area" id="fileDropArea">
<div class="upload-icon">📎</div>
<p>Drag & drop files here or click to browse</p>
<input type="file" 
id="attachments" 
name="attachments[]" 
multiple 
style="display: none;"
accept="<?php echo '.' . str_replace(',', ',.', ALLOWED_FILE_TYPES); ?>">
<button type="button" class="btn-browse" onclick="document.getElementById('attachments').click()">
Browse Files
</button>
</div>
<div id="fileList" class="file-list"></div>
<div id="fileError" class="error-message"></div>
</div>
</div>
<!-- Terms and Submit -->
<div class="form-group terms-group">
<label class="checkbox-label">
<input type="checkbox" name="terms" required>
<span>I confirm that the information provided is accurate and complete. I understand that false information may result in delayed processing.</span>
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn-submit" id="submitBtn">
<span class="btn-text">Submit Complaint</span>
<span class="btn-loader" style="display: none;">⏳</span>
</button>
<a href="index.php" class="btn-cancel">Cancel</a>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const desc = document.getElementById('description');
const counter = document.getElementById('descLength');
desc.addEventListener('input', function() {
counter.textContent = this.value.length;
});
// File upload handling
const fileInput = document.getElementById('attachments');
const fileList = document.getElementById('fileList');
const dropArea = document.getElementById('fileDropArea');
const fileError = document.getElementById('fileError');
const maxFiles = <?php echo MAX_ATTACHMENTS; ?>;
const maxSize = <?php echo MAX_FILE_SIZE; ?>;
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.classList.add('dragover');
});
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('dragover');
});
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', function() {
handleFiles(this.files);
});
function handleFiles(files) {
fileList.innerHTML = '';
fileError.textContent = '';
if (files.length > maxFiles) {
fileError.textContent = `Maximum ${maxFiles} files allowed`;
return;
}
for (let file of files) {
if (file.size > maxSize) {
fileError.textContent = `File ${file.name} exceeds ${maxSize / 1048576}MB limit`;
continue;
}
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<span class="file-name">${file.name}</span>
<span class="file-size">${(file.size / 1024).toFixed(1)} KB</span>
<button type="button" class="file-remove" onclick="this.parentElement.remove()">✕</button>
`;
fileList.appendChild(fileItem);
}
}
// Form submission
const form = document.getElementById('complaintForm');
const submitBtn = document.getElementById('submitBtn');
form.addEventListener('submit', function(e) {
const btnText = submitBtn.querySelector('.btn-text');
const btnLoader = submitBtn.querySelector('.btn-loader');
btnText.style.display = 'none';
btnLoader.style.display = 'inline-block';
submitBtn.disabled = true;
});
});
</script>
<style>
.submit-container {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.submit-header {
text-align: center;
margin-bottom: 2rem;
}
.submit-header h1 {
font-size: 2rem;
color: #333;
margin-bottom: 0.5rem;
}
.submit-header p {
color: #666;
}
.submit-form {
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
padding: 2rem;
}
.form-section {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid #eee;
}
.form-section h2 {
color: #333;
font-size: 1.2rem;
margin-bottom: 1.5rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #667eea;
}
.form-control.error {
border-color: #dc3545;
}
.help-text {
display: block;
color: #888;
font-size: 0.85rem;
margin-top: 0.3rem;
}
.error-message {
color: #dc3545;
font-size: 0.85rem;
margin-top: 0.3rem;
}
.character-counter {
text-align: right;
color: #888;
font-size: 0.85rem;
margin-top: 0.3rem;
}
/* File Upload */
.file-upload-area {
border: 2px dashed #ddd;
border-radius: 5px;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 1rem;
}
.file-upload-area:hover,
.file-upload-area.dragover {
border-color: #667eea;
background: #f8f9fa;
}
.upload-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.btn-browse {
margin-top: 1rem;
padding: 0.5rem 1rem;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.file-list {
margin-bottom: 1rem;
}
.file-item {
display: flex;
align-items: center;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 5px;
margin-bottom: 0.5rem;
}
.file-name {
flex: 1;
font-weight: 500;
}
.file-size {
color: #888;
margin-right: 1rem;
}
.file-remove {
background: none;
border: none;
color: #dc3545;
cursor: pointer;
font-size: 1.2rem;
}
/* Terms */
.terms-group {
margin: 2rem 0;
}
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 0.5rem;
cursor: pointer;
color: #666;
}
.checkbox-label input[type="checkbox"] {
margin-top: 0.2rem;
}
/* Form Actions */
.form-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.btn-submit {
flex: 1;
padding: 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
font-size: 1.1rem;
cursor: pointer;
transition: opacity 0.3s;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.btn-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-cancel {
padding: 1rem 2rem;
background: #6c757d;
color: white;
text-decoration: none;
border-radius: 5px;
text-align: center;
}
/* Alert */
.alert {
padding: 1rem;
border-radius: 5px;
margin-bottom: 1rem;
}
.alert.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert.error ul {
margin-top: 0.5rem;
margin-left: 1.5rem;
}
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.btn-cancel {
text-align: center;
}
}
</style>
<?php include '../includes/footer.php'; ?>

4. complaints/view.php (View Single Complaint)

<?php
require_once '../includes/config.php';
require_once 'includes/complaint-config.php';
require_once 'includes/complaint-functions.php';
$id = $_GET['id'] ?? 0;
$pdo = getDB();
// Get complaint details
$stmt = $pdo->prepare("
SELECT c.*, 
u.full_name as user_name, 
u.email as user_email,
cat.name as category_name,
assigned.full_name as assigned_name,
escalated.full_name as escalated_name
FROM complaints c
JOIN users u ON c.user_id = u.id
JOIN categories cat ON c.category_id = cat.id
LEFT JOIN users assigned ON c.assigned_to = assigned.id
LEFT JOIN users escalated ON c.escalated_to = escalated.id
WHERE c.id = ?
");
$stmt->execute([$id]);
$complaint = $stmt->fetch();
if (!$complaint) {
header('Location: index.php');
exit;
}
// Check permissions
$user = getCurrentUser();
if (!$user || 
($user['user_type'] == 'customer' && $user['id'] != $complaint['user_id']) ||
($user['user_type'] == 'staff' && $complaint['assigned_to'] != $user['id'] && $complaint['assigned_to'] != null)) {
header('Location: index.php');
exit;
}
// Get timeline updates
$stmt = $pdo->prepare("
SELECT u.*, users.full_name, users.user_type
FROM complaint_updates u
JOIN users ON u.user_id = users.id
WHERE u.complaint_id = ?
ORDER BY u.created_at DESC
");
$stmt->execute([$id]);
$timeline = $stmt->fetchAll();
// Get messages
$stmt = $pdo->prepare("
SELECT m.*, u.full_name, u.user_type
FROM messages m
JOIN users u ON m.sender_id = u.id
WHERE m.complaint_id = ?
ORDER BY m.created_at ASC
");
$stmt->execute([$id]);
$messages = $stmt->fetchAll();
// Get internal notes (for staff only)
$internalNotes = [];
if ($user['user_type'] != 'customer') {
$stmt = $pdo->prepare("
SELECT n.*, u.full_name
FROM internal_notes n
JOIN users u ON n.user_id = u.id
WHERE n.complaint_id = ?
ORDER BY n.created_at DESC
");
$stmt->execute([$id]);
$internalNotes = $stmt->fetchAll();
}
// Get SLA tracking
$stmt = $pdo->prepare("
SELECT s.*, sla.name as sla_name
FROM sla_tracking s
JOIN sla_definitions sla ON s.sla_definition_id = sla.id
WHERE s.complaint_id = ?
");
$stmt->execute([$id]);
$sla = $stmt->fetch();
// Handle message submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['message'])) {
$message = trim($_POST['message']);
$isStaffReply = $user['user_type'] != 'customer';
if (!empty($message)) {
$stmt = $pdo->prepare("
INSERT INTO messages (complaint_id, sender_id, message, is_staff_reply)
VALUES (?, ?, ?, ?)
");
$stmt->execute([$id, $user['id'], $message, $isStaffReply]);
// Add to timeline
$timelineStmt = $pdo->prepare("
INSERT INTO complaint_updates (complaint_id, user_id, update_type, note, is_public)
VALUES (?, ?, 'note', ?, TRUE)
");
$timelineStmt->execute([$id, $user['id'], 'New message added']);
// Send notification to other party
if ($isStaffReply) {
sendNotification($complaint['user_id'], 'new_message', 'New Reply on Your Complaint',
"You have a new reply on complaint #{$complaint['complaint_number']}");
} else {
if ($complaint['assigned_to']) {
sendNotification($complaint['assigned_to'], 'new_message', 'New Message from Customer',
"Customer replied on complaint #{$complaint['complaint_number']}");
}
}
header('Location: view.php?id=' . $id . '#messages');
exit;
}
}
// Handle status update (staff only)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['status']) && $user['user_type'] != 'customer') {
$newStatus = $_POST['status'];
$oldStatus = $complaint['status'];
if ($newStatus != $oldStatus) {
$stmt = $pdo->prepare("UPDATE complaints SET status = ? WHERE id = ?");
$stmt->execute([$newStatus, $id]);
// Add to timeline
$timelineStmt = $pdo->prepare("
INSERT INTO complaint_updates (complaint_id, user_id, update_type, old_value, new_value, is_public)
VALUES (?, ?, 'status_change', ?, ?, TRUE)
");
$timelineStmt->execute([$id, $user['id'], $oldStatus, $newStatus]);
// If resolved, set resolved_at
if ($newStatus == 'resolved') {
$pdo->prepare("UPDATE complaints SET resolved_at = NOW() WHERE id = ?")->execute([$id]);
// Update SLA
$pdo->prepare("UPDATE sla_tracking SET resolved_at = NOW() WHERE complaint_id = ?")->execute([$id]);
// Send notification to customer
sendNotification($complaint['user_id'], 'complaint_resolved', 'Complaint Resolved',
"Your complaint #{$complaint['complaint_number']} has been resolved.");
}
header('Location: view.php?id=' . $id);
exit;
}
}
// Handle internal note (staff only)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['internal_note']) && $user['user_type'] != 'customer') {
$note = trim($_POST['internal_note']);
if (!empty($note)) {
$stmt = $pdo->prepare("
INSERT INTO internal_notes (complaint_id, user_id, note)
VALUES (?, ?, ?)
");
$stmt->execute([$id, $user['id'], $note]);
header('Location: view.php?id=' . $id . '#internal-notes');
exit;
}
}
$page_title = 'Complaint #' . $complaint['complaint_number'];
include '../includes/header.php';
?>
<link rel="stylesheet" href="css/complaints.css">
<div class="complaint-view-container">
<!-- Header -->
<div class="complaint-header">
<div class="header-left">
<h1>Complaint #<?php echo htmlspecialchars($complaint['complaint_number']); ?></h1>
<span class="badge <?php echo getStatusBadge($complaint['status']); ?>">
<?php echo ucfirst(str_replace('_', ' ', $complaint['status'])); ?>
</span>
<span class="badge <?php echo getPriorityBadge($complaint['priority']); ?>">
<?php echo ucfirst($complaint['priority']); ?> Priority
</span>
</div>
<div class="header-right">
<a href="index.php" class="btn-back">← Back</a>
<?php if ($user['user_type'] != 'customer' && $complaint['status'] != 'resolved' && $complaint['status'] != 'closed'): ?>
<button class="btn-edit" onclick="showStatusModal()">Update Status</button>
<?php endif; ?>
</div>
</div>
<!-- Main Content -->
<div class="complaint-content">
<!-- Left Column - Details -->
<div class="left-column">
<!-- Basic Info -->
<div class="info-card">
<h3>Complaint Details</h3>
<div class="info-grid">
<div class="info-item">
<label>Submitted By:</label>
<span><?php echo htmlspecialchars($complaint['user_name']); ?></span>
</div>
<div class="info-item">
<label>Email:</label>
<span><?php echo htmlspecialchars($complaint['user_email']); ?></span>
</div>
<div class="info-item">
<label>Category:</label>
<span><?php echo htmlspecialchars($complaint['category_name']); ?></span>
</div>
<div class="info-item">
<label>Submitted:</label>
<span><?php echo formatDate($complaint['created_at']); ?></span>
</div>
<?php if ($complaint['assigned_name']): ?>
<div class="info-item">
<label>Assigned To:</label>
<span><?php echo htmlspecialchars($complaint['assigned_name']); ?></span>
</div>
<?php endif; ?>
<?php if ($complaint['resolved_at']): ?>
<div class="info-item">
<label>Resolved:</label>
<span><?php echo formatDate($complaint['resolved_at']); ?></span>
</div>
<?php endif; ?>
</div>
</div>
<!-- Subject & Description -->
<div class="description-card">
<h3><?php echo htmlspecialchars($complaint['subject']); ?></h3>
<div class="description-content">
<?php echo nl2br(htmlspecialchars($complaint['description'])); ?>
</div>
</div>
<!-- Attachments -->
<?php if (!empty($complaint['attachments'])): 
$attachments = json_decode($complaint['attachments'], true);
?>
<div class="attachments-card">
<h3>Attachments</h3>
<div class="attachments-list">
<?php foreach ($attachments as $file): ?>
<a href="/blog-website/<?php echo $file['path']; ?>" class="attachment-item" target="_blank">
<span class="file-icon">📎</span>
<span class="file-name"><?php echo htmlspecialchars($file['name']); ?></span>
<span class="file-size">(<?php echo round($file['size'] / 1024, 1); ?> KB)</span>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- SLA Information -->
<?php if ($sla): ?>
<div class="sla-card">
<h3>SLA Tracking</h3>
<div class="sla-info">
<div class="sla-item">
<label>Response Deadline:</label>
<span class="<?php echo $sla['response_breached'] ? 'text-danger' : ''; ?>">
<?php echo formatDate($sla['response_deadline']); ?>
<?php if ($sla['response_breached']): ?> (Breached)<?php endif; ?>
</span>
</div>
<div class="sla-item">
<label>Resolution Deadline:</label>
<span class="<?php echo $sla['resolution_breached'] ? 'text-danger' : ''; ?>">
<?php echo formatDate($sla['resolution_deadline']); ?>
<?php if ($sla['resolution_breached']): ?> (Breached)<?php endif; ?>
</span>
</div>
<?php if ($sla['first_response_at']): ?>
<div class="sla-item">
<label>First Response:</label>
<span><?php echo formatDate($sla['first_response_at']); ?></span>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Timeline -->
<div class="timeline-card">
<h3>Timeline</h3>
<div class="timeline">
<?php foreach ($timeline as $event): ?>
<div class="timeline-item">
<div class="timeline-time"><?php echo formatDate($event['created_at']); ?></div>
<div class="timeline-content">
<div class="timeline-author">
<?php echo htmlspecialchars($event['full_name']); ?>
(<?php echo $event['user_type']; ?>)
</div>
<?php if ($event['update_type'] == 'status_change'): ?>
<div class="timeline-action">
Changed status from 
<span class="badge-small"><?php echo $event['old_value']; ?></span>
to 
<span class="badge-small"><?php echo $event['new_value']; ?></span>
</div>
<?php elseif ($event['update_type'] == 'assignment'): ?>
<div class="timeline-action">
Assigned to <?php echo $event['new_value']; ?>
</div>
<?php else: ?>
<div class="timeline-note"><?php echo nl2br(htmlspecialchars($event['note'])); ?></div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Right Column - Messages & Notes -->
<div class="right-column">
<!-- Messages -->
<div class="messages-card" id="messages">
<h3>Messages</h3>
<div class="messages-list">
<?php foreach ($messages as $msg): ?>
<div class="message <?php echo $msg['is_staff_reply'] ? 'staff-message' : 'customer-message'; ?>">
<div class="message-header">
<span class="message-author"><?php echo htmlspecialchars($msg['full_name']); ?></span>
<span class="message-time"><?php echo formatDate($msg['created_at']); ?></span>
</div>
<div class="message-body">
<?php echo nl2br(htmlspecialchars($msg['message'])); ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Message Form -->
<form method="POST" class="message-form">
<textarea name="message" 
placeholder="Type your message..." 
rows="3"
required></textarea>
<button type="submit" class="btn-send">Send Message</button>
</form>
</div>
<!-- Internal Notes (Staff Only) -->
<?php if ($user['user_type'] != 'customer'): ?>
<div class="notes-card" id="internal-notes">
<h3>Internal Notes</h3>
<div class="notes-list">
<?php foreach ($internalNotes as $note): ?>
<div class="note-item">
<div class="note-header">
<span class="note-author"><?php echo htmlspecialchars($note['full_name']); ?></span>
<span class="note-time"><?php echo formatDate($note['created_at']); ?></span>
</div>
<div class="note-body">
<?php echo nl2br(htmlspecialchars($note['note'])); ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Internal Note Form -->
<form method="POST" class="note-form">
<textarea name="internal_note" 
placeholder="Add private note..." 
rows="2"
required></textarea>
<button type="submit" class="btn-add-note">Add Note</button>
</form>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Status Update Modal (Staff Only) -->
<?php if ($user['user_type'] != 'customer'): ?>
<div id="statusModal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" onclick="hideStatusModal()">&times;</span>
<h3>Update Complaint Status</h3>
<form method="POST" class="status-form">
<div class="form-group">
<label for="status">New Status:</label>
<select name="status" id="status" class="form-control">
<option value="pending" <?php echo $complaint['status'] == 'pending' ? 'selected' : ''; ?>>Pending</option>
<option value="in_progress" <?php echo $complaint['status'] == 'in_progress' ? 'selected' : ''; ?>>In Progress</option>
<option value="resolved" <?php echo $complaint['status'] == 'resolved' ? 'selected' : ''; ?>>Resolved</option>
<option value="closed" <?php echo $complaint['status'] == 'closed' ? 'selected' : ''; ?>>Closed</option>
<option value="escalated" <?php echo $complaint['status'] == 'escalated' ? 'selected' : ''; ?>>Escalated</option>
</select>
</div>
<button type="submit" class="btn-update">Update Status</button>
</form>
</div>
</div>
<script>
function showStatusModal() {
document.getElementById('statusModal').style.display = 'flex';
}
function hideStatusModal() {
document.getElementById('statusModal').style.display = 'none';
}
window.onclick = function(event) {
const modal = document.getElementById('statusModal');
if (event.target == modal) {
modal.style.display = 'none';
}
}
</script>
<?php endif; ?>
<style>
.complaint-view-container {
max-width: 1400px;
margin: 2rem auto;
padding: 0 1rem;
}
/* Header */
.complaint-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
background: white;
padding: 1.5rem;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.header-left h1 {
font-size: 1.8rem;
color: #333;
margin-bottom: 0.5rem;
}
.header-left .badge {
margin-right: 0.5rem;
}
.btn-back {
padding: 0.5rem 1rem;
background: #6c757d;
color: white;
text-decoration: none;
border-radius: 5px;
}
.btn-edit {
padding: 0.5rem 1rem;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin-left: 0.5rem;
}
/* Main Content Layout */
.complaint-content {
display: grid;
grid-template-columns: 1fr 350px;
gap: 2rem;
}
/* Left Column Cards */
.info-card, .description-card, .attachments-card, .sla-card, .timeline-card {
background: white;
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.info-card h3, .description-card h3, .attachments-card h3, .sla-card h3, .timeline-card h3 {
color: #333;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #f0f0f0;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.info-item label {
display: block;
color: #888;
font-size: 0.85rem;
margin-bottom: 0.3rem;
}
.info-item span {
color: #333;
font-weight: 500;
}
.description-content {
color: #666;
line-height: 1.8;
white-space: pre-wrap;
}
/* Attachments */
.attachments-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.attachment-item {
display: flex;
align-items: center;
padding: 0.8rem;
background: #f8f9fa;
border-radius: 5px;
text-decoration: none;
color: #333;
transition: background 0.3s;
}
.attachment-item:hover {
background: #e9ecef;
}
.file-icon {
margin-right: 0.5rem;
font-size: 1.2rem;
}
.file-name {
flex: 1;
}
.file-size {
color: #888;
font-size: 0.85rem;
}
/* SLA Card */
.sla-info {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.sla-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.sla-item label {
color: #666;
}
.text-danger {
color: #dc3545;
font-weight: 500;
}
/* Timeline */
.timeline {
position: relative;
padding-left: 1.5rem;
}
.timeline::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background: #e9ecef;
}
.timeline-item {
position: relative;
padding-bottom: 1.5rem;
}
.timeline-item::before {
content: '';
position: absolute;
left: -1.45rem;
top: 0;
width: 12px;
height: 12px;
border-radius: 50%;
background: #667eea;
border: 2px solid white;
}
.timeline-time {
color: #888;
font-size: 0.85rem;
margin-bottom: 0.3rem;
}
.timeline-author {
font-weight: 500;
color: #333;
margin-bottom: 0.3rem;
}
.timeline-action {
color: #666;
}
.timeline-note {
color: #666;
line-height: 1.6;
}
.badge-small {
display: inline-block;
padding: 0.2rem 0.4rem;
background: #f8f9fa;
border-radius: 3px;
font-size: 0.8rem;
}
/* Right Column */
.messages-card, .notes-card {
background: white;
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.messages-card h3, .notes-card h3 {
color: #333;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #f0f0f0;
}
.messages-list {
max-height: 400px;
overflow-y: auto;
margin-bottom: 1rem;
padding-right: 0.5rem;
}
.message {
margin-bottom: 1rem;
padding: 1rem;
border-radius: 5px;
}
.customer-message {
background: #e3f2fd;
margin-left: 20%;
}
.staff-message {
background: #f8f9fa;
margin-right: 20%;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.message-author {
font-weight: 500;
color: #333;
}
.message-time {
color: #888;
font-size: 0.85rem;
}
.message-body {
color: #666;
line-height: 1.6;
}
.message-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.message-form textarea {
width: 100%;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 5px;
resize: vertical;
}
.btn-send {
align-self: flex-end;
padding: 0.5rem 1.5rem;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
/* Internal Notes */
.notes-list {
max-height: 300px;
overflow-y: auto;
margin-bottom: 1rem;
}
.note-item {
padding: 0.8rem;
background: #fff3cd;
border-left: 3px solid #ffc107;
margin-bottom: 0.5rem;
border-radius: 3px;
}
.note-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.3rem;
}
.note-author {
font-weight: 500;
color: #856404;
}
.note-time {
color: #856404;
font-size: 0.8rem;
}
.note-body {
color: #856404;
font-size: 0.9rem;
}
.note-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.note-form textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 5px;
resize: vertical;
}
.btn-add-note {
align-self: flex-end;
padding: 0.3rem 1rem;
background: #ffc107;
color: #856404;
border: none;
border-radius: 3px;
cursor: pointer;
}
/* Modal */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 10px;
max-width: 400px;
width: 90%;
position: relative;
}
.close {
position: absolute;
top: 1rem;
right: 1.5rem;
font-size: 1.5rem;
cursor: pointer;
color: #999;
}
.close:hover {
color: #333;
}
.status-form {
margin-top: 1.5rem;
}
.btn-update {
width: 100%;
padding: 0.8rem;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 1rem;
}
/* Responsive */
@media (max-width: 1024px) {
.complaint-content {
grid-template-columns: 1fr;
}
.info-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.complaint-header {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.customer-message,
.staff-message {
margin-left: 0;
margin-right: 0;
}
}
</style>
<?php include '../includes/footer.php'; ?>

5. complaints/admin/dashboard.php (Admin Dashboard)

<?php
require_once '../../includes/config.php';
require_once '../includes/complaint-config.php';
require_once '../includes/complaint-functions.php';
requireUserType('admin');
$pdo = getDB();
$page_title = 'Admin Dashboard - Complaint System';
// Get statistics
$stats = [
'total' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE is_deleted = FALSE")->fetchColumn(),
'pending' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE status = 'pending' AND is_deleted = FALSE")->fetchColumn(),
'in_progress' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE status = 'in_progress' AND is_deleted = FALSE")->fetchColumn(),
'resolved' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE status = 'resolved' AND is_deleted = FALSE")->fetchColumn(),
'escalated' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE status = 'escalated' AND is_deleted = FALSE")->fetchColumn(),
'critical' => $pdo->query("SELECT COUNT(*) FROM complaints WHERE priority = 'critical' AND status NOT IN ('resolved', 'closed')")->fetchColumn(),
'breached' => $pdo->query("SELECT COUNT(*) FROM sla_tracking WHERE response_breached = TRUE OR resolution_breached = TRUE")->fetchColumn(),
'avg_response' => $pdo->query("
SELECT AVG(TIMESTAMPDIFF(HOUR, c.created_at, 
(SELECT created_at FROM complaint_updates WHERE complaint_id = c.id AND update_type = 'assignment' LIMIT 1)
)) FROM complaints c WHERE c.status != 'pending'
")->fetchColumn(),
'avg_resolution' => $pdo->query("
SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, resolved_at)) 
FROM complaints WHERE resolved_at IS NOT NULL
")->fetchColumn(),
'users' => $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(),
'staff' => $pdo->query("SELECT COUNT(*) FROM users WHERE user_type IN ('staff', 'admin')")->fetchColumn()
];
// Get recent complaints
$recent = $pdo->query("
SELECT c.*, u.full_name as user_name, cat.name as category_name
FROM complaints c
JOIN users u ON c.user_id = u.id
JOIN categories cat ON c.category_id = cat.id
WHERE c.is_deleted = FALSE
ORDER BY 
CASE c.priority 
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
c.created_at DESC
LIMIT 10
")->fetchAll();
// Get performance metrics by staff
$staffPerformance = $pdo->query("
SELECT u.id, u.full_name, 
COUNT(c.id) as assigned_count,
SUM(CASE WHEN c.status = 'resolved' THEN 1 ELSE 0 END) as resolved_count,
AVG(TIMESTAMPDIFF(HOUR, c.created_at, c.resolved_at)) as avg_resolution_time,
SUM(CASE WHEN s.response_breached = TRUE THEN 1 ELSE 0 END) as response_breaches,
SUM(CASE WHEN s.resolution_breached = TRUE THEN 1 ELSE 0 END) as resolution_breaches
FROM users u
LEFT JOIN complaints c ON u.id = c.assigned_to
LEFT JOIN sla_tracking s ON c.id = s.complaint_id
WHERE u.user_type IN ('staff', 'admin')
GROUP BY u.id
ORDER BY resolved_count DESC
LIMIT 5
")->fetchAll();
// Get category statistics
$categoryStats = $pdo->query("
SELECT cat.name, COUNT(c.id) as total,
SUM(CASE WHEN c.status = 'resolved' THEN 1 ELSE 0 END) as resolved
FROM categories cat
LEFT JOIN complaints c ON cat.id = c.category_id
GROUP BY cat.id
ORDER BY total DESC
")->fetchAll();
include '../../includes/header.php';
?>
<link rel="stylesheet" href="../css/complaints.css">
<div class="admin-dashboard">
<div class="dashboard-header">
<h1>Admin Dashboard</h1>
<div class="header-actions">
<a href="users.php" class="btn-primary">Manage Users</a>
<a href="categories.php" class="btn-secondary">Categories</a>
<a href="sla.php" class="btn-secondary">SLA Settings</a>
<a href="reports.php" class="btn-secondary">Reports</a>
</div>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value"><?php echo $stats['total']; ?></div>
<div class="stat-label">Total Complaints</div>
</div>
<div class="stat-card">
<div class="stat-value <?php echo $stats['pending'] > 0 ? 'text-warning' : ''; ?>">
<?php echo $stats['pending']; ?>
</div>
<div class="stat-label">Pending</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['in_progress']; ?></div>
<div class="stat-label">In Progress</div>
</div>
<div class="stat-card">
<div class="stat-value text-success"><?php echo $stats['resolved']; ?></div>
<div class="stat-label">Resolved</div>
</div>
<div class="stat-card">
<div class="stat-value <?php echo $stats['escalated'] > 0 ? 'text-danger' : ''; ?>">
<?php echo $stats['escalated']; ?>
</div>
<div class="stat-label">Escalated</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo round($stats['avg_response']); ?>h</div>
<div class="stat-label">Avg Response</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo round($stats['avg_resolution']); ?>h</div>
<div class="stat-label">Avg Resolution</div>
</div>
<div class="stat-card">
<div class="stat-value <?php echo $stats['breached'] > 0 ? 'text-danger' : ''; ?>">
<?php echo $stats['breached']; ?>
</div>
<div class="stat-label">SLA Breaches</div>
</div>
</div>
<!-- Critical Alerts -->
<?php if ($stats['critical'] > 0 || $stats['breached'] > 0): ?>
<div class="alerts-section">
<h2>⚠️ Alerts</h2>
<div class="alerts-grid">
<?php if ($stats['critical'] > 0): ?>
<div class="alert-card critical">
<h3>Critical Priority Complaints</h3>
<p><?php echo $stats['critical']; ?> complaints require immediate attention</p>
<a href="?priority=critical">View →</a>
</div>
<?php endif; ?>
<?php if ($stats['breached'] > 0): ?>
<div class="alert-card breach">
<h3>SLA Breaches</h3>
<p><?php echo $stats['breached']; ?> complaints have breached SLA deadlines</p>
<a href="?sla_breached=1">View →</a>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<div class="dashboard-grid">
<!-- Recent Complaints -->
<div class="recent-section">
<div class="section-header">
<h2>Recent Complaints</h2>
<a href="?view=all" class="view-all">View All →</a>
</div>
<table class="complaints-table">
<thead>
<tr>
<th>ID</th>
<th>User</th>
<th>Category</th>
<th>Priority</th>
<th>Status</th>
<th>Created</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent as $complaint): ?>
<tr>
<td><?php echo htmlspecialchars($complaint['complaint_number']); ?></td>
<td><?php echo htmlspecialchars($complaint['user_name']); ?></td>
<td><?php echo htmlspecialchars($complaint['category_name']); ?></td>
<td>
<span class="badge <?php echo getPriorityBadge($complaint['priority']); ?>">
<?php echo ucfirst($complaint['priority']); ?>
</span>
</td>
<td>
<span class="badge <?php echo getStatusBadge($complaint['status']); ?>">
<?php echo ucfirst(str_replace('_', ' ', $complaint['status'])); ?>
</span>
</td>
<td><?php echo formatDate($complaint['created_at']); ?></td>
<td>
<a href="../view.php?id=<?php echo $complaint['id']; ?>" class="btn-view">View</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Staff Performance -->
<div class="performance-section">
<h2>Staff Performance</h2>
<div class="performance-list">
<?php foreach ($staffPerformance as $staff): ?>
<div class="performance-item">
<div class="staff-info">
<h4><?php echo htmlspecialchars($staff['full_name']); ?></h4>
<span class="staff-stats">
<?php echo $staff['resolved_count']; ?>/<?php echo $staff['assigned_count']; ?> resolved
</span>
</div>
<div class="performance-metrics">
<div class="metric">
<label>Avg Resolution</label>
<span><?php echo round($staff['avg_resolution_time']); ?>h</span>
</div>
<div class="metric <?php echo $staff['response_breaches'] > 0 ? 'text-danger' : ''; ?>">
<label>Response Breaches</label>
<span><?php echo $staff['response_breaches']; ?></span>
</div>
<div class="metric <?php echo $staff['resolution_breaches'] > 0 ? 'text-danger' : ''; ?>">
<label>Resolution Breaches</label>
<span><?php echo $staff['resolution_breaches']; ?></span>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Category Statistics -->
<div class="category-section">
<h2>By Category</h2>
<div class="category-list">
<?php foreach ($categoryStats as $cat): ?>
<div class="category-item">
<div class="category-name"><?php echo htmlspecialchars($cat['name']); ?></div>
<div class="category-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: <?php echo ($cat['resolved'] / max($cat['total'], 1)) * 100; ?>%"></div>
</div>
<span class="category-stats">
<?php echo $cat['resolved']; ?>/<?php echo $cat['total']; ?> resolved
</span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<style>
.admin-dashboard {
max-width: 1400px;
margin: 2rem auto;
padding: 0 1rem;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.dashboard-header h1 {
font-size: 2rem;
color: #333;
}
.header-actions {
display: flex;
gap: 1rem;
}
.btn-primary, .btn-secondary {
padding: 0.5rem 1rem;
border-radius: 5px;
text-decoration: none;
transition: opacity 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-primary:hover, .btn-secondary:hover {
opacity: 0.9;
}
/* Alerts */
.alerts-section {
margin-bottom: 2rem;
}
.alerts-section h2 {
color: #333;
margin-bottom: 1rem;
}
.alerts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.alert-card {
padding: 1.5rem;
border-radius: 10px;
color: white;
}
.alert-card.critical {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
}
.alert-card.breach {
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
}
.alert-card h3 {
margin-bottom: 0.5rem;
}
.alert-card p {
margin-bottom: 1rem;
opacity: 0.9;
}
.alert-card a {
color: white;
text-decoration: underline;
}
/* Dashboard Grid */
.dashboard-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
}
.recent-section, .performance-section, .category-section {
background: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.section-header h2 {
color: #333;
font-size: 1.2rem;
}
.view-all {
color: #667eea;
text-decoration: none;
}
/* Performance List */
.performance-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.performance-item {
padding: 1rem;
background: #f8f9fa;
border-radius: 5px;
}
.staff-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.staff-info h4 {
color: #333;
margin: 0;
}
.staff-stats {
color: #888;
font-size: 0.9rem;
}
.performance-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-top: 0.5rem;
}
.metric {
text-align: center;
}
.metric label {
display: block;
color: #888;
font-size: 0.8rem;
margin-bottom: 0.2rem;
}
.metric span {
font-weight: 500;
color: #333;
}
.text-danger span {
color: #dc3545;
}
/* Category List */
.category-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.category-item {
display: flex;
align-items: center;
gap: 1rem;
}
.category-name {
min-width: 150px;
color: #333;
}
.category-progress {
flex: 1;
display: flex;
align-items: center;
gap: 1rem;
}
.progress-bar {
flex: 1;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
}
.category-stats {
min-width: 80px;
color: #888;
font-size: 0.9rem;
}
@media (max-width: 1024px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.dashboard-header {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.header-actions {
flex-wrap: wrap;
justify-content: center;
}
.alerts-grid {
grid-template-columns: 1fr;
}
.category-item {
flex-direction: column;
align-items: flex-start;
}
.category-progress {
width: 100%;
}
}
</style>
<?php include '../../includes/footer.php'; ?>

🚀 How to Use This Project Step by Step

Step 1: Environment Setup

  1. Ensure PHP 7.4+ and MySQL are installed
  2. Install Composer
  3. Enable required PHP extensions: pdo_mysql, json, fileinfo

Step 2: Database Setup

  1. Create database: complaints_db
  2. Import database/complaints.sql
  3. Update .env with database credentials

Step 3: Configure Settings

  1. Copy .env.example to .env
  2. Update email settings for notifications
  3. Configure working hours and SLA defaults
  4. Set file upload limits and allowed types

Step 4: Directory Permissions

chmod -R 777 uploads/complaints/
chmod -R 777 logs/

Step 5: Setup Cron Jobs

Add to crontab for SLA monitoring:

*/5 * * * * php /path/to/blog-website/complaints/includes/sla-monitor.php

For email notifications:

*/1 * * * * php /path/to/blog-website/complaints/includes/process-notifications.php

Step 6: Initial Login

Step 7: Configure Categories

  1. Login as admin
  2. Go to Admin Dashboard → Categories
  3. Add/modify complaint categories
  4. Set default priorities and SLA times

Step 8: Add Staff Users

  1. Go to Admin Dashboard → Users
  2. Add support staff members
  3. Assign roles and departments

🔒 Security Features

Authentication & Authorization

  • ✅ Role-based access control (Customer, Staff, Admin)
  • ✅ Password hashing with bcrypt
  • ✅ Session management
  • ✅ Login attempt limiting

Data Protection

  • ✅ Input sanitization and validation
  • ✅ SQL injection prevention (prepared statements)
  • ✅ XSS protection (htmlspecialchars)
  • ✅ CSRF tokens on forms
  • ✅ File upload validation

Audit & Compliance

  • ✅ Complete audit logging
  • ✅ User action tracking
  • ✅ IP address logging
  • ✅ Timestamp tracking for all actions

📊 Reports & Analytics

Available Reports

  1. Complaint Volume Report: Daily/weekly/monthly trends
  2. Response Time Analysis: Average response times by category
  3. Resolution Rate Report: Percentage of complaints resolved
  4. Staff Performance: Individual and team metrics
  5. SLA Compliance: Breach analysis by priority
  6. Customer Satisfaction: Feedback ratings over time
  7. Category Analysis: Most common complaint types

Export Options

  • CSV export for all reports
  • PDF generation for formal reports
  • Email scheduled reports
  • Dashboard widgets for real-time monitoring

🔧 Customization Options

Branding

  • Custom logo and colors
  • Email template customization
  • Portal

Leave a Reply

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


Macro Nepal Helper