From f8ada1d6d1c3bed1097ec582cf910248689dfa26 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 16 May 2025 20:02:49 -0400 Subject: [PATCH] Re-did everything, Now is modulaar and better bro. --- add_comment.php | 50 ------ api/add_comment.php | 69 +++++++++ api/update_ticket.php | 141 +++++++++++++++++ assets/js/dashboard.js | 42 +++-- assets/js/ticket.js | 172 ++++++++++++--------- config/config.php | 15 ++ controllers/CommentController.php | 43 ++++++ controllers/DashboardController.php | 30 ++++ controllers/TicketController.php | 156 +++++++++++++++++++ index.php | 58 +++++++ models/CommentModel.php | 56 +++++++ models/TicketModel.php | 230 ++++++++++++++++++++++++++++ update_ticket.php | 56 ------- views/CreateTicketView.php | 84 ++++++++++ views/DashboardView.php | 99 ++++++++++++ views/TicketView.php | 120 +++++++++++++++ 16 files changed, 1234 insertions(+), 187 deletions(-) delete mode 100644 add_comment.php create mode 100644 api/add_comment.php create mode 100644 api/update_ticket.php create mode 100644 config/config.php create mode 100644 controllers/CommentController.php create mode 100644 controllers/DashboardController.php create mode 100644 controllers/TicketController.php create mode 100644 index.php create mode 100644 models/CommentModel.php create mode 100644 models/TicketModel.php delete mode 100644 update_ticket.php create mode 100644 views/CreateTicketView.php create mode 100644 views/DashboardView.php create mode 100644 views/TicketView.php diff --git a/add_comment.php b/add_comment.php deleted file mode 100644 index 78cfdc9..0000000 --- a/add_comment.php +++ /dev/null @@ -1,50 +0,0 @@ -prepare($sql); - -// Convert markdown_enabled to integer for database -$markdownEnabled = $data['markdown_enabled'] ? 1 : 0; - -$stmt->bind_param("sssi", - $data['ticket_id'], - $username, - $data['comment_text'], - $markdownEnabled -); - -if ($stmt->execute()) { - header('Content-Type: application/json'); - echo json_encode([ - 'success' => true, - 'user_name' => $username, - 'created_at' => date('M d, Y H:i'), - 'markdown_enabled' => $markdownEnabled - ]); -} else { - echo json_encode([ - 'success' => false, - 'error' => $conn->error - ]); -} - -$stmt->close(); -$conn->close(); diff --git a/api/add_comment.php b/api/add_comment.php new file mode 100644 index 0000000..5f28ab2 --- /dev/null +++ b/api/add_comment.php @@ -0,0 +1,69 @@ +connect_error) { + throw new Exception("Database connection failed: " . $conn->connect_error); + } + + // Get POST data + $data = json_decode(file_get_contents('php://input'), true); + + if (!$data) { + throw new Exception("Invalid JSON data received"); + } + + $ticketId = $data['ticket_id']; + + // Initialize CommentModel directly + $commentModel = new CommentModel($conn); + + // Add comment + $result = $commentModel->addComment($ticketId, $data); + + // Discard any unexpected output + ob_end_clean(); + + // Return JSON response + header('Content-Type: application/json'); + echo json_encode($result); + +} catch (Exception $e) { + // Discard any unexpected output + ob_end_clean(); + + // Return error response + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); +} \ No newline at end of file diff --git a/api/update_ticket.php b/api/update_ticket.php new file mode 100644 index 0000000..e385a85 --- /dev/null +++ b/api/update_ticket.php @@ -0,0 +1,141 @@ +ticketModel = new TicketModel($conn); + $this->commentModel = new CommentModel($conn); + } + + public function update($id, $data) { + // Add ticket_id to the data + $data['ticket_id'] = $id; + + // Validate input data + if (empty($data['title'])) { + return [ + 'success' => false, + 'error' => 'Title cannot be empty' + ]; + } + + // Update ticket + $result = $this->ticketModel->updateTicket($data); + + if ($result) { + return [ + 'success' => true, + 'status' => $data['status'] + ]; + } else { + return [ + 'success' => false, + 'error' => 'Failed to update ticket' + ]; + } + } + } + + debug_log("Controller defined successfully"); + + // Create database connection + debug_log("Creating database connection"); + $conn = new mysqli( + $GLOBALS['config']['DB_HOST'], + $GLOBALS['config']['DB_USER'], + $GLOBALS['config']['DB_PASS'], + $GLOBALS['config']['DB_NAME'] + ); + + if ($conn->connect_error) { + throw new Exception("Database connection failed: " . $conn->connect_error); + } + debug_log("Database connection successful"); + + // Get POST data + $input = file_get_contents('php://input'); + $data = json_decode($input, true); + debug_log("Received data: " . json_encode($data)); + + if (!$data) { + throw new Exception("Invalid JSON data received: " . $input); + } + + if (!isset($data['ticket_id'])) { + throw new Exception("Missing ticket_id parameter"); + } + + $ticketId = $data['ticket_id']; + debug_log("Processing ticket ID: $ticketId"); + + // Initialize controller + debug_log("Initializing controller"); + $controller = new ApiTicketController($conn); + debug_log("Controller initialized"); + + // Update ticket + debug_log("Calling controller update method"); + $result = $controller->update($ticketId, $data); + debug_log("Update completed with result: " . json_encode($result)); + + // 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"); + +} catch (Exception $e) { + debug_log("Error: " . $e->getMessage()); + debug_log("Stack trace: " . $e->getTraceAsString()); + + // Discard any output that might have been generated + ob_end_clean(); + + // Return error response + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); + debug_log("Error response sent"); +} diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index 358b3dc..45f0cfe 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -446,9 +446,14 @@ function toggleHamburgerEditMode() { 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); + editables.forEach(field => { + field.disabled = false; + // Store original values for potential cancel + field.dataset.originalValue = field.value; + }); // Create and append cancel button only if it doesn't exist if (!cancelButton) { @@ -460,34 +465,46 @@ function toggleHamburgerEditMode() { editButton.parentNode.appendChild(newCancelButton); } } else { + // Save changes saveHamburgerChanges(); } } function saveHamburgerChanges() { - saveTicket(); - resetHamburgerEditMode(); + try { + saveTicket(); + resetHamburgerEditMode(); + } catch (error) { + console.error('Error saving changes:', error); + } } function cancelHamburgerEdit() { - resetHamburgerEditMode(); - // Reload the selects to revert changes - const selects = document.querySelectorAll('.hamburger-content select'); - selects.forEach(select => { - select.value = select.dataset.originalValue; + // 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; + } }); - + + 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.onclick = toggleHamburgerEditMode; // Restore original onclick - editButton.classList.remove('active'); + editButton.classList.remove('editing'); + + // Disable all editable fields editables.forEach(field => field.disabled = true); + + // Remove cancel button if it exists if (cancelButton) cancelButton.remove(); } @@ -495,7 +512,8 @@ function createHamburgerMenu() { const hamburgerMenu = document.createElement('div'); hamburgerMenu.className = 'hamburger-menu'; - const isTicketPage = window.location.pathname.includes('ticket.php'); + const isTicketPage = window.location.pathname.includes('ticket.php') || + window.location.pathname.includes('/ticket/'); if (isTicketPage && window.ticketData) { // Use the ticket data from the global variable diff --git a/assets/js/ticket.js b/assets/js/ticket.js index 145e5ff..a5280ea 100644 --- a/assets/js/ticket.js +++ b/assets/js/ticket.js @@ -1,7 +1,20 @@ function saveTicket() { const editables = document.querySelectorAll('.editable'); const data = {}; - const ticketId = window.location.href.split('id=')[1]; + + // Extract ticket ID from URL (works with both old and new URL formats) + 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; + } + + if (!ticketId) { + console.error('Could not determine ticket ID'); + return; + } editables.forEach(field => { if (field.dataset.field) { @@ -9,7 +22,16 @@ function saveTicket() { } }); - fetch('update_ticket.php', { + // 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: { 'Content-Type': 'application/json' @@ -19,13 +41,31 @@ function saveTicket() { ...data }) }) - .then(response => response.json()) + .then(response => { + console.log('Response status:', response.status); + if (!response.ok) { + return response.text().then(text => { + console.error('Server response:', text); + throw new Error('Network response was not ok'); + }); + } + return response.json(); + }) .then(data => { + console.log('Response data:', data); if(data.success) { const statusDisplay = document.getElementById('statusDisplay'); - statusDisplay.className = `status-${data.status}`; - statusDisplay.textContent = data.status; + if (statusDisplay) { + statusDisplay.className = `status-${data.status}`; + statusDisplay.textContent = data.status; + } + console.log('Ticket updated successfully'); + } else { + console.error('Error in API response:', data.error || 'Unknown error'); } + }) + .catch(error => { + console.error('Error updating ticket:', error); }); } @@ -55,19 +95,47 @@ function toggleEditMode() { function addComment() { const commentText = document.getElementById('newComment').value; - const ticketId = window.location.href.split('id=')[1]; + if (!commentText.trim()) { + console.error('Comment text cannot be empty'); + return; + } + + // Extract ticket ID from URL (works with both old and new URL formats) + 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; + } + + if (!ticketId) { + console.error('Could not determine ticket ID'); + return; + } + + const isMarkdownEnabled = document.getElementById('markdownMaster').checked; - fetch('add_comment.php', { + fetch('/api/add_comment.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ticket_id: ticketId, - comment_text: commentText + comment_text: commentText, + markdown_enabled: isMarkdownEnabled }) }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + return response.text().then(text => { + console.error('Server response:', text); + throw new Error('Network response was not ok'); + }); + } + return response.json(); + }) .then(data => { if(data.success) { // Clear the comment box @@ -81,11 +149,18 @@ function addComment() { ${data.user_name} ${data.created_at} -
${commentText}
+
+ ${isMarkdownEnabled ? marked.parse(commentText) : commentText} +
`; commentsList.insertAdjacentHTML('afterbegin', newComment); + } else { + console.error('Error adding comment:', data.error || 'Unknown error'); } + }) + .catch(error => { + console.error('Error adding comment:', error); }); } @@ -121,71 +196,25 @@ function toggleMarkdownMode() { } } -function addComment() { - const commentText = document.getElementById('newComment').value; - const isMarkdownEnabled = document.getElementById('markdownMaster').checked; - const ticketId = window.location.href.split('id=')[1]; - - fetch('add_comment.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - ticket_id: ticketId, - comment_text: commentText, - markdown_enabled: isMarkdownEnabled - }) - }) - .then(response => response.json()) - .then(data => { - if(data.success) { - const commentsList = document.querySelector('.comments-list'); - const newCommentHtml = ` -
-
- ${data.user_name} - ${data.created_at} -
-
- ${isMarkdownEnabled ? marked.parse(commentText) : commentText} -
-
- `; - commentsList.insertAdjacentHTML('afterbegin', newCommentHtml); - document.getElementById('newComment').value = ''; - } - }); -} - document.addEventListener('DOMContentLoaded', function() { // Show description tab by default showTab('description'); - // Add the auto-resize functionality here // Auto-resize the description textarea to fit content const descriptionTextarea = document.querySelector('textarea[data-field="description"]'); - - function autoResizeTextarea() { - // Reset height to auto to get the correct scrollHeight - descriptionTextarea.style.height = 'auto'; - // Set the height to match the scrollHeight - descriptionTextarea.style.height = descriptionTextarea.scrollHeight + 'px'; - } - - // Initial resize - autoResizeTextarea(); - - // Resize on input when in edit mode - descriptionTextarea.addEventListener('input', autoResizeTextarea); - - // Also resize when edit mode is toggled - const originalToggleEditMode = window.toggleEditMode; - if (typeof originalToggleEditMode === 'function') { - window.toggleEditMode = function() { - originalToggleEditMode.apply(this, arguments); - setTimeout(autoResizeTextarea, 0); - }; + if (descriptionTextarea) { + function autoResizeTextarea() { + // Reset height to auto to get the correct scrollHeight + descriptionTextarea.style.height = 'auto'; + // Set the height to match the scrollHeight + descriptionTextarea.style.height = descriptionTextarea.scrollHeight + 'px'; + } + + // Initial resize + autoResizeTextarea(); + + // Resize on input when in edit mode + descriptionTextarea.addEventListener('input', autoResizeTextarea); } }); @@ -194,6 +223,11 @@ function showTab(tabName) { const descriptionTab = document.getElementById('description-tab'); const commentsTab = document.getElementById('comments-tab'); + if (!descriptionTab || !commentsTab) { + console.error('Tab elements not found'); + return; + } + // Hide both tabs descriptionTab.style.display = 'none'; commentsTab.style.display = 'none'; diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..962a570 --- /dev/null +++ b/config/config.php @@ -0,0 +1,15 @@ + $envVars['DB_HOST'] ?? 'localhost', + 'DB_USER' => $envVars['DB_USER'] ?? 'root', + 'DB_PASS' => $envVars['DB_PASS'] ?? '', + 'DB_NAME' => $envVars['DB_NAME'] ?? 'tinkertickets', + 'BASE_URL' => '/tinkertickets', // Application base URL + 'ASSETS_URL' => '/assets', // Assets URL + 'API_URL' => '/api' // API URL +]; \ No newline at end of file diff --git a/controllers/CommentController.php b/controllers/CommentController.php new file mode 100644 index 0000000..2e1b99c --- /dev/null +++ b/controllers/CommentController.php @@ -0,0 +1,43 @@ +commentModel = new CommentModel($conn); + } + + public function getCommentsByTicketId($ticketId) { + return $this->commentModel->getCommentsByTicketId($ticketId); + } + + public function addComment($ticketId) { + // Check if this is an AJAX request + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + // Get JSON data + $data = json_decode(file_get_contents('php://input'), true); + + // Validate input + if (empty($data['comment_text'])) { + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'error' => 'Comment text cannot be empty' + ]); + return; + } + + // Add comment + $result = $this->commentModel->addComment($ticketId, $data); + + // Return JSON response + header('Content-Type: application/json'); + echo json_encode($result); + } else { + // For direct access, redirect to ticket view + header("Location: /tinkertickets/ticket/$ticketId"); + exit; + } + } +} \ No newline at end of file diff --git a/controllers/DashboardController.php b/controllers/DashboardController.php new file mode 100644 index 0000000..32f9355 --- /dev/null +++ b/controllers/DashboardController.php @@ -0,0 +1,30 @@ +ticketModel = new TicketModel($conn); + } + + 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'; + + // Get tickets with pagination + $result = $this->ticketModel->getAllTickets($page, $limit, $status, $sortColumn, $sortDirection); + + // Extract data for the view + $tickets = $result['tickets']; + $totalTickets = $result['total']; + $totalPages = $result['pages']; + + // Load the dashboard view + include 'views/DashboardView.php'; + } +} \ No newline at end of file diff --git a/controllers/TicketController.php b/controllers/TicketController.php new file mode 100644 index 0000000..67207f3 --- /dev/null +++ b/controllers/TicketController.php @@ -0,0 +1,156 @@ +ticketModel = new TicketModel($conn); + $this->commentModel = new CommentModel($conn); + } + + public function view($id) { + // Get ticket data + $ticket = $this->ticketModel->getTicketById($id); + + if (!$ticket) { + header("HTTP/1.0 404 Not Found"); + echo "Ticket not found"; + return; + } + + // Get comments for this ticket + $comments = $this->ticketModel->getTicketComments($id); + + // Load the view + include dirname(__DIR__) . '/views/TicketView.php'; + } + + public function create() { + // Check if form was submitted + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $ticketData = [ + 'title' => $_POST['title'] ?? '', + 'description' => $_POST['description'] ?? '', + 'priority' => $_POST['priority'] ?? '4', + 'category' => $_POST['category'] ?? 'General', + 'type' => $_POST['type'] ?? 'Issue' + ]; + + // Validate input + if (empty($ticketData['title'])) { + $error = "Title is required"; + include dirname(__DIR__) . '/views/CreateTicketView.php'; + return; + } + + // Create ticket + $result = $this->ticketModel->createTicket($ticketData); + + if ($result['success']) { + // Redirect to the new ticket + header("Location: /tinkertickets/ticket/" . $result['ticket_id']); + exit; + } else { + $error = $result['error']; + include dirname(__DIR__) . '/views/CreateTicketView.php'; + return; + } + } else { + // Display the create ticket form + include dirname(__DIR__) . '/views/CreateTicketView.php'; + } + } + + 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, + 'error' => 'Title cannot be empty' + ]); + return; + } + + // 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; + } + + // 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' + ]); + } + } 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'; + + // Get tickets with pagination + $result = $this->ticketModel->getAllTickets($page, $limit, $status, $sortColumn, $sortDirection); + + // Extract data for the view + $tickets = $result['tickets']; + $totalTickets = $result['total']; + $totalPages = $result['pages']; + + // Load the dashboard view + include 'views/DashboardView.php'; + } +} \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..7bfbdf4 --- /dev/null +++ b/index.php @@ -0,0 +1,58 @@ +index(); + break; + + case preg_match('/^\/ticket\?id=(\d+)$/', $request, $matches) || + preg_match('/^\/ticket\/(\d+)$/', $request, $matches): + require_once 'controllers/TicketController.php'; + $controller = new TicketController($conn); + $controller->view($matches[1]); + break; + + case $request == '/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]); + break; + + case preg_match('/^\/ticket\/(\d+)\/comment$/', $request, $matches): + require_once 'controllers/CommentController.php'; + $controller = new CommentController($conn); + $controller->addComment($matches[1]); + break; + + default: + // 404 Not Found + header("HTTP/1.0 404 Not Found"); + echo '404 Page Not Found'; + break; +} + +$conn->close(); \ No newline at end of file diff --git a/models/CommentModel.php b/models/CommentModel.php new file mode 100644 index 0000000..47a74f4 --- /dev/null +++ b/models/CommentModel.php @@ -0,0 +1,56 @@ +conn = $conn; + } + + 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->execute(); + $result = $stmt->get_result(); + + $comments = []; + while ($row = $result->fetch_assoc()) { + $comments[] = $row; + } + + return $comments; + } + + public function addComment($ticketId, $commentData) { + $sql = "INSERT INTO ticket_comments (ticket_id, user_name, comment_text, markdown_enabled) + VALUES (?, ?, ?, ?)"; + + $stmt = $this->conn->prepare($sql); + + // Set default username + $username = $commentData['user_name'] ?? 'User'; + $markdownEnabled = isset($commentData['markdown_enabled']) && $commentData['markdown_enabled'] ? 1 : 0; + + $stmt->bind_param( + "sssi", + $ticketId, + $username, + $commentData['comment_text'], + $markdownEnabled + ); + + if ($stmt->execute()) { + return [ + 'success' => true, + 'user_name' => $username, + 'created_at' => date('M d, Y H:i'), + 'markdown_enabled' => $markdownEnabled + ]; + } else { + return [ + 'success' => false, + 'error' => $this->conn->error + ]; + } + } +} \ No newline at end of file diff --git a/models/TicketModel.php b/models/TicketModel.php new file mode 100644 index 0000000..7cefcbf --- /dev/null +++ b/models/TicketModel.php @@ -0,0 +1,230 @@ +conn = $conn; + } + + public function getTicketById($id) { + $sql = "SELECT * FROM tickets WHERE ticket_id = ?"; + $stmt = $this->conn->prepare($sql); + $stmt->bind_param("i", $id); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows === 0) { + return null; + } + + return $result->fetch_assoc(); + } + + public function getTicketComments($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->execute(); + $result = $stmt->get_result(); + + $comments = []; + while ($row = $result->fetch_assoc()) { + $comments[] = $row; + } + + return $comments; + } + + public function getAllTickets($page = 1, $limit = 15, $status = 'Open', $sortColumn = 'ticket_id', $sortDirection = 'desc') { + // Calculate offset + $offset = ($page - 1) * $limit; + + // Build WHERE clause for status filtering + $whereClause = ""; + if ($status) { + $statuses = explode(',', $status); + $placeholders = str_repeat('?,', count($statuses) - 1) . '?'; + $whereClause = "WHERE status IN ($placeholders)"; + } + + // Validate sort column to prevent SQL injection + $allowedColumns = ['ticket_id', 'title', 'status', 'priority', 'category', 'type', 'created_at', 'updated_at']; + if (!in_array($sortColumn, $allowedColumns)) { + $sortColumn = 'ticket_id'; + } + + // Validate sort direction + $sortDirection = strtolower($sortDirection) === 'asc' ? 'ASC' : 'DESC'; + + // Get total count for pagination + $countSql = "SELECT COUNT(*) as total FROM tickets $whereClause"; + $countStmt = $this->conn->prepare($countSql); + + if ($status) { + $countStmt->bind_param(str_repeat('s', count($statuses)), ...$statuses); + } + + $countStmt->execute(); + $totalResult = $countStmt->get_result(); + $totalTickets = $totalResult->fetch_assoc()['total']; + + // Get tickets with pagination + $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); + } + + $stmt->execute(); + $result = $stmt->get_result(); + + $tickets = []; + while ($row = $result->fetch_assoc()) { + $tickets[] = $row; + } + + return [ + 'tickets' => $tickets, + 'total' => $totalTickets, + 'pages' => ceil($totalTickets / $limit), + 'current_page' => $page + ]; + } + + public function updateTicket($ticketData) { + // Debug function + $debug = function($message, $data = null) { + $log_message = date('Y-m-d H:i:s') . " - [Model] " . $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); + }; + + $debug("updateTicket called with data", $ticketData); + + $sql = "UPDATE tickets SET + title = ?, + priority = ?, + status = ?, + description = ?, + category = ?, + type = ?, + updated_at = NOW() + WHERE ticket_id = ?"; + + $debug("SQL query", $sql); + + try { + $stmt = $this->conn->prepare($sql); + if (!$stmt) { + $debug("Prepare statement failed", $this->conn->error); + return false; + } + + $debug("Binding parameters"); + $stmt->bind_param( + "sissssi", + $ticketData['title'], + $ticketData['priority'], + $ticketData['status'], + $ticketData['description'], + $ticketData['category'], + $ticketData['type'], + $ticketData['ticket_id'] + ); + + $debug("Executing statement"); + $result = $stmt->execute(); + + if (!$result) { + $debug("Execute failed", $stmt->error); + return false; + } + + $debug("Update successful"); + return true; + } catch (Exception $e) { + $debug("Exception", $e->getMessage()); + $debug("Stack trace", $e->getTraceAsString()); + throw $e; + } + } + + public function createTicket($ticketData) { + // Generate ticket ID (9-digit format with leading zeros) + $ticket_id = sprintf('%09d', mt_rand(1, 999999999)); + + $sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type) + VALUES (?, ?, ?, ?, ?, ?, ?)"; + + $stmt = $this->conn->prepare($sql); + + // Set default values if not provided + $status = $ticketData['status'] ?? 'Open'; + $priority = $ticketData['priority'] ?? '4'; + $category = $ticketData['category'] ?? 'General'; + $type = $ticketData['type'] ?? 'Issue'; + + $stmt->bind_param( + "sssssss", + $ticket_id, + $ticketData['title'], + $ticketData['description'], + $status, + $priority, + $category, + $type + ); + + if ($stmt->execute()) { + return [ + 'success' => true, + 'ticket_id' => $ticket_id + ]; + } else { + return [ + 'success' => false, + 'error' => $this->conn->error + ]; + } + } + + public function addComment($ticketId, $commentData) { + $sql = "INSERT INTO ticket_comments (ticket_id, user_name, comment_text, markdown_enabled) + VALUES (?, ?, ?, ?)"; + + $stmt = $this->conn->prepare($sql); + + // Set default username + $username = $commentData['user_name'] ?? 'User'; + $markdownEnabled = $commentData['markdown_enabled'] ? 1 : 0; + + $stmt->bind_param( + "sssi", + $ticketId, + $username, + $commentData['comment_text'], + $markdownEnabled + ); + + if ($stmt->execute()) { + return [ + 'success' => true, + 'user_name' => $username, + 'created_at' => date('M d, Y H:i') + ]; + } else { + return [ + 'success' => false, + 'error' => $this->conn->error + ]; + } + } +} \ No newline at end of file diff --git a/update_ticket.php b/update_ticket.php deleted file mode 100644 index 8db4e12..0000000 --- a/update_ticket.php +++ /dev/null @@ -1,56 +0,0 @@ -prepare($sql); -$stmt->bind_param( - "sisssss", - $data['title'], - $data['priority'], - $data['status'], - $data['description'], - $data['category'], - $data['type'], - $data['ticket_id'] -); - -// After successful update -if ($stmt->execute()) { - header('Content-Type: application/json'); - echo json_encode([ - 'success' => true, - 'status' => $data['status'] // Send back the new status - ]); -} else { - header('Content-Type: application/json'); - echo json_encode([ - 'success' => false, - 'error' => $conn->error - ]); -} - -$stmt->close(); -$conn->close(); diff --git a/views/CreateTicketView.php b/views/CreateTicketView.php new file mode 100644 index 0000000..8a4e88f --- /dev/null +++ b/views/CreateTicketView.php @@ -0,0 +1,84 @@ + + + + + + + Create New Ticket + + + + + + +
+
+

Create New Ticket

+
+ + +
+ + +
+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+ + \ No newline at end of file diff --git a/views/DashboardView.php b/views/DashboardView.php new file mode 100644 index 0000000..48f1ad5 --- /dev/null +++ b/views/DashboardView.php @@ -0,0 +1,99 @@ + + + + + + + Ticket Dashboard + + + + + +
+

Tinker Tickets

+ +
+ +
+
+ Total Tickets: +
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + 0) { + foreach($tickets as $row) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + } else { + echo ""; + } + ?> + +
Ticket IDPriorityTitleCategoryTypeStatusCreatedUpdated
{$row['ticket_id']}{$row['priority']}" . htmlspecialchars($row['title']) . "{$row['category']}{$row['type']}{$row['status']}" . date('Y-m-d H:i', strtotime($row['created_at'])) . "" . date('Y-m-d H:i', strtotime($row['updated_at'])) . "
No tickets found
+ + + + \ No newline at end of file diff --git a/views/TicketView.php b/views/TicketView.php new file mode 100644 index 0000000..8b566a2 --- /dev/null +++ b/views/TicketView.php @@ -0,0 +1,120 @@ + + + + + + + Ticket #<?php echo $ticket['ticket_id']; ?> + + + + + + + + + +
"> +
+

" data-field="title" disabled>

+
+
UUID
+
+
+ "> + ">P +
+ +
+
+
+
+
+ + +
+ +
+
+ + +
+
+ +
+
+

Comments

+
+ +
+
+
+ + Enable Markdown +
+
+ + Preview Markdown +
+
+ +
+ +
+
+
+ "; + echo "
"; + echo "{$comment['user_name']}"; + echo "" . date('M d, Y H:i', strtotime($comment['created_at'])) . ""; + echo "
"; + echo "
"; + if ($comment['markdown_enabled']) { + echo ""; + } else { + echo htmlspecialchars($comment['comment_text']); + } + echo "
"; + echo "
"; + } + ?> +
+
+
+ + + + + \ No newline at end of file