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');
|
||||
|
||||
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
|
||||
|
||||
@ -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() {
|
||||
<span class="comment-user">${data.user_name}</span>
|
||||
<span class="comment-date">${data.created_at}</span>
|
||||
</div>
|
||||
<div class="comment-text">${commentText}</div>
|
||||
<div class="comment-text">
|
||||
${isMarkdownEnabled ? marked.parse(commentText) : commentText}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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() {
|
||||
// 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';
|
||||
|
||||
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