value), link_duplicate_of * * @return array ['success' => true, 'ticket_id' => ...] or ['success' => false, 'error' => ...] */ public static function create(mysqli $conn, array $currentUser, array $input): array { $userId = $currentUser['user_id'] ?? null; // Handle visibility groups (a list from the web form's checkboxes, or a string) $visibilityGroups = null; if (isset($input['visibility_groups']) && is_array($input['visibility_groups'])) { $visibilityGroups = implode(',', array_map('trim', $input['visibility_groups'])); } elseif (isset($input['visibility_groups']) && is_string($input['visibility_groups']) && trim($input['visibility_groups']) !== '') { $visibilityGroups = trim($input['visibility_groups']); } // Honor the posted status, validated against the app's canonical list $validStatuses = $GLOBALS['config']['TICKET_STATUSES'] ?? ['Open', 'Pending', 'In Progress', 'Closed']; $status = $input['status'] ?? 'Open'; if (!in_array($status, $validStatuses, true)) { $status = 'Open'; } $ticketData = [ 'title' => trim($input['title'] ?? ''), 'description' => $input['description'] ?? '', 'priority' => $input['priority'] ?? '4', 'category' => $input['category'] ?? 'General', 'type' => $input['type'] ?? 'Issue', 'status' => $status, 'visibility' => $input['visibility'] ?? 'public', 'visibility_groups' => $visibilityGroups, 'assigned_to' => !empty($input['assigned_to']) ? $input['assigned_to'] : null ]; if ($ticketData['title'] === '') { return ['success' => false, 'error' => 'Title is required']; } if (trim($ticketData['description']) === '') { return ['success' => false, 'error' => 'Description is required']; } // Custom fields applicable to the submitted category — validate // is_required server-side (the form is novalidate, and a field // hidden by the client-side category toggle must not silently // bypass a requirement that applies to the category actually // submitted). $customFieldModel = new CustomFieldModel($conn); $allCustomFieldDefs = $customFieldModel->getAllDefinitions(null, true); $submittedCustomFields = is_array($input['custom_fields'] ?? null) ? $input['custom_fields'] : []; $applicableFieldDefs = array_filter( $allCustomFieldDefs, fn($def) => $def['category'] === null || $def['category'] === $ticketData['category'] ); $customFieldsToSave = []; foreach ($applicableFieldDefs as $def) { $fieldId = (int)$def['field_id']; $raw = $submittedCustomFields[$fieldId] ?? null; $normalized = $def['field_type'] === 'checkbox' ? (!empty($raw) ? '1' : '0') : (is_scalar($raw) ? trim((string)$raw) : ''); if (!empty($def['is_required']) && $def['field_type'] !== 'checkbox' && $normalized === '') { return ['success' => false, 'error' => $def['field_label'] . ' is required']; } if ($normalized !== '') { $customFieldsToSave[$fieldId] = $normalized; } } // Create ticket with user tracking $result = (new TicketModel($conn))->createTicket($ticketData, $userId); if (!$result['success']) { return ['success' => false, 'error' => $result['error']]; } // Log ticket creation to audit log if ($userId) { (new AuditLogModel($conn))->logTicketCreate($userId, $result['ticket_id'], $ticketData); } // Ticket counts changed — invalidate the cached dashboard stats (new StatsModel($conn))->invalidateCache(); // Persist custom field values for the fields applicable to this ticket's category if (!empty($customFieldsToSave)) { $customFieldModel->setValues($result['ticket_id'], $customFieldsToSave); } // Auto-link as duplicate if requested $linkDupOfRaw = trim((string)($input['link_duplicate_of'] ?? '')); if ($linkDupOfRaw !== '' && ctype_digit($linkDupOfRaw)) { $depSql = "INSERT IGNORE INTO ticket_dependencies (ticket_id, depends_on_id, dependency_type, created_by) VALUES (?, ?, 'duplicates', ?)"; $depStmt = $conn->prepare($depSql); $depStmt->bind_param("ssi", $result['ticket_id'], $linkDupOfRaw, $userId); $depStmt->execute(); $depStmt->close(); } // Send Matrix notification for new ticket NotificationHelper::sendTicketNotification($result['ticket_id'], $ticketData, 'manual'); return ['success' => true, 'ticket_id' => $result['ticket_id']]; } }