Compare commits
5 Commits
84b104a501
...
54887ffa24
| Author | SHA1 | Date | |
|---|---|---|---|
| 54887ffa24 | |||
| 613886068d | |||
| 847d6b2656 | |||
| c2cd923d32 | |||
| 67a7d769f0 |
@@ -1319,7 +1319,9 @@ select option:checked {
|
|||||||
.lt-modal-body {
|
.lt-modal-body {
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lt-modal-footer {
|
.lt-modal-footer {
|
||||||
@@ -3187,6 +3189,29 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
.lt-kv-val--green { color: var(--accent-green); }
|
.lt-kv-val--green { color: var(--accent-green); }
|
||||||
.lt-kv-val--red { color: var(--accent-red); }
|
.lt-kv-val--red { color: var(--accent-red); }
|
||||||
|
|
||||||
|
/* lt-kv-row / lt-kv-label / lt-kv-value — alternate KV row pattern */
|
||||||
|
.lt-kv-row {
|
||||||
|
display: contents; /* children become direct grid items of lt-kv-grid */
|
||||||
|
}
|
||||||
|
.lt-kv-label {
|
||||||
|
padding: var(--space-xs) var(--space-md) var(--space-xs) 0;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-bottom: 1px solid var(--border-dim);
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.lt-kv-value {
|
||||||
|
padding: var(--space-xs) 0 var(--space-xs) var(--space-md);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid var(--border-dim);
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
43. HERO / BANNER SECTION
|
43. HERO / BANNER SECTION
|
||||||
|
|||||||
+18
-11
@@ -191,7 +191,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
break;
|
break;
|
||||||
// View mode toggle
|
// View mode toggle
|
||||||
case 'set-view-mode':
|
case 'set-view-mode':
|
||||||
if (target.dataset.mode === 'card') populateKanbanCards();
|
setViewMode(target.dataset.mode);
|
||||||
break;
|
break;
|
||||||
// Settings
|
// Settings
|
||||||
case 'open-settings':
|
case 'open-settings':
|
||||||
@@ -686,7 +686,9 @@ function closeBulkPriorityModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function performBulkPriority() {
|
function performBulkPriority() {
|
||||||
const priority = document.getElementById('bulkPriority').value;
|
const priorityEl = document.getElementById('bulkPriority');
|
||||||
|
if (!priorityEl) return;
|
||||||
|
const priority = priorityEl.value;
|
||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (!priority) {
|
if (!priority) {
|
||||||
@@ -789,7 +791,9 @@ function closeBulkStatusModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function performBulkStatusChange() {
|
function performBulkStatusChange() {
|
||||||
const status = document.getElementById('bulkStatus').value;
|
const bulkStatusEl = document.getElementById('bulkStatus');
|
||||||
|
if (!bulkStatusEl) return;
|
||||||
|
const status = bulkStatusEl.value;
|
||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
@@ -986,7 +990,9 @@ function closeQuickStatusModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function performQuickStatusChange(ticketId) {
|
function performQuickStatusChange(ticketId) {
|
||||||
const newStatus = document.getElementById('quickStatusSelect').value;
|
const quickStatusEl = document.getElementById('quickStatusSelect');
|
||||||
|
if (!quickStatusEl) return;
|
||||||
|
const newStatus = quickStatusEl.value;
|
||||||
|
|
||||||
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, status: newStatus })
|
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, status: newStatus })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -1239,14 +1245,15 @@ function populateKanbanCards() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore view mode on page load — click the kanban tab button to trigger lt.tabs
|
// Restore view mode on page load — lt.tabs already restores the active panel visually
|
||||||
|
// via lt_activeTab_<path>; we just need to populate kanban cards if that panel is active
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const savedMode = localStorage.getItem('ticketViewMode');
|
try {
|
||||||
if (savedMode === 'card') {
|
const savedTab = localStorage.getItem('lt_activeTab_' + location.pathname);
|
||||||
const cardBtn = document.getElementById('cardViewBtn');
|
if (savedTab === 'tab-kanban') {
|
||||||
if (cardBtn) cardBtn.click();
|
populateKanbanCards();
|
||||||
else populateKanbanCards();
|
}
|
||||||
}
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
@@ -136,10 +136,16 @@ class DashboardController {
|
|||||||
|
|
||||||
// Validate user ID filters
|
// Validate user ID filters
|
||||||
$createdBy = $this->validateUserId($_GET['created_by'] ?? null);
|
$createdBy = $this->validateUserId($_GET['created_by'] ?? null);
|
||||||
$assignedTo = $this->validateUserId($_GET['assigned_to'] ?? null);
|
|
||||||
|
|
||||||
if ($createdBy !== null) $filters['created_by'] = $createdBy;
|
if ($createdBy !== null) $filters['created_by'] = $createdBy;
|
||||||
if ($assignedTo !== null) $filters['assigned_to'] = $assignedTo;
|
|
||||||
|
// assigned_to accepts a numeric user ID or the special string 'unassigned'
|
||||||
|
$assignedToRaw = $_GET['assigned_to'] ?? null;
|
||||||
|
if ($assignedToRaw === 'unassigned') {
|
||||||
|
$filters['assigned_to'] = 'unassigned';
|
||||||
|
} else {
|
||||||
|
$assignedTo = $this->validateUserId($assignedToRaw);
|
||||||
|
if ($assignedTo !== null) $filters['assigned_to'] = $assignedTo;
|
||||||
|
}
|
||||||
|
|
||||||
// Get tickets with pagination, sorting, search, and advanced filters
|
// Get tickets with pagination, sorting, search, and advanced filters
|
||||||
$result = $this->ticketModel->getAllTickets($page, $limit, $status, $sortColumn, $sortDirection, $category, $type, $search, $filters, $GLOBALS['currentUser'] ?? []);
|
$result = $this->ticketModel->getAllTickets($page, $limit, $status, $sortColumn, $sortDirection, $category, $type, $search, $filters, $GLOBALS['currentUser'] ?? []);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class RecurringTicketModel {
|
|||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
|
||||||
$stmt = $this->conn->prepare($sql);
|
$stmt = $this->conn->prepare($sql);
|
||||||
$stmt->bind_param('ssssiiisssis',
|
$stmt->bind_param('ssssiiisssii',
|
||||||
$data['title_template'],
|
$data['title_template'],
|
||||||
$data['description_template'],
|
$data['description_template'],
|
||||||
$data['category'],
|
$data['category'],
|
||||||
|
|||||||
@@ -81,9 +81,12 @@ class TicketModel {
|
|||||||
if ($search && !empty($search)) {
|
if ($search && !empty($search)) {
|
||||||
if ($this->hasFulltextIndex()) {
|
if ($this->hasFulltextIndex()) {
|
||||||
// MATCH...AGAINST for indexed full-text search (much faster at scale)
|
// MATCH...AGAINST for indexed full-text search (much faster at scale)
|
||||||
|
// Strip MySQL boolean mode special chars to prevent parse errors on user input
|
||||||
|
$ftSearch = preg_replace('/[+\-><()\~*"@]+/', ' ', $search);
|
||||||
|
$ftSearch = trim(preg_replace('/\s+/', ' ', $ftSearch)) . '*';
|
||||||
$whereConditions[] = "(MATCH(t.title, t.description) AGAINST (? IN BOOLEAN MODE) OR t.ticket_id LIKE ? OR t.category LIKE ? OR t.type LIKE ?)";
|
$whereConditions[] = "(MATCH(t.title, t.description) AGAINST (? IN BOOLEAN MODE) OR t.ticket_id LIKE ? OR t.category LIKE ? OR t.type LIKE ?)";
|
||||||
$searchTerm = "%$search%";
|
$searchTerm = "%$search%";
|
||||||
$params = array_merge($params, [$search . '*', $searchTerm, $searchTerm, $searchTerm]);
|
$params = array_merge($params, [$ftSearch, $searchTerm, $searchTerm, $searchTerm]);
|
||||||
$paramTypes .= 'ssss';
|
$paramTypes .= 'ssss';
|
||||||
} else {
|
} else {
|
||||||
$whereConditions[] = "(t.title LIKE ? OR t.description LIKE ? OR t.ticket_id LIKE ? OR t.category LIKE ? OR t.type LIKE ?)";
|
$whereConditions[] = "(t.title LIKE ? OR t.description LIKE ? OR t.ticket_id LIKE ? OR t.category LIKE ? OR t.type LIKE ?)";
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
|||||||
<?php if ($_lt_userId > 0): ?>
|
<?php if ($_lt_userId > 0): ?>
|
||||||
<img src="/api/user_avatar.php?user_id=<?= $_lt_userId ?>"
|
<img src="/api/user_avatar.php?user_id=<?= $_lt_userId ?>"
|
||||||
alt=""
|
alt=""
|
||||||
class="lt-avatar-img"
|
class="lt-avatar-img">
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
<span class="lt-avatar-initials"><?= htmlspecialchars($_lt_initials) ?></span>
|
<span class="lt-avatar-initials"><?= htmlspecialchars($_lt_initials) ?></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,7 +195,7 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
|||||||
<div style="padding:0.75rem;font-size:0.75rem;color:var(--text-muted);text-align:center">Loading…</div>
|
<div style="padding:0.75rem;font-size:0.75rem;color:var(--text-muted);text-align:center">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-notif-panel-footer">
|
<div class="lt-notif-panel-footer">
|
||||||
<a href="/admin/audit-log" class="lt-btn lt-btn-ghost lt-btn-sm" style="width:100%;text-align:center">View activity log</a>
|
<a href="/admin/audit-log" class="lt-btn lt-btn-ghost lt-btn-sm lt-w-full lt-text-center">View activity log</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user