false, 'error' => 'Invalid CSRF token']); exit; } } $currentUser = $_SESSION['user']; $userId = $currentUser['user_id']; $isAdmin = $currentUser['is_admin'] ?? false; // Updated controller class that handles partial updates class ApiTicketController { private $ticketModel; private $commentModel; private $auditLog; private $workflowModel; private $userId; private $isAdmin; private $currentUser; public function __construct($conn, $userId = null, $isAdmin = false, $currentUser = []) { $this->ticketModel = new TicketModel($conn); $this->commentModel = new CommentModel($conn); $this->auditLog = new AuditLogModel($conn); $this->workflowModel = new WorkflowModel($conn); $this->userId = $userId; $this->isAdmin = $isAdmin; $this->currentUser = $currentUser; } public function update($id, $data) { // First, get the current ticket data to fill in missing fields $currentTicket = $this->ticketModel->getTicketById($id); if (!$currentTicket) { return [ 'success' => false, 'error' => 'Ticket not found' ]; } // Visibility check: return 404 for tickets the user cannot access if (!$this->ticketModel->canUserAccessTicket($currentTicket, $this->currentUser)) { return [ 'success' => false, 'error' => 'Ticket not found', 'http_status' => 404 ]; } // Authorization: admins can edit any ticket; others only their own or assigned if (!$this->isAdmin && $currentTicket['created_by'] != $this->userId && $currentTicket['assigned_to'] != $this->userId ) { return [ 'success' => false, 'error' => 'Permission denied' ]; } // Merge current data with updates, keeping existing values for missing fields $updateData = [ 'ticket_id' => $id, 'title' => $data['title'] ?? $currentTicket['title'], 'description' => $data['description'] ?? $currentTicket['description'], 'category' => $data['category'] ?? $currentTicket['category'], 'type' => $data['type'] ?? $currentTicket['type'], 'status' => $data['status'] ?? $currentTicket['status'], 'priority' => isset($data['priority']) ? (int)$data['priority'] : (int)$currentTicket['priority'] ]; // Validate required fields if (empty($updateData['title'])) { return [ 'success' => false, 'error' => 'Title cannot be empty' ]; } // Validate priority range if ($updateData['priority'] < 1 || $updateData['priority'] > 5) { return [ 'success' => false, 'error' => 'Priority must be between 1 and 5' ]; } // Validate status transition using workflow model if ($currentTicket['status'] !== $updateData['status']) { $allowed = $this->workflowModel->isTransitionAllowed( $currentTicket['status'], $updateData['status'], $this->isAdmin ); if (!$allowed) { return [ 'success' => false, 'error' => 'Status transition not allowed: ' . $currentTicket['status'] . ' → ' . $updateData['status'] ]; } } // Update ticket with user tracking and optional optimistic locking $expectedUpdatedAt = $data['expected_updated_at'] ?? null; $result = $this->ticketModel->updateTicket($updateData, $this->userId, $expectedUpdatedAt); // Handle conflict case if (!$result['success']) { $response = [ 'success' => false, 'error' => $result['error'] ?? 'Failed to update ticket in database' ]; if (!empty($result['conflict'])) { $response['conflict'] = true; $response['current_updated_at'] = $result['current_updated_at'] ?? null; } return $response; } // Handle visibility update if provided if (isset($data['visibility'])) { $visibilityGroups = $data['visibility_groups'] ?? null; // Convert array to comma-separated string if needed if (is_array($visibilityGroups)) { $visibilityGroups = implode(',', array_map('trim', $visibilityGroups)); } // Validate internal visibility requires groups if ($data['visibility'] === 'internal' && (empty($visibilityGroups) || trim($visibilityGroups) === '')) { return [ 'success' => false, 'error' => 'Internal visibility requires at least one group to be specified' ]; } $this->ticketModel->updateVisibility($id, $data['visibility'], $visibilityGroups, $this->userId); } // Log ticket update to audit log if ($this->userId) { $this->auditLog->logTicketUpdate($this->userId, $id, $data); } return [ 'success' => true, 'status' => $updateData['status'], 'priority' => $updateData['priority'], 'message' => 'Ticket updated successfully' ]; } } // Use centralized database connection $conn = Database::getConnection(); // Check request method if ($_SERVER['REQUEST_METHOD'] !== 'POST') { throw new Exception("Method not allowed. Expected POST, got " . $_SERVER['REQUEST_METHOD']); } // Get POST data $input = file_get_contents('php://input'); $data = json_decode($input, true); if (!$data) { throw new Exception("Invalid JSON data received: " . $input); } if (!isset($data['ticket_id'])) { throw new Exception("Missing ticket_id parameter"); } $ticketId = (int)$data['ticket_id']; // Initialize controller $controller = new ApiTicketController($conn, $userId, $isAdmin, $currentUser); // Update ticket $result = $controller->update($ticketId, $data); // Discard any output that might have been generated ob_end_clean(); // Return response if (!empty($result['http_status'])) { http_response_code($result['http_status']); unset($result['http_status']); } header('Content-Type: application/json'); echo json_encode($result); } catch (Exception $e) { // Discard any output that might have been generated ob_end_clean(); // Log error details but don't expose to client error_log("Update ticket API error: " . $e->getMessage()); // Return error response header('Content-Type: application/json'); http_response_code(500); echo json_encode([ 'success' => false, 'error' => 'An internal error occurred' ]); } ?>