Re-did everything, Now is modulaar and better bro.
This commit is contained in:
@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Load environment variables
|
|
||||||
$envFile = __DIR__ . '/.env';
|
|
||||||
$envVars = parse_ini_file($envFile);
|
|
||||||
|
|
||||||
// Database connection
|
|
||||||
$conn = new mysqli(
|
|
||||||
$envVars['DB_HOST'],
|
|
||||||
$envVars['DB_USER'],
|
|
||||||
$envVars['DB_PASS'],
|
|
||||||
$envVars['DB_NAME']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get POST data
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
|
|
||||||
// Set default username (you can modify this based on your user system)
|
|
||||||
$username = "User";
|
|
||||||
|
|
||||||
// Prepare insert query
|
|
||||||
$sql = "INSERT INTO ticket_comments (ticket_id, user_name, comment_text, markdown_enabled) VALUES (?, ?, ?, ?)";
|
|
||||||
$stmt = $conn->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();
|
|
||||||
69
api/add_comment.php
Normal file
69
api/add_comment.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
// Disable error display in the output
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
// Start output buffering to capture any errors
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Include required files with proper error handling
|
||||||
|
$configPath = dirname(__DIR__) . '/config/config.php';
|
||||||
|
$commentModelPath = dirname(__DIR__) . '/models/CommentModel.php';
|
||||||
|
|
||||||
|
if (!file_exists($configPath)) {
|
||||||
|
throw new Exception("Config file not found: $configPath");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($commentModelPath)) {
|
||||||
|
throw new Exception("CommentModel file not found: $commentModelPath");
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once $configPath;
|
||||||
|
require_once $commentModelPath;
|
||||||
|
|
||||||
|
// Create 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
141
api/update_ticket.php
Normal file
141
api/update_ticket.php
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
// Enable error reporting for debugging
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', 0); // Don't display errors in the response
|
||||||
|
|
||||||
|
// Define a debug log function
|
||||||
|
function debug_log($message) {
|
||||||
|
file_put_contents('/tmp/api_debug.log', date('Y-m-d H:i:s') . " - $message\n", FILE_APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start output buffering to capture any errors
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
debug_log("Script started");
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
$configPath = dirname(__DIR__) . '/config/config.php';
|
||||||
|
debug_log("Loading config from: $configPath");
|
||||||
|
require_once $configPath;
|
||||||
|
debug_log("Config loaded successfully");
|
||||||
|
|
||||||
|
// Load models directly with absolute paths
|
||||||
|
$ticketModelPath = dirname(__DIR__) . '/models/TicketModel.php';
|
||||||
|
$commentModelPath = dirname(__DIR__) . '/models/CommentModel.php';
|
||||||
|
|
||||||
|
debug_log("Loading models from: $ticketModelPath and $commentModelPath");
|
||||||
|
require_once $ticketModelPath;
|
||||||
|
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
|
||||||
|
|
||||||
|
class ApiTicketController {
|
||||||
|
private $ticketModel;
|
||||||
|
private $commentModel;
|
||||||
|
|
||||||
|
public function __construct($conn) {
|
||||||
|
$this->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");
|
||||||
|
}
|
||||||
@ -446,9 +446,14 @@ function toggleHamburgerEditMode() {
|
|||||||
const isEditing = editButton.classList.contains('editing');
|
const isEditing = editButton.classList.contains('editing');
|
||||||
|
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
|
// Switch to edit mode
|
||||||
editButton.textContent = 'Save Changes';
|
editButton.textContent = 'Save Changes';
|
||||||
editButton.classList.add('editing');
|
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
|
// Create and append cancel button only if it doesn't exist
|
||||||
if (!cancelButton) {
|
if (!cancelButton) {
|
||||||
@ -460,34 +465,46 @@ function toggleHamburgerEditMode() {
|
|||||||
editButton.parentNode.appendChild(newCancelButton);
|
editButton.parentNode.appendChild(newCancelButton);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Save changes
|
||||||
saveHamburgerChanges();
|
saveHamburgerChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveHamburgerChanges() {
|
function saveHamburgerChanges() {
|
||||||
saveTicket();
|
try {
|
||||||
resetHamburgerEditMode();
|
saveTicket();
|
||||||
|
resetHamburgerEditMode();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving changes:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelHamburgerEdit() {
|
function cancelHamburgerEdit() {
|
||||||
resetHamburgerEditMode();
|
// Revert all fields to their original values
|
||||||
// Reload the selects to revert changes
|
const editables = document.querySelectorAll('.hamburger-content .editable');
|
||||||
const selects = document.querySelectorAll('.hamburger-content select');
|
editables.forEach(field => {
|
||||||
selects.forEach(select => {
|
if (field.dataset.originalValue) {
|
||||||
select.value = select.dataset.originalValue;
|
field.value = field.dataset.originalValue;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
resetHamburgerEditMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function resetHamburgerEditMode() {
|
function resetHamburgerEditMode() {
|
||||||
const editButton = document.getElementById('hamburgerEditButton');
|
const editButton = document.getElementById('hamburgerEditButton');
|
||||||
const cancelButton = document.getElementById('hamburgerCancelButton');
|
const cancelButton = document.getElementById('hamburgerCancelButton');
|
||||||
const editables = document.querySelectorAll('.hamburger-content .editable');
|
const editables = document.querySelectorAll('.hamburger-content .editable');
|
||||||
|
|
||||||
|
// Reset button text and remove editing class
|
||||||
editButton.textContent = 'Edit Ticket';
|
editButton.textContent = 'Edit Ticket';
|
||||||
editButton.onclick = toggleHamburgerEditMode; // Restore original onclick
|
editButton.classList.remove('editing');
|
||||||
editButton.classList.remove('active');
|
|
||||||
|
// Disable all editable fields
|
||||||
editables.forEach(field => field.disabled = true);
|
editables.forEach(field => field.disabled = true);
|
||||||
|
|
||||||
|
// Remove cancel button if it exists
|
||||||
if (cancelButton) cancelButton.remove();
|
if (cancelButton) cancelButton.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,7 +512,8 @@ function createHamburgerMenu() {
|
|||||||
const hamburgerMenu = document.createElement('div');
|
const hamburgerMenu = document.createElement('div');
|
||||||
hamburgerMenu.className = 'hamburger-menu';
|
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) {
|
if (isTicketPage && window.ticketData) {
|
||||||
// Use the ticket data from the global variable
|
// Use the ticket data from the global variable
|
||||||
|
|||||||
@ -1,7 +1,20 @@
|
|||||||
function saveTicket() {
|
function saveTicket() {
|
||||||
const editables = document.querySelectorAll('.editable');
|
const editables = document.querySelectorAll('.editable');
|
||||||
const data = {};
|
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 => {
|
editables.forEach(field => {
|
||||||
if (field.dataset.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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@ -19,13 +41,31 @@ function saveTicket() {
|
|||||||
...data
|
...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 => {
|
.then(data => {
|
||||||
|
console.log('Response data:', data);
|
||||||
if(data.success) {
|
if(data.success) {
|
||||||
const statusDisplay = document.getElementById('statusDisplay');
|
const statusDisplay = document.getElementById('statusDisplay');
|
||||||
statusDisplay.className = `status-${data.status}`;
|
if (statusDisplay) {
|
||||||
statusDisplay.textContent = data.status;
|
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() {
|
function addComment() {
|
||||||
const commentText = document.getElementById('newComment').value;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
fetch('add_comment.php', {
|
// 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('/api/add_comment.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
ticket_id: ticketId,
|
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 => {
|
.then(data => {
|
||||||
if(data.success) {
|
if(data.success) {
|
||||||
// Clear the comment box
|
// Clear the comment box
|
||||||
@ -81,11 +149,18 @@ function addComment() {
|
|||||||
<span class="comment-user">${data.user_name}</span>
|
<span class="comment-user">${data.user_name}</span>
|
||||||
<span class="comment-date">${data.created_at}</span>
|
<span class="comment-date">${data.created_at}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-text">${commentText}</div>
|
<div class="comment-text">
|
||||||
|
${isMarkdownEnabled ? marked.parse(commentText) : commentText}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
commentsList.insertAdjacentHTML('afterbegin', newComment);
|
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 = `
|
|
||||||
<div class="comment">
|
|
||||||
<div class="comment-header">
|
|
||||||
<span class="comment-user">${data.user_name}</span>
|
|
||||||
<span class="comment-date">${data.created_at}</span>
|
|
||||||
</div>
|
|
||||||
<div class="comment-text">
|
|
||||||
${isMarkdownEnabled ? marked.parse(commentText) : commentText}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
commentsList.insertAdjacentHTML('afterbegin', newCommentHtml);
|
|
||||||
document.getElementById('newComment').value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Show description tab by default
|
// Show description tab by default
|
||||||
showTab('description');
|
showTab('description');
|
||||||
|
|
||||||
// Add the auto-resize functionality here
|
|
||||||
// Auto-resize the description textarea to fit content
|
// Auto-resize the description textarea to fit content
|
||||||
const descriptionTextarea = document.querySelector('textarea[data-field="description"]');
|
const descriptionTextarea = document.querySelector('textarea[data-field="description"]');
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
function autoResizeTextarea() {
|
// Initial resize
|
||||||
// Reset height to auto to get the correct scrollHeight
|
autoResizeTextarea();
|
||||||
descriptionTextarea.style.height = 'auto';
|
|
||||||
// Set the height to match the scrollHeight
|
|
||||||
descriptionTextarea.style.height = descriptionTextarea.scrollHeight + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial resize
|
// Resize on input when in edit mode
|
||||||
autoResizeTextarea();
|
descriptionTextarea.addEventListener('input', 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);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -194,6 +223,11 @@ function showTab(tabName) {
|
|||||||
const descriptionTab = document.getElementById('description-tab');
|
const descriptionTab = document.getElementById('description-tab');
|
||||||
const commentsTab = document.getElementById('comments-tab');
|
const commentsTab = document.getElementById('comments-tab');
|
||||||
|
|
||||||
|
if (!descriptionTab || !commentsTab) {
|
||||||
|
console.error('Tab elements not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Hide both tabs
|
// Hide both tabs
|
||||||
descriptionTab.style.display = 'none';
|
descriptionTab.style.display = 'none';
|
||||||
commentsTab.style.display = 'none';
|
commentsTab.style.display = 'none';
|
||||||
|
|||||||
15
config/config.php
Normal file
15
config/config.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
// Load environment variables
|
||||||
|
$envFile = __DIR__ . '/../.env';
|
||||||
|
$envVars = parse_ini_file($envFile);
|
||||||
|
|
||||||
|
// Global configuration
|
||||||
|
$GLOBALS['config'] = [
|
||||||
|
'DB_HOST' => $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
|
||||||
|
];
|
||||||
43
controllers/CommentController.php
Normal file
43
controllers/CommentController.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'models/CommentModel.php';
|
||||||
|
|
||||||
|
class CommentController {
|
||||||
|
private $commentModel;
|
||||||
|
|
||||||
|
public function __construct($conn) {
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
controllers/DashboardController.php
Normal file
30
controllers/DashboardController.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'models/TicketModel.php';
|
||||||
|
|
||||||
|
class DashboardController {
|
||||||
|
private $ticketModel;
|
||||||
|
|
||||||
|
public function __construct($conn) {
|
||||||
|
$this->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';
|
||||||
|
}
|
||||||
|
}
|
||||||
156
controllers/TicketController.php
Normal file
156
controllers/TicketController.php
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
// Use absolute paths for model includes
|
||||||
|
require_once dirname(__DIR__) . '/models/TicketModel.php';
|
||||||
|
require_once dirname(__DIR__) . '/models/CommentModel.php';
|
||||||
|
|
||||||
|
class TicketController {
|
||||||
|
private $ticketModel;
|
||||||
|
private $commentModel;
|
||||||
|
|
||||||
|
public function __construct($conn) {
|
||||||
|
$this->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';
|
||||||
|
}
|
||||||
|
}
|
||||||
58
index.php
Normal file
58
index.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
// Main entry point for the application
|
||||||
|
require_once 'config/config.php';
|
||||||
|
|
||||||
|
// Parse the URL
|
||||||
|
$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']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simple router
|
||||||
|
switch (true) {
|
||||||
|
case $request == '/' || $request == '':
|
||||||
|
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):
|
||||||
|
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();
|
||||||
56
models/CommentModel.php
Normal file
56
models/CommentModel.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
class CommentModel {
|
||||||
|
private $conn;
|
||||||
|
|
||||||
|
public function __construct($conn) {
|
||||||
|
$this->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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
230
models/TicketModel.php
Normal file
230
models/TicketModel.php
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
<?php
|
||||||
|
class TicketModel {
|
||||||
|
private $conn;
|
||||||
|
|
||||||
|
public function __construct($conn) {
|
||||||
|
$this->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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Load environment variables
|
|
||||||
$envFile = __DIR__ . '/.env';
|
|
||||||
$envVars = parse_ini_file($envFile);
|
|
||||||
|
|
||||||
// Database connection
|
|
||||||
$conn = new mysqli(
|
|
||||||
$envVars['DB_HOST'],
|
|
||||||
$envVars['DB_USER'],
|
|
||||||
$envVars['DB_PASS'],
|
|
||||||
$envVars['DB_NAME']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get POST data
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
|
|
||||||
// Prepare update query
|
|
||||||
$sql = "UPDATE tickets SET
|
|
||||||
title = ?,
|
|
||||||
priority = ?,
|
|
||||||
status = ?,
|
|
||||||
description = ?,
|
|
||||||
category = ?,
|
|
||||||
type = ?,
|
|
||||||
updated_at = NOW()
|
|
||||||
WHERE ticket_id = ?";
|
|
||||||
|
|
||||||
$stmt = $conn->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();
|
|
||||||
84
views/CreateTicketView.php
Normal file
84
views/CreateTicketView.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
// This file contains the HTML template for creating a new ticket
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Create New Ticket</title>
|
||||||
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ticket-container">
|
||||||
|
<div class="ticket-header">
|
||||||
|
<h2>Create New Ticket</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($error)): ?>
|
||||||
|
<div class="error-message"><?php echo $error; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="<?php echo $GLOBALS['config']['BASE_URL']; ?>/ticket/create" class="ticket-form">
|
||||||
|
<div class="ticket-details">
|
||||||
|
<div class="detail-group">
|
||||||
|
<label for="title">Title</label>
|
||||||
|
<input type="text" id="title" name="title" class="editable" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-group status-priority-row">
|
||||||
|
<div class="detail-quarter">
|
||||||
|
<label for="status">Status</label>
|
||||||
|
<select id="status" name="status" class="editable">
|
||||||
|
<option value="Open" selected>Open</option>
|
||||||
|
<option value="Closed">Closed</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="detail-quarter">
|
||||||
|
<label for="priority">Priority</label>
|
||||||
|
<select id="priority" name="priority" class="editable">
|
||||||
|
<option value="1">P1 - Critical Impact</option>
|
||||||
|
<option value="2">P2 - High Impact</option>
|
||||||
|
<option value="3">P3 - Medium Impact</option>
|
||||||
|
<option value="4" selected>P4 - Low Impact</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="detail-quarter">
|
||||||
|
<label for="category">Category</label>
|
||||||
|
<select id="category" name="category" class="editable">
|
||||||
|
<option value="Hardware">Hardware</option>
|
||||||
|
<option value="Software">Software</option>
|
||||||
|
<option value="Network">Network</option>
|
||||||
|
<option value="Security">Security</option>
|
||||||
|
<option value="General" selected>General</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="detail-quarter">
|
||||||
|
<label for="type">Type</label>
|
||||||
|
<select id="type" name="type" class="editable">
|
||||||
|
<option value="Maintenance">Maintenance</option>
|
||||||
|
<option value="Install">Install</option>
|
||||||
|
<option value="Task">Task</option>
|
||||||
|
<option value="Upgrade">Upgrade</option>
|
||||||
|
<option value="Issue" selected>Issue</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-group full-width">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description" class="editable" rows="15" required></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ticket-footer">
|
||||||
|
<button type="submit" class="btn primary">Create Ticket</button>
|
||||||
|
<button type="button" onclick="window.location.href='<?php echo $GLOBALS['config']['BASE_URL']; ?>'" class="btn back-btn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
99
views/DashboardView.php
Normal file
99
views/DashboardView.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
// This file contains the HTML template for the dashboard
|
||||||
|
// It receives $tickets, $totalTickets, $totalPages, $page, and $status variables from the controller
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Ticket Dashboard</title>
|
||||||
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<h1>Tinker Tickets</h1>
|
||||||
|
<button onclick="window.location.href='<?php echo $GLOBALS['config']['BASE_URL']; ?>/ticket/create'" class="btn create-ticket">New Ticket</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-controls">
|
||||||
|
<div class="ticket-count">
|
||||||
|
Total Tickets: <?php echo $totalTickets; ?>
|
||||||
|
</div>
|
||||||
|
<div class="table-actions">
|
||||||
|
<div class="pagination">
|
||||||
|
<?php
|
||||||
|
// Previous page button
|
||||||
|
if ($page > 1) {
|
||||||
|
echo "<button onclick='window.location.href=\"?page=" . ($page - 1) . "&status=$status\"'>«</button>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page number buttons
|
||||||
|
for ($i = 1; $i <= $totalPages; $i++) {
|
||||||
|
$activeClass = ($i === $page) ? 'active' : '';
|
||||||
|
echo "<button class='$activeClass' onclick='window.location.href=\"?page=$i&status=$status\"'>$i</button>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next page button
|
||||||
|
if ($page < $totalPages) {
|
||||||
|
echo "<button onclick='window.location.href=\"?page=" . ($page + 1) . "&status=$status\"'>»</button>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<div class="settings-icon">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Ticket ID</th>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Updated</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php
|
||||||
|
if (count($tickets) > 0) {
|
||||||
|
foreach($tickets as $row) {
|
||||||
|
echo "<tr class='priority-{$row['priority']}'>";
|
||||||
|
echo "<td><a href='" . $GLOBALS['config']['BASE_URL'] . "/ticket/{$row['ticket_id']}' class='ticket-link'>{$row['ticket_id']}</a></td>";
|
||||||
|
echo "<td><span>{$row['priority']}</span></td>";
|
||||||
|
echo "<td>" . htmlspecialchars($row['title']) . "</td>";
|
||||||
|
echo "<td>{$row['category']}</td>";
|
||||||
|
echo "<td>{$row['type']}</td>";
|
||||||
|
echo "<td class='status-{$row['status']}'>{$row['status']}</td>";
|
||||||
|
echo "<td>" . date('Y-m-d H:i', strtotime($row['created_at'])) . "</td>";
|
||||||
|
echo "<td>" . date('Y-m-d H:i', strtotime($row['updated_at'])) . "</td>";
|
||||||
|
echo "</tr>";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "<tr><td colspan='8'>No tickets found</td></tr>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!--<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize the dashboard
|
||||||
|
if (document.querySelector('table')) {
|
||||||
|
initSearch();
|
||||||
|
initStatusFilter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
120
views/TicketView.php
Normal file
120
views/TicketView.php
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
// This file contains the HTML template for a ticket
|
||||||
|
// It receives $ticket and $comments variables from the controller
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Ticket #<?php echo $ticket['ticket_id']; ?></title>
|
||||||
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
||||||
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Store ticket data in a global variable
|
||||||
|
window.ticketData = {
|
||||||
|
ticket_id: "<?php echo $ticket['ticket_id']; ?>",
|
||||||
|
title: "<?php echo htmlspecialchars($ticket['title']); ?>",
|
||||||
|
status: "<?php echo $ticket['status']; ?>",
|
||||||
|
priority: "<?php echo $ticket['priority']; ?>",
|
||||||
|
category: "<?php echo $ticket['category']; ?>",
|
||||||
|
type: "<?php echo $ticket['type']; ?>"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ticket-container" data-priority="<?php echo $ticket["priority"]; ?>">
|
||||||
|
<div class="ticket-header">
|
||||||
|
<h2><input type="text" class="editable title-input" value="<?php echo htmlspecialchars($ticket["title"]); ?>" data-field="title" disabled></h2>
|
||||||
|
<div class="ticket-subheader">
|
||||||
|
<div class="ticket-id">UUID <?php echo $ticket['ticket_id']; ?></div>
|
||||||
|
<div class="header-controls">
|
||||||
|
<div class="status-priority-group">
|
||||||
|
<span id="statusDisplay" class="status-<?php echo $ticket["status"]; ?>"><?php echo $ticket["status"]; ?></span>
|
||||||
|
<span class="priority-indicator priority-<?php echo $ticket["priority"]; ?>">P<?php echo $ticket["priority"]; ?></span>
|
||||||
|
</div>
|
||||||
|
<button id="editButton" class="btn" onclick="toggleEditMode()">Edit Ticket</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-details">
|
||||||
|
<div class="ticket-tabs">
|
||||||
|
<button class="tab-btn active" onclick="showTab('description')">Description</button>
|
||||||
|
<button class="tab-btn" onclick="showTab('comments')">Comments</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="description-tab" class="tab-content active">
|
||||||
|
<div class="detail-group full-width">
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea class="editable" data-field="description" disabled><?php echo $ticket["description"]; ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="comments-tab" class="tab-content">
|
||||||
|
<div class="comments-section">
|
||||||
|
<h2>Comments</h2>
|
||||||
|
<div class="comment-form">
|
||||||
|
<textarea id="newComment" placeholder="Add a comment..."></textarea>
|
||||||
|
<div class="comment-controls">
|
||||||
|
<div class="markdown-toggles">
|
||||||
|
<div class="preview-toggle">
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="markdownMaster" onchange="toggleMarkdownMode()">
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
<span class="toggle-label">Enable Markdown</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-toggle">
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="markdownToggle" onchange="togglePreview()" disabled>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
<span class="toggle-label">Preview Markdown</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="addComment()" class="btn">Add Comment</button>
|
||||||
|
</div>
|
||||||
|
<div id="markdownPreview" class="markdown-preview" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="comments-list">
|
||||||
|
<?php
|
||||||
|
foreach ($comments as $comment) {
|
||||||
|
echo "<div class='comment'>";
|
||||||
|
echo "<div class='comment-header'>";
|
||||||
|
echo "<span class='comment-user'>{$comment['user_name']}</span>";
|
||||||
|
echo "<span class='comment-date'>" . date('M d, Y H:i', strtotime($comment['created_at'])) . "</span>";
|
||||||
|
echo "</div>";
|
||||||
|
echo "<div class='comment-text'>";
|
||||||
|
if ($comment['markdown_enabled']) {
|
||||||
|
echo "<script>document.write(marked.parse(" . json_encode($comment['comment_text']) . "))</script>";
|
||||||
|
} else {
|
||||||
|
echo htmlspecialchars($comment['comment_text']);
|
||||||
|
}
|
||||||
|
echo "</div>";
|
||||||
|
echo "</div>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-footer">
|
||||||
|
<button onclick="window.location.href='<?php echo $GLOBALS['config']['BASE_URL']; ?>'" class="btn back-btn">Back to Dashboard</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Initialize the ticket view
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (typeof showTab === 'function') {
|
||||||
|
showTab('description');
|
||||||
|
} else {
|
||||||
|
console.error('showTab function not defined');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user