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>
231 lines
6.8 KiB
PHP
231 lines
6.8 KiB
PHP
<?php
|
|
/**
|
|
* CustomFieldModel - Manages custom field definitions and values
|
|
*/
|
|
|
|
class CustomFieldModel {
|
|
private $conn;
|
|
|
|
public function __construct($conn) {
|
|
$this->conn = $conn;
|
|
}
|
|
|
|
// ========================================
|
|
// Field Definitions
|
|
// ========================================
|
|
|
|
/**
|
|
* Get all field definitions
|
|
*/
|
|
public function getAllDefinitions($category = null, $activeOnly = true) {
|
|
$sql = "SELECT * FROM custom_field_definitions WHERE 1=1";
|
|
$params = [];
|
|
$types = '';
|
|
|
|
if ($activeOnly) {
|
|
$sql .= " AND is_active = 1";
|
|
}
|
|
|
|
if ($category !== null) {
|
|
$sql .= " AND (category = ? OR category IS NULL)";
|
|
$params[] = $category;
|
|
$types .= 's';
|
|
}
|
|
|
|
$sql .= " ORDER BY display_order ASC, field_id ASC";
|
|
|
|
if (!empty($params)) {
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param($types, ...$params);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
} else {
|
|
$result = $this->conn->query($sql);
|
|
}
|
|
|
|
$fields = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
if ($row['field_options']) {
|
|
$row['field_options'] = json_decode($row['field_options'], true);
|
|
}
|
|
$fields[] = $row;
|
|
}
|
|
|
|
if (isset($stmt)) {
|
|
$stmt->close();
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* Get a single field definition
|
|
*/
|
|
public function getDefinition($fieldId) {
|
|
$sql = "SELECT * FROM custom_field_definitions WHERE field_id = ?";
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('i', $fieldId);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
$row = $result->fetch_assoc();
|
|
$stmt->close();
|
|
|
|
if ($row && $row['field_options']) {
|
|
$row['field_options'] = json_decode($row['field_options'], true);
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* Create a new field definition
|
|
*/
|
|
public function createDefinition($data) {
|
|
$options = null;
|
|
if (isset($data['field_options']) && !empty($data['field_options'])) {
|
|
$options = json_encode($data['field_options']);
|
|
}
|
|
|
|
$sql = "INSERT INTO custom_field_definitions
|
|
(field_name, field_label, field_type, field_options, category, is_required, display_order, is_active)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('sssssiii',
|
|
$data['field_name'],
|
|
$data['field_label'],
|
|
$data['field_type'],
|
|
$options,
|
|
$data['category'],
|
|
$data['is_required'] ?? 0,
|
|
$data['display_order'] ?? 0,
|
|
$data['is_active'] ?? 1
|
|
);
|
|
|
|
if ($stmt->execute()) {
|
|
$id = $this->conn->insert_id;
|
|
$stmt->close();
|
|
return ['success' => true, 'field_id' => $id];
|
|
}
|
|
|
|
$error = $stmt->error;
|
|
$stmt->close();
|
|
return ['success' => false, 'error' => $error];
|
|
}
|
|
|
|
/**
|
|
* Update a field definition
|
|
*/
|
|
public function updateDefinition($fieldId, $data) {
|
|
$options = null;
|
|
if (isset($data['field_options']) && !empty($data['field_options'])) {
|
|
$options = json_encode($data['field_options']);
|
|
}
|
|
|
|
$sql = "UPDATE custom_field_definitions SET
|
|
field_name = ?, field_label = ?, field_type = ?, field_options = ?,
|
|
category = ?, is_required = ?, display_order = ?, is_active = ?
|
|
WHERE field_id = ?";
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('sssssiiiii',
|
|
$data['field_name'],
|
|
$data['field_label'],
|
|
$data['field_type'],
|
|
$options,
|
|
$data['category'],
|
|
$data['is_required'] ?? 0,
|
|
$data['display_order'] ?? 0,
|
|
$data['is_active'] ?? 1,
|
|
$fieldId
|
|
);
|
|
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
return ['success' => $success];
|
|
}
|
|
|
|
/**
|
|
* Delete a field definition
|
|
*/
|
|
public function deleteDefinition($fieldId) {
|
|
// This will cascade delete all values due to FK constraint
|
|
$sql = "DELETE FROM custom_field_definitions WHERE field_id = ?";
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('i', $fieldId);
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
return ['success' => $success];
|
|
}
|
|
|
|
// ========================================
|
|
// Field Values
|
|
// ========================================
|
|
|
|
/**
|
|
* Get all field values for a ticket
|
|
*/
|
|
public function getValuesForTicket($ticketId) {
|
|
$sql = "SELECT cfv.*, cfd.field_name, cfd.field_label, cfd.field_type, cfd.field_options
|
|
FROM custom_field_values cfv
|
|
JOIN custom_field_definitions cfd ON cfv.field_id = cfd.field_id
|
|
WHERE cfv.ticket_id = ?
|
|
ORDER BY cfd.display_order ASC";
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('s', $ticketId);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
|
|
$values = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
if ($row['field_options']) {
|
|
$row['field_options'] = json_decode($row['field_options'], true);
|
|
}
|
|
$values[$row['field_name']] = $row;
|
|
}
|
|
|
|
$stmt->close();
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Set a field value for a ticket (insert or update)
|
|
*/
|
|
public function setValue($ticketId, $fieldId, $value) {
|
|
$sql = "INSERT INTO custom_field_values (ticket_id, field_id, field_value)
|
|
VALUES (?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE field_value = VALUES(field_value), updated_at = CURRENT_TIMESTAMP";
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('sis', $ticketId, $fieldId, $value);
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
return ['success' => $success];
|
|
}
|
|
|
|
/**
|
|
* Set multiple field values for a ticket
|
|
*/
|
|
public function setValues($ticketId, $values) {
|
|
$results = [];
|
|
foreach ($values as $fieldId => $value) {
|
|
$results[$fieldId] = $this->setValue($ticketId, $fieldId, $value);
|
|
}
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Delete all field values for a ticket
|
|
*/
|
|
public function deleteValuesForTicket($ticketId) {
|
|
$sql = "DELETE FROM custom_field_values WHERE ticket_id = ?";
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('s', $ticketId);
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
return ['success' => $success];
|
|
}
|
|
}
|
|
?>
|