Added discord webhooks, better filtering, and automated ticket creation
This commit is contained in:
@ -139,6 +139,61 @@ body {
|
|||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(239, 68, 68, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-header {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
min-width: 160px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
z-index: 100;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dropdown.active .dropdown-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content label {
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content label:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content .save-filter {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/*UNCHECKED BELOW*/
|
/*UNCHECKED BELOW*/
|
||||||
|
|
||||||
body.menu-open {
|
body.menu-open {
|
||||||
|
|||||||
@ -291,25 +291,62 @@ function initSearch() {
|
|||||||
|
|
||||||
// Filter by status
|
// Filter by status
|
||||||
function initStatusFilter() {
|
function initStatusFilter() {
|
||||||
const filter = document.createElement('select');
|
const filterContainer = document.createElement('div');
|
||||||
filter.innerHTML = `
|
filterContainer.className = 'status-filter-container';
|
||||||
<option value="">All Status</option>
|
|
||||||
<option value="Open">Open</option>
|
// Create dropdown container
|
||||||
<option value="Closed">Closed</option>
|
const dropdown = document.createElement('div');
|
||||||
`;
|
dropdown.className = 'status-dropdown';
|
||||||
filter.className = 'status-filter';
|
|
||||||
filter.onchange = (e) => {
|
// Create dropdown header
|
||||||
const status = e.target.value;
|
const dropdownHeader = document.createElement('div');
|
||||||
const rows = document.querySelectorAll('tbody tr');
|
dropdownHeader.className = 'dropdown-header';
|
||||||
rows.forEach(row => {
|
dropdownHeader.textContent = 'Status Filter';
|
||||||
if (!status || row.querySelector('.status-' + status)) {
|
|
||||||
row.style.display = '';
|
// Create dropdown content
|
||||||
} else {
|
const dropdownContent = document.createElement('div');
|
||||||
row.style.display = 'none';
|
dropdownContent.className = 'dropdown-content';
|
||||||
}
|
|
||||||
});
|
const statuses = ['Open', 'Closed'];
|
||||||
|
statuses.forEach(status => {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.value = status;
|
||||||
|
checkbox.id = `status-${status.toLowerCase()}`;
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const currentStatuses = urlParams.get('status') ? urlParams.get('status').split(',') : [];
|
||||||
|
checkbox.checked = currentStatuses.includes(status);
|
||||||
|
|
||||||
|
label.appendChild(checkbox);
|
||||||
|
label.appendChild(document.createTextNode(status));
|
||||||
|
dropdownContent.appendChild(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveButton = document.createElement('button');
|
||||||
|
saveButton.className = 'btn save-filter';
|
||||||
|
saveButton.textContent = 'Apply Filter';
|
||||||
|
|
||||||
|
saveButton.onclick = () => {
|
||||||
|
const checkedBoxes = dropdownContent.querySelectorAll('input:checked');
|
||||||
|
const selectedStatuses = Array.from(checkedBoxes).map(cb => cb.value);
|
||||||
|
localStorage.setItem('statusFilter', selectedStatuses.join(','));
|
||||||
|
window.location.href = selectedStatuses.length ? `?status=${selectedStatuses.join(',')}` : '?';
|
||||||
|
dropdown.classList.remove('active');
|
||||||
};
|
};
|
||||||
document.querySelector('.table-controls .table-actions').prepend(filter);
|
|
||||||
|
// Toggle dropdown on header click
|
||||||
|
dropdownHeader.onclick = () => {
|
||||||
|
dropdown.classList.toggle('active');
|
||||||
|
};
|
||||||
|
|
||||||
|
dropdown.appendChild(dropdownHeader);
|
||||||
|
dropdown.appendChild(dropdownContent);
|
||||||
|
dropdownContent.appendChild(saveButton);
|
||||||
|
filterContainer.appendChild(dropdown);
|
||||||
|
|
||||||
|
document.querySelector('.table-controls .table-actions').prepend(filterContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortTable(table, column) {
|
function sortTable(table, column) {
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
file_put_contents('debug.log', print_r($_POST, true) . "\n" . file_get_contents('php://input') . "\n", FILE_APPEND);
|
||||||
|
|
||||||
|
|
||||||
// Load environment variables with error check
|
// Load environment variables with error check
|
||||||
$envFile = __DIR__ . '/.env';
|
$envFile = __DIR__ . '/.env';
|
||||||
if (!file_exists($envFile)) {
|
if (!file_exists($envFile)) {
|
||||||
@ -22,10 +27,10 @@ if (!$envVars) {
|
|||||||
|
|
||||||
// Database connection with detailed error handling
|
// Database connection with detailed error handling
|
||||||
$conn = new mysqli(
|
$conn = new mysqli(
|
||||||
$envVars['REACT_APP_DB_HOST'],
|
$envVars['DB_HOST'],
|
||||||
$envVars['REACT_APP_DB_USER'],
|
$envVars['DB_USER'],
|
||||||
$envVars['REACT_APP_DB_PASS'],
|
$envVars['DB_PASS'],
|
||||||
$envVars['REACT_APP_DB_NAME']
|
$envVars['DB_NAME']
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($conn->connect_error) {
|
if ($conn->connect_error) {
|
||||||
@ -36,8 +41,17 @@ if ($conn->connect_error) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get POST data
|
// Force JSON content type for all incoming requests
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Parse input regardless of content-type header
|
||||||
|
$rawInput = file_get_contents('php://input');
|
||||||
|
$data = json_decode($rawInput, true);
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
// Try parsing as URL-encoded data
|
||||||
|
parse_str($rawInput, $data);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate ticket ID (9-digit format with leading zeros)
|
// Generate ticket ID (9-digit format with leading zeros)
|
||||||
$ticket_id = sprintf('%09d', mt_rand(1, 999999999));
|
$ticket_id = sprintf('%09d', mt_rand(1, 999999999));
|
||||||
@ -82,3 +96,37 @@ if ($stmt->execute()) {
|
|||||||
|
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
$conn->close();
|
$conn->close();
|
||||||
|
|
||||||
|
// Discord webhook
|
||||||
|
$discord_webhook_url = $envVars['DISCORD_WEBHOOK_URL'];
|
||||||
|
|
||||||
|
// Map priorities to Discord colors (decimal format)
|
||||||
|
$priorityColors = [
|
||||||
|
"1" => 16736589, // --priority-1: #ff4d4d
|
||||||
|
"2" => 16753958, // --priority-2: #ffa726
|
||||||
|
"3" => 4363509, // --priority-3: #42a5f5
|
||||||
|
"4" => 6736490 // --priority-4: #66bb6a
|
||||||
|
];
|
||||||
|
|
||||||
|
$discord_data = [
|
||||||
|
"content" => "",
|
||||||
|
"embeds" => [[
|
||||||
|
"title" => "New Ticket Created: #" . $ticket_id,
|
||||||
|
"description" => $title,
|
||||||
|
"url" => "http://10.10.10.45/ticket.php?id=" . $ticket_id,
|
||||||
|
"color" => $priorityColors[$priority],
|
||||||
|
"fields" => [
|
||||||
|
["name" => "Priority", "value" => $priority, "inline" => true],
|
||||||
|
["name" => "Category", "value" => $category, "inline" => true],
|
||||||
|
["name" => "Type", "value" => $type, "inline" => true]
|
||||||
|
]
|
||||||
|
]]
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($discord_webhook_url);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($discord_data));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|||||||
@ -26,14 +26,21 @@ $defaultSortColumn = isset($_COOKIE['defaultSortColumn']) ? $_COOKIE['defaultSor
|
|||||||
$sortDirection = isset($_COOKIE['sortDirection']) ? $_COOKIE['sortDirection'] : 'desc';
|
$sortDirection = isset($_COOKIE['sortDirection']) ? $_COOKIE['sortDirection'] : 'desc';
|
||||||
$offset = ($page - 1) * $limit;
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
// Get total number of tickets
|
// Get total number of tickets based on current filter
|
||||||
$totalTicketsQuery = "SELECT COUNT(*) as total FROM tickets";
|
$status = isset($_GET['status']) ? $_GET['status'] : 'Open';
|
||||||
|
$statuses = explode(',', $status);
|
||||||
|
$whereClause = "";
|
||||||
|
if (isset($_GET['status']) && !empty($_GET['status'])) {
|
||||||
|
$statuses = explode(',', $_GET['status']);
|
||||||
|
$whereClause = "WHERE status IN ('" . implode("','", $statuses) . "')";
|
||||||
|
}
|
||||||
|
$totalTicketsQuery = "SELECT COUNT(*) as total FROM tickets $whereClause";
|
||||||
$totalTicketsResult = $conn->query($totalTicketsQuery);
|
$totalTicketsResult = $conn->query($totalTicketsQuery);
|
||||||
$totalTickets = $totalTicketsResult->fetch_assoc()['total'];
|
$totalTickets = $totalTicketsResult->fetch_assoc()['total'];
|
||||||
$totalPages = ceil($totalTickets / $limit);
|
$totalPages = ceil($totalTickets / $limit);
|
||||||
|
|
||||||
// Modify SQL to use these settings
|
// Modify SQL to use these settings and filter
|
||||||
$sql = "SELECT * FROM tickets ORDER BY $defaultSortColumn $sortDirection LIMIT $limit OFFSET $offset";
|
$sql = "SELECT * FROM tickets $whereClause ORDER BY $defaultSortColumn $sortDirection LIMIT $limit OFFSET $offset";
|
||||||
$result = $conn->query($sql);
|
$result = $conn->query($sql);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@ -62,18 +69,18 @@ $result = $conn->query($sql);
|
|||||||
<?php
|
<?php
|
||||||
// Previous page button
|
// Previous page button
|
||||||
if ($page > 1) {
|
if ($page > 1) {
|
||||||
echo "<button onclick='window.location.href=\"?page=" . ($page - 1) . "\"'>«</button>";
|
echo "<button onclick='window.location.href=\"?page=" . ($page - 1) . "&status=$status\"'>«</button>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page number buttons
|
// Page number buttons
|
||||||
for ($i = 1; $i <= $totalPages; $i++) {
|
for ($i = 1; $i <= $totalPages; $i++) {
|
||||||
$activeClass = ($i === $page) ? 'active' : '';
|
$activeClass = ($i === $page) ? 'active' : '';
|
||||||
echo "<button class='$activeClass' onclick='window.location.href=\"?page=$i\"'>$i</button>";
|
echo "<button class='$activeClass' onclick='window.location.href=\"?page=$i&status=$status\"'>$i</button>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next page button
|
// Next page button
|
||||||
if ($page < $totalPages) {
|
if ($page < $totalPages) {
|
||||||
echo "<button onclick='window.location.href=\"?page=" . ($page + 1) . "\"'>»</button>";
|
echo "<button onclick='window.location.href=\"?page=" . ($page + 1) . "&status=$status\"'>»</button>";
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user