2025-05-16 20:02:49 -04:00
|
|
|
<?php
|
|
|
|
|
class TicketModel {
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
private mysqli $conn;
|
|
|
|
|
|
|
|
|
|
public function __construct(mysqli $conn) {
|
2025-05-16 20:02:49 -04:00
|
|
|
$this->conn = $conn;
|
|
|
|
|
}
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
|
|
|
|
|
public function getTicketById(int $id): ?array {
|
2026-01-01 16:14:56 -05:00
|
|
|
$sql = "SELECT t.*,
|
|
|
|
|
u_created.username as creator_username,
|
|
|
|
|
u_created.display_name as creator_display_name,
|
|
|
|
|
u_updated.username as updater_username,
|
2026-01-01 18:36:34 -05:00
|
|
|
u_updated.display_name as updater_display_name,
|
|
|
|
|
u_assigned.username as assigned_username,
|
|
|
|
|
u_assigned.display_name as assigned_display_name
|
2026-01-01 16:14:56 -05:00
|
|
|
FROM tickets t
|
|
|
|
|
LEFT JOIN users u_created ON t.created_by = u_created.user_id
|
|
|
|
|
LEFT JOIN users u_updated ON t.updated_by = u_updated.user_id
|
2026-01-01 18:36:34 -05:00
|
|
|
LEFT JOIN users u_assigned ON t.assigned_to = u_assigned.user_id
|
2026-01-01 16:14:56 -05:00
|
|
|
WHERE t.ticket_id = ?";
|
2025-05-16 20:02:49 -04:00
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
|
|
|
$stmt->bind_param("i", $id);
|
|
|
|
|
$stmt->execute();
|
|
|
|
|
$result = $stmt->get_result();
|
2026-01-01 16:14:56 -05:00
|
|
|
|
2025-05-16 20:02:49 -04:00
|
|
|
if ($result->num_rows === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-01-01 16:14:56 -05:00
|
|
|
|
2025-05-16 20:02:49 -04:00
|
|
|
return $result->fetch_assoc();
|
|
|
|
|
}
|
|
|
|
|
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function getTicketComments(int $ticketId): array {
|
2025-05-16 20:02:49 -04:00
|
|
|
$sql = "SELECT * FROM ticket_comments WHERE ticket_id = ? ORDER BY created_at DESC";
|
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
|
|
|
$stmt->bind_param("i", $ticketId);
|
|
|
|
|
$stmt->execute();
|
|
|
|
|
$result = $stmt->get_result();
|
|
|
|
|
|
|
|
|
|
$comments = [];
|
|
|
|
|
while ($row = $result->fetch_assoc()) {
|
|
|
|
|
$comments[] = $row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $comments;
|
|
|
|
|
}
|
|
|
|
|
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function getAllTickets(int $page = 1, int $limit = 15, ?string $status = 'Open', string $sortColumn = 'ticket_id', string $sortDirection = 'desc', ?string $category = null, ?string $type = null, ?string $search = null, array $filters = []): array {
|
2025-05-16 20:02:49 -04:00
|
|
|
// Calculate offset
|
|
|
|
|
$offset = ($page - 1) * $limit;
|
2026-01-09 11:20:27 -05:00
|
|
|
|
2025-09-05 11:08:56 -04:00
|
|
|
// Build WHERE clause
|
|
|
|
|
$whereConditions = [];
|
|
|
|
|
$params = [];
|
|
|
|
|
$paramTypes = '';
|
2026-01-09 11:20:27 -05:00
|
|
|
|
2025-09-05 11:08:56 -04:00
|
|
|
// Status filtering
|
2025-05-16 20:02:49 -04:00
|
|
|
if ($status) {
|
|
|
|
|
$statuses = explode(',', $status);
|
|
|
|
|
$placeholders = str_repeat('?,', count($statuses) - 1) . '?';
|
2025-09-05 11:08:56 -04:00
|
|
|
$whereConditions[] = "status IN ($placeholders)";
|
|
|
|
|
$params = array_merge($params, $statuses);
|
|
|
|
|
$paramTypes .= str_repeat('s', count($statuses));
|
|
|
|
|
}
|
2026-01-09 11:20:27 -05:00
|
|
|
|
2025-09-05 11:08:56 -04:00
|
|
|
// Category filtering
|
|
|
|
|
if ($category) {
|
|
|
|
|
$categories = explode(',', $category);
|
|
|
|
|
$placeholders = str_repeat('?,', count($categories) - 1) . '?';
|
|
|
|
|
$whereConditions[] = "category IN ($placeholders)";
|
|
|
|
|
$params = array_merge($params, $categories);
|
|
|
|
|
$paramTypes .= str_repeat('s', count($categories));
|
|
|
|
|
}
|
2026-01-09 11:20:27 -05:00
|
|
|
|
2025-09-05 11:08:56 -04:00
|
|
|
// Type filtering
|
|
|
|
|
if ($type) {
|
|
|
|
|
$types = explode(',', $type);
|
|
|
|
|
$placeholders = str_repeat('?,', count($types) - 1) . '?';
|
|
|
|
|
$whereConditions[] = "type IN ($placeholders)";
|
|
|
|
|
$params = array_merge($params, $types);
|
|
|
|
|
$paramTypes .= str_repeat('s', count($types));
|
|
|
|
|
}
|
2026-01-09 11:20:27 -05:00
|
|
|
|
2025-09-05 12:40:38 -04:00
|
|
|
// Search Functionality
|
|
|
|
|
if ($search && !empty($search)) {
|
|
|
|
|
$whereConditions[] = "(title LIKE ? OR description LIKE ? OR ticket_id LIKE ? OR category LIKE ? OR type LIKE ?)";
|
|
|
|
|
$searchTerm = "%$search%";
|
|
|
|
|
$params = array_merge($params, [$searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm]);
|
|
|
|
|
$paramTypes .= 'sssss';
|
|
|
|
|
}
|
2026-01-09 11:20:27 -05:00
|
|
|
|
|
|
|
|
// Advanced search filters
|
|
|
|
|
// Date range - created_at
|
|
|
|
|
if (!empty($filters['created_from'])) {
|
|
|
|
|
$whereConditions[] = "DATE(t.created_at) >= ?";
|
|
|
|
|
$params[] = $filters['created_from'];
|
|
|
|
|
$paramTypes .= 's';
|
|
|
|
|
}
|
|
|
|
|
if (!empty($filters['created_to'])) {
|
|
|
|
|
$whereConditions[] = "DATE(t.created_at) <= ?";
|
|
|
|
|
$params[] = $filters['created_to'];
|
|
|
|
|
$paramTypes .= 's';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Date range - updated_at
|
|
|
|
|
if (!empty($filters['updated_from'])) {
|
|
|
|
|
$whereConditions[] = "DATE(t.updated_at) >= ?";
|
|
|
|
|
$params[] = $filters['updated_from'];
|
|
|
|
|
$paramTypes .= 's';
|
|
|
|
|
}
|
|
|
|
|
if (!empty($filters['updated_to'])) {
|
|
|
|
|
$whereConditions[] = "DATE(t.updated_at) <= ?";
|
|
|
|
|
$params[] = $filters['updated_to'];
|
|
|
|
|
$paramTypes .= 's';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Priority range
|
|
|
|
|
if (!empty($filters['priority_min'])) {
|
|
|
|
|
$whereConditions[] = "t.priority >= ?";
|
|
|
|
|
$params[] = (int)$filters['priority_min'];
|
|
|
|
|
$paramTypes .= 'i';
|
|
|
|
|
}
|
|
|
|
|
if (!empty($filters['priority_max'])) {
|
|
|
|
|
$whereConditions[] = "t.priority <= ?";
|
|
|
|
|
$params[] = (int)$filters['priority_max'];
|
|
|
|
|
$paramTypes .= 'i';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Created by user
|
|
|
|
|
if (!empty($filters['created_by'])) {
|
|
|
|
|
$whereConditions[] = "t.created_by = ?";
|
|
|
|
|
$params[] = (int)$filters['created_by'];
|
|
|
|
|
$paramTypes .= 'i';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assigned to user (including unassigned option)
|
|
|
|
|
if (!empty($filters['assigned_to'])) {
|
|
|
|
|
if ($filters['assigned_to'] === 'unassigned') {
|
|
|
|
|
$whereConditions[] = "t.assigned_to IS NULL";
|
|
|
|
|
} else {
|
|
|
|
|
$whereConditions[] = "t.assigned_to = ?";
|
|
|
|
|
$params[] = (int)$filters['assigned_to'];
|
|
|
|
|
$paramTypes .= 'i';
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-05 12:40:38 -04:00
|
|
|
|
2025-09-05 11:08:56 -04:00
|
|
|
$whereClause = '';
|
|
|
|
|
if (!empty($whereConditions)) {
|
|
|
|
|
$whereClause = 'WHERE ' . implode(' AND ', $whereConditions);
|
2025-05-16 20:02:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate sort column to prevent SQL injection
|
2026-01-09 16:42:13 -05:00
|
|
|
$allowedColumns = ['ticket_id', 'title', 'status', 'priority', 'category', 'type', 'created_at', 'updated_at', 'created_by', 'assigned_to'];
|
2025-05-16 20:02:49 -04:00
|
|
|
if (!in_array($sortColumn, $allowedColumns)) {
|
|
|
|
|
$sortColumn = 'ticket_id';
|
|
|
|
|
}
|
2026-01-09 16:42:13 -05:00
|
|
|
|
|
|
|
|
// Map column names to actual sort expressions
|
|
|
|
|
// For user columns, sort by display name with NULL handling for unassigned
|
|
|
|
|
$sortExpression = $sortColumn;
|
|
|
|
|
if ($sortColumn === 'created_by') {
|
|
|
|
|
$sortExpression = "COALESCE(u_created.display_name, u_created.username, 'System')";
|
|
|
|
|
} elseif ($sortColumn === 'assigned_to') {
|
|
|
|
|
// Put unassigned (NULL) at the end regardless of sort direction
|
|
|
|
|
$sortExpression = "CASE WHEN t.assigned_to IS NULL THEN 1 ELSE 0 END, COALESCE(u_assigned.display_name, u_assigned.username)";
|
|
|
|
|
} else {
|
|
|
|
|
$sortExpression = "t.$sortColumn";
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-16 20:02:49 -04:00
|
|
|
// Validate sort direction
|
|
|
|
|
$sortDirection = strtolower($sortDirection) === 'asc' ? 'ASC' : 'DESC';
|
2026-01-09 16:42:13 -05:00
|
|
|
|
2025-05-16 20:02:49 -04:00
|
|
|
// Get total count for pagination
|
2026-01-20 21:36:42 -05:00
|
|
|
$countSql = "SELECT COUNT(*) as total FROM tickets t $whereClause";
|
2025-05-16 20:02:49 -04:00
|
|
|
$countStmt = $this->conn->prepare($countSql);
|
2026-01-09 16:42:13 -05:00
|
|
|
|
2025-09-05 11:08:56 -04:00
|
|
|
if (!empty($params)) {
|
|
|
|
|
$countStmt->bind_param($paramTypes, ...$params);
|
2025-05-16 20:02:49 -04:00
|
|
|
}
|
2026-01-09 16:42:13 -05:00
|
|
|
|
2025-05-16 20:02:49 -04:00
|
|
|
$countStmt->execute();
|
|
|
|
|
$totalResult = $countStmt->get_result();
|
|
|
|
|
$totalTickets = $totalResult->fetch_assoc()['total'];
|
2026-01-09 16:42:13 -05:00
|
|
|
|
2026-01-01 17:37:01 -05:00
|
|
|
// Get tickets with pagination and creator info
|
|
|
|
|
$sql = "SELECT t.*,
|
2026-01-01 19:28:07 -05:00
|
|
|
u_created.username as creator_username,
|
|
|
|
|
u_created.display_name as creator_display_name,
|
|
|
|
|
u_assigned.username as assigned_username,
|
|
|
|
|
u_assigned.display_name as assigned_display_name
|
2026-01-01 17:37:01 -05:00
|
|
|
FROM tickets t
|
2026-01-01 19:28:07 -05:00
|
|
|
LEFT JOIN users u_created ON t.created_by = u_created.user_id
|
|
|
|
|
LEFT JOIN users u_assigned ON t.assigned_to = u_assigned.user_id
|
2026-01-01 17:37:01 -05:00
|
|
|
$whereClause
|
2026-01-09 16:42:13 -05:00
|
|
|
ORDER BY $sortExpression $sortDirection
|
2026-01-01 17:37:01 -05:00
|
|
|
LIMIT ? OFFSET ?";
|
2025-05-16 20:02:49 -04:00
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
|
|
|
|
2025-09-05 11:08:56 -04:00
|
|
|
// Add limit and offset parameters
|
|
|
|
|
$params[] = $limit;
|
|
|
|
|
$params[] = $offset;
|
|
|
|
|
$paramTypes .= 'ii';
|
|
|
|
|
|
|
|
|
|
if (!empty($params)) {
|
|
|
|
|
$stmt->bind_param($paramTypes, ...$params);
|
2025-05-16 20:02:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$stmt->execute();
|
|
|
|
|
$result = $stmt->get_result();
|
|
|
|
|
|
|
|
|
|
$tickets = [];
|
|
|
|
|
while ($row = $result->fetch_assoc()) {
|
|
|
|
|
$tickets[] = $row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'tickets' => $tickets,
|
|
|
|
|
'total' => $totalTickets,
|
|
|
|
|
'pages' => ceil($totalTickets / $limit),
|
|
|
|
|
'current_page' => $page
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function updateTicket(array $ticketData, ?int $updatedBy = null): bool {
|
2026-01-01 15:40:32 -05:00
|
|
|
$sql = "UPDATE tickets SET
|
|
|
|
|
title = ?,
|
|
|
|
|
priority = ?,
|
|
|
|
|
status = ?,
|
2025-05-16 20:02:49 -04:00
|
|
|
description = ?,
|
|
|
|
|
category = ?,
|
|
|
|
|
type = ?,
|
2026-01-01 15:40:32 -05:00
|
|
|
updated_by = ?,
|
|
|
|
|
updated_at = NOW()
|
2025-05-16 20:02:49 -04:00
|
|
|
WHERE ticket_id = ?";
|
2026-01-01 15:40:32 -05:00
|
|
|
|
Implement comprehensive improvement plan (Phases 1-6)
Security (Phase 1-2):
- Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc.
- Add RateLimitMiddleware for API rate limiting
- Add security event logging to AuditLogModel
- Add ResponseHelper for standardized API responses
- Update config.php with security constants
Database (Phase 3):
- Add migration 014 for additional indexes
- Add migration 015 for ticket dependencies
- Add migration 016 for ticket attachments
- Add migration 017 for recurring tickets
- Add migration 018 for custom fields
Features (Phase 4-5):
- Add ticket dependencies with DependencyModel and API
- Add duplicate detection with check_duplicates API
- Add file attachments with AttachmentModel and upload/download APIs
- Add @mentions with autocomplete and highlighting
- Add quick actions on dashboard rows
Collaboration (Phase 5):
- Add mention extraction in CommentModel
- Add mention autocomplete dropdown in ticket.js
- Add mention highlighting CSS styles
Admin & Export (Phase 6):
- Add StatsModel for dashboard widgets
- Add dashboard stats cards (open, critical, unassigned, etc.)
- Add CSV/JSON export via export_tickets API
- Add rich text editor toolbar in markdown.js
- Add RecurringTicketModel with cron job
- Add CustomFieldModel for per-category fields
- Add admin views: RecurringTickets, CustomFields, Workflow,
Templates, AuditLog, UserActivity
- Add admin APIs: manage_workflows, manage_templates,
manage_recurring, custom_fields, get_users
- Add admin routes in index.php
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 09:55:01 -05:00
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
|
|
|
if (!$stmt) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-01 15:40:32 -05:00
|
|
|
|
Implement comprehensive improvement plan (Phases 1-6)
Security (Phase 1-2):
- Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc.
- Add RateLimitMiddleware for API rate limiting
- Add security event logging to AuditLogModel
- Add ResponseHelper for standardized API responses
- Update config.php with security constants
Database (Phase 3):
- Add migration 014 for additional indexes
- Add migration 015 for ticket dependencies
- Add migration 016 for ticket attachments
- Add migration 017 for recurring tickets
- Add migration 018 for custom fields
Features (Phase 4-5):
- Add ticket dependencies with DependencyModel and API
- Add duplicate detection with check_duplicates API
- Add file attachments with AttachmentModel and upload/download APIs
- Add @mentions with autocomplete and highlighting
- Add quick actions on dashboard rows
Collaboration (Phase 5):
- Add mention extraction in CommentModel
- Add mention autocomplete dropdown in ticket.js
- Add mention highlighting CSS styles
Admin & Export (Phase 6):
- Add StatsModel for dashboard widgets
- Add dashboard stats cards (open, critical, unassigned, etc.)
- Add CSV/JSON export via export_tickets API
- Add rich text editor toolbar in markdown.js
- Add RecurringTicketModel with cron job
- Add CustomFieldModel for per-category fields
- Add admin views: RecurringTickets, CustomFields, Workflow,
Templates, AuditLog, UserActivity
- Add admin APIs: manage_workflows, manage_templates,
manage_recurring, custom_fields, get_users
- Add admin routes in index.php
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 09:55:01 -05:00
|
|
|
$stmt->bind_param(
|
|
|
|
|
"sissssii",
|
|
|
|
|
$ticketData['title'],
|
|
|
|
|
$ticketData['priority'],
|
|
|
|
|
$ticketData['status'],
|
|
|
|
|
$ticketData['description'],
|
|
|
|
|
$ticketData['category'],
|
|
|
|
|
$ticketData['type'],
|
|
|
|
|
$updatedBy,
|
|
|
|
|
$ticketData['ticket_id']
|
|
|
|
|
);
|
2026-01-01 15:40:32 -05:00
|
|
|
|
Implement comprehensive improvement plan (Phases 1-6)
Security (Phase 1-2):
- Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc.
- Add RateLimitMiddleware for API rate limiting
- Add security event logging to AuditLogModel
- Add ResponseHelper for standardized API responses
- Update config.php with security constants
Database (Phase 3):
- Add migration 014 for additional indexes
- Add migration 015 for ticket dependencies
- Add migration 016 for ticket attachments
- Add migration 017 for recurring tickets
- Add migration 018 for custom fields
Features (Phase 4-5):
- Add ticket dependencies with DependencyModel and API
- Add duplicate detection with check_duplicates API
- Add file attachments with AttachmentModel and upload/download APIs
- Add @mentions with autocomplete and highlighting
- Add quick actions on dashboard rows
Collaboration (Phase 5):
- Add mention extraction in CommentModel
- Add mention autocomplete dropdown in ticket.js
- Add mention highlighting CSS styles
Admin & Export (Phase 6):
- Add StatsModel for dashboard widgets
- Add dashboard stats cards (open, critical, unassigned, etc.)
- Add CSV/JSON export via export_tickets API
- Add rich text editor toolbar in markdown.js
- Add RecurringTicketModel with cron job
- Add CustomFieldModel for per-category fields
- Add admin views: RecurringTickets, CustomFields, Workflow,
Templates, AuditLog, UserActivity
- Add admin APIs: manage_workflows, manage_templates,
manage_recurring, custom_fields, get_users
- Add admin routes in index.php
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 09:55:01 -05:00
|
|
|
$result = $stmt->execute();
|
|
|
|
|
$stmt->close();
|
2026-01-01 15:40:32 -05:00
|
|
|
|
Implement comprehensive improvement plan (Phases 1-6)
Security (Phase 1-2):
- Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc.
- Add RateLimitMiddleware for API rate limiting
- Add security event logging to AuditLogModel
- Add ResponseHelper for standardized API responses
- Update config.php with security constants
Database (Phase 3):
- Add migration 014 for additional indexes
- Add migration 015 for ticket dependencies
- Add migration 016 for ticket attachments
- Add migration 017 for recurring tickets
- Add migration 018 for custom fields
Features (Phase 4-5):
- Add ticket dependencies with DependencyModel and API
- Add duplicate detection with check_duplicates API
- Add file attachments with AttachmentModel and upload/download APIs
- Add @mentions with autocomplete and highlighting
- Add quick actions on dashboard rows
Collaboration (Phase 5):
- Add mention extraction in CommentModel
- Add mention autocomplete dropdown in ticket.js
- Add mention highlighting CSS styles
Admin & Export (Phase 6):
- Add StatsModel for dashboard widgets
- Add dashboard stats cards (open, critical, unassigned, etc.)
- Add CSV/JSON export via export_tickets API
- Add rich text editor toolbar in markdown.js
- Add RecurringTicketModel with cron job
- Add CustomFieldModel for per-category fields
- Add admin views: RecurringTickets, CustomFields, Workflow,
Templates, AuditLog, UserActivity
- Add admin APIs: manage_workflows, manage_templates,
manage_recurring, custom_fields, get_users
- Add admin routes in index.php
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 09:55:01 -05:00
|
|
|
return $result;
|
2025-05-16 20:02:49 -04:00
|
|
|
}
|
|
|
|
|
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function createTicket(array $ticketData, ?int $createdBy = null): array {
|
2026-01-28 20:27:15 -05:00
|
|
|
// Generate unique ticket ID (9-digit format with leading zeros)
|
|
|
|
|
// Loop until we find an ID that doesn't exist to prevent collisions
|
|
|
|
|
$maxAttempts = 10;
|
|
|
|
|
$attempts = 0;
|
|
|
|
|
$ticket_id = null;
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
$candidate_id = sprintf('%09d', mt_rand(100000000, 999999999));
|
|
|
|
|
|
|
|
|
|
// Check if this ID already exists
|
|
|
|
|
$checkSql = "SELECT ticket_id FROM tickets WHERE ticket_id = ? LIMIT 1";
|
|
|
|
|
$checkStmt = $this->conn->prepare($checkSql);
|
|
|
|
|
$checkStmt->bind_param("s", $candidate_id);
|
|
|
|
|
$checkStmt->execute();
|
|
|
|
|
$checkResult = $checkStmt->get_result();
|
|
|
|
|
|
|
|
|
|
if ($checkResult->num_rows === 0) {
|
|
|
|
|
$ticket_id = $candidate_id;
|
|
|
|
|
}
|
|
|
|
|
$checkStmt->close();
|
|
|
|
|
$attempts++;
|
|
|
|
|
} while ($ticket_id === null && $attempts < $maxAttempts);
|
|
|
|
|
|
|
|
|
|
if ($ticket_id === null) {
|
|
|
|
|
return [
|
|
|
|
|
'success' => false,
|
|
|
|
|
'error' => 'Failed to generate unique ticket ID after ' . $maxAttempts . ' attempts'
|
|
|
|
|
];
|
|
|
|
|
}
|
2026-01-01 15:40:32 -05:00
|
|
|
|
2026-01-23 10:01:50 -05:00
|
|
|
$sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, created_by, visibility, visibility_groups)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
2026-01-01 15:40:32 -05:00
|
|
|
|
2025-05-16 20:02:49 -04:00
|
|
|
$stmt = $this->conn->prepare($sql);
|
2026-01-01 15:40:32 -05:00
|
|
|
|
2025-05-16 20:02:49 -04:00
|
|
|
// Set default values if not provided
|
|
|
|
|
$status = $ticketData['status'] ?? 'Open';
|
|
|
|
|
$priority = $ticketData['priority'] ?? '4';
|
|
|
|
|
$category = $ticketData['category'] ?? 'General';
|
|
|
|
|
$type = $ticketData['type'] ?? 'Issue';
|
2026-01-23 10:01:50 -05:00
|
|
|
$visibility = $ticketData['visibility'] ?? 'public';
|
|
|
|
|
$visibilityGroups = $ticketData['visibility_groups'] ?? null;
|
|
|
|
|
|
|
|
|
|
// Validate visibility
|
|
|
|
|
$allowedVisibilities = ['public', 'internal', 'confidential'];
|
|
|
|
|
if (!in_array($visibility, $allowedVisibilities)) {
|
|
|
|
|
$visibility = 'public';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:27:15 -05:00
|
|
|
// Validate internal visibility requires groups
|
|
|
|
|
if ($visibility === 'internal') {
|
|
|
|
|
if (empty($visibilityGroups) || trim($visibilityGroups) === '') {
|
|
|
|
|
return [
|
|
|
|
|
'success' => false,
|
|
|
|
|
'error' => 'Internal visibility requires at least one group to be specified'
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Clear visibility_groups if not internal
|
2026-01-23 10:01:50 -05:00
|
|
|
$visibilityGroups = null;
|
|
|
|
|
}
|
2026-01-01 15:40:32 -05:00
|
|
|
|
2025-05-16 20:02:49 -04:00
|
|
|
$stmt->bind_param(
|
2026-01-23 10:01:50 -05:00
|
|
|
"sssssssiss",
|
2025-05-16 20:02:49 -04:00
|
|
|
$ticket_id,
|
|
|
|
|
$ticketData['title'],
|
|
|
|
|
$ticketData['description'],
|
|
|
|
|
$status,
|
|
|
|
|
$priority,
|
|
|
|
|
$category,
|
2026-01-01 15:40:32 -05:00
|
|
|
$type,
|
2026-01-23 10:01:50 -05:00
|
|
|
$createdBy,
|
|
|
|
|
$visibility,
|
|
|
|
|
$visibilityGroups
|
2025-05-16 20:02:49 -04:00
|
|
|
);
|
2026-01-01 15:40:32 -05:00
|
|
|
|
2025-05-16 20:02:49 -04:00
|
|
|
if ($stmt->execute()) {
|
|
|
|
|
return [
|
|
|
|
|
'success' => true,
|
|
|
|
|
'ticket_id' => $ticket_id
|
|
|
|
|
];
|
|
|
|
|
} else {
|
|
|
|
|
return [
|
|
|
|
|
'success' => false,
|
|
|
|
|
'error' => $this->conn->error
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function addComment(int $ticketId, array $commentData): array {
|
2025-05-16 20:02:49 -04:00
|
|
|
$sql = "INSERT INTO ticket_comments (ticket_id, user_name, comment_text, markdown_enabled)
|
|
|
|
|
VALUES (?, ?, ?, ?)";
|
|
|
|
|
|
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
|
|
|
|
|
|
|
|
// Set default username
|
|
|
|
|
$username = $commentData['user_name'] ?? 'User';
|
|
|
|
|
$markdownEnabled = $commentData['markdown_enabled'] ? 1 : 0;
|
|
|
|
|
|
|
|
|
|
$stmt->bind_param(
|
|
|
|
|
"sssi",
|
|
|
|
|
$ticketId,
|
|
|
|
|
$username,
|
|
|
|
|
$commentData['comment_text'],
|
|
|
|
|
$markdownEnabled
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($stmt->execute()) {
|
|
|
|
|
return [
|
|
|
|
|
'success' => true,
|
|
|
|
|
'user_name' => $username,
|
|
|
|
|
'created_at' => date('M d, Y H:i')
|
|
|
|
|
];
|
|
|
|
|
} else {
|
|
|
|
|
return [
|
|
|
|
|
'success' => false,
|
|
|
|
|
'error' => $this->conn->error
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-01 18:36:34 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Assign ticket to a user
|
|
|
|
|
*
|
|
|
|
|
* @param int $ticketId Ticket ID
|
|
|
|
|
* @param int $userId User ID to assign to
|
|
|
|
|
* @param int $assignedBy User ID performing the assignment
|
|
|
|
|
* @return bool Success status
|
|
|
|
|
*/
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function assignTicket(int $ticketId, int $userId, int $assignedBy): bool {
|
2026-01-01 18:36:34 -05:00
|
|
|
$sql = "UPDATE tickets SET assigned_to = ?, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?";
|
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
|
|
|
$stmt->bind_param("iii", $userId, $assignedBy, $ticketId);
|
|
|
|
|
$result = $stmt->execute();
|
|
|
|
|
$stmt->close();
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unassign ticket (set assigned_to to NULL)
|
|
|
|
|
*
|
|
|
|
|
* @param int $ticketId Ticket ID
|
|
|
|
|
* @param int $updatedBy User ID performing the unassignment
|
|
|
|
|
* @return bool Success status
|
|
|
|
|
*/
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function unassignTicket(int $ticketId, int $updatedBy): bool {
|
2026-01-01 18:36:34 -05:00
|
|
|
$sql = "UPDATE tickets SET assigned_to = NULL, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?";
|
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
|
|
|
$stmt->bind_param("ii", $updatedBy, $ticketId);
|
|
|
|
|
$result = $stmt->execute();
|
|
|
|
|
$stmt->close();
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
2026-01-09 16:24:36 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get multiple tickets by IDs in a single query (batch loading)
|
|
|
|
|
* Eliminates N+1 query problem in bulk operations
|
|
|
|
|
*
|
|
|
|
|
* @param array $ticketIds Array of ticket IDs
|
|
|
|
|
* @return array Associative array keyed by ticket_id
|
|
|
|
|
*/
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function getTicketsByIds(array $ticketIds): array {
|
2026-01-09 16:24:36 -05:00
|
|
|
if (empty($ticketIds)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sanitize ticket IDs
|
|
|
|
|
$ticketIds = array_map('intval', $ticketIds);
|
|
|
|
|
|
|
|
|
|
// Create placeholders for IN clause
|
|
|
|
|
$placeholders = str_repeat('?,', count($ticketIds) - 1) . '?';
|
|
|
|
|
|
|
|
|
|
$sql = "SELECT t.*,
|
|
|
|
|
u_created.username as creator_username,
|
|
|
|
|
u_created.display_name as creator_display_name,
|
|
|
|
|
u_updated.username as updater_username,
|
|
|
|
|
u_updated.display_name as updater_display_name,
|
|
|
|
|
u_assigned.username as assigned_username,
|
|
|
|
|
u_assigned.display_name as assigned_display_name
|
|
|
|
|
FROM tickets t
|
|
|
|
|
LEFT JOIN users u_created ON t.created_by = u_created.user_id
|
|
|
|
|
LEFT JOIN users u_updated ON t.updated_by = u_updated.user_id
|
|
|
|
|
LEFT JOIN users u_assigned ON t.assigned_to = u_assigned.user_id
|
|
|
|
|
WHERE t.ticket_id IN ($placeholders)";
|
|
|
|
|
|
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
|
|
|
$types = str_repeat('i', count($ticketIds));
|
|
|
|
|
$stmt->bind_param($types, ...$ticketIds);
|
|
|
|
|
$stmt->execute();
|
|
|
|
|
$result = $stmt->get_result();
|
|
|
|
|
|
|
|
|
|
$tickets = [];
|
|
|
|
|
while ($row = $result->fetch_assoc()) {
|
|
|
|
|
$tickets[$row['ticket_id']] = $row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$stmt->close();
|
|
|
|
|
return $tickets;
|
|
|
|
|
}
|
2026-01-23 10:01:50 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a user can access a ticket based on visibility settings
|
|
|
|
|
*
|
|
|
|
|
* @param array $ticket The ticket data
|
|
|
|
|
* @param array $user The user data (must include user_id, is_admin, groups)
|
|
|
|
|
* @return bool True if user can access the ticket
|
|
|
|
|
*/
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function canUserAccessTicket(array $ticket, array $user): bool {
|
2026-01-23 10:01:50 -05:00
|
|
|
// Admins can access all tickets
|
|
|
|
|
if (!empty($user['is_admin'])) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$visibility = $ticket['visibility'] ?? 'public';
|
|
|
|
|
|
|
|
|
|
// Public tickets are accessible to all authenticated users
|
|
|
|
|
if ($visibility === 'public') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Confidential tickets: only creator, assignee, and admins
|
|
|
|
|
if ($visibility === 'confidential') {
|
|
|
|
|
$userId = $user['user_id'] ?? null;
|
|
|
|
|
return ($ticket['created_by'] == $userId || $ticket['assigned_to'] == $userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Internal tickets: check if user is in any of the allowed groups
|
|
|
|
|
if ($visibility === 'internal') {
|
|
|
|
|
$allowedGroups = array_filter(array_map('trim', explode(',', $ticket['visibility_groups'] ?? '')));
|
|
|
|
|
if (empty($allowedGroups)) {
|
|
|
|
|
return false; // No groups specified means no access
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$userGroups = array_filter(array_map('trim', explode(',', $user['groups'] ?? '')));
|
|
|
|
|
// Check if any user group matches any allowed group
|
|
|
|
|
return !empty(array_intersect($userGroups, $allowedGroups));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Build visibility filter SQL for queries
|
|
|
|
|
*
|
|
|
|
|
* @param array $user The current user
|
|
|
|
|
* @return array ['sql' => string, 'params' => array, 'types' => string]
|
|
|
|
|
*/
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function getVisibilityFilter(array $user): array {
|
2026-01-23 10:01:50 -05:00
|
|
|
// Admins see all tickets
|
|
|
|
|
if (!empty($user['is_admin'])) {
|
|
|
|
|
return ['sql' => '1=1', 'params' => [], 'types' => ''];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$userId = $user['user_id'] ?? 0;
|
|
|
|
|
$userGroups = array_filter(array_map('trim', explode(',', $user['groups'] ?? '')));
|
|
|
|
|
|
|
|
|
|
// Build the visibility filter
|
|
|
|
|
// 1. Public tickets
|
|
|
|
|
// 2. Confidential tickets where user is creator or assignee
|
|
|
|
|
// 3. Internal tickets where user's groups overlap with visibility_groups
|
|
|
|
|
$conditions = [];
|
|
|
|
|
$params = [];
|
|
|
|
|
$types = '';
|
|
|
|
|
|
|
|
|
|
// Public visibility
|
|
|
|
|
$conditions[] = "(t.visibility = 'public' OR t.visibility IS NULL)";
|
|
|
|
|
|
|
|
|
|
// Confidential - user is creator or assignee
|
|
|
|
|
$conditions[] = "(t.visibility = 'confidential' AND (t.created_by = ? OR t.assigned_to = ?))";
|
|
|
|
|
$params[] = $userId;
|
|
|
|
|
$params[] = $userId;
|
|
|
|
|
$types .= 'ii';
|
|
|
|
|
|
|
|
|
|
// Internal - check group membership
|
|
|
|
|
if (!empty($userGroups)) {
|
|
|
|
|
$groupConditions = [];
|
|
|
|
|
foreach ($userGroups as $group) {
|
|
|
|
|
$groupConditions[] = "FIND_IN_SET(?, REPLACE(t.visibility_groups, ' ', ''))";
|
|
|
|
|
$params[] = $group;
|
|
|
|
|
$types .= 's';
|
|
|
|
|
}
|
|
|
|
|
$conditions[] = "(t.visibility = 'internal' AND (" . implode(' OR ', $groupConditions) . "))";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'sql' => '(' . implode(' OR ', $conditions) . ')',
|
|
|
|
|
'params' => $params,
|
|
|
|
|
'types' => $types
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update ticket visibility settings
|
|
|
|
|
*
|
|
|
|
|
* @param int $ticketId
|
|
|
|
|
* @param string $visibility ('public', 'internal', 'confidential')
|
|
|
|
|
* @param string|null $visibilityGroups Comma-separated group names for 'internal' visibility
|
|
|
|
|
* @param int $updatedBy User ID
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
Add PHP 7.4+ type hints to helpers, models, and middleware
Added strict typing with parameter types, return types, and property
types across all core classes:
- helpers: Database, ErrorHandler, CacheHelper
- models: TicketModel, UserModel, WorkflowModel, TemplateModel, UserPreferencesModel
- middleware: RateLimitMiddleware, CsrfMiddleware, SecurityHeadersMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:04:36 -05:00
|
|
|
public function updateVisibility(int $ticketId, string $visibility, ?string $visibilityGroups, int $updatedBy): bool {
|
2026-01-23 10:01:50 -05:00
|
|
|
$allowedVisibilities = ['public', 'internal', 'confidential'];
|
|
|
|
|
if (!in_array($visibility, $allowedVisibilities)) {
|
|
|
|
|
$visibility = 'public';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:27:15 -05:00
|
|
|
// Validate internal visibility requires groups
|
|
|
|
|
if ($visibility === 'internal') {
|
|
|
|
|
if (empty($visibilityGroups) || trim($visibilityGroups) === '') {
|
|
|
|
|
return false; // Internal visibility requires groups
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Clear visibility_groups if not internal
|
2026-01-23 10:01:50 -05:00
|
|
|
$visibilityGroups = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sql = "UPDATE tickets SET visibility = ?, visibility_groups = ?, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?";
|
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
|
|
|
$stmt->bind_param("ssii", $visibility, $visibilityGroups, $updatedBy, $ticketId);
|
|
|
|
|
$result = $stmt->execute();
|
|
|
|
|
$stmt->close();
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
2025-05-16 20:02:49 -04:00
|
|
|
}
|