From e05434137c63ba77ca80c16b78ddff21b0acb8cd Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 5 Sep 2025 11:08:56 -0400 Subject: [PATCH] Fixed MAJOR bugs, currently at a semi-stable state --- api/update_ticket.php | 194 ++++- assets/css/dashboard.css | 643 +++++++-------- assets/css/ticket.css | 191 ++--- assets/js/dashboard.js | 1176 ++++++++++++++++----------- assets/js/ticket.js | 42 +- config/config.php | 5 +- controllers/DashboardController.php | 50 +- controllers/TicketController.php | 145 ++-- create_ticket_api.php | 2 +- index.php | 62 +- models/CommentModel.php | 13 +- models/TicketModel.php | 54 +- views/DashboardView.php | 66 +- views/TicketView.php | 22 +- 14 files changed, 1559 insertions(+), 1106 deletions(-) diff --git a/api/update_ticket.php b/api/update_ticket.php index e385a85..77c691d 100644 --- a/api/update_ticket.php +++ b/api/update_ticket.php @@ -20,6 +20,20 @@ try { require_once $configPath; debug_log("Config loaded successfully"); + // Load environment variables (for Discord webhook) + $envPath = dirname(__DIR__) . '/.env'; + $envVars = []; + if (file_exists($envPath)) { + $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + if (strpos($line, '=') !== false && strpos($line, '#') !== 0) { + list($key, $value) = explode('=', $line, 2); + $envVars[trim($key)] = trim($value); + } + } + debug_log("Environment variables loaded"); + } + // Load models directly with absolute paths $ticketModelPath = dirname(__DIR__) . '/models/TicketModel.php'; $commentModelPath = dirname(__DIR__) . '/models/CommentModel.php'; @@ -29,49 +43,180 @@ try { require_once $commentModelPath; debug_log("Models loaded successfully"); - // Now load the controller with a modified approach - $controllerPath = dirname(__DIR__) . '/controllers/TicketController.php'; - debug_log("Loading controller from: $controllerPath"); - - // Instead of directly including the controller file, we'll define a new controller class - // that extends the functionality we need without the problematic require_once statements - + // Updated controller class that handles partial updates class ApiTicketController { private $ticketModel; private $commentModel; + private $envVars; - public function __construct($conn) { + public function __construct($conn, $envVars = []) { $this->ticketModel = new TicketModel($conn); $this->commentModel = new CommentModel($conn); + $this->envVars = $envVars; } public function update($id, $data) { - // Add ticket_id to the data - $data['ticket_id'] = $id; + debug_log("ApiTicketController->update called with ID: $id and data: " . json_encode($data)); - // Validate input data - if (empty($data['title'])) { + // 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' + ]; + } + + debug_log("Current ticket data: " . json_encode($currentTicket)); + + // 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'] + ]; + + debug_log("Merged update data: " . json_encode($updateData)); + + // 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 + $validStatuses = ['Open', 'Closed', 'In Progress', 'Pending']; + if (!in_array($updateData['status'], $validStatuses)) { + return [ + 'success' => false, + 'error' => 'Invalid status value' + ]; + } + + debug_log("Validation passed, calling ticketModel->updateTicket"); + // Update ticket - $result = $this->ticketModel->updateTicket($data); + $result = $this->ticketModel->updateTicket($updateData); + + debug_log("TicketModel->updateTicket returned: " . ($result ? 'true' : 'false')); if ($result) { + // Send Discord webhook notification + $this->sendDiscordWebhook($id, $currentTicket, $updateData, $data); + return [ 'success' => true, - 'status' => $data['status'] + 'status' => $updateData['status'], + 'priority' => $updateData['priority'], + 'message' => 'Ticket updated successfully' ]; } else { return [ 'success' => false, - 'error' => 'Failed to update ticket' + 'error' => 'Failed to update ticket in database' ]; } } + + private function sendDiscordWebhook($ticketId, $oldData, $newData, $changedFields) { + if (!isset($this->envVars['DISCORD_WEBHOOK_URL']) || empty($this->envVars['DISCORD_WEBHOOK_URL'])) { + debug_log("Discord webhook URL not configured, skipping webhook"); + return; + } + + $webhookUrl = $this->envVars['DISCORD_WEBHOOK_URL']; + debug_log("Sending Discord webhook to: $webhookUrl"); + + // Determine what fields actually changed + $changes = []; + foreach ($changedFields as $field => $newValue) { + if ($field === 'ticket_id') continue; // Skip ticket_id + + $oldValue = $oldData[$field] ?? 'N/A'; + if ($oldValue != $newValue) { + $changes[] = [ + 'name' => ucfirst($field), + 'value' => "$oldValue โ†’ $newValue", + 'inline' => true + ]; + } + } + + if (empty($changes)) { + debug_log("No actual changes detected, skipping webhook"); + return; + } + + // Create ticket URL + $ticketUrl = "http://t.lotusguild.org/ticket/$ticketId"; + + // Determine embed color based on priority + $colors = [ + 1 => 0xff4d4d, // Red + 2 => 0xffa726, // Orange + 3 => 0x42a5f5, // Blue + 4 => 0x66bb6a, // Green + 5 => 0x9e9e9e // Gray + ]; + $color = $colors[$newData['priority']] ?? 0x3498db; + + $embed = [ + 'title' => '๐Ÿ”„ Ticket Updated', + 'description' => "**#{$ticketId}** - " . $newData['title'], + 'color' => $color, + 'fields' => array_merge($changes, [ + [ + 'name' => '๐Ÿ”— View Ticket', + 'value' => "[Click here to view]($ticketUrl)", + 'inline' => false + ] + ]), + 'footer' => [ + 'text' => 'Tinker Tickets' + ], + 'timestamp' => date('c') + ]; + + $payload = [ + 'embeds' => [$embed] + ]; + + debug_log("Discord payload: " . json_encode($payload)); + + // Send webhook + $ch = curl_init($webhookUrl); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + $webhookResult = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($curlError) { + debug_log("Discord webhook cURL error: $curlError"); + } else { + debug_log("Discord webhook sent. HTTP Code: $httpCode, Response: $webhookResult"); + } + } } debug_log("Controller defined successfully"); @@ -90,10 +235,16 @@ try { } debug_log("Database connection successful"); + // 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); - debug_log("Received data: " . json_encode($data)); + debug_log("Received raw input: " . $input); + debug_log("Decoded data: " . json_encode($data)); if (!$data) { throw new Exception("Invalid JSON data received: " . $input); @@ -103,12 +254,12 @@ try { throw new Exception("Missing ticket_id parameter"); } - $ticketId = $data['ticket_id']; + $ticketId = (int)$data['ticket_id']; debug_log("Processing ticket ID: $ticketId"); // Initialize controller debug_log("Initializing controller"); - $controller = new ApiTicketController($conn); + $controller = new ApiTicketController($conn, $envVars); debug_log("Controller initialized"); // Update ticket @@ -116,13 +267,16 @@ try { $result = $controller->update($ticketId, $data); debug_log("Update completed with result: " . json_encode($result)); + // Close database connection + $conn->close(); + // Discard any output that might have been generated ob_end_clean(); // Return response header('Content-Type: application/json'); echo json_encode($result); - debug_log("Response sent"); + debug_log("Response sent successfully"); } catch (Exception $e) { debug_log("Error: " . $e->getMessage()); @@ -133,9 +287,11 @@ try { // Return error response header('Content-Type: application/json'); + http_response_code(500); echo json_encode([ 'success' => false, 'error' => $e->getMessage() ]); debug_log("Error response sent"); } +?> \ No newline at end of file diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 1bc0f6f..5cf3b3f 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -1,4 +1,4 @@ -/* Variables */ +/* ===== CSS VARIABLES ===== */ :root { --bg-primary: #f5f7fa; --bg-secondary: #ffffff; @@ -11,6 +11,12 @@ --priority-2: #ffa726; --priority-3: #42a5f5; --priority-4: #66bb6a; + --priority-5: #9e9e9e; + + /* Status Colors */ + --status-open: #28a745; + --status-in-progress: #ffc107; + --status-closed: #dc3545; /* Spacing */ --spacing-xs: 0.5rem; @@ -31,13 +37,9 @@ --border-color: #4a5568; --shadow: 0 2px 4px rgba(0,0,0,0.3); --hover-bg: #374151; - --priority-1: #7f1d1d; - --priority-2: #854d0e; - --priority-3: #075985; - --priority-4: #166534; } -/* Base Elements */ +/* ===== BASE ELEMENTS ===== */ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; @@ -47,14 +49,47 @@ body { transition: var(--transition-default); } -/* Reusable Components */ -.card-base { - background: var(--bg-secondary); - border-radius: 12px; - box-shadow: var(--shadow); - padding: var(--spacing-md); +body.menu-open { + padding-left: 260px; } +h1 { + color: var(--text-primary); + margin: 0; +} + +/* ===== LAYOUT COMPONENTS ===== */ +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-md); + margin-left: 3.75rem; +} + +.table-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding: 10px; + background: var(--bg-secondary); + border-radius: 8px; + box-shadow: var(--shadow); +} + +.ticket-count { + font-weight: 500; + color: var(--text-secondary); +} + +.table-actions { + display: flex; + gap: 15px; + align-items: center; +} + +/* ===== BUTTON STYLES ===== */ .btn-base { padding: var(--spacing-sm) var(--spacing-md); border-radius: 6px; @@ -68,81 +103,140 @@ body { color: white; } -/* Layout Components */ -.dashboard-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--spacing-md); - margin-left: 3.75rem; +.create-ticket { + background: #3b82f6; + color: white; + padding: 0.625rem 1.25rem; + border-radius: 0.375rem; + border: none; + cursor: pointer; + font-weight: 500; + transition: background-color 0.3s ease; + margin-right: 3.75rem; } -.ticket-container { - max-width: 800px; - margin: var(--spacing-lg) auto; - border-left: 6px solid; - transition: var(--transition-default); +.create-ticket:hover { + background: #2563eb; } -.flex-row { - display: flex; - gap: var(--spacing-sm); -} - -.flex-between { - justify-content: space-between; - align-items: center; -} - -/* Table Styles */ -.table-base { +/* ===== TABLE STYLES ===== */ +table { width: 100%; border-collapse: separate; border-spacing: 0; + background: var(--bg-secondary); + border-radius: 12px; + box-shadow: var(--shadow); overflow: hidden; } -.table-cell { - padding: var(--spacing-md); +th, td { + padding: 16px; text-align: left; border-bottom: 1px solid var(--border-color); + color: var(--text-primary); } -/* Priority Styles */ -.priority-indicator { +th { + background-color: var(--bg-secondary); + font-weight: 600; + text-transform: uppercase; + font-size: 0.9em; + letter-spacing: 0.05em; + position: relative; + cursor: pointer; +} + +tr:hover { + background-color: var(--hover-bg); +} + +tbody tr td:first-child { + border-left: 6px solid; +} + +/* Priority row colors */ +tbody tr.priority-1 td:first-child { border-left-color: var(--priority-1); } +tbody tr.priority-2 td:first-child { border-left-color: var(--priority-2); } +tbody tr.priority-3 td:first-child { border-left-color: var(--priority-3); } +tbody tr.priority-4 td:first-child { border-left-color: var(--priority-4); } +tbody tr.priority-5 td:first-child { border-left-color: var(--priority-5); } + +/* Priority number styling */ +td:nth-child(2) { + text-align: center; +} + +td:nth-child(2) span { font-weight: bold; font-family: 'Courier New', monospace; - padding: var(--spacing-xs) var(--spacing-sm); + padding: 4px 8px; border-radius: 4px; display: inline-block; + background: var(--hover-bg); } -.priority-1 { color: var(--priority-1); } -.priority-2 { color: var(--priority-2); } -.priority-3 { color: var(--priority-3); } -.priority-4 { color: var(--priority-4); } - -/* Status Styles */ -.status-base { - font-weight: bold; - padding: var(--spacing-xs) var(--spacing-sm); - border-radius: 4px; -} +.priority-1 td:nth-child(2) { color: var(--priority-1); } +.priority-2 td:nth-child(2) { color: var(--priority-2); } +.priority-3 td:nth-child(2) { color: var(--priority-3); } +.priority-4 td:nth-child(2) { color: var(--priority-4); } +.priority-5 td:nth-child(2) { color: var(--priority-5); } +/* ===== STATUS STYLES ===== */ .status-Open { - color: #10b981; - background: rgba(16, 185, 129, 0.1); + background-color: var(--status-open) !important; + color: white !important; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.875rem; + font-weight: 500; +} + +.status-In-Progress { + background-color: var(--status-in-progress) !important; + color: #212529 !important; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.875rem; + font-weight: 500; } .status-Closed { - color: #ef4444; - background: rgba(239, 68, 68, 0.1); + background-color: var(--status-closed) !important; + color: white !important; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.875rem; + font-weight: 500; +} + +/* ===== SEARCH AND FILTER STYLES ===== */ +.search-box { + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 0.375rem; + background: var(--bg-secondary); + color: var(--text-primary); + margin-left: 1.25rem; + width: 40%; +} + +.search-box:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); +} + +/* Status Filter Dropdown */ +.status-filter-container { + position: relative; + display: inline-block; + margin-right: 15px; } .status-dropdown { position: relative; display: inline-block; - margin-right: 15px; } .dropdown-header { @@ -194,118 +288,149 @@ body { cursor: pointer; } -/*UNCHECKED BELOW*/ - -body.menu-open { - padding-left: 260px; +/* ===== HAMBURGER MENU STYLES ===== */ +.hamburger-menu { + position: absolute; + top: 20px; + left: 20px; + z-index: 100; } -.create-ticket { - background: #3b82f6; - color: white; - padding: 0.625rem 1.25rem; - border-radius: 0.375rem; - border: none; +.hamburger-icon { cursor: pointer; - font-weight: 500; - transition: background-color 0.3s ease; - margin-right: 3.75rem; -} - -.create-ticket:hover { - background: #2563eb; -} - -h1 { - color: var(--text-primary); - margin: 0; -} - -table { - width: 100%; - border-collapse: separate; - border-spacing: 0; + font-size: 24px; background: var(--bg-secondary); - border-radius: 12px; + padding: 10px; + border-radius: 4px; box-shadow: var(--shadow); - overflow: hidden; } -th, td { - padding: 16px; - text-align: left; - border-bottom: 1px solid var(--border-color); - color: var(--text-primary); +.hamburger-content { + position: fixed; + top: 0; + left: -250px; + width: 200px; + height: 100%; + background: var(--bg-secondary); + box-shadow: 2px 0 5px rgba(0,0,0,0.1); + transition: left 0.3s ease; + padding: 40px 20px 20px; + overflow-y: auto; + z-index: 99; } -th { - background-color: var(--bg-secondary); - font-weight: 600; - text-transform: uppercase; - font-size: 0.9em; - letter-spacing: 0.05em; +.hamburger-content.open { + left: 0; } -tr:hover { - background-color: var(--hover-bg); +.close-hamburger { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + font-size: 24px; + background: var(--bg-secondary); + padding: 10px; + border-radius: 4px; + box-shadow: var(--shadow); } -tbody tr td:first-child { - border-left: 6px solid; +/* Hamburger menu inline editing styles */ +.ticket-info-editable { + padding: 10px 0; } -tbody tr.priority-1 td:first-child { border-left-color: var(--priority-1); } -tbody tr.priority-2 td:first-child { border-left-color: var(--priority-2); } -tbody tr.priority-3 td:first-child { border-left-color: var(--priority-3); } -tbody tr.priority-4 td:first-child { border-left-color: var(--priority-4); } - -/* Priority number styling */ -td:nth-child(2) { - text-align: center; +.editable-field, .info-field { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + position: relative; } -td:nth-child(2) span { - font-weight: bold; - font-family: 'Courier New', monospace; +.editable-field label, .info-field label { + flex: 0 0 auto; + margin-right: 10px; +} + +.editable-value { + flex: 1; + text-align: right; + min-height: 20px; + display: inline-block; + cursor: pointer; padding: 4px 8px; border-radius: 4px; - display: inline-block; - background: var(--hover-bg); + transition: background-color 0.2s; } -.priority-1 td:nth-child(2) { color: var(--priority-1); } -.priority-2 td:nth-child(2) { color: var(--priority-2); } -.priority-3 td:nth-child(2) { color: var(--priority-3); } -.priority-4 td:nth-child(2) { color: var(--priority-4); } - -.search-box { - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color); - border-radius: 0.375rem; - background: var(--bg-secondary); - color: var(--text-primary); - margin-left: 1.25rem; - width: 40%; +.editable-value:hover { + background-color: var(--hover-bg) !important; } -.status-filter { - padding: 0.5rem 0.75rem; +.edit-dropdown { + position: absolute; + top: 100%; + right: 0; + background: var(--bg-primary); border: 1px solid var(--border-color); - border-radius: 0.375rem; - background: var(--bg-secondary); + border-radius: 4px; + padding: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + z-index: 1000; + min-width: 150px; +} + +.field-select { + width: 100%; + padding: 4px 8px; + border: 1px solid var(--border-color); + border-radius: 4px; + margin-bottom: 8px; + background: var(--bg-primary); color: var(--text-primary); +} + +.edit-actions { + display: flex; + gap: 4px; + justify-content: flex-end; +} + +.save-btn, .cancel-btn { + padding: 4px 8px; + border: none; + border-radius: 3px; cursor: pointer; - min-width: 120px; - margin-right: 1rem; + font-size: 12px; + min-width: 24px; } -.search-box:focus, -.status-filter:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); +.save-btn { + background: #28a745; + color: white; } +.save-btn:hover { + background: #218838; +} + +.cancel-btn { + background: #dc3545; + color: white; +} + +.cancel-btn:hover { + background: #c82333; +} + +.info-field span { + flex: 1; + text-align: right; + color: var(--text-secondary); +} + +/* ===== UTILITY STYLES ===== */ .theme-toggle { position: absolute; top: 20px; @@ -325,27 +450,22 @@ td:nth-child(2) span { transform: scale(1.1); } -.table-controls { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 15px; - padding: 10px; - background: var(--bg-secondary); - border-radius: 8px; - box-shadow: var(--shadow); +.ticket-link { + font-family: 'Courier New', monospace; + font-weight: bold; + color: var(--text-primary) !important; + text-decoration: none; + background: var(--hover-bg); + padding: 4px 8px; + border-radius: 4px; + display: inline-block; } -.ticket-count { - font-weight: 500; - color: var(--text-secondary); +.ticket-link:hover { + background: var(--border-color); } -.table-actions { - display: flex; - gap: 15px; - align-items: center; -} +/* ===== PAGINATION STYLES ===== */ .pagination { display: flex; gap: 0.5rem; @@ -372,18 +492,31 @@ td:nth-child(2) span { border-color: #3b82f6; } -.settings-icon { - cursor: pointer; - padding: 8px; - border-radius: 4px; - transition: background-color 0.2s; +/* ===== SORTING STYLES ===== */ +th::after { + content: ''; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + opacity: 0.5; } -.settings-icon:hover { - background: var(--hover-bg); +th.sort-asc::after { + border-bottom: 7px solid var(--text-primary); + opacity: 1; } -/* Settings Modal Styles */ +th.sort-desc::after { + border-top: 7px solid var(--text-primary); + opacity: 1; +} + +/* ===== SETTINGS MODAL STYLES ===== */ .settings-modal-backdrop { position: fixed; top: 0; @@ -471,186 +604,4 @@ td:nth-child(2) span { .cancel-settings { background: var(--hover-bg); color: var(--text-primary); -} - -/* Sorting indicator styles */ -th { - position: relative; /* Ensure proper positioning of arrows */ - cursor: pointer; /* Show it's clickable */ -} - -th::after { - content: ''; - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - width: 0; - height: 0; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - opacity: 0.5; /* Make arrows less prominent when not active */ -} - -th.sort-asc::after { - border-bottom: 7px solid var(--text-primary); - opacity: 1; -} - -th.sort-desc::after { - border-top: 7px solid var(--text-primary); - opacity: 1; -} -/* Column toggle styles */ -.column-toggles { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 10px; -} - -.column-toggles label { - display: flex; - align-items: center; - gap: 8px; -} - -.hamburger-menu { - position: absolute; - top: 20px; - left: 20px; - z-index: 100; -} - -.hamburger-icon { - cursor: pointer; - font-size: 24px; - background: var(--bg-secondary); - padding: 10px; - border-radius: 4px; - box-shadow: var(--shadow); -} - -.hamburger-content { - position: fixed; - top: 0; - left: -250px; - width: 200px; - height: 100%; - background: var(--bg-secondary); - box-shadow: 2px 0 5px rgba(0,0,0,0.1); - transition: left 0.3s ease, margin-left 0.3s ease; - padding: 40px 20px 20px; - overflow-y: auto; - z-index: 99; -} - -.hamburger-content.open { - left: 0; -} - -.close-hamburger { - position: absolute; - top: 10px; - right: 10px; - cursor: pointer; - font-size: 24px; - background: var(--bg-secondary); - padding: 10px; - border-radius: 4px; - box-shadow: var(--shadow); -} - -.filter-section { - margin-bottom: 20px; -} - -.filter-section label { - display: block; - margin-bottom: 10px; -} - -.filter-actions { - display: flex; - gap: 10px; -} - -.filter-actions button { - flex: 1; - padding: 10px; - border: none; - border-radius: 4px; - cursor: pointer; -} - -#apply-filters { - background: #3b82f6; - color: white; -} - -#clear-filters { - background: var(--hover-bg); - color: var(--text-primary); -} - -.ticket-link { - font-family: 'Courier New', monospace; - font-weight: bold; - color: var(--text-primary) !important; - text-decoration: none; - background: var(--hover-bg); - padding: 4px 8px; - border-radius: 4px; - display: inline-block; -} - -.ticket-link:hover { - background: var(--border-color); -} - -/* Hamburger Menu Styles */ - -.menu-group { - margin-bottom: 20px; - padding: 15px; - border-bottom: 1px solid var(--border-color); -} - -.menu-group label { - display: block; - margin-bottom: 8px; - color: var(--text-secondary); - font-weight: 500; -} - -.menu-group select { - width: 100%; - padding: 10px; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--bg-primary); - color: var(--text-primary); - transition: all 0.3s ease; -} - -.menu-actions { - padding: 15px; - text-align: center; -} - -.menu-actions .btn.primary { - width: 100%; - padding: 12px; - font-weight: 500; -} - -.menu-controls { - padding: 15px; - text-align: center; - border-bottom: 1px solid var(--border-color); -} - -.menu-controls .btn { - width: 100%; - padding: 12px; - font-weight: 500; } \ No newline at end of file diff --git a/assets/css/ticket.css b/assets/css/ticket.css index 6765b68..bf3e9c2 100644 --- a/assets/css/ticket.css +++ b/assets/css/ticket.css @@ -1,3 +1,42 @@ +/* ===== TICKET PAGE SPECIFIC STYLES ===== */ + +/* Status colors for ticket page */ +.status-Open, +[id="statusDisplay"].status-Open { + background-color: var(--status-open) !important; + color: white !important; + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + text-transform: uppercase; + font-size: 0.9em; + letter-spacing: 0.5px; +} + +.status-In-Progress, +[id="statusDisplay"].status-In-Progress { + background-color: var(--status-in-progress) !important; + color: #212529 !important; + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + text-transform: uppercase; + font-size: 0.9em; + letter-spacing: 0.5px; +} + +.status-Closed, +[id="statusDisplay"].status-Closed { + background-color: var(--status-closed) !important; + color: white !important; + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + text-transform: uppercase; + font-size: 0.9em; + letter-spacing: 0.5px; +} + /* Base Layout Components */ .ticket-container { width: 90%; @@ -14,9 +53,13 @@ transition: border-color 0.3s ease; } -.full-width { - grid-column: 1 / -1; -} +/* Priority border colors */ +[data-priority="1"] { border-color: var(--priority-1); } +[data-priority="2"] { border-color: var(--priority-2); } +[data-priority="3"] { border-color: var(--priority-3); } +[data-priority="4"] { border-color: var(--priority-4); } +[data-priority="5"] { border-color: var(--priority-5); } + /* Header Components */ .ticket-header { display: flex; @@ -51,11 +94,17 @@ margin-right: 20px; } -h1 { - margin: 0; - padding: 0; - width: 100%; - display: block; +.status-priority-group { + display: flex; + gap: 10px; + align-items: center; + margin-right: 15px; +} + +.priority-indicator { + padding: 4px 8px; + border-radius: 4px; + font-weight: bold; } /* Title Input Styles */ @@ -74,7 +123,9 @@ h1 { line-height: 1.4; min-height: fit-content; height: auto; -}.title-input:not(:disabled) { +} + +.title-input:not(:disabled) { border-color: var(--border-color); background: var(--bg-primary); } @@ -102,13 +153,8 @@ h1 { font-weight: 500; } -#statusDisplay { - padding: 8px 16px; - border-radius: 6px; - font-weight: 500; - text-transform: uppercase; - font-size: 0.9em; - letter-spacing: 0.5px; +.full-width { + grid-column: 1 / -1; } .editable { @@ -128,17 +174,17 @@ input.editable { textarea.editable { width: calc(100% - 20px); min-height: 800px !important; - height: auto !important; /* Allow it to grow with content */ + height: auto !important; box-sizing: border-box; - white-space: pre; /* Preserve formatting */ - font-family: monospace; /* Better for ASCII art */ - line-height: 1.2; /* Tighter line spacing for ASCII art */ + white-space: pre; + font-family: monospace; + line-height: 1.2; } #description-tab { - min-height: 850px !important; /* Slightly larger than the textarea */ + min-height: 850px !important; height: auto !important; - padding-bottom: 20px; /* Add some padding at the bottom */ + padding-bottom: 20px; } .editable:disabled { @@ -174,89 +220,6 @@ textarea.editable { box-shadow: 0 2px 4px rgba(0,0,0,0.1); } -/* Status and Priority Styles */ -.status-priority-row { - display: flex; - gap: 20px; -} - -.detail-half { - flex: 1; -} - -.status-priority-group { - display: flex; - gap: 10px; - align-items: center; - margin-right: 15px; -} - -.priority-indicator { - padding: 4px 8px; - border-radius: 4px; - font-weight: bold; -} - -/* Priority Select Styles */ -select[data-field="priority"] { - border-left: 4px solid; -} - -select[data-field="priority"] option { - padding: 10px; -} - -select[data-field="priority"] option[value="1"] { - background-color: var(--priority-1); -} -select[data-field="priority"] option[value="2"] { - background-color: var(--priority-2); -} -select[data-field="priority"] option[value="3"] { - background-color: var(--priority-3); -} -select[data-field="priority"] option[value="4"] { - background-color: var(--priority-4); -} - -select[data-field="priority"][value="1"] { - border-left-color: var(--priority-1); -} -select[data-field="priority"][value="2"] { - border-left-color: var(--priority-2); -} -select[data-field="priority"][value="3"] { - border-left-color: var(--priority-3); -} -select[data-field="priority"][value="4"] { - border-left-color: var(--priority-4); -} - -select[data-field="priority"] option[value="1"]:hover { - background-color: #ffc9c9; - color: var(--text-primary); -} - -select[data-field="priority"] option[value="2"]:hover { - background-color: #ffe0b2; - color: var(--text-primary); -} - -select[data-field="priority"] option[value="3"]:hover { - background-color: #bbdefb; - color: var(--text-primary); -} - -select[data-field="priority"] option[value="4"]:hover { - background-color: #c8e6c9; - color: var(--text-primary); -} - -[data-priority="1"] { border-color: var(--priority-1); } -[data-priority="2"] { border-color: var(--priority-2); } -[data-priority="3"] { border-color: var(--priority-3); } -[data-priority="4"] { border-color: var(--priority-4); } - /* Comments Section */ .comments-section { margin-top: 40px; @@ -306,7 +269,23 @@ select[data-field="priority"] option[value="4"]:hover { .comment-text { color: var(--text-primary); - line-height: 1.4; + line-height: 1.6; + word-wrap: break-word; + margin: 0; + padding: 0; + white-space: normal; +} + +.comment-text p { + margin: 0.5em 0; +} + +.comment-text p:first-child { + margin-top: 0; +} + +.comment-text p:last-child { + margin-bottom: 0; } .comment-controls { diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index 45f0cfe..62da745 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -1,29 +1,63 @@ +// Main initialization document.addEventListener('DOMContentLoaded', function() { - // Only initialize filters if we're on the dashboard - if (document.querySelector('table')) { + console.log('DOM loaded, initializing dashboard...'); + + // Check if we're on the dashboard page + const hasTable = document.querySelector('table'); + const isTicketPage = window.location.pathname.includes('/ticket/') || + window.location.href.includes('ticket.php') || + document.querySelector('.ticket-details') !== null; + const isDashboard = hasTable && !isTicketPage; + + console.log('Has table:', hasTable); + console.log('Is ticket page:', isTicketPage); + console.log('Is dashboard:', isDashboard); + + if (isDashboard) { + // Dashboard-specific initialization initSearch(); initStatusFilter(); + initTableSorting(); + + console.log('Creating hamburger menu for dashboard...'); + try { + createHamburgerMenu(); + console.log('Hamburger menu created successfully'); + } catch (error) { + console.error('Error creating hamburger menu:', error); + } + } else if (isTicketPage) { + // Ticket page initialization + console.log('Creating hamburger menu for ticket page...'); + try { + createHamburgerMenu(); + console.log('Hamburger menu created successfully'); + } catch (error) { + console.error('Error creating hamburger menu:', error); + } } - // Keep theme toggle for all pages + // Initialize for all pages initThemeToggle(); - createHamburgerMenu(); - + initSettingsModal(); + // Load saved theme preference const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); +}); - // Add sorting functionality +function initTableSorting() { const tableHeaders = document.querySelectorAll('th'); - tableHeaders.forEach(header => { + tableHeaders.forEach((header, index) => { + header.style.cursor = 'pointer'; header.addEventListener('click', () => { const table = header.closest('table'); - const index = Array.from(header.parentElement.children).indexOf(header); sortTable(table, index); }); }); +} - // Add settings modal functionality +function initSettingsModal() { const settingsIcon = document.querySelector('.settings-icon'); if (settingsIcon) { settingsIcon.addEventListener('click', function(e) { @@ -31,323 +65,6 @@ document.addEventListener('DOMContentLoaded', function() { createSettingsModal(); }); } -}); - -function sortTable(table, column) { - // Remove existing sort indicators from all headers - const headers = table.querySelectorAll('th'); - headers.forEach(header => { - header.classList.remove('sort-asc', 'sort-desc'); - }); - - const rows = Array.from(table.querySelectorAll('tbody tr')); - - // Determine current sort direction - const currentDirection = table.dataset.sortColumn === column - ? (table.dataset.sortDirection === 'asc' ? 'desc' : 'asc') - : 'asc'; - - // Store current sorting column and direction - table.dataset.sortColumn = column; - table.dataset.sortDirection = currentDirection; - - rows.sort((a, b) => { - const aValue = a.children[column].textContent.trim(); - const bValue = b.children[column].textContent.trim(); - - // Try numeric sorting first, fallback to string comparison - const numA = parseFloat(aValue); - const numB = parseFloat(bValue); - - if (!isNaN(numA) && !isNaN(numB)) { - return currentDirection === 'asc' ? numA - numB : numB - numA; - } - - // String comparison - return currentDirection === 'asc' - ? aValue.localeCompare(bValue) - : bValue.localeCompare(aValue); - }); - - // Add sort indicator to the current header - const currentHeader = headers[column]; - currentHeader.classList.add(currentDirection === 'asc' ? 'sort-asc' : 'sort-desc'); - - // Reorder rows in the tbody - const tbody = table.querySelector('tbody'); - rows.forEach(row => tbody.appendChild(row)); -} - -// Add this to the DOMContentLoaded event listener to persist sorting on page load -document.addEventListener('DOMContentLoaded', function() { - const table = document.querySelector('table'); - if (table) { - const savedSortColumn = localStorage.getItem('sortColumn'); - const savedSortDirection = localStorage.getItem('sortDirection'); - - if (savedSortColumn !== null && savedSortDirection !== null) { - const headers = table.querySelectorAll('th'); - const columnIndex = Array.from(headers).findIndex(header => - header.textContent.toLowerCase().replace(' ', '_') === savedSortColumn - ); - - if (columnIndex !== -1) { - table.dataset.sortColumn = columnIndex; - table.dataset.sortDirection = savedSortDirection; - - const header = headers[columnIndex]; - header.classList.add(savedSortDirection === 'asc' ? 'sort-asc' : 'sort-desc'); - } - } - } -}); - -// Modify the existing event listeners for table headers -document.addEventListener('DOMContentLoaded', function() { - const tableHeaders = document.querySelectorAll('th'); - tableHeaders.forEach((header, index) => { - header.addEventListener('click', () => { - const table = header.closest('table'); - sortTable(table, index); - - // Save sorting preferences - const columnName = header.textContent.toLowerCase().replace(' ', '_'); - localStorage.setItem('sortColumn', columnName); - localStorage.setItem('sortDirection', table.dataset.sortDirection); - }); - }); -}); -function createSettingsModal() { - // Create modal backdrop - const backdrop = document.createElement('div'); - backdrop.className = 'settings-modal-backdrop'; - backdrop.innerHTML = ` -
-
-

Dashboard Settings

- -
-
-
-

Toggle Columns

-
- - - - - - - - -
-
-
-

Rows per Page

- -
-
- -
- `; - - // Add to body - document.body.appendChild(backdrop); - - // Load saved column visibility settings - const savedColumnSettings = JSON.parse(localStorage.getItem('columnVisibility') || '{}'); - const checkboxes = backdrop.querySelectorAll('.column-toggles input'); - checkboxes.forEach(checkbox => { - checkbox.checked = savedColumnSettings[checkbox.value] !== false; - }); - - // Load saved rows per page setting - const savedRowsPerPage = localStorage.getItem('ticketsPerPage') || '5'; - const rowsPerPageSelect = backdrop.querySelector('#rows-per-page'); - rowsPerPageSelect.value = savedRowsPerPage; - - // Close modal events - backdrop.querySelector('.close-modal').addEventListener('click', closeSettingsModal); - backdrop.querySelector('.cancel-settings').addEventListener('click', closeSettingsModal); - backdrop.querySelector('.save-settings').addEventListener('click', saveSettings); - - // Close modal on backdrop click - backdrop.addEventListener('click', (e) => { - if (e.target === backdrop) { - closeSettingsModal(); - } - }); -} - -function closeSettingsModal() { - const backdrop = document.querySelector('.settings-modal-backdrop'); - if (backdrop) { - backdrop.remove(); - } -} - -function saveSettings() { - // Save column visibility - const checkboxes = document.querySelectorAll('.column-toggles input'); - const columnVisibility = {}; - - checkboxes.forEach(checkbox => { - columnVisibility[checkbox.value] = checkbox.checked; - }); - localStorage.setItem('columnVisibility', JSON.stringify(columnVisibility)); - - // Save rows per page - const rowsPerPage = document.querySelector('#rows-per-page').value; - localStorage.setItem('ticketsPerPage', rowsPerPage); - - // Set cookie for PHP to read - document.cookie = `ticketsPerPage=${rowsPerPage}; path=/`; - - // Apply column visibility - applyColumnVisibility(); - - // Reload page to apply pagination changes - window.location.reload(); - - // Close modal - closeSettingsModal(); -} - -function applyColumnVisibility() { - const savedColumnSettings = JSON.parse(localStorage.getItem('columnVisibility') || '{}'); - const table = document.querySelector('table'); - - if (table) { - const headers = table.querySelectorAll('th'); - const rows = table.querySelectorAll('tbody tr'); - - headers.forEach((header, index) => { - const columnValue = header.textContent.toLowerCase().replace(' ', '_'); - const isVisible = savedColumnSettings[columnValue] !== false; - - header.style.display = isVisible ? '' : 'none'; - - rows.forEach(row => { - row.children[index].style.display = isVisible ? '' : 'none'; - }); - }); - } -} - -// Apply column visibility on page load -document.addEventListener('DOMContentLoaded', applyColumnVisibility); -// Dark mode toggle -function initThemeToggle() { - const toggle = document.createElement('button'); - toggle.className = 'theme-toggle'; - toggle.innerHTML = '๐ŸŒ“'; - toggle.onclick = () => { - document.documentElement.setAttribute('data-theme', - document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark' - ); - localStorage.setItem('theme', document.documentElement.getAttribute('data-theme')); - }; - document.body.appendChild(toggle); -} - -// Search functionality -function initSearch() { - const searchBox = document.createElement('input'); - searchBox.type = 'text'; - searchBox.placeholder = 'Search tickets...'; - searchBox.className = 'search-box'; - searchBox.oninput = (e) => { - const searchTerm = e.target.value.toLowerCase(); - const rows = document.querySelectorAll('tbody tr'); - rows.forEach(row => { - const text = row.textContent.toLowerCase(); - row.style.display = text.includes(searchTerm) ? '' : 'none'; - }); - }; - document.querySelector('h1').after(searchBox); -} - -// Filter by status -function initStatusFilter() { - const filterContainer = document.createElement('div'); - filterContainer.className = 'status-filter-container'; - - // Create dropdown container - const dropdown = document.createElement('div'); - dropdown.className = 'status-dropdown'; - - // Create dropdown header - const dropdownHeader = document.createElement('div'); - dropdownHeader.className = 'dropdown-header'; - dropdownHeader.textContent = 'Status Filter'; - - // Create dropdown content - const dropdownContent = document.createElement('div'); - dropdownContent.className = 'dropdown-content'; - - const statuses = ['Open', 'Closed']; - statuses.forEach(status => { - const label = document.createElement('label'); - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.value = status; - checkbox.id = `status-${status.toLowerCase()}`; - - const urlParams = new URLSearchParams(window.location.search); - const currentStatuses = urlParams.get('status') ? urlParams.get('status').split(',') : []; - checkbox.checked = currentStatuses.includes(status); - - label.appendChild(checkbox); - label.appendChild(document.createTextNode(status)); - dropdownContent.appendChild(label); - }); - - const saveButton = document.createElement('button'); - saveButton.className = 'btn save-filter'; - saveButton.textContent = 'Apply Filter'; - - saveButton.onclick = () => { - const checkedBoxes = dropdownContent.querySelectorAll('input:checked'); - const selectedStatuses = Array.from(checkedBoxes).map(cb => cb.value); - localStorage.setItem('statusFilter', selectedStatuses.join(',')); - window.location.href = selectedStatuses.length ? `?status=${selectedStatuses.join(',')}` : '?'; - dropdown.classList.remove('active'); - }; - - // Toggle dropdown on header click - dropdownHeader.onclick = () => { - dropdown.classList.toggle('active'); - }; - - dropdown.appendChild(dropdownHeader); - dropdown.appendChild(dropdownContent); - dropdownContent.appendChild(saveButton); - filterContainer.appendChild(dropdown); - - document.querySelector('.table-controls .table-actions').prepend(filterContainer); } function sortTable(table, column) { @@ -368,7 +85,7 @@ function sortTable(table, column) { const aValue = a.children[column].textContent.trim(); const bValue = b.children[column].textContent.trim(); - // Check if this is a date column (Created or Updated) + // Check if this is a date column const headerText = headers[column].textContent.toLowerCase(); if (headerText === 'created' || headerText === 'updated') { const dateA = new Date(aValue); @@ -376,7 +93,7 @@ function sortTable(table, column) { return currentDirection === 'asc' ? dateA - dateB : dateB - dateA; } - // Existing numeric and string comparison logic + // Numeric comparison const numA = parseFloat(aValue); const numB = parseFloat(bValue); @@ -384,6 +101,7 @@ function sortTable(table, column) { return currentDirection === 'asc' ? numA - numB : numB - numA; } + // String comparison return currentDirection === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); @@ -396,186 +114,391 @@ function sortTable(table, column) { rows.forEach(row => tbody.appendChild(row)); } -// Modify the CSS to ensure arrows are more visible -document.addEventListener('DOMContentLoaded', function() { - const tableHeaders = document.querySelectorAll('th'); - tableHeaders.forEach((header, index) => { - header.style.cursor = 'pointer'; // Make headers look clickable - header.addEventListener('click', () => { - const table = header.closest('table'); - sortTable(table, index); - }); - }); -}); +function createSettingsModal() { + const backdrop = document.createElement('div'); + backdrop.className = 'settings-modal-backdrop'; + backdrop.innerHTML = ` +
+
+

Dashboard Settings

+ +
+
+
+

Rows per Page

+ +
+
+ +
+ `; -function saveTicket() { - const editables = document.querySelectorAll('.editable'); - const data = {}; - const ticketId = window.location.href.split('id=')[1]; - - editables.forEach(field => { - if (field.dataset.field) { - data[field.dataset.field] = field.value; + document.body.appendChild(backdrop); + + // Load saved rows per page setting + const savedRowsPerPage = localStorage.getItem('ticketsPerPage') || '15'; + const rowsPerPageSelect = backdrop.querySelector('#rows-per-page'); + rowsPerPageSelect.value = savedRowsPerPage; + + // Event listeners + backdrop.querySelector('.close-modal').addEventListener('click', closeSettingsModal); + backdrop.querySelector('.cancel-settings').addEventListener('click', closeSettingsModal); + backdrop.querySelector('.save-settings').addEventListener('click', saveSettings); + + backdrop.addEventListener('click', (e) => { + if (e.target === backdrop) { + closeSettingsModal(); } }); +} - fetch('update_ticket.php', { +function closeSettingsModal() { + const backdrop = document.querySelector('.settings-modal-backdrop'); + if (backdrop) { + backdrop.remove(); + } +} + +function saveSettings() { + // Save rows per page + const rowsPerPage = document.querySelector('#rows-per-page').value; + localStorage.setItem('ticketsPerPage', rowsPerPage); + + // Set cookie for PHP to read + document.cookie = `ticketsPerPage=${rowsPerPage}; path=/`; + + // Reload page to apply pagination changes + window.location.reload(); +} + +function initThemeToggle() { + const toggle = document.createElement('button'); + toggle.className = 'theme-toggle'; + toggle.innerHTML = '๐ŸŒ“'; + toggle.onclick = () => { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', newTheme); + localStorage.setItem('theme', newTheme); + }; + document.body.appendChild(toggle); +} + +function initSearch() { + const searchBox = document.createElement('input'); + searchBox.type = 'text'; + searchBox.placeholder = 'Search tickets...'; + searchBox.className = 'search-box'; + searchBox.oninput = (e) => { + const searchTerm = e.target.value.toLowerCase(); + const rows = document.querySelectorAll('tbody tr'); + rows.forEach(row => { + const text = row.textContent.toLowerCase(); + row.style.display = text.includes(searchTerm) ? '' : 'none'; + }); + }; + document.querySelector('h1').after(searchBox); +} + +function initStatusFilter() { + const filterContainer = document.createElement('div'); + filterContainer.className = 'status-filter-container'; + + const dropdown = document.createElement('div'); + dropdown.className = 'status-dropdown'; + + const dropdownHeader = document.createElement('div'); + dropdownHeader.className = 'dropdown-header'; + dropdownHeader.textContent = 'Status Filter'; + + const dropdownContent = document.createElement('div'); + dropdownContent.className = 'dropdown-content'; + + const statuses = ['Open', 'In Progress', 'Closed']; + statuses.forEach(status => { + const label = document.createElement('label'); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.value = status; + checkbox.id = `status-${status.toLowerCase().replace(/\s+/g, '-')}`; + + const urlParams = new URLSearchParams(window.location.search); + const currentStatuses = urlParams.get('status') ? urlParams.get('status').split(',') : []; + const showAll = urlParams.get('show_all'); + + // FIXED LOGIC: Determine checkbox state + if (showAll === '1') { + // If show_all=1 parameter exists, all should be checked + checkbox.checked = true; + } else if (currentStatuses.length === 0) { + // No status parameter - default: Open and In Progress checked, Closed unchecked + checkbox.checked = status !== 'Closed'; + } else { + // Status parameter exists - check if this status is in the list + checkbox.checked = currentStatuses.includes(status); + } + + label.appendChild(checkbox); + label.appendChild(document.createTextNode(' ' + status)); + dropdownContent.appendChild(label); + }); + + const saveButton = document.createElement('button'); + saveButton.className = 'btn save-filter'; + saveButton.textContent = 'Apply Filter'; + + saveButton.onclick = () => { + const checkedBoxes = dropdownContent.querySelectorAll('input:checked'); + const selectedStatuses = Array.from(checkedBoxes).map(cb => cb.value); + + const params = new URLSearchParams(window.location.search); + + if (selectedStatuses.length === 0) { + // No statuses selected - show default (Open + In Progress) + params.delete('status'); + params.delete('show_all'); + } else if (selectedStatuses.length === 3) { + // All statuses selected - show all tickets + params.delete('status'); + params.set('show_all', '1'); + } else { + // Some statuses selected - set the parameter + params.set('status', selectedStatuses.join(',')); + params.delete('show_all'); + } + + params.set('page', '1'); + window.location.search = params.toString(); + dropdown.classList.remove('active'); + }; + + dropdownHeader.onclick = () => { + dropdown.classList.toggle('active'); + }; + + dropdown.appendChild(dropdownHeader); + dropdown.appendChild(dropdownContent); + dropdownContent.appendChild(saveButton); + filterContainer.appendChild(dropdown); + + const tableActions = document.querySelector('.table-controls .table-actions'); + if (tableActions) { + tableActions.prepend(filterContainer); + } +} + +function quickSave() { + if (!window.ticketData) { + console.error('No ticket data available'); + return; + } + + const statusSelect = document.getElementById('status-select'); + const prioritySelect = document.getElementById('priority-select'); + + if (!statusSelect || !prioritySelect) { + console.error('Status or priority select not found'); + return; + } + + const data = { + ticket_id: parseInt(window.ticketData.id), + status: statusSelect.value, + priority: parseInt(prioritySelect.value) + }; + + console.log('Saving ticket data:', data); + + fetch('/api/update_ticket.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - ticket_id: ticketId, - ...data - }) + body: JSON.stringify(data) }) - .then(response => response.json()) - .then(data => { - if(data.success) { - const statusDisplay = document.getElementById('statusDisplay'); - statusDisplay.className = `status-${data.status}`; - statusDisplay.textContent = data.status; - } - }); -} - -function toggleHamburgerEditMode() { - const editButton = document.getElementById('hamburgerEditButton'); - const editables = document.querySelectorAll('.hamburger-content .editable'); - const cancelButton = document.getElementById('hamburgerCancelButton'); // Get cancel button - const isEditing = editButton.classList.contains('editing'); - - if (!isEditing) { - // Switch to edit mode - editButton.textContent = 'Save Changes'; - editButton.classList.add('editing'); - editables.forEach(field => { - field.disabled = false; - // Store original values for potential cancel - field.dataset.originalValue = field.value; + .then(response => { + console.log('Response status:', response.status); + return response.text().then(text => { + console.log('Raw response:', text); + try { + return JSON.parse(text); + } catch (e) { + throw new Error('Invalid JSON response: ' + text); + } }); - - // Create and append cancel button only if it doesn't exist - if (!cancelButton) { - const newCancelButton = document.createElement('button'); - newCancelButton.id = 'hamburgerCancelButton'; - newCancelButton.className = 'btn'; - newCancelButton.textContent = 'Cancel'; - newCancelButton.onclick = cancelHamburgerEdit; - editButton.parentNode.appendChild(newCancelButton); - } - } else { - // Save changes - saveHamburgerChanges(); - } -} - -function saveHamburgerChanges() { - try { - saveTicket(); - resetHamburgerEditMode(); - } catch (error) { - console.error('Error saving changes:', error); - } -} - -function cancelHamburgerEdit() { - // Revert all fields to their original values - const editables = document.querySelectorAll('.hamburger-content .editable'); - editables.forEach(field => { - if (field.dataset.originalValue) { - field.value = field.dataset.originalValue; + }) + .then(result => { + console.log('Update result:', result); + if (result.success) { + // Update the hamburger menu display + const hamburgerStatus = document.getElementById('hamburger-status'); + const hamburgerPriority = document.getElementById('hamburger-priority'); + + if (hamburgerStatus) hamburgerStatus.textContent = statusSelect.value; + if (hamburgerPriority) hamburgerPriority.textContent = 'P' + prioritySelect.value; + + // Update window.ticketData + window.ticketData.status = statusSelect.value; + window.ticketData.priority = parseInt(prioritySelect.value); + + // Update main page elements if they exist + const statusDisplay = document.getElementById('statusDisplay'); + if (statusDisplay) { + statusDisplay.className = `status-${statusSelect.value}`; + statusDisplay.textContent = statusSelect.value; + } + + console.log('Ticket updated successfully'); + + // Close hamburger menu after successful save + const hamburgerContent = document.querySelector('.hamburger-content'); + if (hamburgerContent) { + hamburgerContent.classList.remove('open'); + document.body.classList.remove('menu-open'); + } + + } else { + console.error('Error updating ticket:', result.error || 'Unknown error'); + alert('Error updating ticket: ' + (result.error || 'Unknown error')); } + }) + .catch(error => { + console.error('Error updating ticket:', error); + alert('Error updating ticket: ' + error.message); }); - - resetHamburgerEditMode(); -} - - -function resetHamburgerEditMode() { - const editButton = document.getElementById('hamburgerEditButton'); - const cancelButton = document.getElementById('hamburgerCancelButton'); - const editables = document.querySelectorAll('.hamburger-content .editable'); - - // Reset button text and remove editing class - editButton.textContent = 'Edit Ticket'; - editButton.classList.remove('editing'); - - // Disable all editable fields - editables.forEach(field => field.disabled = true); - - // Remove cancel button if it exists - if (cancelButton) cancelButton.remove(); } function createHamburgerMenu() { + console.log('createHamburgerMenu called'); + + // Remove any existing hamburger menu first + const existingMenu = document.querySelector('.hamburger-menu'); + if (existingMenu) { + console.log('Removing existing menu'); + existingMenu.remove(); + } + const hamburgerMenu = document.createElement('div'); hamburgerMenu.className = 'hamburger-menu'; - const isTicketPage = window.location.pathname.includes('ticket.php') || - window.location.pathname.includes('/ticket/'); + // Better detection for ticket pages + const isTicketPage = window.location.pathname.includes('/ticket/') || + window.location.href.includes('ticket.php') || + document.querySelector('.ticket-details') !== null; - if (isTicketPage && window.ticketData) { - // Use the ticket data from the global variable - const values = window.ticketData; + console.log('Is ticket page:', isTicketPage); + console.log('Has ticketData:', !!window.ticketData); + console.log('TicketData contents:', window.ticketData); + + if (isTicketPage) { + // Wait for ticketData if it's not loaded yet + if (!window.ticketData) { + console.log('Waiting for ticket data...'); + setTimeout(() => { + if (window.ticketData) { + console.log('Ticket data now available, recreating menu'); + createHamburgerMenu(); + } + }, 100); + return; + } + console.log('Creating ticket hamburger menu with data:', window.ticketData); + + // Ticket page hamburger menu with inline editing hamburgerMenu.innerHTML = `
โ˜ฐ
โ˜ฐ
-

Ticket Controls

- - - - - `; - const hamburgerEditButton = hamburgerMenu.querySelector('#hamburgerEditButton'); - if (hamburgerEditButton) { - hamburgerEditButton.addEventListener('click', toggleHamburgerEditMode); - } - - document.addEventListener('keydown', (event) => { - if (event.key === 'Escape') { - cancelHamburgerEdit(); - } - }); + console.log('Ticket hamburger menu HTML created'); + + // Add inline editing functionality + setupInlineEditing(hamburgerMenu); } else { + console.log('Creating dashboard hamburger menu'); + + // Dashboard hamburger menu (your existing code) hamburgerMenu.innerHTML = `
โ˜ฐ
@@ -596,38 +519,60 @@ function createHamburgerMenu() {
`; - // Populate categories and types from data attributes + // Get current URL parameters + const urlParams = new URLSearchParams(window.location.search); + const currentCategories = urlParams.get('category') ? urlParams.get('category').split(',') : []; + const currentTypes = urlParams.get('type') ? urlParams.get('type').split(',') : []; + + // Get containers const categoriesContainer = hamburgerMenu.querySelector('#category-filters'); const typesContainer = hamburgerMenu.querySelector('#type-filters'); + // Get data from body attributes const categories = JSON.parse(document.body.dataset.categories || '[]'); const types = JSON.parse(document.body.dataset.types || '[]'); // Create checkboxes for categories categories.forEach(category => { const label = document.createElement('label'); + label.style.display = 'block'; + label.style.marginBottom = '5px'; + const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = category; checkbox.name = 'category'; + + const isChecked = currentCategories.includes(category); + label.appendChild(checkbox); - label.appendChild(document.createTextNode(category)); + label.appendChild(document.createTextNode(' ' + category)); categoriesContainer.appendChild(label); + + checkbox.checked = isChecked; }); // Create checkboxes for types types.forEach(type => { const label = document.createElement('label'); + label.style.display = 'block'; + label.style.marginBottom = '5px'; + const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = type; checkbox.name = 'type'; + + const isChecked = currentTypes.includes(type); + label.appendChild(checkbox); - label.appendChild(document.createTextNode(type)); + label.appendChild(document.createTextNode(' ' + type)); typesContainer.appendChild(label); + + checkbox.checked = isChecked; }); - // Apply filters + // Apply filters event const applyFiltersBtn = hamburgerMenu.querySelector('#apply-filters'); applyFiltersBtn.addEventListener('click', () => { const selectedCategories = Array.from( @@ -638,7 +583,6 @@ function createHamburgerMenu() { typesContainer.querySelectorAll('input:checked') ).map(cb => cb.value); - // Construct URL with filters const params = new URLSearchParams(window.location.search); if (selectedCategories.length > 0) { @@ -653,37 +597,275 @@ function createHamburgerMenu() { params.delete('type'); } - // Reload with new filters + params.set('page', '1'); window.location.search = params.toString(); }); - // Clear filters + // Clear filters event const clearFiltersBtn = hamburgerMenu.querySelector('#clear-filters'); clearFiltersBtn.addEventListener('click', () => { const params = new URLSearchParams(window.location.search); params.delete('category'); params.delete('type'); + params.set('page', '1'); window.location.search = params.toString(); }); } + + console.log('Adding hamburger menu to body'); + // Add to body document.body.appendChild(hamburgerMenu); + + console.log('Hamburger menu added, setting up event listeners'); // Toggle hamburger menu const hamburgerIcon = hamburgerMenu.querySelector('.hamburger-icon'); const hamburgerContent = hamburgerMenu.querySelector('.hamburger-content'); - hamburgerIcon.addEventListener('click', () => { - hamburgerContent.classList.toggle('open'); - document.body.classList.toggle('menu-open'); - }); + + if (hamburgerIcon && hamburgerContent) { + hamburgerIcon.addEventListener('click', () => { + console.log('Hamburger icon clicked'); + hamburgerContent.classList.toggle('open'); + document.body.classList.toggle('menu-open'); + }); - // Close hamburger menu - const closeButton = hamburgerMenu.querySelector('.close-hamburger'); - closeButton.addEventListener('click', () => { - hamburgerContent.classList.remove('open'); - document.body.classList.remove('menu-open'); + // Close hamburger menu + const closeButton = hamburgerMenu.querySelector('.close-hamburger'); + if (closeButton) { + closeButton.addEventListener('click', () => { + console.log('Close button clicked'); + hamburgerContent.classList.remove('open'); + document.body.classList.remove('menu-open'); + }); + } + + console.log('Hamburger menu created successfully'); + } else { + console.error('Failed to find hamburger icon or content'); + } +} + +function setupInlineEditing(hamburgerMenu) { + const editableFields = hamburgerMenu.querySelectorAll('.editable-field'); + + editableFields.forEach(field => { + const valueSpan = field.querySelector('.editable-value'); + const dropdown = field.querySelector('.edit-dropdown'); + const select = field.querySelector('.field-select'); + const saveBtn = field.querySelector('.save-btn'); + const cancelBtn = field.querySelector('.cancel-btn'); + const fieldName = field.dataset.field; + + // Make value span clickable + valueSpan.style.cursor = 'pointer'; + valueSpan.style.padding = '4px 8px'; + valueSpan.style.borderRadius = '4px'; + valueSpan.style.transition = 'background-color 0.2s'; + + // Hover effect + valueSpan.addEventListener('mouseenter', () => { + valueSpan.style.backgroundColor = 'var(--hover-bg, #f0f0f0)'; + }); + + valueSpan.addEventListener('mouseleave', () => { + if (dropdown.style.display === 'none') { + valueSpan.style.backgroundColor = 'transparent'; + } + }); + + // Click to edit + valueSpan.addEventListener('click', () => { + dropdown.style.display = 'block'; + valueSpan.style.backgroundColor = 'var(--hover-bg, #f0f0f0)'; + select.focus(); + }); + + // Save changes + saveBtn.addEventListener('click', () => { + const newValue = select.value; + const oldValue = valueSpan.dataset.current; + + if (newValue !== oldValue) { + saveFieldChange(fieldName, newValue, valueSpan, dropdown); + } else { + cancelEdit(valueSpan, dropdown); + } + }); + + // Cancel changes + cancelBtn.addEventListener('click', () => { + select.value = valueSpan.dataset.current; + cancelEdit(valueSpan, dropdown); + }); + + // Cancel on escape key + select.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + select.value = valueSpan.dataset.current; + cancelEdit(valueSpan, dropdown); + } else if (e.key === 'Enter') { + saveBtn.click(); + } + }); + + // Cancel when clicking outside + document.addEventListener('click', (e) => { + if (!field.contains(e.target) && dropdown.style.display === 'block') { + select.value = valueSpan.dataset.current; + cancelEdit(valueSpan, dropdown); + } + }); }); } -// Add to DOMContentLoaded -document.addEventListener('DOMContentLoaded', createHamburgerMenu); +function saveFieldChange(fieldName, newValue, valueSpan, dropdown) { + if (!window.ticketData) { + console.error('No ticket data available'); + return; + } + + const data = { + ticket_id: parseInt(window.ticketData.id), + [fieldName]: fieldName === 'priority' ? parseInt(newValue) : newValue + }; + + console.log('Saving field change:', data); + + // Show loading state + valueSpan.style.opacity = '0.6'; + + fetch('/api/update_ticket.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + .then(response => { + return response.text().then(text => { + try { + return JSON.parse(text); + } catch (e) { + throw new Error('Invalid JSON response: ' + text); + } + }); + }) + .then(result => { + console.log('Update result:', result); + if (result.success) { + // Update the hamburger menu display + if (fieldName === 'priority') { + valueSpan.textContent = 'P' + newValue; + } else { + valueSpan.textContent = newValue; + } + + valueSpan.dataset.current = newValue; + + // Update window.ticketData + window.ticketData[fieldName] = fieldName === 'priority' ? parseInt(newValue) : newValue; + + // Update main page elements + if (fieldName === 'status') { + const statusDisplay = document.getElementById('statusDisplay'); + if (statusDisplay) { + // Remove all existing status classes + statusDisplay.className = statusDisplay.className.replace(/status-\S+/g, '').trim(); + + // Create the correct CSS class name to match your CSS + // "Open" -> "status-Open" + // "In Progress" -> "status-In-Progress" + // "Closed" -> "status-Closed" + const cssClass = newValue.replace(/\s+/g, '-'); // "In Progress" -> "In-Progress" + const fullClassName = `status-${cssClass}`; + + statusDisplay.className = fullClassName; + statusDisplay.textContent = newValue; + + console.log('Updated status display class to:', fullClassName); + console.log('Status display element:', statusDisplay); + } + } + + if (fieldName === 'priority') { + const priorityDisplay = document.querySelector('.priority-indicator'); + if (priorityDisplay) { + // Remove all priority classes first + priorityDisplay.className = priorityDisplay.className.replace(/priority-\d+/g, ''); + priorityDisplay.className = `priority-indicator priority-${newValue}`; + priorityDisplay.textContent = 'P' + newValue; + } + + // Update the ticket container's data-priority attribute for styling + const ticketContainer = document.querySelector('.ticket-container'); + if (ticketContainer) { + ticketContainer.setAttribute('data-priority', newValue); + } + } + + console.log('Field updated successfully'); + cancelEdit(valueSpan, dropdown); + + } else { + console.error('Error updating field:', result.error || 'Unknown error'); + alert('Error updating ' + fieldName + ': ' + (result.error || 'Unknown error')); + cancelEdit(valueSpan, dropdown); + } + }) + .catch(error => { + console.error('Error updating field:', error); + alert('Error updating ' + fieldName + ': ' + error.message); + cancelEdit(valueSpan, dropdown); + }) + .finally(() => { + valueSpan.style.opacity = '1'; + }); +} + +function cancelEdit(valueSpan, dropdown) { + dropdown.style.display = 'none'; + valueSpan.style.backgroundColor = 'transparent'; +} + + +// Ticket page functions (if needed) +function saveTicket() { + const editables = document.querySelectorAll('.editable'); + const data = {}; + + let ticketId; + if (window.location.href.includes('?id=')) { + ticketId = window.location.href.split('id=')[1]; + } else { + const matches = window.location.pathname.match(/\/ticket\/(\d+)/); + ticketId = matches ? matches[1] : null; + } + + editables.forEach(field => { + if (field.dataset.field) { + data[field.dataset.field] = field.value; + } + }); + + fetch('/api/update_ticket.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + ticket_id: ticketId, + ...data + }) + }) + .then(response => response.json()) + .then(data => { + if(data.success) { + const statusDisplay = document.getElementById('statusDisplay'); + if (statusDisplay) { + statusDisplay.className = `status-${data.status}`; + statusDisplay.textContent = data.status; + } + } + }); +} \ No newline at end of file diff --git a/assets/js/ticket.js b/assets/js/ticket.js index a5280ea..30720c7 100644 --- a/assets/js/ticket.js +++ b/assets/js/ticket.js @@ -25,12 +25,6 @@ function saveTicket() { // Use the correct API path const apiUrl = '/api/update_ticket.php'; - console.log('Sending request to:', apiUrl); - console.log('Sending data:', JSON.stringify({ - ticket_id: ticketId, - ...data - })); - fetch(apiUrl, { method: 'POST', headers: { @@ -42,7 +36,6 @@ function saveTicket() { }) }) .then(response => { - console.log('Response status:', response.status); if (!response.ok) { return response.text().then(text => { console.error('Server response:', text); @@ -52,7 +45,6 @@ function saveTicket() { return response.json(); }) .then(data => { - console.log('Response data:', data); if(data.success) { const statusDisplay = document.getElementById('statusDisplay'); if (statusDisplay) { @@ -141,6 +133,22 @@ function addComment() { // Clear the comment box document.getElementById('newComment').value = ''; + // Format the comment text for display + let displayText; + if (isMarkdownEnabled) { + // For markdown, use marked.parse + displayText = marked.parse(commentText); + } else { + // For non-markdown, convert line breaks to
and escape HTML + displayText = commentText + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\n/g, '
'); + } + // Add new comment to the list const commentsList = document.querySelector('.comments-list'); const newComment = ` @@ -149,9 +157,7 @@ function addComment() { ${data.user_name} ${data.created_at}
-
- ${isMarkdownEnabled ? marked.parse(commentText) : commentText} -
+
${displayText}
`; commentsList.insertAdjacentHTML('afterbegin', newComment); @@ -180,9 +186,17 @@ function togglePreview() { } function updatePreview() { - const preview = document.getElementById('markdownPreview'); - const textarea = document.getElementById('newComment'); - preview.innerHTML = marked.parse(textarea.value); + const commentText = document.getElementById('newComment').value; + const previewDiv = document.getElementById('markdownPreview'); + const isMarkdownEnabled = document.getElementById('markdownMaster').checked; + + if (isMarkdownEnabled && commentText.trim()) { + // For markdown preview, use marked.parse which handles line breaks correctly + previewDiv.innerHTML = marked.parse(commentText); + previewDiv.style.display = 'block'; + } else { + previewDiv.style.display = 'none'; + } } function toggleMarkdownMode() { diff --git a/config/config.php b/config/config.php index 962a570..f5c7898 100644 --- a/config/config.php +++ b/config/config.php @@ -9,7 +9,8 @@ $GLOBALS['config'] = [ 'DB_USER' => $envVars['DB_USER'] ?? 'root', 'DB_PASS' => $envVars['DB_PASS'] ?? '', 'DB_NAME' => $envVars['DB_NAME'] ?? 'tinkertickets', - 'BASE_URL' => '/tinkertickets', // Application base URL + 'BASE_URL' => '', // Empty since we're serving from document root 'ASSETS_URL' => '/assets', // Assets URL 'API_URL' => '/api' // API URL -]; \ No newline at end of file +]; +?> \ No newline at end of file diff --git a/controllers/DashboardController.php b/controllers/DashboardController.php index 32f9355..b0c7dd9 100644 --- a/controllers/DashboardController.php +++ b/controllers/DashboardController.php @@ -3,8 +3,10 @@ require_once 'models/TicketModel.php'; class DashboardController { private $ticketModel; + private $conn; public function __construct($conn) { + $this->conn = $conn; $this->ticketModel = new TicketModel($conn); } @@ -12,12 +14,27 @@ class DashboardController { // Get query parameters $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; $limit = isset($_COOKIE['ticketsPerPage']) ? (int)$_COOKIE['ticketsPerPage'] : 15; - $status = isset($_GET['status']) ? $_GET['status'] : 'Open'; - $sortColumn = isset($_COOKIE['defaultSortColumn']) ? $_COOKIE['defaultSortColumn'] : 'ticket_id'; - $sortDirection = isset($_COOKIE['sortDirection']) ? $_COOKIE['sortDirection'] : 'desc'; + $sortColumn = isset($_GET['sort']) ? $_GET['sort'] : 'ticket_id'; + $sortDirection = isset($_GET['dir']) ? $_GET['dir'] : 'desc'; + $category = isset($_GET['category']) ? $_GET['category'] : null; + $type = isset($_GET['type']) ? $_GET['type'] : null; - // Get tickets with pagination - $result = $this->ticketModel->getAllTickets($page, $limit, $status, $sortColumn, $sortDirection); + // Handle status filtering + $status = null; + if (isset($_GET['status']) && !empty($_GET['status'])) { + $status = $_GET['status']; + } else if (!isset($_GET['show_all'])) { + // Default: show Open and In Progress (exclude Closed) + $status = 'Open,In Progress'; + } + // If $_GET['show_all'] exists or no status param with show_all, show all tickets (status = null) + + // Get tickets with pagination and sorting + $result = $this->ticketModel->getAllTickets($page, $limit, $status, $sortColumn, $sortDirection, $category, $type); + + // Get categories and types for filters + $categories = $this->getCategories(); + $types = $this->getTypes(); // Extract data for the view $tickets = $result['tickets']; @@ -27,4 +44,25 @@ class DashboardController { // Load the dashboard view include 'views/DashboardView.php'; } -} \ No newline at end of file + + private function getCategories() { + $sql = "SELECT DISTINCT category FROM tickets WHERE category IS NOT NULL ORDER BY category"; + $result = $this->conn->query($sql); + $categories = []; + while($row = $result->fetch_assoc()) { + $categories[] = $row['category']; + } + return $categories; + } + + private function getTypes() { + $sql = "SELECT DISTINCT type FROM tickets WHERE type IS NOT NULL ORDER BY type"; + $result = $this->conn->query($sql); + $types = []; + while($row = $result->fetch_assoc()) { + $types[] = $row['type']; + } + return $types; + } +} +?> \ No newline at end of file diff --git a/controllers/TicketController.php b/controllers/TicketController.php index 67207f3..2f8ea2b 100644 --- a/controllers/TicketController.php +++ b/controllers/TicketController.php @@ -6,10 +6,24 @@ require_once dirname(__DIR__) . '/models/CommentModel.php'; class TicketController { private $ticketModel; private $commentModel; + private $envVars; public function __construct($conn) { $this->ticketModel = new TicketModel($conn); $this->commentModel = new CommentModel($conn); + + // Load environment variables for Discord webhook + $envPath = dirname(__DIR__) . '/.env'; + $this->envVars = []; + if (file_exists($envPath)) { + $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + if (strpos($line, '=') !== false && strpos($line, '#') !== 0) { + list($key, $value) = explode('=', $line, 2); + $this->envVars[trim($key)] = trim($value); + } + } + } } public function view($id) { @@ -22,8 +36,8 @@ class TicketController { return; } - // Get comments for this ticket - $comments = $this->ticketModel->getTicketComments($id); + // Get comments for this ticket using CommentModel + $comments = $this->commentModel->getCommentsByTicketId($id); // Load the view include dirname(__DIR__) . '/views/TicketView.php'; @@ -51,8 +65,11 @@ class TicketController { $result = $this->ticketModel->createTicket($ticketData); if ($result['success']) { + // Send Discord webhook notification for new ticket + $this->sendDiscordWebhook($result['ticket_id'], $ticketData); + // Redirect to the new ticket - header("Location: /tinkertickets/ticket/" . $result['ticket_id']); + header("Location: " . $GLOBALS['config']['BASE_URL'] . "/ticket/" . $result['ticket_id']); exit; } else { $error = $result['error']; @@ -66,32 +83,17 @@ class TicketController { } public function update($id) { - // Debug function - $debug = function($message, $data = null) { - $log_message = date('Y-m-d H:i:s') . " - [Controller] " . $message; - if ($data !== null) { - $log_message .= ": " . (is_string($data) ? $data : json_encode($data)); - } - $log_message .= "\n"; - file_put_contents('/tmp/api_debug.log', $log_message, FILE_APPEND); - }; - // Check if this is an AJAX request - $debug("Request method", $_SERVER['REQUEST_METHOD']); if ($_SERVER['REQUEST_METHOD'] === 'POST') { // For AJAX requests, get JSON data $input = file_get_contents('php://input'); - $debug("Raw input", $input); $data = json_decode($input, true); - $debug("Decoded data", $data); // Add ticket_id to the data $data['ticket_id'] = $id; - $debug("Added ticket_id to data", $id); // Validate input data if (empty($data['title'])) { - $debug("Title is empty"); header('Content-Type: application/json'); echo json_encode([ 'success' => false, @@ -101,26 +103,16 @@ class TicketController { } // Update ticket - $debug("Calling model updateTicket method"); - try { - $result = $this->ticketModel->updateTicket($data); - $debug("Model updateTicket result", $result); - } catch (Exception $e) { - $debug("Exception in model updateTicket", $e->getMessage()); - $debug("Stack trace", $e->getTraceAsString()); - throw $e; - } + $result = $this->ticketModel->updateTicket($data); // Return JSON response header('Content-Type: application/json'); if ($result) { - $debug("Update successful, sending success response"); echo json_encode([ 'success' => true, 'status' => $data['status'] ]); } else { - $debug("Update failed, sending error response"); echo json_encode([ 'success' => false, 'error' => 'Failed to update ticket' @@ -128,29 +120,90 @@ class TicketController { } } else { // For direct access, redirect to view - $debug("Not a POST request, redirecting"); header("Location: " . $GLOBALS['config']['BASE_URL'] . "/ticket/$id"); exit; } } - public function index() { - // Get query parameters - $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; - $limit = isset($_COOKIE['ticketsPerPage']) ? (int)$_COOKIE['ticketsPerPage'] : 15; - $status = isset($_GET['status']) ? $_GET['status'] : 'Open'; - $sortColumn = isset($_COOKIE['defaultSortColumn']) ? $_COOKIE['defaultSortColumn'] : 'ticket_id'; - $sortDirection = isset($_COOKIE['sortDirection']) ? $_COOKIE['sortDirection'] : 'desc'; + private function sendDiscordWebhook($ticketId, $ticketData) { + if (!isset($this->envVars['DISCORD_WEBHOOK_URL']) || empty($this->envVars['DISCORD_WEBHOOK_URL'])) { + error_log("Discord webhook URL not configured, skipping webhook for ticket creation"); + return; + } - // Get tickets with pagination - $result = $this->ticketModel->getAllTickets($page, $limit, $status, $sortColumn, $sortDirection); + $webhookUrl = $this->envVars['DISCORD_WEBHOOK_URL']; - // Extract data for the view - $tickets = $result['tickets']; - $totalTickets = $result['total']; - $totalPages = $result['pages']; + // Create ticket URL + $ticketUrl = "http://tinkertickets.local/ticket/$ticketId"; - // Load the dashboard view - include 'views/DashboardView.php'; + // Map priorities to Discord colors + $priorityColors = [ + 1 => 0xff4d4d, // Red + 2 => 0xffa726, // Orange + 3 => 0x42a5f5, // Blue + 4 => 0x66bb6a, // Green + 5 => 0x9e9e9e // Gray + ]; + + $priority = (int)($ticketData['priority'] ?? 4); + $color = $priorityColors[$priority] ?? 0x3498db; + + $embed = [ + 'title' => '๐ŸŽซ New Ticket Created', + 'description' => "**#{$ticketId}** - " . $ticketData['title'], + 'url' => $ticketUrl, + 'color' => $color, + 'fields' => [ + [ + 'name' => 'Priority', + 'value' => 'P' . $priority, + 'inline' => true + ], + [ + 'name' => 'Category', + 'value' => $ticketData['category'] ?? 'General', + 'inline' => true + ], + [ + 'name' => 'Type', + 'value' => $ticketData['type'] ?? 'Issue', + 'inline' => true + ], + [ + 'name' => 'Status', + 'value' => $ticketData['status'] ?? 'Open', + 'inline' => true + ] + ], + 'footer' => [ + 'text' => 'Tinker Tickets' + ], + 'timestamp' => date('c') + ]; + + $payload = [ + 'embeds' => [$embed] + ]; + + // Send webhook + $ch = curl_init($webhookUrl); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + $webhookResult = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($curlError) { + error_log("Discord webhook cURL error: $curlError"); + } else { + error_log("Discord webhook sent for new ticket. HTTP Code: $httpCode"); + } } -} \ No newline at end of file +} +?> \ No newline at end of file diff --git a/create_ticket_api.php b/create_ticket_api.php index d91b63d..b65b274 100644 --- a/create_ticket_api.php +++ b/create_ticket_api.php @@ -180,7 +180,7 @@ $discord_data = [ "embeds" => [[ "title" => "New Ticket Created: #" . $ticket_id, "description" => $title, - "url" => "http://10.10.10.45/ticket.php?id=" . $ticket_id, + "url" => "http://tinkertickets.local/ticket/" . $ticket_id, "color" => $priorityColors[$priority], "fields" => [ ["name" => "Priority", "value" => $priority, "inline" => true], diff --git a/index.php b/index.php index 7bfbdf4..80454c8 100644 --- a/index.php +++ b/index.php @@ -2,52 +2,64 @@ // Main entry point for the application require_once 'config/config.php'; -// Parse the URL +// Parse the URL - no need to remove base path since we're at document root $request = $_SERVER['REQUEST_URI']; -$basePath = '/tinkertickets'; // Adjust based on your installation path -$request = str_replace($basePath, '', $request); -// Create database connection -$conn = new mysqli( - $GLOBALS['config']['DB_HOST'], - $GLOBALS['config']['DB_USER'], - $GLOBALS['config']['DB_PASS'], - $GLOBALS['config']['DB_NAME'] -); +// Remove query string for routing (but keep it available) +$requestPath = strtok($request, '?'); + +// Create database connection for non-API routes +if (!str_starts_with($requestPath, '/api/')) { + $conn = new mysqli( + $GLOBALS['config']['DB_HOST'], + $GLOBALS['config']['DB_USER'], + $GLOBALS['config']['DB_PASS'], + $GLOBALS['config']['DB_NAME'] + ); + + if ($conn->connect_error) { + die("Connection failed: " . $conn->connect_error); + } +} // Simple router switch (true) { - case $request == '/' || $request == '': + case $requestPath == '/' || $requestPath == '': require_once 'controllers/DashboardController.php'; $controller = new DashboardController($conn); $controller->index(); break; - case preg_match('/^\/ticket\?id=(\d+)$/', $request, $matches) || - preg_match('/^\/ticket\/(\d+)$/', $request, $matches): + case preg_match('/^\/ticket\/(\d+)$/', $requestPath, $matches): require_once 'controllers/TicketController.php'; $controller = new TicketController($conn); $controller->view($matches[1]); break; - case $request == '/ticket/create': + case $requestPath == '/ticket/create': require_once 'controllers/TicketController.php'; $controller = new TicketController($conn); $controller->create(); break; - case preg_match('/^\/ticket\/(\d+)\/update$/', $request, $matches): - require_once 'controllers/TicketController.php'; - $controller = new TicketController($conn); - $controller->update($matches[1]); + // API Routes - these handle their own database connections + case $requestPath == '/api/update_ticket.php': + require_once 'api/update_ticket.php'; break; - case preg_match('/^\/ticket\/(\d+)\/comment$/', $request, $matches): - require_once 'controllers/CommentController.php'; - $controller = new CommentController($conn); - $controller->addComment($matches[1]); + case $requestPath == '/api/add_comment.php': + require_once 'api/add_comment.php'; break; + // Legacy support for old URLs + case $requestPath == '/dashboard.php': + header("Location: /"); + exit; + + case preg_match('/^\/ticket\.php/', $requestPath) && isset($_GET['id']): + header("Location: /ticket/" . $_GET['id']); + exit; + default: // 404 Not Found header("HTTP/1.0 404 Not Found"); @@ -55,4 +67,8 @@ switch (true) { break; } -$conn->close(); \ No newline at end of file +// Close database connection if it was opened +if (isset($conn)) { + $conn->close(); +} +?> \ No newline at end of file diff --git a/models/CommentModel.php b/models/CommentModel.php index 47a74f4..5455210 100644 --- a/models/CommentModel.php +++ b/models/CommentModel.php @@ -9,7 +9,7 @@ class CommentModel { public function getCommentsByTicketId($ticketId) { $sql = "SELECT * FROM ticket_comments WHERE ticket_id = ? ORDER BY created_at DESC"; $stmt = $this->conn->prepare($sql); - $stmt->bind_param("i", $ticketId); + $stmt->bind_param("s", $ticketId); // Changed to string since ticket_id is varchar $stmt->execute(); $result = $stmt->get_result(); @@ -31,11 +31,14 @@ class CommentModel { $username = $commentData['user_name'] ?? 'User'; $markdownEnabled = isset($commentData['markdown_enabled']) && $commentData['markdown_enabled'] ? 1 : 0; + // Preserve line breaks in the comment text + $commentText = $commentData['comment_text']; + $stmt->bind_param( "sssi", $ticketId, $username, - $commentData['comment_text'], + $commentText, $markdownEnabled ); @@ -44,7 +47,8 @@ class CommentModel { 'success' => true, 'user_name' => $username, 'created_at' => date('M d, Y H:i'), - 'markdown_enabled' => $markdownEnabled + 'markdown_enabled' => $markdownEnabled, + 'comment_text' => $commentText ]; } else { return [ @@ -53,4 +57,5 @@ class CommentModel { ]; } } -} \ No newline at end of file +} +?> \ No newline at end of file diff --git a/models/TicketModel.php b/models/TicketModel.php index 7cefcbf..4bb1e2e 100644 --- a/models/TicketModel.php +++ b/models/TicketModel.php @@ -35,16 +35,45 @@ class TicketModel { return $comments; } - public function getAllTickets($page = 1, $limit = 15, $status = 'Open', $sortColumn = 'ticket_id', $sortDirection = 'desc') { + public function getAllTickets($page = 1, $limit = 15, $status = 'Open', $sortColumn = 'ticket_id', $sortDirection = 'desc', $category = null, $type = null) { // Calculate offset $offset = ($page - 1) * $limit; - // Build WHERE clause for status filtering - $whereClause = ""; + // Build WHERE clause + $whereConditions = []; + $params = []; + $paramTypes = ''; + + // Status filtering if ($status) { $statuses = explode(',', $status); $placeholders = str_repeat('?,', count($statuses) - 1) . '?'; - $whereClause = "WHERE status IN ($placeholders)"; + $whereConditions[] = "status IN ($placeholders)"; + $params = array_merge($params, $statuses); + $paramTypes .= str_repeat('s', count($statuses)); + } + + // 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)); + } + + // 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)); + } + + $whereClause = ''; + if (!empty($whereConditions)) { + $whereClause = 'WHERE ' . implode(' AND ', $whereConditions); } // Validate sort column to prevent SQL injection @@ -60,8 +89,8 @@ class TicketModel { $countSql = "SELECT COUNT(*) as total FROM tickets $whereClause"; $countStmt = $this->conn->prepare($countSql); - if ($status) { - $countStmt->bind_param(str_repeat('s', count($statuses)), ...$statuses); + if (!empty($params)) { + $countStmt->bind_param($paramTypes, ...$params); } $countStmt->execute(); @@ -72,12 +101,13 @@ class TicketModel { $sql = "SELECT * FROM tickets $whereClause ORDER BY $sortColumn $sortDirection LIMIT ? OFFSET ?"; $stmt = $this->conn->prepare($sql); - if ($status) { - $types = str_repeat('s', count($statuses)) . 'ii'; - $params = array_merge($statuses, [$limit, $offset]); - $stmt->bind_param($types, ...$params); - } else { - $stmt->bind_param("ii", $limit, $offset); + // Add limit and offset parameters + $params[] = $limit; + $params[] = $offset; + $paramTypes .= 'ii'; + + if (!empty($params)) { + $stmt->bind_param($paramTypes, ...$params); } $stmt->execute(); diff --git a/views/DashboardView.php b/views/DashboardView.php index 48f1ad5..0bfe6f7 100644 --- a/views/DashboardView.php +++ b/views/DashboardView.php @@ -1,6 +1,6 @@ @@ -12,10 +12,10 @@ - +

Tinker Tickets

- +
@@ -25,20 +25,28 @@
@@ -54,14 +62,30 @@ - - - - - - - - + 'Ticket ID', + 'priority' => 'Priority', + 'title' => 'Title', + 'category' => 'Category', + 'type' => 'Type', + 'status' => 'Status', + 'created_at' => 'Created', + 'updated_at' => 'Updated' + ]; + + foreach($columns as $col => $label) { + $newDir = ($currentSort === $col && $currentDir === 'asc') ? 'desc' : 'asc'; + $sortClass = ($currentSort === $col) ? "sort-$currentDir" : ''; + $sortParams = array_merge($_GET, ['sort' => $col, 'dir' => $newDir]); + $sortUrl = '?' . http_build_query($sortParams); + + echo ""; + } + ?> @@ -69,12 +93,12 @@ if (count($tickets) > 0) { foreach($tickets as $row) { echo ""; - echo ""; + echo ""; echo ""; echo ""; echo ""; echo ""; - echo ""; + echo ""; echo ""; echo ""; echo ""; @@ -85,15 +109,5 @@ ?>
Ticket IDPriorityTitleCategoryTypeStatusCreatedUpdated$label
{$row['ticket_id']}{$row['ticket_id']}{$row['priority']}" . htmlspecialchars($row['title']) . "{$row['category']}{$row['type']}{$row['status']}{$row['status']}" . date('Y-m-d H:i', strtotime($row['created_at'])) . "" . date('Y-m-d H:i', strtotime($row['updated_at'])) . "
- - \ No newline at end of file diff --git a/views/TicketView.php b/views/TicketView.php index 8b566a2..b8ef1d1 100644 --- a/views/TicketView.php +++ b/views/TicketView.php @@ -34,7 +34,7 @@
UUID
- "> + "> ">P
@@ -86,14 +86,16 @@ foreach ($comments as $comment) { echo "
"; echo "
"; - echo "{$comment['user_name']}"; + echo "" . htmlspecialchars($comment['user_name']) . ""; echo "" . date('M d, Y H:i', strtotime($comment['created_at'])) . ""; echo "
"; echo "
"; if ($comment['markdown_enabled']) { + // For markdown comments, use JavaScript to render echo ""; } else { - echo htmlspecialchars($comment['comment_text']); + // For non-markdown comments, convert line breaks to
and escape HTML + echo nl2br(htmlspecialchars($comment['comment_text'])); } echo "
"; echo "
"; @@ -103,7 +105,7 @@
+ \ No newline at end of file