From f9629f60b607a5e7488fe8b5beb2e16b151d2104 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 1 Jan 2026 18:25:19 -0500 Subject: [PATCH] Add Activity Timeline feature and database migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Activity Timeline tab to ticket view showing chronological history - Create getTicketTimeline() method in AuditLogModel - Update TicketController to load timeline data - Add timeline UI with helper functions for formatting events - Add comprehensive timeline CSS with dark mode support - Create migrations 007-010 for upcoming features: - 007: Ticket assignment functionality - 008: Status workflow transitions - 009: Ticket templates - 010: Bulk operations tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- assets/css/ticket.css | 74 ++++++++++++++++++++++++ controllers/TicketController.php | 6 ++ migrations/007_add_ticket_assignment.sql | 13 +++++ migrations/008_add_status_workflows.sql | 31 ++++++++++ migrations/009_add_ticket_templates.sql | 24 ++++++++ migrations/010_add_bulk_operations.sql | 19 ++++++ models/AuditLogModel.php | 32 ++++++++++ views/TicketView.php | 66 ++++++++++++++++++++- 8 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 migrations/007_add_ticket_assignment.sql create mode 100644 migrations/008_add_status_workflows.sql create mode 100644 migrations/009_add_ticket_templates.sql create mode 100644 migrations/010_add_bulk_operations.sql diff --git a/assets/css/ticket.css b/assets/css/ticket.css index bf3e9c2..f190d6a 100644 --- a/assets/css/ticket.css +++ b/assets/css/ticket.css @@ -441,4 +441,78 @@ input:checked + .slider:before { .back-btn:hover { background: var(--border-color); +} + +/* Activity Timeline Styles */ +.timeline-container { + padding: 1rem; + max-width: 800px; +} + +.timeline-event { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + position: relative; +} + +.timeline-event:not(:last-child)::before { + content: ''; + position: absolute; + left: 12px; + top: 30px; + bottom: -24px; + width: 2px; + background: var(--border-color, #ddd); +} + +.timeline-icon { + font-size: 1.5rem; + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.timeline-content { + flex: 1; + background: var(--card-bg, #f8f9fa); + padding: 0.75rem 1rem; + border-radius: 8px; + border: 1px solid var(--border-color, #ddd); +} + +.timeline-header { + display: flex; + gap: 0.5rem; + align-items: center; + margin-bottom: 0.5rem; + flex-wrap: wrap; +} + +.timeline-action { + color: var(--text-muted, #666); + font-size: 0.9rem; +} + +.timeline-date { + margin-left: auto; + color: var(--text-muted, #999); + font-size: 0.85rem; +} + +.timeline-details { + font-size: 0.9rem; + color: var(--text-secondary, #555); + padding-top: 0.5rem; + border-top: 1px solid var(--border-color, #eee); +} + +body.dark-mode .timeline-content { + --card-bg: #2d3748; + --border-color: #444; + --text-muted: #a0aec0; + --text-secondary: #cbd5e0; } \ No newline at end of file diff --git a/controllers/TicketController.php b/controllers/TicketController.php index d3a0f47..33b35e9 100644 --- a/controllers/TicketController.php +++ b/controllers/TicketController.php @@ -2,15 +2,18 @@ // Use absolute paths for model includes require_once dirname(__DIR__) . '/models/TicketModel.php'; require_once dirname(__DIR__) . '/models/CommentModel.php'; +require_once dirname(__DIR__) . '/models/AuditLogModel.php'; class TicketController { private $ticketModel; private $commentModel; + private $auditLogModel; private $envVars; public function __construct($conn) { $this->ticketModel = new TicketModel($conn); $this->commentModel = new CommentModel($conn); + $this->auditLogModel = new AuditLogModel($conn); // Load environment variables for Discord webhook $envPath = dirname(__DIR__) . '/.env'; @@ -55,6 +58,9 @@ class TicketController { // Get comments for this ticket using CommentModel $comments = $this->commentModel->getCommentsByTicketId($id); + // Get timeline for this ticket + $timeline = $this->auditLogModel->getTicketTimeline($id); + // Load the view include dirname(__DIR__) . '/views/TicketView.php'; } diff --git a/migrations/007_add_ticket_assignment.sql b/migrations/007_add_ticket_assignment.sql new file mode 100644 index 0000000..7f7fa90 --- /dev/null +++ b/migrations/007_add_ticket_assignment.sql @@ -0,0 +1,13 @@ +-- Migration 007: Add ticket assignment functionality +-- Adds assigned_to column to tickets table + +-- Add assigned_to column to tickets table +ALTER TABLE tickets + ADD COLUMN assigned_to INT NULL, + ADD CONSTRAINT fk_tickets_assigned_to + FOREIGN KEY (assigned_to) + REFERENCES users(user_id) + ON DELETE SET NULL; + +-- Add index for performance +CREATE INDEX idx_assigned_to ON tickets(assigned_to); diff --git a/migrations/008_add_status_workflows.sql b/migrations/008_add_status_workflows.sql new file mode 100644 index 0000000..29524bb --- /dev/null +++ b/migrations/008_add_status_workflows.sql @@ -0,0 +1,31 @@ +-- Migration 008: Add status workflow management +-- Creates status_transitions table for workflow validation + +-- Table to define allowed status transitions +CREATE TABLE status_transitions ( + transition_id INT AUTO_INCREMENT PRIMARY KEY, + from_status VARCHAR(50) NOT NULL, + to_status VARCHAR(50) NOT NULL, + requires_comment BOOLEAN DEFAULT FALSE, + requires_admin BOOLEAN DEFAULT FALSE, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY unique_transition (from_status, to_status), + INDEX idx_from_status (from_status) +); + +-- Insert default transitions +INSERT INTO status_transitions (from_status, to_status, requires_comment) VALUES + ('Open', 'In Progress', FALSE), + ('Open', 'Closed', TRUE), + ('In Progress', 'Open', FALSE), + ('In Progress', 'Closed', TRUE), + ('Closed', 'Open', TRUE), + ('Closed', 'In Progress', FALSE); + +-- Add new status "Resolved" +INSERT INTO status_transitions (from_status, to_status, requires_comment) VALUES + ('In Progress', 'Resolved', FALSE), + ('Resolved', 'Closed', FALSE), + ('Resolved', 'In Progress', TRUE), + ('Open', 'Resolved', FALSE); diff --git a/migrations/009_add_ticket_templates.sql b/migrations/009_add_ticket_templates.sql new file mode 100644 index 0000000..100fa87 --- /dev/null +++ b/migrations/009_add_ticket_templates.sql @@ -0,0 +1,24 @@ +-- Migration 009: Add ticket templates +-- Creates ticket_templates table for reusable ticket templates + +CREATE TABLE ticket_templates ( + template_id INT AUTO_INCREMENT PRIMARY KEY, + template_name VARCHAR(100) NOT NULL, + title_template VARCHAR(255) NOT NULL, + description_template TEXT NOT NULL, + category VARCHAR(50), + type VARCHAR(50), + default_priority INT DEFAULT 4, + created_by INT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (created_by) REFERENCES users(user_id), + INDEX idx_template_name (template_name) +); + +-- Insert default templates +INSERT INTO ticket_templates (template_name, title_template, description_template, category, type, default_priority) VALUES +('Hardware Failure', 'Hardware Failure: [Device Name]', 'Device: \nIssue: \nError Messages: \nTroubleshooting Done: ', 'Hardware', 'Problem', 2), +('Software Installation', 'Install [Software Name]', 'Software: \nVersion: \nLicense Key: \nInstallation Path: ', 'Software', 'Install', 3), +('Network Issue', 'Network Issue: [Brief Description]', 'Affected System: \nSymptoms: \nIP Address: \nConnectivity Tests: ', 'Hardware', 'Problem', 2), +('Maintenance Request', 'Scheduled Maintenance: [System Name]', 'System: \nMaintenance Type: \nScheduled Date: \nDowntime Expected: ', 'Hardware', 'Maintenance', 4); diff --git a/migrations/010_add_bulk_operations.sql b/migrations/010_add_bulk_operations.sql new file mode 100644 index 0000000..a307ea1 --- /dev/null +++ b/migrations/010_add_bulk_operations.sql @@ -0,0 +1,19 @@ +-- Migration 010: Add bulk operations tracking +-- Creates bulk_operations table for admin bulk actions + +CREATE TABLE bulk_operations ( + operation_id INT AUTO_INCREMENT PRIMARY KEY, + operation_type VARCHAR(50) NOT NULL, + ticket_ids TEXT NOT NULL, -- Comma-separated + performed_by INT NOT NULL, + parameters JSON, + status VARCHAR(20) DEFAULT 'pending', + total_tickets INT, + processed_tickets INT DEFAULT 0, + failed_tickets INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP NULL, + FOREIGN KEY (performed_by) REFERENCES users(user_id), + INDEX idx_performed_by (performed_by), + INDEX idx_created_at (created_at) +); diff --git a/models/AuditLogModel.php b/models/AuditLogModel.php index 957b8bf..ec7b287 100644 --- a/models/AuditLogModel.php +++ b/models/AuditLogModel.php @@ -289,4 +289,36 @@ class AuditLogModel { public function logTicketView($userId, $ticketId) { return $this->log($userId, 'view', 'ticket', $ticketId); } + + /** + * Get formatted timeline for a specific ticket + * Includes all ticket updates and comments + * + * @param string $ticketId Ticket ID + * @return array Timeline events + */ + public function getTicketTimeline($ticketId) { + $stmt = $this->conn->prepare( + "SELECT al.*, u.username, u.display_name + FROM audit_log al + LEFT JOIN users u ON al.user_id = u.user_id + WHERE (al.entity_type = 'ticket' AND al.entity_id = ?) + OR (al.entity_type = 'comment' AND JSON_EXTRACT(al.details, '$.ticket_id') = ?) + ORDER BY al.created_at DESC" + ); + $stmt->bind_param("ss", $ticketId, $ticketId); + $stmt->execute(); + $result = $stmt->get_result(); + + $timeline = []; + while ($row = $result->fetch_assoc()) { + if ($row['details']) { + $row['details'] = json_decode($row['details'], true); + } + $timeline[] = $row; + } + + $stmt->close(); + return $timeline; + } } diff --git a/views/TicketView.php b/views/TicketView.php index b7f40d7..d79e84f 100644 --- a/views/TicketView.php +++ b/views/TicketView.php @@ -1,6 +1,43 @@ '✨', + 'update' => '📝', + 'comment' => '💬', + 'view' => '👁️', + 'assign' => '👤', + 'status_change' => '🔄' + ]; + return $icons[$actionType] ?? '•'; +} + +function formatAction($event) { + $actions = [ + 'create' => 'created this ticket', + 'update' => 'updated this ticket', + 'comment' => 'added a comment', + 'view' => 'viewed this ticket', + 'assign' => 'assigned this ticket', + 'status_change' => 'changed the status' + ]; + return $actions[$event['action_type']] ?? $event['action_type']; +} + +function formatDetails($details, $actionType) { + if ($actionType === 'update' && is_array($details)) { + $changes = []; + foreach ($details as $field => $value) { + if ($field === 'old_value' || $field === 'new_value') continue; + $changes[] = "" . htmlspecialchars($field) . ": " . htmlspecialchars($value); + } + return implode(', ', $changes); + } + return ''; +} ?> @@ -144,6 +181,7 @@
+
@@ -204,6 +242,32 @@ ?>
+ +
+
+ +

No activity recorded yet.

+ + +
+
+
+
+ + + +
+ +
+ +
+ +
+
+ + +
+