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 = `
-
-
-
-
-
-
Rows per Page
-
- 15
- 25
- 50
- 100
-
-
-
-
-
- `;
-
- // 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 = `
+
+
+
+
+
Rows per Page
+
+ 15
+ 25
+ 50
+ 100
+
+
+
+
+
+ `;
-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() {
-
+
`;
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 @@
-
+
@@ -25,20 +25,28 @@
@@ -54,14 +62,30 @@
- Ticket ID
- Priority
- Title
- Category
- Type
- Status
- Created
- Updated
+ '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 "$label ";
+ }
+ ?>
@@ -69,12 +93,12 @@
if (count($tickets) > 0) {
foreach($tickets as $row) {
echo "";
- echo "{$row['ticket_id']} ";
+ echo "{$row['ticket_id']} ";
echo "{$row['priority']} ";
echo "" . htmlspecialchars($row['title']) . " ";
echo "{$row['category']} ";
echo "{$row['type']} ";
- echo "{$row['status']} ";
+ echo "{$row['status']} ";
echo "" . date('Y-m-d H:i', strtotime($row['created_at'])) . " ";
echo "" . date('Y-m-d H:i', strtotime($row['updated_at'])) . " ";
echo " ";
@@ -85,15 +109,5 @@
?>
-
-
\ 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
+