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() {
-
+
`;
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 = `
-
- `;
- 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
+
+
+
+
+
+
+
+
+
\ 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
+
+
+
+
+
+
+
+
+
+
+
+
+ | Ticket ID |
+ Priority |
+ Title |
+ Category |
+ Type |
+ Status |
+ Created |
+ Updated |
+
+
+
+ 0) {
+ foreach($tickets as $row) {
+ echo "";
+ echo "| {$row['ticket_id']} | ";
+ echo "{$row['priority']} | ";
+ echo "" . htmlspecialchars($row['title']) . " | ";
+ echo "{$row['category']} | ";
+ echo "{$row['type']} | ";
+ 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 "
";
+ }
+ } else {
+ echo "| 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 #
+
+
+
+
+
+
+
+
+
+ ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file