Re-did everything, Now is modulaar and better bro.

This commit is contained in:
2025-05-16 20:02:49 -04:00
parent 5b50964d06
commit f8ada1d6d1
16 changed files with 1234 additions and 187 deletions

View File

@ -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
View 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
View 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");
}

View File

@ -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

View File

@ -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
View 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
];

View 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;
}
}
}

View 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';
}
}

View 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
View 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
View 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
View 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
];
}
}
}

View File

@ -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();

View 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
View 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
View 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>