diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 3b6376e..2c92a38 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -160,6 +160,215 @@ body::after { 100% { content: '10101010'; opacity: 0.1; } } +/* ===== ENHANCED TERMINAL ANIMATIONS ===== */ + +/* Typing cursor effect for focused inputs */ +input:focus::placeholder, +textarea:focus::placeholder { + animation: typing-cursor 1s steps(1) infinite; +} + +@keyframes typing-cursor { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} + +/* Glowing border pulse on focus */ +input:focus, +textarea:focus, +select:focus { + animation: focus-pulse 2s ease-in-out infinite; +} + +@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); } +} + +/* Boot-up fade in effect */ +@keyframes boot-up { + 0% { + opacity: 0; + filter: brightness(2) saturate(0); + } + 30% { + opacity: 1; + filter: brightness(1.5) saturate(0.5); + } + 100% { + opacity: 1; + filter: brightness(1) saturate(1); + } +} + +.ascii-frame-outer { + animation: boot-up 0.8s ease-out; +} + +/* Phosphor text glow on important elements */ +.stat-value, +.ticket-id, +h1, h2, h3 { + text-shadow: 0 0 2px currentColor, 0 0 4px currentColor; +} + +/* Enhanced link hover with underline animation */ +a:not(.btn) { + position: relative; + text-decoration: none; +} + +a:not(.btn)::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 1px; + background: currentColor; + box-shadow: 0 0 5px currentColor; + transition: width 0.3s ease; +} + +a:not(.btn):hover::after { + width: 100%; +} + +/* Matrix-style rain effect on hover for stats */ +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(180deg, + transparent 0%, + rgba(0, 255, 65, 0.03) 50%, + transparent 100% + ); + background-size: 100% 200%; + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; +} + +.stat-card:hover::before { + opacity: 1; + animation: matrix-rain 2s linear infinite; +} + +@keyframes matrix-rain { + 0% { background-position: 0 -100%; } + 100% { background-position: 0 100%; } +} + +/* Smooth table row selection animation */ +tbody tr { + transition: all 0.2s ease; + position: relative; +} + +tbody tr::before { + content: '>'; + position: absolute; + left: -20px; + opacity: 0; + color: var(--terminal-green); + transition: all 0.2s ease; +} + +tbody tr:hover::before { + opacity: 1; + left: -15px; +} + +/* Button press effect */ +.btn { + transition: all 0.15s ease; + 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 */ +.keyboard-selected td:first-child::before { + content: '█'; + position: absolute; + left: -25px; + animation: cursor-blink 0.8s steps(1) infinite; + color: var(--terminal-green); +} + +@keyframes cursor-blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} + +/* Neon glow effect for priority badges */ +.priority-badge { + position: relative; +} + +.priority-badge::after { + content: ''; + position: absolute; + inset: -2px; + border-radius: inherit; + background: inherit; + filter: blur(6px); + opacity: 0.4; + z-index: -1; +} + +/* Smooth scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-primary); + border: 1px solid var(--terminal-green); +} + +::-webkit-scrollbar-thumb { + background: var(--terminal-green); + box-shadow: 0 0 5px var(--terminal-green); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--terminal-amber); + box-shadow: 0 0 10px var(--terminal-amber); +} + +/* Firefox scrollbar */ +* { + scrollbar-width: thin; + scrollbar-color: var(--terminal-green) var(--bg-primary); +} + body.menu-open { padding-left: 260px; } @@ -711,6 +920,148 @@ h1 { 100% { content: '|]'; } } +/* ===== SKELETON LOADING SCREENS ===== */ +.skeleton { + position: relative; + overflow: hidden; + background: linear-gradient(90deg, + rgba(0, 255, 65, 0.05) 0%, + rgba(0, 255, 65, 0.1) 50%, + rgba(0, 255, 65, 0.05) 100% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; +} + +@keyframes skeleton-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* Skeleton text line */ +.skeleton-text { + height: 1em; + margin: 0.5em 0; + border-radius: 0; + border: 1px solid rgba(0, 255, 65, 0.2); +} + +.skeleton-text.short { width: 40%; } +.skeleton-text.medium { width: 70%; } +.skeleton-text.long { width: 90%; } + +/* Skeleton table row */ +.skeleton-row { + display: flex; + align-items: center; + padding: 12px 8px; + border-bottom: 1px solid rgba(0, 255, 65, 0.1); + gap: 1rem; +} + +.skeleton-row .skeleton-cell { + height: 1.2em; + flex: 1; + border: 1px solid rgba(0, 255, 65, 0.15); +} + +.skeleton-row .skeleton-cell:first-child { flex: 0 0 100px; } +.skeleton-row .skeleton-cell:nth-child(2) { flex: 0 0 60px; } +.skeleton-row .skeleton-cell:nth-child(3) { flex: 3; } + +/* Skeleton stat card */ +.skeleton-stat { + padding: 1rem; + border: 2px solid rgba(0, 255, 65, 0.2); + min-height: 80px; +} + +.skeleton-stat .skeleton-value { + height: 2rem; + width: 60%; + margin-bottom: 0.5rem; +} + +.skeleton-stat .skeleton-label { + height: 1rem; + width: 80%; +} + +/* Skeleton comment */ +.skeleton-comment { + padding: 1rem; + border: 1px solid rgba(0, 255, 65, 0.2); + margin-bottom: 1rem; +} + +.skeleton-comment-header { + display: flex; + gap: 1rem; + margin-bottom: 0.75rem; +} + +.skeleton-avatar { + width: 32px; + height: 32px; + border-radius: 0; + border: 1px solid rgba(0, 255, 65, 0.2); + flex-shrink: 0; +} + +.skeleton-comment-meta { + flex: 1; +} + +/* Terminal-style loading indicator */ +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 10, 2, 0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 100; + font-family: var(--font-mono); + color: var(--terminal-green); +} + +.loading-overlay .loading-text { + margin-top: 1rem; + animation: blink-cursor 1s step-end infinite; +} + +.loading-overlay .loading-spinner { + font-size: 2rem; + animation: terminal-spin 0.5s linear infinite; +} + +@keyframes terminal-spin { + 0% { content: '⠋'; } + 10% { content: '⠙'; } + 20% { content: '⠹'; } + 30% { content: '⠸'; } + 40% { content: '⠼'; } + 50% { content: '⠴'; } + 60% { content: '⠦'; } + 70% { content: '⠧'; } + 80% { content: '⠇'; } + 90% { content: '⠏'; } +} + +.loading-spinner::before { + content: '⠋'; + animation: terminal-spin 0.8s linear infinite; +} + +@keyframes blink-cursor { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} + /* ===== MESSAGE STYLES ===== */ .error-message { padding: 15px 20px; @@ -3324,19 +3675,59 @@ tr:hover .quick-actions { align-items: center; gap: 0.75rem; padding: 1rem; - background: var(--bg-secondary); + background: linear-gradient(135deg, var(--bg-secondary) 0%, rgba(0, 255, 65, 0.03) 100%); border: 2px solid var(--terminal-green); transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +/* Corner accent */ +.stat-card::after { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 30px; + height: 30px; + background: linear-gradient(135deg, transparent 50%, rgba(0, 255, 65, 0.1) 50%); + transition: all 0.3s ease; } .stat-card:hover { border-color: var(--terminal-amber); box-shadow: var(--glow-amber); + background: linear-gradient(135deg, var(--bg-secondary) 0%, rgba(255, 176, 0, 0.05) 100%); +} + +.stat-card:hover::after { + background: linear-gradient(135deg, transparent 50%, rgba(255, 176, 0, 0.15) 50%); +} + +/* Critical stat card styling */ +.stat-card.stat-critical { + border-color: var(--priority-1); + background: linear-gradient(135deg, var(--bg-secondary) 0%, rgba(255, 77, 77, 0.08) 100%); +} + +.stat-card.stat-critical .stat-value { + color: var(--priority-1); + text-shadow: var(--glow-priority-1); + animation: pulse-glow 2s ease-in-out infinite; +} + +.stat-card.stat-critical::after { + background: linear-gradient(135deg, transparent 50%, rgba(255, 77, 77, 0.15) 50%); } .stat-icon { font-size: 1.5rem; opacity: 0.9; + transition: transform 0.3s ease; +} + +.stat-card:hover .stat-icon { + transform: scale(1.1); } .stat-content { diff --git a/assets/css/ticket.css b/assets/css/ticket.css index b444a1e..8353b8b 100644 --- a/assets/css/ticket.css +++ b/assets/css/ticket.css @@ -372,6 +372,29 @@ textarea[data-field="description"]:not(:disabled)::after { 50% { box-shadow: 0 0 20px rgba(255, 77, 77, 0.8); } } +/* Tab transition animations */ +@keyframes tab-fade-in { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes tab-slide-in { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + .ticket-age .age-icon { font-size: 1rem; } @@ -601,13 +624,37 @@ textarea.editable { } .comment { - background: var(--bg-primary); + background: linear-gradient(135deg, var(--bg-primary) 0%, rgba(0, 255, 65, 0.02) 100%); padding: 15px; border: 1px solid var(--terminal-green); border-radius: 0; margin-bottom: 1rem; position: relative; box-shadow: none; + transition: all 0.3s ease; + animation: comment-appear 0.4s ease-out; +} + +@keyframes comment-appear { + from { + opacity: 0; + transform: translateX(-10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.comment:hover { + border-color: var(--terminal-amber); + background: linear-gradient(135deg, var(--bg-primary) 0%, rgba(255, 176, 0, 0.03) 100%); + box-shadow: 0 0 15px rgba(0, 255, 65, 0.1); +} + +.comment:hover::before, +.comment:hover::after { + color: var(--terminal-amber); } /* Add corner decorations to individual comments */ @@ -802,6 +849,182 @@ textarea.editable { min-height: auto !important; } +/* ===== COMMENT THREADING STYLES ===== */ + +/* Thread depth indentation */ +.comment.thread-depth-1 { + margin-left: 2rem; +} + +.comment.thread-depth-2 { + margin-left: 4rem; +} + +.comment.thread-depth-3 { + margin-left: 6rem; +} + +/* Reply styling */ +.comment.comment-reply { + position: relative; + border-left: 2px solid var(--terminal-green-dim); + margin-top: 0.5rem; + padding-left: 1rem; +} + +.comment.comment-reply::before { + content: none; +} + +/* Thread connector line */ +.comment .thread-line { + position: absolute; + left: -1rem; + top: 0; + width: 0.75rem; + height: 1rem; + border-left: 2px solid var(--terminal-green-dim); + border-bottom: 2px solid var(--terminal-green-dim); +} + +/* Comment content wrapper for threading */ +.comment .comment-content { + flex: 1; +} + +/* Replies container */ +.comment-replies { + margin-top: 0.5rem; +} + +/* Reply button styling */ +.comment-action-btn.reply-btn { + color: var(--terminal-green); + font-size: 1rem; +} + +.comment-action-btn.reply-btn:hover { + color: var(--terminal-cyan); + border-color: var(--terminal-cyan); +} + +/* Reply form inline */ +.reply-form-container { + margin-top: 1rem; + padding: 1rem; + background: rgba(0, 255, 65, 0.03); + border: 1px dashed var(--terminal-green); + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.reply-form-container .reply-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + color: var(--terminal-green); + font-family: var(--font-mono); + font-size: 0.85rem; +} + +.reply-form-container .reply-header .replying-to { + color: var(--terminal-amber); +} + +.reply-form-container .close-reply-btn { + background: none; + border: 1px solid var(--terminal-red); + color: var(--terminal-red); + padding: 0.25rem 0.5rem; + cursor: pointer; + font-family: var(--font-mono); + font-size: 0.8rem; +} + +.reply-form-container .close-reply-btn:hover { + background: rgba(255, 0, 0, 0.1); +} + +.reply-form-container textarea { + width: 100%; + min-height: 80px; + padding: 0.75rem; + background: var(--bg-secondary); + border: 2px solid var(--terminal-green); + color: var(--terminal-green); + font-family: var(--font-mono); + font-size: 0.9rem; + resize: vertical; + margin-bottom: 0.75rem; +} + +.reply-form-container textarea:focus { + outline: none; + border-color: var(--terminal-amber); + box-shadow: 0 0 10px rgba(255, 176, 0, 0.3); +} + +.reply-form-container .reply-actions { + display: flex; + justify-content: space-between; + align-items: center; +} + +.reply-form-container .reply-buttons { + display: flex; + gap: 0.5rem; +} + +/* Quote block in replies */ +.comment-text blockquote { + border-left: 3px solid var(--terminal-green-dim); + padding-left: 1rem; + margin: 0.5rem 0; + color: var(--terminal-green-dim); + font-style: italic; +} + +/* Thread collapse indicator */ +.thread-collapse-btn { + background: none; + border: none; + color: var(--terminal-green-dim); + cursor: pointer; + font-family: var(--font-mono); + font-size: 0.8rem; + padding: 0.25rem; + margin-left: 0.5rem; +} + +.thread-collapse-btn:hover { + color: var(--terminal-green); +} + +.comment.collapsed .comment-replies { + display: none; +} + +.comment.collapsed .thread-collapse-btn::after { + content: ' [+]'; +} + +.comment:not(.collapsed) .thread-collapse-btn::after { + content: ' [-]'; +} + +/* ===== END COMMENT THREADING STYLES ===== */ + /* Comment Tabs - TERMINAL STYLE */ .ticket-tabs { display: flex; @@ -857,6 +1080,7 @@ textarea.editable { .tab-content { display: none; padding: 20px; + animation: tab-fade-in 0.3s ease-out; border: 2px solid var(--terminal-green); border-top: none; background: var(--bg-secondary); diff --git a/assets/js/advanced-search.js b/assets/js/advanced-search.js index 8cbc812..26fe318 100644 --- a/assets/js/advanced-search.js +++ b/assets/js/advanced-search.js @@ -35,7 +35,9 @@ function closeOnAdvancedSearchBackdropClick(event) { // Load users for dropdown async function loadUsersForSearch() { try { - const response = await fetch('/api/get_users.php'); + const response = await fetch('/api/get_users.php', { + credentials: 'same-origin' + }); const data = await response.json(); if (data.success && data.users) { @@ -163,6 +165,7 @@ async function saveCurrentFilter() { try { const response = await fetch('/api/saved_filters.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -230,7 +233,9 @@ function getCurrentFilterCriteria() { // Load saved filters async function loadSavedFilters() { try { - const response = await fetch('/api/saved_filters.php'); + const response = await fetch('/api/saved_filters.php', { + credentials: 'same-origin' + }); const data = await response.json(); if (data.success && data.filters) { @@ -326,6 +331,7 @@ async function deleteSavedFilter() { try { const response = await fetch('/api/saved_filters.php', { method: 'DELETE', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index d3c97de..eeb852f 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -539,6 +539,7 @@ function quickSave() { fetch('/api/update_ticket.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -607,6 +608,7 @@ function saveTicket() { fetch('/api/update_ticket.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -645,7 +647,9 @@ function loadTemplate() { } // Fetch template data - fetch(`/api/get_template.php?template_id=${templateId}`) + fetch(`/api/get_template.php?template_id=${templateId}`, { + credentials: 'same-origin' + }) .then(response => { if (!response.ok) { throw new Error('Failed to fetch template'); @@ -776,6 +780,7 @@ function performBulkCloseAction(ticketIds) { fetch('/api/bulk_operation.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -848,7 +853,9 @@ function showBulkAssignModal() { document.body.insertAdjacentHTML('beforeend', modalHtml); // Fetch users for the dropdown - fetch('/api/get_users.php') + fetch('/api/get_users.php', { + credentials: 'same-origin' + }) .then(response => response.json()) .then(data => { if (data.success && data.users) { @@ -884,6 +891,7 @@ function performBulkAssign() { fetch('/api/bulk_operation.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -979,6 +987,7 @@ function performBulkPriority() { fetch('/api/bulk_operation.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -1113,6 +1122,7 @@ function performBulkStatusChange() { fetch('/api/bulk_operation.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -1201,6 +1211,7 @@ function performBulkDelete() { fetch('/api/bulk_operation.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -1474,6 +1485,7 @@ function performQuickStatusChange(ticketId) { fetch('/api/update_ticket.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -1539,7 +1551,9 @@ function quickAssign(ticketId) { document.body.insertAdjacentHTML('beforeend', modalHtml); // Load users - fetch('/api/get_users.php') + fetch('/api/get_users.php', { + credentials: 'same-origin' + }) .then(response => response.json()) .then(data => { if (data.success && data.users) { @@ -1565,6 +1579,7 @@ function performQuickAssign(ticketId) { fetch('/api/assign_ticket.php', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN @@ -1859,3 +1874,108 @@ function exportSelectedTickets(format) { const dropdown = document.getElementById('exportDropdown'); if (dropdown) dropdown.classList.remove('open'); } + +// ======================================== +// Skeleton Loading Helpers +// ======================================== + +/** + * Generate skeleton table rows + */ +function generateSkeletonRows(count = 5) { + let html = ''; + for (let i = 0; i < count; i++) { + html += ` +
and escape HTML echo nl2br(htmlspecialchars($comment['comment_text'])); } echo "