Fix bracket buttons rendering below text + UI/security improvements
CSS fixes: - Fix [ ] brackets appearing below button text by replacing display:inline-flex with display:inline-block + white-space:nowrap on .btn — removes cross-browser flex pseudo-element inconsistency as root cause - Remove conflicting .btn::before ripple block (position:absolute was overriding bracket content positioning) - Remove overflow:hidden from .btn which was clipping bracket content - Fix body::after duplicate rule causing GPU layer blink (second position:fixed rule re-created compositor layer, overriding display:none suppression) - Replace all transition:all with scoped property transitions in dashboard.css, ticket.css, base.css (prevents full CSS property evaluation on every hover) - Convert pulse-warning/pulse-critical keyframes from box-shadow to opacity animation (GPU-composited, eliminates CPU repaints at 60fps) - Fix mobile *::before/*::after blanket content:none rule — now targets only decorative frame glyphs, preserving button brackets and status indicators - Remove --terminal-green-dim override that broke .lt-btn hover backgrounds JS fixes: - Fix all lt.lt.toast.* double-prefix instances in dashboard.js - Add null guard before .appendChild() on bulkAssignUser select - Replace all remaining emoji with terminal bracket notation (dashboard.js, ticket.js, markdown.js) - Migrate all toast.*() shim calls to lt.toast.* across all JS files View fixes: - Remove hardcoded [ ] brackets from .btn buttons (CSS now adds them) - Replace all emoji with terminal bracket notation in all views and admin views - Add missing CSP nonces to AuditLogView.php and UserActivityView.php script tags - Bump CSS version strings to ?v=20260319b for cache busting Security fixes: - update_ticket.php: add authorization check (non-admins can only edit their own or assigned tickets) - add_comment.php: validate and cast ticket_id to integer with 400 response - clone_ticket.php: fix unconditional session_start(), add ticket ID validation, add internal ticket access check - bulk_operation.php: add HTTP 401/403 status codes on auth failures - upload_attachment.php: fix missing $conn arg in AttachmentModel constructor - assign_ticket.php: add ticket existence check and permission verification Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,14 @@ try {
|
|||||||
throw new Exception("Invalid JSON data received");
|
throw new Exception("Invalid JSON data received");
|
||||||
}
|
}
|
||||||
|
|
||||||
$ticketId = $data['ticket_id'];
|
$ticketId = isset($data['ticket_id']) ? (int)$data['ticket_id'] : 0;
|
||||||
|
if ($ticketId <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
ob_end_clean();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid ticket ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize models
|
// Initialize models
|
||||||
$commentModel = new CommentModel($conn);
|
$commentModel = new CommentModel($conn);
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ require_once dirname(__DIR__) . '/models/UserModel.php';
|
|||||||
|
|
||||||
// Get request data
|
// Get request data
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
$ticketId = $data['ticket_id'] ?? null;
|
if (!is_array($data)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid JSON body']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticketId = isset($data['ticket_id']) ? (int)$data['ticket_id'] : null;
|
||||||
$assignedTo = $data['assigned_to'] ?? null;
|
$assignedTo = $data['assigned_to'] ?? null;
|
||||||
|
|
||||||
if (!$ticketId) {
|
if (!$ticketId) {
|
||||||
|
http_response_code(400);
|
||||||
echo json_encode(['success' => false, 'error' => 'Ticket ID required']);
|
echo json_encode(['success' => false, 'error' => 'Ticket ID required']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -18,6 +25,21 @@ $ticketModel = new TicketModel($conn);
|
|||||||
$auditLogModel = new AuditLogModel($conn);
|
$auditLogModel = new AuditLogModel($conn);
|
||||||
$userModel = new UserModel($conn);
|
$userModel = new UserModel($conn);
|
||||||
|
|
||||||
|
// Verify ticket exists
|
||||||
|
$ticket = $ticketModel->getTicketById($ticketId);
|
||||||
|
if (!$ticket) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Ticket not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization: only admins or the ticket creator/assignee can reassign
|
||||||
|
if (!$isAdmin && $ticket['created_by'] !== $userId && $ticket['assigned_to'] !== $userId) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Permission denied']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($assignedTo === null || $assignedTo === '') {
|
if ($assignedTo === null || $assignedTo === '') {
|
||||||
// Unassign ticket
|
// Unassign ticket
|
||||||
$success = $ticketModel->unassignTicket($ticketId, $userId);
|
$success = $ticketModel->unassignTicket($ticketId, $userId);
|
||||||
@@ -40,4 +62,9 @@ if ($assignedTo === null || $assignedTo === '') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => $success]);
|
if (!$success) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Failed to update ticket assignment']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ header('Content-Type: application/json');
|
|||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
echo json_encode(['success' => false, 'error' => 'Not authenticated']);
|
echo json_encode(['success' => false, 'error' => 'Not authenticated']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -32,6 +33,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
// Check admin status - bulk operations are admin-only
|
// Check admin status - bulk operations are admin-only
|
||||||
$isAdmin = $_SESSION['user']['is_admin'] ?? false;
|
$isAdmin = $_SESSION['user']['is_admin'] ?? false;
|
||||||
if (!$isAdmin) {
|
if (!$isAdmin) {
|
||||||
|
http_response_code(403);
|
||||||
echo json_encode(['success' => false, 'error' => 'Admin access required']);
|
echo json_encode(['success' => false, 'error' => 'Admin access required']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ try {
|
|||||||
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
|
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
|
||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
|
}
|
||||||
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(['success' => false, 'error' => 'Authentication required']);
|
echo json_encode(['success' => false, 'error' => 'Authentication required']);
|
||||||
@@ -50,8 +52,14 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sourceTicketId = $data['ticket_id'];
|
$sourceTicketId = (int)$data['ticket_id'];
|
||||||
|
if ($sourceTicketId <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid ticket ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
$userId = $_SESSION['user']['user_id'];
|
$userId = $_SESSION['user']['user_id'];
|
||||||
|
$isAdmin = $_SESSION['user']['is_admin'] ?? false;
|
||||||
|
|
||||||
// Get database connection
|
// Get database connection
|
||||||
$conn = Database::getConnection();
|
$conn = Database::getConnection();
|
||||||
@@ -66,6 +74,15 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authorization: non-admins cannot clone internal tickets unless they created/are assigned
|
||||||
|
if (!$isAdmin && ($sourceTicket['visibility'] ?? 'public') === 'internal') {
|
||||||
|
if ($sourceTicket['created_by'] != $userId && $sourceTicket['assigned_to'] != $userId) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Permission denied']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare cloned ticket data
|
// Prepare cloned ticket data
|
||||||
$clonedTicketData = [
|
$clonedTicketData = [
|
||||||
'title' => '[CLONE] ' . $sourceTicket['title'],
|
'title' => '[CLONE] ' . $sourceTicket['title'],
|
||||||
|
|||||||
@@ -79,6 +79,17 @@ try {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authorization: admins can edit any ticket; others only their own or assigned
|
||||||
|
if (!$this->isAdmin
|
||||||
|
&& $currentTicket['created_by'] != $this->userId
|
||||||
|
&& $currentTicket['assigned_to'] != $this->userId
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Permission denied'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Merge current data with updates, keeping existing values for missing fields
|
// Merge current data with updates, keeping existing values for missing fields
|
||||||
$updateData = [
|
$updateData = [
|
||||||
'ticket_id' => $id,
|
'ticket_id' => $id,
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ if (empty($originalFilename)) {
|
|||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
try {
|
try {
|
||||||
$attachmentModel = new AttachmentModel();
|
$attachmentModel = new AttachmentModel($conn);
|
||||||
$attachmentId = $attachmentModel->addAttachment(
|
$attachmentId = $attachmentModel->addAttachment(
|
||||||
$ticketId,
|
$ticketId,
|
||||||
$uniqueFilename,
|
$uniqueFilename,
|
||||||
|
|||||||
@@ -128,9 +128,9 @@
|
|||||||
--header-height: 58px;
|
--header-height: 58px;
|
||||||
--container-max: 1600px;
|
--container-max: 1600px;
|
||||||
|
|
||||||
/* --- Transitions --- */
|
/* --- Transitions — scoped to GPU-safe properties (no box-shadow/filter) --- */
|
||||||
--transition-fast: all 0.15s ease;
|
--transition-fast: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
|
||||||
--transition-default: all 0.3s ease;
|
--transition-default: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||||
|
|
||||||
/* --- Z-index ladder --- */
|
/* --- Z-index ladder --- */
|
||||||
--z-base: 1;
|
--z-base: 1;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
/* Terminal Colors */
|
/* Terminal Colors */
|
||||||
--terminal-green: #00ff41;
|
--terminal-green: #00ff41;
|
||||||
--terminal-green-dim: #00cc33;
|
/* Note: --terminal-green-dim is NOT overridden here — base.css defines it as
|
||||||
|
rgba(0,255,65,0.15) for button hover backgrounds. Use --text-secondary for dim text. */
|
||||||
--terminal-amber: #ffb000;
|
--terminal-amber: #ffb000;
|
||||||
--terminal-cyan: #00ffff;
|
--terminal-cyan: #00ffff;
|
||||||
--terminal-red: #ff4444;
|
--terminal-red: #ff4444;
|
||||||
@@ -56,8 +57,8 @@
|
|||||||
--spacing-md: 1.5rem;
|
--spacing-md: 1.5rem;
|
||||||
--spacing-lg: 2rem;
|
--spacing-lg: 2rem;
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions — scoped to GPU-safe properties only (no box-shadow, no filter) */
|
||||||
--transition-default: all 0.3s ease;
|
--transition-default: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||||
|
|
||||||
/* Z-Index Scale - Centralized stacking context management */
|
/* Z-Index Scale - Centralized stacking context management */
|
||||||
--z-base: 1;
|
--z-base: 1;
|
||||||
@@ -116,7 +117,9 @@ body::before {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Suppress body::after binary text watermark (also position:fixed → GPU layer) */
|
/* Suppress body::after binary text watermark (also position:fixed → GPU layer).
|
||||||
|
Do NOT add a second body::after rule below — the last rule wins in the cascade,
|
||||||
|
overriding this display:none and re-creating the fixed GPU compositing layer. */
|
||||||
body::after {
|
body::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -139,30 +142,6 @@ body::after {
|
|||||||
100% { transform: translate(0); }
|
100% { transform: translate(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Subtle data stream effect in corner */
|
|
||||||
body::after {
|
|
||||||
content: '10101010';
|
|
||||||
position: fixed;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 0.6rem;
|
|
||||||
color: var(--terminal-green);
|
|
||||||
opacity: 0.1;
|
|
||||||
pointer-events: none;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
animation: data-stream 3s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes data-stream {
|
|
||||||
0% { content: '10101010'; opacity: 0.1; }
|
|
||||||
25% { content: '01010101'; opacity: 0.15; }
|
|
||||||
50% { content: '11001100'; opacity: 0.1; }
|
|
||||||
75% { content: '00110011'; opacity: 0.15; }
|
|
||||||
100% { content: '10101010'; opacity: 0.1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== ENHANCED TERMINAL ANIMATIONS ===== */
|
/* ===== ENHANCED TERMINAL ANIMATIONS ===== */
|
||||||
|
|
||||||
/* Typing cursor effect for focused inputs */
|
/* Typing cursor effect for focused inputs */
|
||||||
@@ -182,12 +161,8 @@ textarea:focus,
|
|||||||
select:focus {
|
select:focus {
|
||||||
outline: 2px solid var(--terminal-amber);
|
outline: 2px solid var(--terminal-amber);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
animation: focus-pulse 2s ease-in-out infinite;
|
/* Static glow on focus — no animation to avoid CPU repaints on every frame */
|
||||||
}
|
box-shadow: var(--glow-amber);
|
||||||
|
|
||||||
@keyframes focus-pulse {
|
|
||||||
0%, 100% { box-shadow: var(--glow-amber), inset 0 0 10px rgba(0, 0, 0, 0.5); }
|
|
||||||
50% { box-shadow: var(--glow-amber-intense), inset 0 0 10px rgba(0, 0, 0, 0.5); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus visible for keyboard navigation */
|
/* Focus visible for keyboard navigation */
|
||||||
@@ -313,29 +288,6 @@ tbody tr {
|
|||||||
.btn {
|
.btn {
|
||||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
transition: width 0.4s, height 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:active::before {
|
|
||||||
width: 200%;
|
|
||||||
height: 200%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Terminal cursor blink for active/selected elements */
|
/* Terminal cursor blink for active/selected elements */
|
||||||
@@ -1584,6 +1536,7 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ===== BUTTON STYLES - TERMINAL EDITION ===== */
|
/* ===== BUTTON STYLES - TERMINAL EDITION ===== */
|
||||||
|
/* Base: apply terminal font/reset to all buttons */
|
||||||
.btn,
|
.btn,
|
||||||
.btn-base,
|
.btn-base,
|
||||||
button {
|
button {
|
||||||
@@ -1597,25 +1550,25 @@ button {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
|
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bracket notation only for explicitly-styled .btn class buttons */
|
||||||
.btn::before,
|
.btn::before,
|
||||||
.btn-base::before,
|
.btn-base::before {
|
||||||
button::before {
|
|
||||||
content: '[ ';
|
content: '[ ';
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn::after,
|
.btn::after,
|
||||||
.btn-base::after,
|
.btn-base::after {
|
||||||
button::after {
|
|
||||||
content: ' ]';
|
content: ' ]';
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover,
|
.btn:hover,
|
||||||
.btn-base:hover,
|
.btn-base:hover {
|
||||||
button:hover {
|
|
||||||
background: rgba(0, 255, 65, 0.15);
|
background: rgba(0, 255, 65, 0.15);
|
||||||
color: var(--terminal-amber);
|
color: var(--terminal-amber);
|
||||||
border-color: var(--terminal-amber);
|
border-color: var(--terminal-amber);
|
||||||
@@ -1623,8 +1576,7 @@ button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn:active,
|
.btn:active,
|
||||||
.btn-base:active,
|
.btn-base:active {
|
||||||
button:active {
|
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2083,7 +2035,7 @@ select {
|
|||||||
border: 2px solid var(--terminal-green);
|
border: 2px solid var(--terminal-green);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
transition: all 0.3s ease;
|
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||||
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
|
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2288,7 +2240,7 @@ input[type="checkbox"]:checked {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
text-shadow: var(--glow-amber);
|
text-shadow: var(--glow-amber);
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner-toggle:hover {
|
.banner-toggle:hover {
|
||||||
@@ -2532,7 +2484,7 @@ input[type="checkbox"]:checked {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
transition: all 0.2s ease;
|
transition: color 0.2s ease, padding-left 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group label:hover {
|
.filter-group label:hover {
|
||||||
@@ -2548,7 +2500,7 @@ input[type="checkbox"]:checked {
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border: 2px solid var(--terminal-green);
|
border: 2px solid var(--terminal-green);
|
||||||
transition: all 0.2s ease;
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group input[type="checkbox"]:checked {
|
.filter-group input[type="checkbox"]:checked {
|
||||||
@@ -2568,7 +2520,7 @@ input[type="checkbox"]:checked {
|
|||||||
border: 2px solid var(--terminal-green);
|
border: 2px solid var(--terminal-green);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-sidebar .btn:hover {
|
.dashboard-sidebar .btn:hover {
|
||||||
@@ -2802,7 +2754,7 @@ input[type="checkbox"]:checked {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
transition: background 0.2s ease, border-color 0.2s ease, text-shadow 0.2s ease;
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
text-shadow: var(--glow-green);
|
text-shadow: var(--glow-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3250,9 +3202,25 @@ body.dark-mode select option {
|
|||||||
font-size: 0.4rem !important;
|
font-size: 0.4rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove all pseudo-element decorations except essential ones */
|
/* Remove purely decorative frame corner glyphs on tiny screens */
|
||||||
*::before,
|
.ascii-frame-outer .bottom-left-corner,
|
||||||
*::after {
|
.ascii-frame-outer .bottom-right-corner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.lt-frame::before, .lt-frame::after,
|
||||||
|
.lt-frame-inner::before, .lt-frame-inner::after,
|
||||||
|
.lt-card::before, .lt-card::after,
|
||||||
|
.lt-stat-card::before, .lt-stat-card::after,
|
||||||
|
.lt-divider::before, .lt-divider::after,
|
||||||
|
.lt-section-header::before, .lt-section-header::after,
|
||||||
|
.lt-subsection-header::before, .lt-subsection-header::after,
|
||||||
|
.lt-sidebar-header::before,
|
||||||
|
.lt-modal::before, .lt-modal::after,
|
||||||
|
.lt-modal-title::before,
|
||||||
|
.lt-timeline::before,
|
||||||
|
h1::before, h3::before, h3::after,
|
||||||
|
.lt-page-title::before,
|
||||||
|
.lt-brand-title::before {
|
||||||
content: none !important;
|
content: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3278,11 +3246,6 @@ body.dark-mode select option {
|
|||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Re-enable essential pseudo-elements */
|
|
||||||
.search-form::before {
|
|
||||||
content: '$ ' !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Simplify modals */
|
/* Simplify modals */
|
||||||
.settings-modal,
|
.settings-modal,
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@@ -3350,7 +3313,7 @@ body.dark-mode select option {
|
|||||||
z-index: var(--z-toast);
|
z-index: var(--z-toast);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(400px);
|
transform: translateX(400px);
|
||||||
transition: all 0.3s ease;
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
@@ -3403,7 +3366,7 @@ body.dark-mode select option {
|
|||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
@@ -3579,7 +3542,7 @@ body.dark-mode select option {
|
|||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-settings:hover {
|
.close-settings:hover {
|
||||||
@@ -3863,7 +3826,7 @@ body.dark-mode select option {
|
|||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4035,7 +3998,7 @@ code.inline-code {
|
|||||||
color: var(--terminal-cyan);
|
color: var(--terminal-cyan);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-bottom: 1px dotted var(--terminal-cyan);
|
border-bottom: 1px dotted var(--terminal-cyan);
|
||||||
transition: all 0.3s ease;
|
transition: color 0.2s ease, border-bottom-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-markdown] a:hover {
|
[data-markdown] a:hover {
|
||||||
@@ -4126,7 +4089,7 @@ code.inline-code {
|
|||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-advanced-search:hover {
|
.close-advanced-search:hover {
|
||||||
@@ -4211,7 +4174,7 @@ code.inline-code {
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4229,7 +4192,7 @@ code.inline-code {
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-search-secondary:hover {
|
.btn-search-secondary:hover {
|
||||||
@@ -4246,7 +4209,7 @@ code.inline-code {
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-search-reset:hover {
|
.btn-search-reset:hover {
|
||||||
@@ -4377,7 +4340,7 @@ tr:hover .quick-actions {
|
|||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--terminal-green-dim, #008822);
|
color: var(--text-secondary);
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
@@ -4456,7 +4419,7 @@ tr:hover .quick-actions {
|
|||||||
color: var(--terminal-green);
|
color: var(--terminal-green);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-dropdown-content a:hover {
|
.export-dropdown-content a:hover {
|
||||||
@@ -4498,7 +4461,7 @@ tr:hover .quick-actions {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
border-bottom: 1px solid var(--bg-tertiary);
|
border-bottom: 1px solid var(--bg-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4545,7 +4508,7 @@ table td:nth-child(4) {
|
|||||||
padding: 0.1rem 0.3rem;
|
padding: 0.1rem 0.3rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: rgba(0, 255, 255, 0.1);
|
background: rgba(0, 255, 255, 0.1);
|
||||||
transition: all 0.2s ease;
|
transition: color 0.15s ease, background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticket-link-ref:hover {
|
.ticket-link-ref:hover {
|
||||||
@@ -4859,7 +4822,7 @@ table td:nth-child(4) {
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-btn::before,
|
.view-btn::before,
|
||||||
@@ -4957,7 +4920,7 @@ table td:nth-child(4) {
|
|||||||
border: 1px solid var(--terminal-green);
|
border: 1px solid var(--terminal-green);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: border-color 0.15s ease, transform 0.15s ease;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ textarea[data-field="description"]:not(:disabled)::after {
|
|||||||
color: var(--terminal-amber);
|
color: var(--terminal-amber);
|
||||||
border-color: var(--terminal-amber);
|
border-color: var(--terminal-amber);
|
||||||
background: rgba(255, 176, 0, 0.1);
|
background: rgba(255, 176, 0, 0.1);
|
||||||
|
box-shadow: 0 0 6px rgba(255, 176, 0, 0.4);
|
||||||
animation: pulse-warning 2s ease-in-out infinite;
|
animation: pulse-warning 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,17 +353,18 @@ textarea[data-field="description"]:not(:disabled)::after {
|
|||||||
color: var(--priority-1);
|
color: var(--priority-1);
|
||||||
border-color: var(--priority-1);
|
border-color: var(--priority-1);
|
||||||
background: rgba(255, 77, 77, 0.15);
|
background: rgba(255, 77, 77, 0.15);
|
||||||
|
box-shadow: 0 0 8px rgba(255, 77, 77, 0.5);
|
||||||
animation: pulse-critical 1s ease-in-out infinite;
|
animation: pulse-critical 1s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-warning {
|
@keyframes pulse-warning {
|
||||||
0%, 100% { box-shadow: 0 0 5px rgba(255, 176, 0, 0.3); }
|
0%, 100% { opacity: 0.75; }
|
||||||
50% { box-shadow: 0 0 15px rgba(255, 176, 0, 0.6); }
|
50% { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-critical {
|
@keyframes pulse-critical {
|
||||||
0%, 100% { box-shadow: 0 0 5px rgba(255, 77, 77, 0.3); }
|
0%, 100% { opacity: 0.7; }
|
||||||
50% { box-shadow: 0 0 20px rgba(255, 77, 77, 0.8); }
|
50% { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tab transition animations */
|
/* Tab transition animations */
|
||||||
@@ -507,7 +509,7 @@ textarea[data-field="description"]:not(:disabled)::after {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transition: all 0.3s ease;
|
transition: border-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.editable {
|
input.editable {
|
||||||
@@ -547,7 +549,7 @@ textarea.editable {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.btn.primary {
|
||||||
@@ -626,7 +628,7 @@ textarea.editable {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: all 0.3s ease;
|
transition: border-color 0.2s ease;
|
||||||
animation: comment-appear 0.4s ease-out;
|
animation: comment-appear 0.4s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,7 +645,6 @@ textarea.editable {
|
|||||||
|
|
||||||
.comment:hover {
|
.comment:hover {
|
||||||
border-color: var(--terminal-amber);
|
border-color: var(--terminal-amber);
|
||||||
background: linear-gradient(135deg, var(--bg-primary) 0%, rgba(255, 176, 0, 0.03) 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment:hover::before,
|
.comment:hover::before,
|
||||||
@@ -760,13 +761,16 @@ textarea.editable {
|
|||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-action-btn:hover {
|
.comment-action-btn:hover,
|
||||||
|
.comment-action-btn:focus-visible {
|
||||||
background: rgba(0, 255, 65, 0.1);
|
background: rgba(0, 255, 65, 0.1);
|
||||||
|
outline: 2px solid var(--terminal-amber);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-action-btn.edit-btn:hover {
|
.comment-action-btn.edit-btn:hover {
|
||||||
@@ -1055,7 +1059,7 @@ textarea.editable {
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--terminal-green);
|
color: var(--terminal-green);
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: -2px;
|
margin-right: -2px;
|
||||||
}
|
}
|
||||||
@@ -1070,9 +1074,12 @@ textarea.editable {
|
|||||||
color: var(--terminal-green);
|
color: var(--terminal-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn:hover {
|
.tab-btn:hover,
|
||||||
|
.tab-btn:focus-visible {
|
||||||
background: rgba(0, 255, 65, 0.05);
|
background: rgba(0, 255, 65, 0.05);
|
||||||
color: var(--terminal-amber);
|
color: var(--terminal-amber);
|
||||||
|
outline: 2px solid var(--terminal-amber);
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn.active {
|
.tab-btn.active {
|
||||||
@@ -1155,7 +1162,7 @@ textarea.editable {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-secondary);
|
||||||
transition: .4s;
|
transition: background-color 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider:before {
|
.slider:before {
|
||||||
@@ -1166,7 +1173,7 @@ textarea.editable {
|
|||||||
left: 4px;
|
left: 4px;
|
||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
transition: .4s;
|
transition: transform 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider.round {
|
.slider.round {
|
||||||
@@ -1353,7 +1360,7 @@ body.dark-mode .timeline-date {
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: opacity 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-select:hover {
|
.status-select:hover {
|
||||||
@@ -1604,7 +1611,7 @@ body.dark-mode .editable {
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1691,7 +1698,7 @@ body.dark-mode .editable {
|
|||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border: 1px solid var(--terminal-green);
|
border: 1px solid var(--terminal-green);
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
transition: all 0.2s ease;
|
transition: border-color 0.15s ease, background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-item:hover {
|
.attachment-item:hover {
|
||||||
@@ -1782,7 +1789,7 @@ body.dark-mode .editable {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention:hover {
|
.mention:hover {
|
||||||
@@ -1816,7 +1823,7 @@ body.dark-mode .editable {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--terminal-green);
|
color: var(--terminal-green);
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -1857,7 +1864,7 @@ body.dark-mode .editable {
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1910,7 +1917,7 @@ body.dark-mode .editable {
|
|||||||
color: var(--terminal-green);
|
color: var(--terminal-green);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-dropdown-content a:hover {
|
.export-dropdown-content a:hover {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ async function saveCurrentFilter() {
|
|||||||
'My Filter',
|
'My Filter',
|
||||||
async (filterName) => {
|
async (filterName) => {
|
||||||
if (!filterName || filterName.trim() === '') {
|
if (!filterName || filterName.trim() === '') {
|
||||||
toast.warning('Filter name cannot be empty', 2000);
|
lt.toast.warning('Filter name cannot be empty', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function initMobileSidebar() {
|
|||||||
const toggleBtn = document.createElement('button');
|
const toggleBtn = document.createElement('button');
|
||||||
toggleBtn.id = 'mobileFilterToggle';
|
toggleBtn.id = 'mobileFilterToggle';
|
||||||
toggleBtn.className = 'mobile-filter-toggle';
|
toggleBtn.className = 'mobile-filter-toggle';
|
||||||
toggleBtn.innerHTML = '☰ Filters & Search Options';
|
toggleBtn.innerHTML = '[ = ] Filters & Search Options';
|
||||||
toggleBtn.onclick = openMobileSidebar;
|
toggleBtn.onclick = openMobileSidebar;
|
||||||
dashboardMain.insertBefore(toggleBtn, dashboardMain.firstChild);
|
dashboardMain.insertBefore(toggleBtn, dashboardMain.firstChild);
|
||||||
}
|
}
|
||||||
@@ -79,20 +79,20 @@ function initMobileSidebar() {
|
|||||||
nav.className = 'mobile-bottom-nav';
|
nav.className = 'mobile-bottom-nav';
|
||||||
nav.innerHTML = `
|
nav.innerHTML = `
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<span class="nav-icon">🏠</span>
|
<span class="nav-icon">[ ~ ]</span>
|
||||||
<span class="nav-label">Home</span>
|
<span class="nav-label">HOME</span>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" data-action="open-mobile-sidebar">
|
<button type="button" data-action="open-mobile-sidebar">
|
||||||
<span class="nav-icon">🔍</span>
|
<span class="nav-icon">[ / ]</span>
|
||||||
<span class="nav-label">Filter</span>
|
<span class="nav-label">FILTER</span>
|
||||||
</button>
|
</button>
|
||||||
<a href="/ticket/create">
|
<a href="/ticket/create">
|
||||||
<span class="nav-icon">➕</span>
|
<span class="nav-icon">[ + ]</span>
|
||||||
<span class="nav-label">New</span>
|
<span class="nav-label">NEW</span>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" data-action="open-settings-modal">
|
<button type="button" data-action="open-settings-modal">
|
||||||
<span class="nav-icon">⚙</span>
|
<span class="nav-icon">[ * ]</span>
|
||||||
<span class="nav-label">Settings</span>
|
<span class="nav-label">CFG</span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(nav);
|
document.body.appendChild(nav);
|
||||||
@@ -566,11 +566,11 @@ function quickSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000);
|
lt.toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Error updating ticket: ' + error.message, 5000);
|
lt.toast.error('Error updating ticket: ' + error.message, 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,11 +671,11 @@ function loadTemplate() {
|
|||||||
document.getElementById('priority').value = template.default_priority;
|
document.getElementById('priority').value = template.default_priority;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000);
|
lt.toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Error loading template: ' + error.message, 4000);
|
lt.toast.error('Error loading template: ' + error.message, 4000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,7 +752,7 @@ function bulkClose() {
|
|||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (ticketIds.length === 0) {
|
if (ticketIds.length === 0) {
|
||||||
toast.warning('No tickets selected', 2000);
|
lt.toast.warning('No tickets selected', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,17 +782,17 @@ function performBulkCloseAction(ticketIds) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
if (data.failed > 0) {
|
if (data.failed > 0) {
|
||||||
toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
lt.toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||||
} else {
|
} else {
|
||||||
toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000);
|
lt.toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000);
|
||||||
}
|
}
|
||||||
setTimeout(() => window.location.reload(), 1500);
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Bulk close failed: ' + error.message, 5000);
|
lt.toast.error('Bulk close failed: ' + error.message, 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,7 +800,7 @@ function showBulkAssignModal() {
|
|||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (ticketIds.length === 0) {
|
if (ticketIds.length === 0) {
|
||||||
toast.warning('No tickets selected', 2000);
|
lt.toast.warning('No tickets selected', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,6 +834,7 @@ function showBulkAssignModal() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.users) {
|
if (data.success && data.users) {
|
||||||
const select = document.getElementById('bulkAssignUser');
|
const select = document.getElementById('bulkAssignUser');
|
||||||
|
if (select) {
|
||||||
data.users.forEach(user => {
|
data.users.forEach(user => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = user.user_id;
|
option.value = user.user_id;
|
||||||
@@ -841,6 +842,7 @@ function showBulkAssignModal() {
|
|||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => lt.toast.error('Error loading users'));
|
.catch(() => lt.toast.error('Error loading users'));
|
||||||
}
|
}
|
||||||
@@ -856,7 +858,7 @@ function performBulkAssign() {
|
|||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
toast.warning('Please select a user', 2000);
|
lt.toast.warning('Please select a user', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,17 +880,17 @@ function performBulkAssign() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
closeBulkAssignModal();
|
closeBulkAssignModal();
|
||||||
if (data.failed > 0) {
|
if (data.failed > 0) {
|
||||||
toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
lt.toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||||
} else {
|
} else {
|
||||||
toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000);
|
lt.toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000);
|
||||||
}
|
}
|
||||||
setTimeout(() => window.location.reload(), 1500);
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Bulk assign failed: ' + error.message, 5000);
|
lt.toast.error('Bulk assign failed: ' + error.message, 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,7 +898,7 @@ function showBulkPriorityModal() {
|
|||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (ticketIds.length === 0) {
|
if (ticketIds.length === 0) {
|
||||||
toast.warning('No tickets selected', 2000);
|
lt.toast.warning('No tickets selected', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -941,7 +943,7 @@ function performBulkPriority() {
|
|||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (!priority) {
|
if (!priority) {
|
||||||
toast.warning('Please select a priority', 2000);
|
lt.toast.warning('Please select a priority', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,17 +965,17 @@ function performBulkPriority() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
closeBulkPriorityModal();
|
closeBulkPriorityModal();
|
||||||
if (data.failed > 0) {
|
if (data.failed > 0) {
|
||||||
toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
lt.toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||||
} else {
|
} else {
|
||||||
toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000);
|
lt.toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000);
|
||||||
}
|
}
|
||||||
setTimeout(() => window.location.reload(), 1500);
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Bulk priority update failed: ' + error.message, 5000);
|
lt.toast.error('Bulk priority update failed: ' + error.message, 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,7 +1015,7 @@ function showBulkStatusModal() {
|
|||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (ticketIds.length === 0) {
|
if (ticketIds.length === 0) {
|
||||||
toast.warning('No tickets selected', 2000);
|
lt.toast.warning('No tickets selected', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1057,7 +1059,7 @@ function performBulkStatusChange() {
|
|||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
toast.warning('Please select a status', 2000);
|
lt.toast.warning('Please select a status', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,17 +1081,17 @@ function performBulkStatusChange() {
|
|||||||
closeBulkStatusModal();
|
closeBulkStatusModal();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
if (data.failed > 0) {
|
if (data.failed > 0) {
|
||||||
toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
lt.toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||||
} else {
|
} else {
|
||||||
toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000);
|
lt.toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000);
|
||||||
}
|
}
|
||||||
setTimeout(() => window.location.reload(), 1500);
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Bulk status change failed: ' + error.message, 5000);
|
lt.toast.error('Bulk status change failed: ' + error.message, 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1098,7 +1100,7 @@ function showBulkDeleteModal() {
|
|||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (ticketIds.length === 0) {
|
if (ticketIds.length === 0) {
|
||||||
toast.warning('No tickets selected', 2000);
|
lt.toast.warning('No tickets selected', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1106,7 +1108,7 @@ function showBulkDeleteModal() {
|
|||||||
<div class="lt-modal-overlay" id="bulkDeleteModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="bulkDeleteModal" aria-hidden="true">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal">
|
||||||
<div class="lt-modal-header" style="color: var(--status-closed);">
|
<div class="lt-modal-header" style="color: var(--status-closed);">
|
||||||
<span class="lt-modal-title">⚠ Delete ${ticketIds.length} Ticket(s)</span>
|
<span class="lt-modal-title">[ ! ] DELETE ${ticketIds.length} TICKET(S)</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close>✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body" style="text-align:center;">
|
<div class="lt-modal-body" style="text-align:center;">
|
||||||
@@ -1150,14 +1152,14 @@ function performBulkDelete() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
closeBulkDeleteModal();
|
closeBulkDeleteModal();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000);
|
lt.toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000);
|
||||||
setTimeout(() => window.location.reload(), 1500);
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Bulk delete failed: ' + error.message, 5000);
|
lt.toast.error('Bulk delete failed: ' + error.message, 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1186,15 +1188,15 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
|
|||||||
|
|
||||||
// Icon based on type
|
// Icon based on type
|
||||||
const icons = {
|
const icons = {
|
||||||
warning: '⚠',
|
warning: '[ ! ]',
|
||||||
error: '✗',
|
error: '[ X ]',
|
||||||
info: 'ℹ'
|
info: '[ i ]',
|
||||||
};
|
};
|
||||||
const icon = icons[type] || icons.warning;
|
const icon = icons[type] || icons.warning;
|
||||||
|
|
||||||
// Escape user-provided content to prevent XSS
|
// Escape user-provided content to prevent XSS
|
||||||
const safeTitle = escapeHtml(title);
|
const safeTitle = lt.escHtml(title);
|
||||||
const safeMessage = escapeHtml(message);
|
const safeMessage = lt.escHtml(message);
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
|
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
|
||||||
@@ -1243,9 +1245,9 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
|
|||||||
const inputId = modalId + '_input';
|
const inputId = modalId + '_input';
|
||||||
|
|
||||||
// Escape user-provided content to prevent XSS
|
// Escape user-provided content to prevent XSS
|
||||||
const safeTitle = escapeHtml(title);
|
const safeTitle = lt.escHtml(title);
|
||||||
const safeLabel = escapeHtml(label);
|
const safeLabel = lt.escHtml(label);
|
||||||
const safePlaceholder = escapeHtml(placeholder);
|
const safePlaceholder = lt.escHtml(placeholder);
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
|
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
|
||||||
@@ -1307,8 +1309,8 @@ function quickStatusChange(ticketId, currentStatus) {
|
|||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close>✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<p style="margin-bottom:0.5rem;">Ticket #${escapeHtml(ticketId)}</p>
|
<p style="margin-bottom:0.5rem;">Ticket #${lt.escHtml(ticketId)}</p>
|
||||||
<p style="margin-bottom:0.5rem;color:var(--terminal-amber);">Current: ${escapeHtml(currentStatus)}</p>
|
<p style="margin-bottom:0.5rem;color:var(--terminal-amber);">Current: ${lt.escHtml(currentStatus)}</p>
|
||||||
<label for="quickStatusSelect">New Status:</label>
|
<label for="quickStatusSelect">New Status:</label>
|
||||||
<select id="quickStatusSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
<select id="quickStatusSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
||||||
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
|
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
|
||||||
@@ -1351,15 +1353,15 @@ function performQuickStatusChange(ticketId) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
closeQuickStatusModal();
|
closeQuickStatusModal();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success(`Status updated to ${newStatus}`, 3000);
|
lt.toast.success(`Status updated to ${newStatus}`, 3000);
|
||||||
setTimeout(() => window.location.reload(), 1000);
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
closeQuickStatusModal();
|
closeQuickStatusModal();
|
||||||
toast.error('Error updating status', 4000);
|
lt.toast.error('Error updating status', 4000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1375,7 +1377,7 @@ function quickAssign(ticketId) {
|
|||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close>✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<p style="margin-bottom:0.5rem;">Ticket #${escapeHtml(ticketId)}</p>
|
<p style="margin-bottom:0.5rem;">Ticket #${lt.escHtml(ticketId)}</p>
|
||||||
<label for="quickAssignSelect">Assign to:</label>
|
<label for="quickAssignSelect">Assign to:</label>
|
||||||
<select id="quickAssignSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
<select id="quickAssignSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
||||||
<option value="">Unassigned</option>
|
<option value="">Unassigned</option>
|
||||||
@@ -1433,15 +1435,15 @@ function performQuickAssign(ticketId) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
closeQuickAssignModal();
|
closeQuickAssignModal();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success('Assignment updated', 3000);
|
lt.toast.success('Assignment updated', 3000);
|
||||||
setTimeout(() => window.location.reload(), 1000);
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
closeQuickAssignModal();
|
closeQuickAssignModal();
|
||||||
toast.error('Error updating assignment', 4000);
|
lt.toast.error('Error updating assignment', 4000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1522,13 +1524,13 @@ function populateKanbanCards() {
|
|||||||
card.onclick = () => window.location.href = `/ticket/${ticketId}`;
|
card.onclick = () => window.location.href = `/ticket/${ticketId}`;
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-id">#${escapeHtml(ticketId)}</span>
|
<span class="card-id">#${lt.escHtml(ticketId)}</span>
|
||||||
<span class="card-priority p${priority}">P${priority}</span>
|
<span class="card-priority p${priority}">P${priority}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-title">${escapeHtml(title)}</div>
|
<div class="card-title">${lt.escHtml(title)}</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<span class="card-category">${escapeHtml(category)}</span>
|
<span class="card-category">${lt.escHtml(category)}</span>
|
||||||
<span class="card-assignee" title="${escapeHtml(assignedTo)}">${escapeHtml(initials)}</span>
|
<span class="card-assignee" title="${lt.escHtml(assignedTo)}">${lt.escHtml(initials)}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
column.appendChild(card);
|
column.appendChild(card);
|
||||||
@@ -1615,17 +1617,17 @@ function showTicketPreview(event) {
|
|||||||
// Build preview content
|
// Build preview content
|
||||||
currentPreview.innerHTML = `
|
currentPreview.innerHTML = `
|
||||||
<div class="preview-header">
|
<div class="preview-header">
|
||||||
<span class="preview-id">#${escapeHtml(ticketId)}</span>
|
<span class="preview-id">#${lt.escHtml(ticketId)}</span>
|
||||||
<span class="preview-status status-${status.replace(/\s+/g, '-')}">${escapeHtml(status)}</span>
|
<span class="preview-status status-${status.replace(/\s+/g, '-')}">${lt.escHtml(status)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-title">${escapeHtml(title)}</div>
|
<div class="preview-title">${lt.escHtml(title)}</div>
|
||||||
<div class="preview-meta">
|
<div class="preview-meta">
|
||||||
<div><strong>Priority:</strong> P${escapeHtml(priority)}</div>
|
<div><strong>Priority:</strong> P${lt.escHtml(priority)}</div>
|
||||||
<div><strong>Category:</strong> ${escapeHtml(category)}</div>
|
<div><strong>Category:</strong> ${lt.escHtml(category)}</div>
|
||||||
<div><strong>Type:</strong> ${escapeHtml(type)}</div>
|
<div><strong>Type:</strong> ${lt.escHtml(type)}</div>
|
||||||
<div><strong>Assigned:</strong> ${escapeHtml(assignedTo)}</div>
|
<div><strong>Assigned:</strong> ${lt.escHtml(assignedTo)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-footer">Created by ${escapeHtml(createdBy)}</div>
|
<div class="preview-footer">Created by ${lt.escHtml(createdBy)}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Position the preview
|
// Position the preview
|
||||||
@@ -1697,7 +1699,7 @@ function exportSelectedTickets(format) {
|
|||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (ticketIds.length === 0) {
|
if (ticketIds.length === 0) {
|
||||||
toast.warning('No tickets selected', 2000);
|
lt.toast.warning('No tickets selected', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ function createEditorToolbar(textareaId, containerId) {
|
|||||||
<button type="button" data-toolbar-action="list" data-textarea="${textareaId}" title="List">≡</button>
|
<button type="button" data-toolbar-action="list" data-textarea="${textareaId}" title="List">≡</button>
|
||||||
<button type="button" data-toolbar-action="quote" data-textarea="${textareaId}" title="Quote">"</button>
|
<button type="button" data-toolbar-action="quote" data-textarea="${textareaId}" title="Quote">"</button>
|
||||||
<span class="toolbar-separator"></span>
|
<span class="toolbar-separator"></span>
|
||||||
<button type="button" data-toolbar-action="link" data-textarea="${textareaId}" title="Link">🔗</button>
|
<button type="button" data-toolbar-action="link" data-textarea="${textareaId}" title="Link">[ @ ]</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add event delegation for toolbar buttons
|
// Add event delegation for toolbar buttons
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function saveTicket() {
|
|||||||
statusDisplay.className = `status-${data.status}`;
|
statusDisplay.className = `status-${data.status}`;
|
||||||
statusDisplay.textContent = data.status;
|
statusDisplay.textContent = data.status;
|
||||||
}
|
}
|
||||||
toast.success('Ticket updated successfully');
|
lt.toast.success('Ticket updated successfully');
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -317,11 +317,11 @@ function handleAssignmentChange() {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
toast.error('Error updating assignment');
|
lt.toast.error('Error updating assignment');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Error updating assignment: ' + error.message);
|
lt.toast.error('Error updating assignment: ' + error.message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -353,7 +353,7 @@ function handleMetadataChanges() {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
toast.error(`Error updating ${fieldName}`);
|
lt.toast.error(`Error updating ${fieldName}`);
|
||||||
} else {
|
} else {
|
||||||
// Update window.ticketData
|
// Update window.ticketData
|
||||||
window.ticketData[fieldName] = fieldName === 'priority' ? parseInt(newValue) : newValue;
|
window.ticketData[fieldName] = fieldName === 'priority' ? parseInt(newValue) : newValue;
|
||||||
@@ -375,7 +375,7 @@ function handleMetadataChanges() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error(`Error updating ${fieldName}: ` + error.message);
|
lt.toast.error(`Error updating ${fieldName}: ` + error.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,13 +492,13 @@ function performStatusChange(statusSelect, selectedOption, newStatus) {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 500);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error updating status: ' + (data.error || 'Unknown error'));
|
lt.toast.error('Error updating status: ' + (data.error || 'Unknown error'));
|
||||||
// Reset to current status
|
// Reset to current status
|
||||||
statusSelect.selectedIndex = 0;
|
statusSelect.selectedIndex = 0;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Error updating status: ' + error.message);
|
lt.toast.error('Error updating status: ' + error.message);
|
||||||
// Reset to current status
|
// Reset to current status
|
||||||
statusSelect.selectedIndex = 0;
|
statusSelect.selectedIndex = 0;
|
||||||
});
|
});
|
||||||
@@ -587,10 +587,10 @@ function showDependencyError(message) {
|
|||||||
const dependentsList = document.getElementById('dependentsList');
|
const dependentsList = document.getElementById('dependentsList');
|
||||||
|
|
||||||
if (dependenciesList) {
|
if (dependenciesList) {
|
||||||
dependenciesList.innerHTML = `<p style="color: var(--terminal-amber);">${escapeHtml(message)}</p>`;
|
dependenciesList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
|
||||||
}
|
}
|
||||||
if (dependentsList) {
|
if (dependentsList) {
|
||||||
dependentsList.innerHTML = `<p style="color: var(--terminal-amber);">${escapeHtml(message)}</p>`;
|
dependentsList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,11 +618,11 @@ function renderDependencies(dependencies) {
|
|||||||
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
|
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
|
||||||
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
|
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
|
||||||
<div>
|
<div>
|
||||||
<a href="/ticket/${escapeHtml(dep.depends_on_id)}" style="color: var(--terminal-green);">
|
<a href="/ticket/${lt.escHtml(dep.depends_on_id)}" style="color: var(--terminal-green);">
|
||||||
#${escapeHtml(dep.depends_on_id)}
|
#${lt.escHtml(dep.depends_on_id)}
|
||||||
</a>
|
</a>
|
||||||
<span style="margin-left: 0.5rem;">${escapeHtml(dep.title)}</span>
|
<span style="margin-left: 0.5rem;">${lt.escHtml(dep.title)}</span>
|
||||||
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${escapeHtml(dep.status)}</span>
|
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${lt.escHtml(dep.status)}</span>
|
||||||
</div>
|
</div>
|
||||||
<button data-action="remove-dependency" data-dependency-id="${dep.dependency_id}" class="btn btn-small" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">Remove</button>
|
<button data-action="remove-dependency" data-dependency-id="${dep.dependency_id}" class="btn btn-small" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">Remove</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -653,12 +653,12 @@ function renderDependents(dependents) {
|
|||||||
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
|
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
|
||||||
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
|
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
|
||||||
<div>
|
<div>
|
||||||
<a href="/ticket/${escapeHtml(dep.ticket_id)}" style="color: var(--terminal-green);">
|
<a href="/ticket/${lt.escHtml(dep.ticket_id)}" style="color: var(--terminal-green);">
|
||||||
#${escapeHtml(dep.ticket_id)}
|
#${lt.escHtml(dep.ticket_id)}
|
||||||
</a>
|
</a>
|
||||||
<span style="margin-left: 0.5rem;">${escapeHtml(dep.title)}</span>
|
<span style="margin-left: 0.5rem;">${lt.escHtml(dep.title)}</span>
|
||||||
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${escapeHtml(dep.status)}</span>
|
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${lt.escHtml(dep.status)}</span>
|
||||||
<span style="margin-left: 0.5rem; color: var(--terminal-amber);">(${escapeHtml(dep.dependency_type)})</span>
|
<span style="margin-left: 0.5rem; color: var(--terminal-amber);">(${lt.escHtml(dep.dependency_type)})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
@@ -672,7 +672,7 @@ function addDependency() {
|
|||||||
const dependencyType = document.getElementById('dependencyType').value;
|
const dependencyType = document.getElementById('dependencyType').value;
|
||||||
|
|
||||||
if (!dependsOnId) {
|
if (!dependsOnId) {
|
||||||
toast.warning('Please enter a ticket ID', 3000);
|
lt.toast.warning('Please enter a ticket ID', 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,15 +692,15 @@ function addDependency() {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success('Dependency added', 3000);
|
lt.toast.success('Dependency added', 3000);
|
||||||
document.getElementById('dependencyTicketId').value = '';
|
document.getElementById('dependencyTicketId').value = '';
|
||||||
loadDependencies();
|
loadDependencies();
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Error adding dependency', 4000);
|
lt.toast.error('Error adding dependency', 4000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,14 +723,14 @@ function removeDependency(dependencyId) {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success('Dependency removed', 3000);
|
lt.toast.success('Dependency removed', 3000);
|
||||||
loadDependencies();
|
loadDependencies();
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Error removing dependency', 4000);
|
lt.toast.error('Error removing dependency', 4000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,18 +828,18 @@ function handleFileUpload(files) {
|
|||||||
const response = JSON.parse(xhr.responseText);
|
const response = JSON.parse(xhr.responseText);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
if (uploadedCount === totalFiles) {
|
if (uploadedCount === totalFiles) {
|
||||||
toast.success(`${totalFiles} file(s) uploaded successfully`, 3000);
|
lt.toast.success(`${totalFiles} file(s) uploaded successfully`, 3000);
|
||||||
loadAttachments();
|
loadAttachments();
|
||||||
resetUploadUI();
|
resetUploadUI();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Error uploading ${file.name}: ${response.error}`, 4000);
|
lt.toast.error(`Error uploading ${file.name}: ${response.error}`, 4000);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(`Error parsing response for ${file.name}`, 4000);
|
lt.toast.error(`Error parsing response for ${file.name}`, 4000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Error uploading ${file.name}: Server error`, 4000);
|
lt.toast.error(`Error uploading ${file.name}: Server error`, 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploadedCount === totalFiles) {
|
if (uploadedCount === totalFiles) {
|
||||||
@@ -849,7 +849,7 @@ function handleFileUpload(files) {
|
|||||||
|
|
||||||
xhr.addEventListener('error', () => {
|
xhr.addEventListener('error', () => {
|
||||||
uploadedCount++;
|
uploadedCount++;
|
||||||
toast.error(`Error uploading ${file.name}: Network error`, 4000);
|
lt.toast.error(`Error uploading ${file.name}: Network error`, 4000);
|
||||||
if (uploadedCount === totalFiles) {
|
if (uploadedCount === totalFiles) {
|
||||||
setTimeout(resetUploadUI, 2000);
|
setTimeout(resetUploadUI, 2000);
|
||||||
}
|
}
|
||||||
@@ -914,15 +914,15 @@ function renderAttachments(attachments) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
html += `<div class="attachment-item" data-id="${att.attachment_id}">
|
html += `<div class="attachment-item" data-id="${att.attachment_id}">
|
||||||
<div class="attachment-icon">${escapeHtml(att.icon || '📎')}</div>
|
<div class="attachment-icon">${lt.escHtml(att.icon || '[ f ]')}</div>
|
||||||
<div class="attachment-info">
|
<div class="attachment-info">
|
||||||
<div class="attachment-name" title="${escapeHtml(att.original_filename)}">
|
<div class="attachment-name" title="${lt.escHtml(att.original_filename)}">
|
||||||
<a href="/api/download_attachment.php?id=${att.attachment_id}" target="_blank" style="color: var(--terminal-green);">
|
<a href="/api/download_attachment.php?id=${att.attachment_id}" target="_blank" style="color: var(--terminal-green);">
|
||||||
${escapeHtml(att.original_filename)}
|
${lt.escHtml(att.original_filename)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="attachment-meta">
|
<div class="attachment-meta">
|
||||||
${escapeHtml(att.file_size_formatted || formatFileSize(att.file_size))} • ${escapeHtml(uploaderName)} • ${escapeHtml(uploadDate)}
|
${lt.escHtml(att.file_size_formatted || formatFileSize(att.file_size))} • ${lt.escHtml(uploaderName)} • ${lt.escHtml(uploadDate)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="attachment-actions">
|
<div class="attachment-actions">
|
||||||
@@ -968,14 +968,14 @@ function deleteAttachment(attachmentId) {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success('Attachment deleted', 3000);
|
lt.toast.success('Attachment deleted', 3000);
|
||||||
loadAttachments();
|
loadAttachments();
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
toast.error('Error deleting attachment', 4000);
|
lt.toast.error('Error deleting attachment', 4000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,9 +1118,9 @@ function showMentionSuggestions(query, textarea) {
|
|||||||
let html = '';
|
let html = '';
|
||||||
filtered.forEach((user, index) => {
|
filtered.forEach((user, index) => {
|
||||||
const isSelected = index === 0 ? 'selected' : '';
|
const isSelected = index === 0 ? 'selected' : '';
|
||||||
html += `<div class="mention-option ${isSelected}" data-username="${escapeHtml(user.username)}" data-action="select-mention">
|
html += `<div class="mention-option ${isSelected}" data-username="${lt.escHtml(user.username)}" data-action="select-mention">
|
||||||
<span class="mention-username">@${escapeHtml(user.username)}</span>
|
<span class="mention-username">@${lt.escHtml(user.username)}</span>
|
||||||
${user.display_name ? `<span class="mention-displayname">${escapeHtml(user.display_name)}</span>` : ''}
|
${user.display_name ? `<span class="mention-displayname">${lt.escHtml(user.display_name)}</span>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1252,7 +1252,7 @@ function editComment(commentId) {
|
|||||||
editForm.className = 'comment-edit-form';
|
editForm.className = 'comment-edit-form';
|
||||||
editForm.id = `comment-edit-form-${commentId}`;
|
editForm.id = `comment-edit-form-${commentId}`;
|
||||||
editForm.innerHTML = `
|
editForm.innerHTML = `
|
||||||
<textarea id="comment-edit-textarea-${commentId}" class="comment-edit-textarea">${escapeHtml(originalText)}</textarea>
|
<textarea id="comment-edit-textarea-${commentId}" class="comment-edit-textarea">${lt.escHtml(originalText)}</textarea>
|
||||||
<div class="comment-edit-controls">
|
<div class="comment-edit-controls">
|
||||||
<label class="markdown-toggle-small">
|
<label class="markdown-toggle-small">
|
||||||
<input type="checkbox" id="comment-edit-markdown-${commentId}" ${markdownEnabled ? 'checked' : ''}>
|
<input type="checkbox" id="comment-edit-markdown-${commentId}" ${markdownEnabled ? 'checked' : ''}>
|
||||||
@@ -1331,7 +1331,7 @@ function saveEditComment(commentId) {
|
|||||||
} else {
|
} else {
|
||||||
textDiv.removeAttribute('data-markdown');
|
textDiv.removeAttribute('data-markdown');
|
||||||
// Convert newlines to <br> and highlight mentions
|
// Convert newlines to <br> and highlight mentions
|
||||||
let displayText = escapeHtml(newText).replace(/\n/g, '<br>');
|
let displayText = lt.escHtml(newText).replace(/\n/g, '<br>');
|
||||||
displayText = highlightMentions(displayText);
|
displayText = highlightMentions(displayText);
|
||||||
// Auto-link URLs
|
// Auto-link URLs
|
||||||
if (typeof autoLinkUrls === 'function') {
|
if (typeof autoLinkUrls === 'function') {
|
||||||
@@ -1543,8 +1543,8 @@ function submitReply(parentCommentId) {
|
|||||||
<span class="comment-date">${data.created_at}</span>
|
<span class="comment-date">${data.created_at}</span>
|
||||||
<div class="comment-actions">
|
<div class="comment-actions">
|
||||||
${newDepth < 3 ? `<button type="button" class="comment-action-btn reply-btn" data-action="reply-comment" data-comment-id="${data.comment_id}" data-user="${data.user_name}" title="Reply">↩</button>` : ''}
|
${newDepth < 3 ? `<button type="button" class="comment-action-btn reply-btn" data-action="reply-comment" data-comment-id="${data.comment_id}" data-user="${data.user_name}" title="Reply">↩</button>` : ''}
|
||||||
<button type="button" class="comment-action-btn edit-btn" data-action="edit-comment" data-comment-id="${data.comment_id}" title="Edit">✏️</button>
|
<button type="button" class="comment-action-btn edit-btn" data-action="edit-comment" data-comment-id="${data.comment_id}" title="Edit">[ EDIT ]</button>
|
||||||
<button type="button" class="comment-action-btn delete-btn" data-action="delete-comment" data-comment-id="${data.comment_id}" title="Delete">🗑️</button>
|
<button type="button" class="comment-action-btn delete-btn" data-action="delete-comment" data-comment-id="${data.comment_id}" title="Delete">[ DEL ]</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-text" id="comment-text-${data.comment_id}" ${isMarkdownEnabled ? 'data-markdown' : ''}>
|
<div class="comment-text" id="comment-text-${data.comment_id}" ${isMarkdownEnabled ? 'data-markdown' : ''}>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Create New Ticket</title>
|
<title>Create New Ticket</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/assets/css/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260126c">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260319b">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260124e">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260319b">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260205"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260205"></script>
|
||||||
@@ -25,13 +25,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">← Dashboard</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name">👤 <?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +63,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<div class="error-message" style="color: var(--priority-1); border: 2px solid var(--priority-1); padding: 1rem; background: rgba(231, 76, 60, 0.1);">
|
<div class="error-message" style="color: var(--priority-1); border: 2px solid var(--priority-1); padding: 1rem; background: rgba(231, 76, 60, 0.1);">
|
||||||
<strong>⚠ Error:</strong> <?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?>
|
<strong>[ ! ] ERROR:</strong> <?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Ticket Dashboard</title>
|
<title>Ticket Dashboard</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/assets/css/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260131e">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260319b">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||||
@@ -80,26 +80,26 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</script>
|
</script>
|
||||||
<header class="user-header" role="banner">
|
<header class="user-header" role="banner">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="app-title">🎫 Tinker Tickets</a>
|
<a href="/" class="app-title">[ TINKER TICKETS ]</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name">👤 <?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
||||||
<div class="admin-dropdown">
|
<div class="admin-dropdown">
|
||||||
<button class="admin-badge" data-action="toggle-admin-menu">Admin ▼</button>
|
<button class="admin-badge" data-action="toggle-admin-menu">[ ADMIN ▼ ]</button>
|
||||||
<div class="admin-dropdown-content" id="adminDropdown">
|
<div class="admin-dropdown-content" id="adminDropdown">
|
||||||
<a href="/admin/templates">📋 Templates</a>
|
<a href="/admin/templates">TEMPLATES</a>
|
||||||
<a href="/admin/workflow">🔄 Workflow</a>
|
<a href="/admin/workflow">WORKFLOW</a>
|
||||||
<a href="/admin/recurring-tickets">🔁 Recurring Tickets</a>
|
<a href="/admin/recurring-tickets">RECURRING</a>
|
||||||
<a href="/admin/custom-fields">📝 Custom Fields</a>
|
<a href="/admin/custom-fields">CUSTOM FIELDS</a>
|
||||||
<a href="/admin/user-activity">👥 User Activity</a>
|
<a href="/admin/user-activity">USER ACTIVITY</a>
|
||||||
<a href="/admin/audit-log">📜 Audit Log</a>
|
<a href="/admin/audit-log">AUDIT LOG</a>
|
||||||
<a href="/admin/api-keys">🔑 API Keys</a>
|
<a href="/admin/api-keys">API KEYS</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button class="settings-icon" title="Settings (Alt+S)" data-action="open-settings" aria-label="Settings">⚙</button>
|
<button class="settings-icon" title="Settings (Alt+S)" data-action="open-settings" aria-label="Settings">[ CFG ]</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -203,42 +203,42 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="stats-widgets">
|
<div class="stats-widgets">
|
||||||
<div class="stats-row">
|
<div class="stats-row">
|
||||||
<div class="stat-card stat-open">
|
<div class="stat-card stat-open">
|
||||||
<div class="stat-icon">📂</div>
|
<div class="stat-icon">[ # ]</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value"><?php echo $stats['open_tickets']; ?></div>
|
<div class="stat-value"><?php echo $stats['open_tickets']; ?></div>
|
||||||
<div class="stat-label">Open Tickets</div>
|
<div class="stat-label">Open Tickets</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card stat-critical">
|
<div class="stat-card stat-critical">
|
||||||
<div class="stat-icon">🔥</div>
|
<div class="stat-icon">[ ! ]</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value"><?php echo $stats['critical']; ?></div>
|
<div class="stat-value"><?php echo $stats['critical']; ?></div>
|
||||||
<div class="stat-label">Critical (P1)</div>
|
<div class="stat-label">Critical (P1)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card stat-unassigned">
|
<div class="stat-card stat-unassigned">
|
||||||
<div class="stat-icon">👤</div>
|
<div class="stat-icon">[ @ ]</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value"><?php echo $stats['unassigned']; ?></div>
|
<div class="stat-value"><?php echo $stats['unassigned']; ?></div>
|
||||||
<div class="stat-label">Unassigned</div>
|
<div class="stat-label">Unassigned</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card stat-today">
|
<div class="stat-card stat-today">
|
||||||
<div class="stat-icon">📅</div>
|
<div class="stat-icon">[ + ]</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value"><?php echo $stats['created_today']; ?></div>
|
<div class="stat-value"><?php echo $stats['created_today']; ?></div>
|
||||||
<div class="stat-label">Created Today</div>
|
<div class="stat-label">Created Today</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card stat-resolved">
|
<div class="stat-card stat-resolved">
|
||||||
<div class="stat-icon">✓</div>
|
<div class="stat-icon">[ ✓ ]</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value"><?php echo $stats['closed_today']; ?></div>
|
<div class="stat-value"><?php echo $stats['closed_today']; ?></div>
|
||||||
<div class="stat-label">Closed Today</div>
|
<div class="stat-label">Closed Today</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card stat-time">
|
<div class="stat-card stat-time">
|
||||||
<div class="stat-icon">⏱</div>
|
<div class="stat-icon">[ t ]</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value"><?php echo $stats['avg_resolution_hours']; ?>h</div>
|
<div class="stat-value"><?php echo $stats['avg_resolution_hours']; ?>h</div>
|
||||||
<div class="stat-label">Avg Resolution</div>
|
<div class="stat-label">Avg Resolution</div>
|
||||||
@@ -252,7 +252,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="dashboard-toolbar">
|
<div class="dashboard-toolbar">
|
||||||
<!-- Left: Title + Search -->
|
<!-- Left: Title + Search -->
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<h1 class="dashboard-title">🎫 Tickets</h1>
|
<h1 class="dashboard-title">[ TICKETS ]</h1>
|
||||||
<form method="GET" action="" class="toolbar-search">
|
<form method="GET" action="" class="toolbar-search">
|
||||||
<!-- Preserve existing parameters -->
|
<!-- Preserve existing parameters -->
|
||||||
<?php if (isset($_GET['status'])): ?>
|
<?php if (isset($_GET['status'])): ?>
|
||||||
@@ -273,11 +273,11 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
|
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="search"
|
name="search"
|
||||||
placeholder="🔍 Search tickets..."
|
placeholder="> Search tickets..."
|
||||||
class="search-box"
|
class="search-box"
|
||||||
value="<?php echo isset($_GET['search']) ? htmlspecialchars($_GET['search']) : ''; ?>">
|
value="<?php echo isset($_GET['search']) ? htmlspecialchars($_GET['search']) : ''; ?>">
|
||||||
<button type="submit" class="btn search-btn">Search</button>
|
<button type="submit" class="btn search-btn">Search</button>
|
||||||
<button type="button" class="btn btn-secondary" data-action="open-advanced-search" title="Advanced Search">⚙ Advanced</button>
|
<button type="button" class="btn btn-secondary" data-action="open-advanced-search" title="Advanced Search">FILTER</button>
|
||||||
<?php if (isset($_GET['search']) && !empty($_GET['search'])): ?>
|
<?php if (isset($_GET['search']) && !empty($_GET['search'])): ?>
|
||||||
<a href="?" class="clear-search-btn">✗</a>
|
<a href="?" class="clear-search-btn">✗</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -468,9 +468,9 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
// Quick actions column
|
// Quick actions column
|
||||||
echo "<td class='quick-actions-cell'>";
|
echo "<td class='quick-actions-cell'>";
|
||||||
echo "<div class='quick-actions'>";
|
echo "<div class='quick-actions'>";
|
||||||
echo "<button data-action='view-ticket' data-ticket-id='{$row['ticket_id']}' class='quick-action-btn' title='View'>👁</button>";
|
echo "<button data-action='view-ticket' data-ticket-id='{$row['ticket_id']}' class='quick-action-btn' title='View'>></button>";
|
||||||
echo "<button data-action='quick-status' data-ticket-id='{$row['ticket_id']}' data-status='{$row['status']}' class='quick-action-btn' title='Change Status'>🔄</button>";
|
echo "<button data-action='quick-status' data-ticket-id='{$row['ticket_id']}' data-status='{$row['status']}' class='quick-action-btn' title='Change Status'>~</button>";
|
||||||
echo "<button data-action='quick-assign' data-ticket-id='{$row['ticket_id']}' class='quick-action-btn' title='Assign'>👤</button>";
|
echo "<button data-action='quick-assign' data-ticket-id='{$row['ticket_id']}' class='quick-action-btn' title='Assign'>@</button>";
|
||||||
echo "</div>";
|
echo "</div>";
|
||||||
echo "</td>";
|
echo "</td>";
|
||||||
echo "</tr>";
|
echo "</tr>";
|
||||||
@@ -509,17 +509,17 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="ticket-card-main">
|
<div class="ticket-card-main">
|
||||||
<div class="ticket-card-title"><?php echo htmlspecialchars($row['title']); ?></div>
|
<div class="ticket-card-title"><?php echo htmlspecialchars($row['title']); ?></div>
|
||||||
<div class="ticket-card-meta">
|
<div class="ticket-card-meta">
|
||||||
<span>📁 <?php echo htmlspecialchars($row['category']); ?></span>
|
<span><?php echo htmlspecialchars($row['category']); ?></span>
|
||||||
<span>👤 <?php echo htmlspecialchars($assignedTo); ?></span>
|
<span>@ <?php echo htmlspecialchars($assignedTo); ?></span>
|
||||||
<span>📅 <?php echo date('M j', strtotime($row['updated_at'])); ?></span>
|
<span><?php echo date('M j', strtotime($row['updated_at'])); ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ticket-card-status <?php echo $statusClass; ?>">
|
<div class="ticket-card-status <?php echo $statusClass; ?>">
|
||||||
<?php echo $row['status']; ?>
|
<?php echo $row['status']; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="ticket-card-actions">
|
<div class="ticket-card-actions">
|
||||||
<button data-action="view-ticket" data-ticket-id="<?php echo $row['ticket_id']; ?>" title="View">👁</button>
|
<button data-action="view-ticket" data-ticket-id="<?php echo $row['ticket_id']; ?>" title="View">></button>
|
||||||
<button data-action="quick-status" data-ticket-id="<?php echo $row['ticket_id']; ?>" data-status="<?php echo $row['status']; ?>" title="Status">🔄</button>
|
<button data-action="quick-status" data-ticket-id="<?php echo $row['ticket_id']; ?>" data-status="<?php echo $row['status']; ?>" title="Status">~</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
@@ -578,7 +578,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="lt-modal-overlay" id="settingsModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="settingsModalTitle">
|
<div class="lt-modal-overlay" id="settingsModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="settingsModalTitle">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title" id="settingsModalTitle">⚙ System Preferences</span>
|
<span class="lt-modal-title" id="settingsModalTitle">[ CFG ] SYSTEM PREFERENCES</span>
|
||||||
<button class="lt-modal-close" data-modal-close aria-label="Close settings">✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close settings">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -734,7 +734,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="lt-modal-overlay" id="advancedSearchModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="advancedSearchModalTitle">
|
<div class="lt-modal-overlay" id="advancedSearchModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="advancedSearchModalTitle">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title" id="advancedSearchModalTitle">🔍 Advanced Search</span>
|
<span class="lt-modal-title" id="advancedSearchModalTitle">[ FILTER ] ADVANCED SEARCH</span>
|
||||||
<button class="lt-modal-close" data-modal-close aria-label="Close advanced search">✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close advanced search">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -750,8 +750,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row setting-row-right">
|
<div class="setting-row setting-row-right">
|
||||||
<button type="button" class="btn btn-secondary btn-setting" data-action="save-filter">💾 Save Current</button>
|
<button type="button" class="btn btn-secondary btn-setting" data-action="save-filter">SAVE</button>
|
||||||
<button type="button" class="btn btn-secondary btn-setting" data-action="delete-filter">🗑 Delete Selected</button>
|
<button type="button" class="btn btn-secondary btn-setting" data-action="delete-filter">DELETE</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
// Helper functions for timeline display
|
// Helper functions for timeline display
|
||||||
function getEventIcon($actionType) {
|
function getEventIcon($actionType) {
|
||||||
$icons = [
|
$icons = [
|
||||||
'create' => '✨',
|
'create' => '[ + ]',
|
||||||
'update' => '📝',
|
'update' => '[ ~ ]',
|
||||||
'comment' => '💬',
|
'comment' => '[ > ]',
|
||||||
'view' => '👁️',
|
'view' => '[ . ]',
|
||||||
'assign' => '👤',
|
'assign' => '[ @ ]',
|
||||||
'status_change' => '🔄'
|
'status_change' => '[ ! ]',
|
||||||
];
|
];
|
||||||
return $icons[$actionType] ?? '•';
|
return $icons[$actionType] ?? '[ * ]';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAction($event) {
|
function formatAction($event) {
|
||||||
@@ -51,8 +51,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Ticket #<?php echo $ticket['ticket_id']; ?></title>
|
<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="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/assets/css/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260131e">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260319b">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260131e">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260319b">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
||||||
@@ -82,15 +82,15 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<body>
|
<body>
|
||||||
<header class="user-header">
|
<header class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">← Dashboard</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name">👤 <?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button class="settings-icon" title="Settings (Alt+S)" id="settingsBtn" aria-label="Settings">⚙</button>
|
<button class="settings-icon" title="Settings (Alt+S)" id="settingsBtn" aria-label="Settings">[ CFG ]</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -134,7 +134,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<div class="ticket-age <?php echo $ageClass; ?>" title="Time since last update">
|
<div class="ticket-age <?php echo $ageClass; ?>" title="Time since last update">
|
||||||
<span class="age-icon"><?php echo $ageClass === 'age-critical' ? '⚠️' : ($ageClass === 'age-warning' ? '⏰' : '📅'); ?></span>
|
<span class="age-icon"><?php echo $ageClass === 'age-critical' ? '[ ! ]' : ($ageClass === 'age-warning' ? '[ ~ ]' : '[ t ]'); ?></span>
|
||||||
<span class="age-text">Last activity: <?php echo $ageStr; ?> ago</span>
|
<span class="age-text">Last activity: <?php echo $ageStr; ?> ago</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ticket-user-info">
|
<div class="ticket-user-info">
|
||||||
@@ -372,8 +372,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
}
|
}
|
||||||
// Edit/Delete buttons for owner or admin
|
// Edit/Delete buttons for owner or admin
|
||||||
if ($canModify) {
|
if ($canModify) {
|
||||||
echo "<button type='button' class='comment-action-btn edit-btn' data-action='edit-comment' data-comment-id='{$commentId}' title='Edit' aria-label='Edit comment'>✏️</button>";
|
echo "<button type='button' class='comment-action-btn edit-btn' data-action='edit-comment' data-comment-id='{$commentId}' title='Edit' aria-label='Edit comment'>[ EDIT ]</button>";
|
||||||
echo "<button type='button' class='comment-action-btn delete-btn' data-action='delete-comment' data-comment-id='{$commentId}' title='Delete' aria-label='Delete comment'>🗑️</button>";
|
echo "<button type='button' class='comment-action-btn delete-btn' data-action='delete-comment' data-comment-id='{$commentId}' title='Delete' aria-label='Delete comment'>[ DEL ]</button>";
|
||||||
}
|
}
|
||||||
echo "</div>";
|
echo "</div>";
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<h3>Upload Files</h3>
|
<h3>Upload Files</h3>
|
||||||
<div class="upload-zone" id="uploadZone">
|
<div class="upload-zone" id="uploadZone">
|
||||||
<div class="upload-zone-content">
|
<div class="upload-zone-content">
|
||||||
<div class="upload-icon">📁</div>
|
<div class="upload-icon">[ + ]</div>
|
||||||
<p>Drag and drop files here or click to browse</p>
|
<p>Drag and drop files here or click to browse</p>
|
||||||
<p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p>
|
<p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p>
|
||||||
<input type="file" id="fileInput" multiple class="sr-only" aria-label="Upload files">
|
<input type="file" id="fileInput" multiple class="sr-only" aria-label="Upload files">
|
||||||
@@ -685,7 +685,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="lt-modal-overlay" id="settingsModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="ticketSettingsTitle">
|
<div class="lt-modal-overlay" id="settingsModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="ticketSettingsTitle">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title" id="ticketSettingsTitle">⚙ System Preferences</span>
|
<span class="lt-modal-title" id="ticketSettingsTitle">[ CFG ] SYSTEM PREFERENCES</span>
|
||||||
<button class="lt-modal-close" data-modal-close aria-label="Close settings">✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close settings">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">← Dashboard</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: API Keys</span>
|
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: API Keys</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name"><?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
// Admin view for browsing audit logs
|
// Admin view for browsing audit logs
|
||||||
// Receives $auditLogs, $totalPages, $page, $filters from controller
|
// Receives $auditLogs, $totalPages, $page, $filters from controller
|
||||||
|
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
||||||
|
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
||||||
|
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -12,18 +15,21 @@
|
|||||||
<link rel="stylesheet" href="/assets/css/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<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">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script src="/assets/js/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">← Dashboard</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Audit Log</span>
|
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Audit Log</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name"><?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">← Dashboard</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Custom Fields</span>
|
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Custom Fields</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name"><?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">← Dashboard</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Recurring Tickets</span>
|
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Recurring Tickets</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name"><?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">← Dashboard</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Templates</span>
|
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Templates</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name"><?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
// Admin view for user activity reports
|
// Admin view for user activity reports
|
||||||
// Receives $userStats, $dateRange from controller
|
// Receives $userStats, $dateRange from controller
|
||||||
|
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
||||||
|
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
||||||
|
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -12,18 +15,21 @@
|
|||||||
<link rel="stylesheet" href="/assets/css/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<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">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script src="/assets/js/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">← Dashboard</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: User Activity</span>
|
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: User Activity</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name"><?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,7 +80,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<strong><?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?></strong>
|
<strong><?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?></strong>
|
||||||
<?php if ($user['is_admin']): ?>
|
<?php if ($user['is_admin']): ?>
|
||||||
<span class="admin-badge" style="font-size: 0.7rem;">Admin</span>
|
<span class="admin-badge" style="font-size: 0.7rem;">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;">
|
<td style="text-align: center;">
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">← Dashboard</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Workflow Designer</span>
|
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Workflow Designer</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
<span class="user-name"><?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
|
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user