e721b33911
- Replace lt-chip priority badges with lt-badge lt-badge-p[1-4] across DashboardView, TemplatesView (matches web_template sticky table pattern) - Add lt-theme-btn theme toggle to header-right; wire lt.theme.toggle() - Replace ASCII art empty state with lt-empty-state component in dashboard - Standardize tab wrapper lt-tabs → lt-tab-bar in Dashboard and TicketView - Add missing lt-keys-help modal to layout_footer (fixes ? key doing nothing) - Add lt-cmd-overlay command palette container + lt.cmdPalette.init() nav - Add .lt-timeline-action CSS rule (used in TicketView, was undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
907 lines
47 KiB
PHP
907 lines
47 KiB
PHP
<?php
|
|
/**
|
|
* DashboardView.php — Main ticket dashboard, redesigned for TDS v1.2.
|
|
*
|
|
* Receives from controller:
|
|
* $tickets, $totalTickets, $totalPages, $page
|
|
* $stats — open_tickets, critical, unassigned, created_today, closed_today, avg_resolution_hours
|
|
* $categories, $types — arrays for sidebar filters
|
|
*/
|
|
|
|
require_once __DIR__ . '/../middleware/SecurityHeadersMiddleware.php';
|
|
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
|
|
|
|
$nonce = SecurityHeadersMiddleware::getNonce();
|
|
$pageTitle = 'Dashboard';
|
|
$activeNav = 'dashboard';
|
|
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
|
$pageScripts = [
|
|
'/assets/js/markdown.js?v=20260327',
|
|
'/assets/js/dashboard.js?v=20260327',
|
|
'/assets/js/advanced-search.js?v=20260327',
|
|
'/assets/js/keyboard-shortcuts.js?v=20260327',
|
|
'/assets/js/settings.js?v=20260327',
|
|
];
|
|
|
|
// ── Pagination helpers ────────────────────────────────────────────────────────
|
|
$currentSort = $_GET['sort'] ?? 'ticket_id';
|
|
$currentDir = (($_GET['dir'] ?? 'desc') === 'asc') ? 'asc' : 'desc';
|
|
|
|
// ── Active filter detection ───────────────────────────────────────────────────
|
|
$activeFilters = [];
|
|
if (!empty($_GET['status'])) {
|
|
foreach (explode(',', $_GET['status']) as $s) {
|
|
$activeFilters[] = ['type' => 'status', 'value' => trim($s), 'label' => 'Status: ' . trim($s)];
|
|
}
|
|
}
|
|
if (!empty($_GET['priority'])) {
|
|
$pArr = is_array($_GET['priority']) ? $_GET['priority'] : explode(',', $_GET['priority']);
|
|
foreach ($pArr as $p) {
|
|
$activeFilters[] = ['type' => 'priority', 'value' => trim($p), 'label' => 'Priority: P' . trim($p)];
|
|
}
|
|
}
|
|
if (!empty($_GET['category'])) {
|
|
$activeFilters[] = ['type' => 'category', 'value' => $_GET['category'], 'label' => 'Category: ' . htmlspecialchars($_GET['category'])];
|
|
}
|
|
if (!empty($_GET['type'])) {
|
|
$activeFilters[] = ['type' => 'type', 'value' => $_GET['type'], 'label' => 'Type: ' . htmlspecialchars($_GET['type'])];
|
|
}
|
|
if (!empty($_GET['assigned_to'])) {
|
|
$label = $_GET['assigned_to'] === 'unassigned' ? 'Unassigned' : 'User #' . htmlspecialchars($_GET['assigned_to']);
|
|
$activeFilters[] = ['type' => 'assigned_to', 'value' => $_GET['assigned_to'], 'label' => 'Assigned: ' . $label];
|
|
}
|
|
|
|
$currentStatus = isset($_GET['status']) ? explode(',', $_GET['status']) : ['Open', 'Pending', 'In Progress'];
|
|
$currentCategories = isset($_GET['category']) ? explode(',', $_GET['category']) : [];
|
|
$currentTypes = isset($_GET['type']) ? explode(',', $_GET['type']) : [];
|
|
$isAdmin = $GLOBALS['currentUser']['is_admin'] ?? false;
|
|
$colCount = $isAdmin ? 12 : 11;
|
|
|
|
include __DIR__ . '/layout_header.php';
|
|
?>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════
|
|
PAGE HEADER
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<div class="lt-page-header">
|
|
<h1 class="lt-page-title">[ TICKETS ]</h1>
|
|
<div class="lt-btn-group">
|
|
<a href="<?= htmlspecialchars($GLOBALS['config']['BASE_URL']) ?>/ticket/create"
|
|
class="lt-btn lt-btn-primary">+ NEW TICKET</a>
|
|
<button type="button" class="lt-btn lt-btn-sm" data-action="manual-refresh"
|
|
title="Refresh now (auto-refreshes every 5 min)">REFRESH</button>
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" data-action="open-settings">CFG</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════
|
|
STATS GRID
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<?php if (isset($stats)): ?>
|
|
<div class="lt-stats-grid" id="statsGrid">
|
|
|
|
<div class="lt-stat-card stat-open" role="button" tabindex="0"
|
|
data-filter-key="status" data-filter-val="Open,Pending,In Progress"
|
|
title="Click to filter by active tickets" aria-label="Open tickets">
|
|
<div class="lt-stat-icon">[ # ]</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= (int)$stats['open_tickets'] ?></div>
|
|
<div class="lt-stat-label">Open Tickets</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lt-stat-card stat-critical" role="button" tabindex="0"
|
|
data-filter-key="priority" data-filter-val="1"
|
|
title="Click to filter critical (P1) tickets" aria-label="Critical P1 tickets">
|
|
<div class="lt-stat-icon lt-text-danger">[ ! ]</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= (int)$stats['critical'] ?></div>
|
|
<div class="lt-stat-label">Critical (P1)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lt-stat-card stat-unassigned" role="button" tabindex="0"
|
|
data-filter-key="assigned_to" data-filter-val="unassigned"
|
|
title="Click to filter unassigned tickets" aria-label="Unassigned tickets">
|
|
<div class="lt-stat-icon lt-text-amber">[ @ ]</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= (int)$stats['unassigned'] ?></div>
|
|
<div class="lt-stat-label">Unassigned</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lt-stat-card stat-today" role="button" tabindex="0"
|
|
title="Tickets created today" aria-label="Tickets created today">
|
|
<div class="lt-stat-icon lt-text-cyan">[ + ]</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= (int)$stats['created_today'] ?></div>
|
|
<div class="lt-stat-label">Created Today</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lt-stat-card stat-resolved" role="button" tabindex="0"
|
|
data-filter-key="status" data-filter-val="Closed"
|
|
title="Click to filter closed tickets" aria-label="Closed tickets today">
|
|
<div class="lt-stat-icon lt-text-muted">[ OK ]</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= (int)$stats['closed_today'] ?></div>
|
|
<div class="lt-stat-label">Closed Today</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lt-stat-card stat-time" title="Average resolution time" aria-label="Avg resolution time">
|
|
<div class="lt-stat-icon lt-text-muted">⏱</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= htmlspecialchars($stats['avg_resolution_hours'] ?? '—') ?>h</div>
|
|
<div class="lt-stat-label">Avg Resolution</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<?php endif ?>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════
|
|
VIEW TABS (Table / Kanban) — dual-purpose: lt.tabs + dashboard.js set-view-mode
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<div class="lt-tab-bar" role="tablist" aria-label="Ticket view mode">
|
|
<button type="button" id="tableViewBtn"
|
|
class="lt-tab active"
|
|
role="tab" aria-selected="true" aria-controls="tab-table"
|
|
data-tab="tab-table"
|
|
data-action="set-view-mode" data-mode="table">
|
|
▤ Table View
|
|
</button>
|
|
<button type="button" id="cardViewBtn"
|
|
class="lt-tab"
|
|
role="tab" aria-selected="false" aria-controls="tab-kanban"
|
|
data-tab="tab-kanban"
|
|
data-action="set-view-mode" data-mode="card">
|
|
⊕ Kanban
|
|
</button>
|
|
</div><!-- /.lt-tab-bar -->
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════
|
|
LAYOUT WRAPPER: sidebar + main content
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<div class="lt-layout" id="dashboardLayout">
|
|
|
|
<!-- ─── SIDEBAR: Filters ──────────────────────────────────── -->
|
|
<aside class="lt-sidebar" id="lt-sidebar" role="complementary" aria-label="Filter options">
|
|
<div class="lt-sidebar-header">
|
|
<span>Filters</span>
|
|
<button type="button" class="lt-sidebar-toggle"
|
|
data-action="toggle-sidebar"
|
|
data-sidebar-toggle="lt-sidebar"
|
|
aria-label="Collapse filter sidebar"
|
|
aria-expanded="true"
|
|
aria-controls="lt-sidebar">◀</button>
|
|
</div>
|
|
<div class="lt-sidebar-body" id="dashboardSidebar">
|
|
|
|
<!-- Status Filter -->
|
|
<fieldset class="lt-filter-group">
|
|
<legend class="lt-filter-label">Status</legend>
|
|
<?php foreach (['Open', 'Pending', 'In Progress', 'Closed'] as $s): ?>
|
|
<label class="lt-filter-option">
|
|
<input type="checkbox" class="lt-checkbox sidebar-filter"
|
|
name="status" value="<?= htmlspecialchars($s) ?>"
|
|
<?= in_array($s, $currentStatus) ? 'checked' : '' ?>>
|
|
<?= htmlspecialchars($s) ?>
|
|
</label>
|
|
<?php endforeach ?>
|
|
</fieldset>
|
|
|
|
<!-- Category Filter -->
|
|
<?php if (!empty($categories)): ?>
|
|
<fieldset class="lt-filter-group">
|
|
<legend class="lt-filter-label">Category</legend>
|
|
<?php foreach ($categories as $cat): ?>
|
|
<label class="lt-filter-option">
|
|
<input type="checkbox" class="lt-checkbox sidebar-filter"
|
|
name="category" value="<?= htmlspecialchars($cat) ?>"
|
|
<?= in_array($cat, $currentCategories) ? 'checked' : '' ?>>
|
|
<?= htmlspecialchars($cat) ?>
|
|
</label>
|
|
<?php endforeach ?>
|
|
</fieldset>
|
|
<?php endif ?>
|
|
|
|
<!-- Type Filter -->
|
|
<?php if (!empty($types)): ?>
|
|
<fieldset class="lt-filter-group">
|
|
<legend class="lt-filter-label">Type</legend>
|
|
<?php foreach ($types as $type): ?>
|
|
<label class="lt-filter-option">
|
|
<input type="checkbox" class="lt-checkbox sidebar-filter"
|
|
name="type" value="<?= htmlspecialchars($type) ?>"
|
|
<?= in_array($type, $currentTypes) ? 'checked' : '' ?>>
|
|
<?= htmlspecialchars($type) ?>
|
|
</label>
|
|
<?php endforeach ?>
|
|
</fieldset>
|
|
<?php endif ?>
|
|
|
|
<div class="lt-btn-group" style="flex-direction:column">
|
|
<button type="button" id="apply-filters-btn" class="lt-btn lt-btn-primary lt-btn-sm">APPLY</button>
|
|
<button type="button" id="clear-filters-btn" class="lt-btn lt-btn-ghost lt-btn-sm">CLEAR ALL</button>
|
|
</div>
|
|
|
|
</div><!-- /.lt-sidebar-body -->
|
|
</aside>
|
|
|
|
<!-- Collapsed expand button -->
|
|
<button type="button" class="lt-sidebar-expand-btn" id="sidebarExpandBtn"
|
|
data-action="toggle-sidebar"
|
|
aria-label="Show filters" aria-expanded="false" aria-controls="lt-sidebar"
|
|
style="display:none">▶ Filters</button>
|
|
|
|
<!-- ─── MAIN CONTENT ─────────────────────────────────────── -->
|
|
<div class="lt-content">
|
|
|
|
<!-- Toolbar: search + export + count -->
|
|
<div class="lt-toolbar">
|
|
<div class="lt-toolbar-left">
|
|
<form method="GET" action="" class="lt-search-form" role="search">
|
|
<?php foreach (['status','category','type','sort','dir'] as $p): ?>
|
|
<?php if (isset($_GET[$p])): ?>
|
|
<input type="hidden" name="<?= $p ?>" value="<?= htmlspecialchars($_GET[$p]) ?>">
|
|
<?php endif ?>
|
|
<?php endforeach ?>
|
|
<div class="lt-search">
|
|
<input type="text" name="search" class="lt-input lt-search-input"
|
|
id="searchInput"
|
|
placeholder="> Search tickets..."
|
|
value="<?= htmlspecialchars($_GET['search'] ?? '') ?>"
|
|
aria-label="Search tickets">
|
|
</div>
|
|
<button type="submit" class="lt-btn lt-btn-sm">SEARCH</button>
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" data-action="open-advanced-search">
|
|
FILTER
|
|
</button>
|
|
<?php if (!empty($_GET['search'])): ?>
|
|
<a href="?" class="lt-btn lt-btn-sm lt-btn-ghost" aria-label="Clear search">✕</a>
|
|
<?php endif ?>
|
|
</form>
|
|
</div>
|
|
<div class="lt-toolbar-right">
|
|
<span class="lt-text-muted lt-text-xs">
|
|
<?= $totalTickets ?> ticket<?= $totalTickets !== 1 ? 's' : '' ?>
|
|
</span>
|
|
<!-- Export dropdown (admin + selection) -->
|
|
<?php if ($isAdmin): ?>
|
|
<div class="lt-dropdown-wrap" id="exportDropdown" style="display:none">
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost lt-dropdown-trigger"
|
|
id="exportDropdownTrigger"
|
|
data-action="toggle-export-menu"
|
|
aria-expanded="false" aria-haspopup="true">
|
|
EXPORT (<span id="exportCount">0</span>) ▾
|
|
</button>
|
|
<div class="lt-dropdown-panel lt-dropdown-panel--right" id="exportDropdownContent" aria-hidden="true">
|
|
<button type="button" class="lt-dropdown-item" data-action="export-tickets" data-format="csv">
|
|
↓ CSV
|
|
</button>
|
|
<button type="button" class="lt-dropdown-item" data-action="export-tickets" data-format="json">
|
|
↓ JSON
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<?php endif ?>
|
|
</div>
|
|
</div><!-- /.lt-toolbar -->
|
|
|
|
<!-- Active filters bar -->
|
|
<?php if (!empty($activeFilters)): ?>
|
|
<div class="active-filters-bar lt-flex lt-flex-wrap lt-flex-gap-sm" role="group" aria-label="Active filters">
|
|
<span class="lt-text-xs lt-text-muted">Active:</span>
|
|
<?php foreach ($activeFilters as $f): ?>
|
|
<span class="lt-badge filter-badge"
|
|
data-filter-type="<?= htmlspecialchars($f['type']) ?>"
|
|
data-filter-value="<?= htmlspecialchars($f['value']) ?>">
|
|
<?= htmlspecialchars($f['label']) ?>
|
|
<button type="button" class="filter-remove"
|
|
data-action="remove-filter"
|
|
data-filter-type="<?= htmlspecialchars($f['type']) ?>"
|
|
data-filter-value="<?= htmlspecialchars($f['value']) ?>"
|
|
aria-label="Remove <?= htmlspecialchars($f['label']) ?> filter">✕</button>
|
|
</span>
|
|
<?php endforeach ?>
|
|
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm"
|
|
data-action="clear-all-filters">CLEAR ALL</button>
|
|
</div>
|
|
<?php endif ?>
|
|
|
|
<!-- Search results info -->
|
|
<?php if (!empty($_GET['search'])): ?>
|
|
<div class="lt-msg lt-msg-info">
|
|
Showing results for: <strong><?= htmlspecialchars($_GET['search']) ?></strong>
|
|
— <?= $totalTickets ?> ticket<?= $totalTickets !== 1 ? 's' : '' ?> found
|
|
</div>
|
|
<?php endif ?>
|
|
|
|
<!-- ══════════════════════════════════════════════════════
|
|
TAB PANEL: TABLE VIEW
|
|
══════════════════════════════════════════════════════ -->
|
|
<div id="tab-table" class="lt-tab-panel active" role="tabpanel" aria-labelledby="tableViewBtn">
|
|
|
|
<!-- Bulk actions (admin only, shown when tickets selected) -->
|
|
<?php if ($isAdmin): ?>
|
|
<div class="bulk-actions-inline" style="display:none" aria-live="polite">
|
|
<span id="selected-count" class="lt-text-amber lt-text-sm">0</span>
|
|
<span class="lt-text-xs lt-text-muted"> tickets selected</span>
|
|
<div class="lt-btn-group">
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" data-action="bulk-status">STATUS</button>
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" data-action="bulk-assign">ASSIGN</button>
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" data-action="bulk-priority">PRIORITY</button>
|
|
<button type="button" class="lt-btn lt-btn-sm" data-action="clear-selection">CLEAR</button>
|
|
</div>
|
|
</div>
|
|
<?php endif ?>
|
|
|
|
<!-- Ticket table frame -->
|
|
<div class="lt-frame">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Ticket Queue</div>
|
|
<div class="lt-table-wrap">
|
|
<table class="lt-table lt-table-responsive" id="tickets-table" aria-label="Ticket queue">
|
|
<caption class="lt-sr-only">Ticket queue sorted by <?= htmlspecialchars($currentSort) ?> <?= $currentDir ?></caption>
|
|
<thead>
|
|
<tr>
|
|
<?php if ($isAdmin): ?>
|
|
<th scope="col" class="col-checkbox">
|
|
<input type="checkbox" class="lt-checkbox" id="selectAllCheckbox"
|
|
data-action="toggle-select-all" aria-label="Select all tickets">
|
|
</th>
|
|
<?php endif ?>
|
|
<?php
|
|
$columns = [
|
|
'ticket_id' => 'Ticket ID',
|
|
'priority' => 'Priority',
|
|
'title' => 'Title',
|
|
'category' => 'Category',
|
|
'type' => 'Type',
|
|
'status' => 'Status',
|
|
'created_by' => 'Created By',
|
|
'assigned_to' => 'Assigned To',
|
|
'created_at' => 'Created',
|
|
'updated_at' => 'Updated',
|
|
'_actions' => 'Actions',
|
|
];
|
|
foreach ($columns as $col => $label):
|
|
if ($col === '_actions'): ?>
|
|
<th scope="col" class="col-actions">Actions</th>
|
|
<?php else:
|
|
$newDir = ($currentSort === $col && $currentDir === 'asc') ? 'desc' : 'asc';
|
|
$sortClass = ($currentSort === $col) ? 'sort-' . $currentDir : '';
|
|
$ariaSort = ($currentSort === $col) ? 'aria-sort="' . ($currentDir === 'asc' ? 'ascending' : 'descending') . '"' : '';
|
|
$sortParams = array_merge($_GET, ['sort' => $col, 'dir' => $newDir]);
|
|
$sortUrl = htmlspecialchars('?' . http_build_query($sortParams), ENT_QUOTES, 'UTF-8');
|
|
?>
|
|
<th scope="col" class="<?= $sortClass ?>"
|
|
data-action="navigate" data-url="<?= $sortUrl ?>"
|
|
<?= $ariaSort ?>
|
|
style="cursor:pointer"><?= $label ?></th>
|
|
<?php endif ?>
|
|
<?php endforeach ?>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($tickets)): ?>
|
|
<tr>
|
|
<td colspan="<?= $colCount ?>" class="lt-empty">
|
|
<div class="lt-empty-state">
|
|
<div class="lt-empty-state-icon">📭</div>
|
|
<div class="lt-empty-state-title">No Tickets Found</div>
|
|
<div class="lt-empty-state-body">No tickets match your current filters.</div>
|
|
<?php if (!empty($activeFilters) || !empty($_GET['search'])): ?>
|
|
<a href="?" class="lt-btn lt-btn-sm lt-btn-ghost">Clear Filters</a>
|
|
<?php endif ?>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($tickets as $row):
|
|
$creator = htmlspecialchars($row['creator_display_name'] ?? $row['creator_username'] ?? 'System');
|
|
$assignedTo = htmlspecialchars($row['assigned_display_name'] ?? $row['assigned_username'] ?? 'Unassigned');
|
|
$pNum = (int)$row['priority'];
|
|
$rowStatusSlug = strtolower(str_replace(' ', '-', $row['status']));
|
|
$critClass = ($pNum === 1) ? ' lt-row-critical' : '';
|
|
$warnClass = ($pNum === 2) ? ' lt-row-warning' : '';
|
|
$createdFmt = date('Y-m-d H:i', strtotime($row['created_at']));
|
|
$updatedFmt = date('Y-m-d H:i', strtotime($row['updated_at']));
|
|
?>
|
|
<tr class="lt-row-p<?= $pNum ?><?= $critClass ?><?= $warnClass ?>">
|
|
<?php if ($isAdmin): ?>
|
|
<td data-label="Select" data-action="toggle-row-checkbox" class="checkbox-cell">
|
|
<input type="checkbox" class="lt-checkbox ticket-checkbox"
|
|
value="<?= htmlspecialchars($row['ticket_id']) ?>"
|
|
data-action="update-selection"
|
|
aria-label="Select ticket <?= htmlspecialchars($row['ticket_id']) ?>">
|
|
</td>
|
|
<?php endif ?>
|
|
<td data-label="Ticket ID">
|
|
<a href="/ticket/<?= htmlspecialchars($row['ticket_id']) ?>"
|
|
class="ticket-link"><?= htmlspecialchars($row['ticket_id']) ?></a>
|
|
</td>
|
|
<td data-label="Priority">
|
|
<?php $badgeClass = match($pNum) { 1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4' }; ?>
|
|
<span class="lt-badge <?= $badgeClass ?>">P<?= $pNum ?></span>
|
|
</td>
|
|
<td data-label="Title"><?= htmlspecialchars($row['title']) ?></td>
|
|
<td data-label="Category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></td>
|
|
<td data-label="Type" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['type']) ?></td>
|
|
<td data-label="Status">
|
|
<span class="lt-status lt-status-<?= $rowStatusSlug ?>"><?= htmlspecialchars($row['status']) ?></span>
|
|
</td>
|
|
<td data-label="Created By" class="lt-text-xs"><?= $creator ?></td>
|
|
<td data-label="Assigned To" class="lt-text-xs">
|
|
<?= ($row['assigned_display_name'] ?? $row['assigned_username'] ?? null) ? $assignedTo : '<span class="lt-text-muted">Unassigned</span>' ?>
|
|
</td>
|
|
<td data-label="Created" class="lt-text-xs lt-text-muted ts-cell"
|
|
data-ts="<?= htmlspecialchars($row['created_at'], ENT_QUOTES, 'UTF-8') ?>"
|
|
title="<?= date('Y-m-d H:i T', strtotime($row['created_at'])) ?>"><?= $createdFmt ?></td>
|
|
<td data-label="Updated" class="lt-text-xs lt-text-muted ts-cell"
|
|
data-ts="<?= htmlspecialchars($row['updated_at'], ENT_QUOTES, 'UTF-8') ?>"
|
|
title="<?= date('Y-m-d H:i T', strtotime($row['updated_at'])) ?>"><?= $updatedFmt ?></td>
|
|
<td data-label="Actions">
|
|
<div class="lt-btn-group">
|
|
<button type="button" class="lt-btn lt-btn-sm"
|
|
data-action="view-ticket"
|
|
data-ticket-id="<?= htmlspecialchars($row['ticket_id']) ?>"
|
|
aria-label="View ticket <?= htmlspecialchars($row['ticket_id']) ?>">View</button>
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost"
|
|
data-action="quick-status"
|
|
data-ticket-id="<?= (int)$row['ticket_id'] ?>"
|
|
data-status="<?= htmlspecialchars($row['status'], ENT_QUOTES) ?>"
|
|
aria-label="Change status">~</button>
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost"
|
|
data-action="quick-assign"
|
|
data-ticket-id="<?= htmlspecialchars($row['ticket_id']) ?>"
|
|
aria-label="Assign ticket">@</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach ?>
|
|
<?php endif ?>
|
|
</tbody>
|
|
</table>
|
|
</div><!-- /.lt-table-wrap -->
|
|
</div><!-- /.lt-frame -->
|
|
|
|
<!-- Pagination -->
|
|
<?php if ($totalPages > 1): ?>
|
|
<div class="lt-pagination" role="navigation" aria-label="Ticket pagination">
|
|
<?php
|
|
$currentParams = $_GET;
|
|
if ($page > 1) {
|
|
$currentParams['page'] = $page - 1;
|
|
$prevUrl = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
|
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $prevUrl . '" aria-label="Previous page">«</button>';
|
|
}
|
|
$range = range(max(1, $page - 2), min($totalPages, $page + 2));
|
|
if (!in_array(1, $range)) {
|
|
$currentParams['page'] = 1;
|
|
$url1 = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
|
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $url1 . '">1</button>';
|
|
if ($range[0] > 2) echo '<span class="lt-text-muted lt-text-xs" style="padding:0 0.25rem">…</span>';
|
|
}
|
|
foreach ($range as $i) {
|
|
$currentParams['page'] = $i;
|
|
$iUrl = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
|
$activeClass = ($i === $page) ? ' lt-btn-primary' : '';
|
|
echo '<button class="lt-btn lt-btn-sm' . $activeClass . '" data-action="navigate" data-url="' . $iUrl . '" ' . ($i === $page ? 'aria-current="page"' : '') . '>' . $i . '</button>';
|
|
}
|
|
if (!in_array($totalPages, $range)) {
|
|
if ($range[count($range)-1] < $totalPages - 1) echo '<span class="lt-text-muted lt-text-xs" style="padding:0 0.25rem">…</span>';
|
|
$currentParams['page'] = $totalPages;
|
|
$urlLast = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
|
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $urlLast . '">' . $totalPages . '</button>';
|
|
}
|
|
if ($page < $totalPages) {
|
|
$currentParams['page'] = $page + 1;
|
|
$nextUrl = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
|
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $nextUrl . '" aria-label="Next page">»</button>';
|
|
}
|
|
?>
|
|
</div>
|
|
<?php endif ?>
|
|
|
|
</div><!-- /#tab-table -->
|
|
|
|
<!-- ══════════════════════════════════════════════════════
|
|
TAB PANEL: KANBAN VIEW
|
|
══════════════════════════════════════════════════════ -->
|
|
<div id="tab-kanban" class="lt-tab-panel" role="tabpanel" aria-labelledby="cardViewBtn">
|
|
<div class="lt-grid-4">
|
|
|
|
<div class="lt-frame">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">
|
|
Open <span class="lt-text-xs lt-text-muted column-count" data-status="Open"></span>
|
|
</div>
|
|
<div class="lt-section-body kanban-cards" id="kanban-col-open" style="min-height:80px"></div>
|
|
</div>
|
|
|
|
<div class="lt-frame">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">
|
|
Pending <span class="lt-text-xs lt-text-muted column-count" data-status="Pending"></span>
|
|
</div>
|
|
<div class="lt-section-body kanban-cards" id="kanban-col-pending" style="min-height:80px"></div>
|
|
</div>
|
|
|
|
<div class="lt-frame">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">
|
|
In Progress <span class="lt-text-xs lt-text-muted column-count" data-status="In Progress"></span>
|
|
</div>
|
|
<div class="lt-section-body kanban-cards" id="kanban-col-inprogress" style="min-height:80px"></div>
|
|
</div>
|
|
|
|
<div class="lt-frame">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">
|
|
Closed <span class="lt-text-xs lt-text-muted column-count" data-status="Closed"></span>
|
|
</div>
|
|
<div class="lt-section-body kanban-cards" id="kanban-col-closed" style="min-height:80px"></div>
|
|
</div>
|
|
|
|
</div><!-- /.lt-grid-4 -->
|
|
</div><!-- /#tab-kanban -->
|
|
|
|
</div><!-- /.lt-content -->
|
|
|
|
</div><!-- /.lt-layout -->
|
|
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════
|
|
SETTINGS MODAL
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<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-header">
|
|
<span class="lt-modal-title" id="settingsModalTitle">[ CFG ] SYSTEM PREFERENCES</span>
|
|
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close settings">✕</button>
|
|
</div>
|
|
<div class="lt-modal-body">
|
|
|
|
<div class="settings-section">
|
|
<h4 class="lt-subsection-header">Display Preferences</h4>
|
|
<div class="lt-kv-grid">
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="rowsPerPage">Rows per page</label>
|
|
<span class="lt-kv-value">
|
|
<select id="rowsPerPage" class="lt-select lt-select-sm">
|
|
<option value="15">15</option>
|
|
<option value="25">25</option>
|
|
<option value="50">50</option>
|
|
<option value="100">100</option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="tableDensity">Table density</label>
|
|
<span class="lt-kv-value">
|
|
<select id="tableDensity" class="lt-select lt-select-sm">
|
|
<option value="compact">Compact</option>
|
|
<option value="normal" selected>Normal</option>
|
|
<option value="comfortable">Comfortable</option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<span class="lt-kv-label">Default status filters</span>
|
|
<span class="lt-kv-value lt-flex lt-flex-wrap lt-flex-gap-sm">
|
|
<?php foreach (['Open','Pending','In Progress','Closed'] as $sf): ?>
|
|
<label class="lt-filter-option">
|
|
<input type="checkbox" class="lt-checkbox" name="defaultFilters" value="<?= $sf ?>"
|
|
<?= in_array($sf, ['Open','Pending','In Progress']) ? 'checked' : '' ?>>
|
|
<?= $sf ?>
|
|
</label>
|
|
<?php endforeach ?>
|
|
</span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="userTimezone">Timezone</label>
|
|
<span class="lt-kv-value">
|
|
<select id="userTimezone" class="lt-select lt-select-sm">
|
|
<option value="America/New_York">Eastern (EST/EDT)</option>
|
|
<option value="America/Chicago">Central (CST/CDT)</option>
|
|
<option value="America/Denver">Mountain (MST/MDT)</option>
|
|
<option value="America/Los_Angeles">Pacific (PST/PDT)</option>
|
|
<option value="UTC">UTC</option>
|
|
<option value="Europe/London">London (GMT/BST)</option>
|
|
<option value="Europe/Paris">Paris (CET/CEST)</option>
|
|
</select>
|
|
<span class="lt-text-xs lt-text-muted">
|
|
Current: <?= htmlspecialchars($GLOBALS['config']['TIMEZONE_ABBREV'] ?? 'UTC') ?>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<h4 class="lt-subsection-header">Notifications</h4>
|
|
<div class="lt-kv-grid">
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="notificationsEnabled">Browser notifications</label>
|
|
<span class="lt-kv-value">
|
|
<label class="lt-filter-option">
|
|
<input type="checkbox" class="lt-checkbox" id="notificationsEnabled" checked> Enabled
|
|
</label>
|
|
</span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="soundEffects">Sound effects</label>
|
|
<span class="lt-kv-value">
|
|
<label class="lt-filter-option">
|
|
<input type="checkbox" class="lt-checkbox" id="soundEffects" checked> Enabled
|
|
</label>
|
|
</span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="toastDuration">Toast duration</label>
|
|
<span class="lt-kv-value">
|
|
<select id="toastDuration" class="lt-select lt-select-sm">
|
|
<option value="3000" selected>3 seconds</option>
|
|
<option value="5000">5 seconds</option>
|
|
<option value="10000">10 seconds</option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<h4 class="lt-subsection-header">Keyboard Shortcuts</h4>
|
|
<div class="shortcuts-list lt-text-xs">
|
|
<div class="shortcut-item"><kbd>Ctrl/Cmd+K</kbd> Focus search</div>
|
|
<div class="shortcut-item"><kbd>Alt+S</kbd> Open settings</div>
|
|
<div class="shortcut-item"><kbd>ESC</kbd> Close modal</div>
|
|
<div class="shortcut-item"><kbd>?</kbd> Show shortcuts</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<h4 class="lt-subsection-header">User Information</h4>
|
|
<div class="lt-kv-grid">
|
|
<div class="lt-kv-row"><span class="lt-kv-label">Display Name</span><span class="lt-kv-value"><?= htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? 'N/A') ?></span></div>
|
|
<div class="lt-kv-row"><span class="lt-kv-label">Username</span><span class="lt-kv-value"><?= htmlspecialchars($GLOBALS['currentUser']['username'] ?? '') ?></span></div>
|
|
<div class="lt-kv-row"><span class="lt-kv-label">Email</span><span class="lt-kv-value"><?= htmlspecialchars($GLOBALS['currentUser']['email'] ?? 'N/A') ?></span></div>
|
|
<div class="lt-kv-row"><span class="lt-kv-label">Role</span><span class="lt-kv-value"><?= ($GLOBALS['currentUser']['is_admin'] ?? false) ? '<span class="lt-badge lt-badge-admin">Admin</span>' : 'User' ?></span></div>
|
|
<div class="lt-kv-row">
|
|
<span class="lt-kv-label">Groups</span>
|
|
<span class="lt-kv-value">
|
|
<?php
|
|
$groups = array_filter(array_map('trim', explode(',', $GLOBALS['currentUser']['groups'] ?? '')));
|
|
if ($groups):
|
|
foreach ($groups as $g): ?>
|
|
<span class="lt-badge lt-badge-sm"><?= htmlspecialchars($g) ?></span>
|
|
<?php endforeach;
|
|
else: ?>
|
|
<span class="lt-text-muted">No groups assigned</span>
|
|
<?php endif ?>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /.lt-modal-body -->
|
|
<div class="lt-modal-footer">
|
|
<button type="button" class="lt-btn lt-btn-primary" data-action="save-settings">SAVE</button>
|
|
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close data-action="close-settings">CANCEL</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════
|
|
ADVANCED SEARCH MODAL
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<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-header">
|
|
<span class="lt-modal-title" id="advancedSearchModalTitle">[ FILTER ] ADVANCED SEARCH</span>
|
|
<button type="button" class="lt-modal-close" data-modal-close
|
|
data-action="close-advanced-search" aria-label="Close">✕</button>
|
|
</div>
|
|
<form id="advancedSearchForm">
|
|
<div class="lt-modal-body">
|
|
|
|
<div class="settings-section">
|
|
<h4 class="lt-subsection-header">Saved Filters</h4>
|
|
<div class="lt-form-group" style="margin:0">
|
|
<label class="lt-label" for="saved-filters-select">Load Filter</label>
|
|
<div class="lt-flex lt-flex-gap-sm">
|
|
<select id="saved-filters-select" class="lt-select" style="flex:1"
|
|
data-action="load-saved-filter">
|
|
<option value="">— Select a saved filter —</option>
|
|
</select>
|
|
<button type="button" class="lt-btn lt-btn-sm" data-action="save-filter">SAVE</button>
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" data-action="delete-filter">DEL</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<h4 class="lt-subsection-header">Search Text</h4>
|
|
<div class="lt-form-group" style="margin:0">
|
|
<label class="lt-sr-only lt-label" for="adv-search-text">Search</label>
|
|
<input type="text" id="adv-search-text" class="lt-input"
|
|
placeholder="Search in title, description...">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<h4 class="lt-subsection-header">Date Range</h4>
|
|
<div class="lt-kv-grid">
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="adv-created-from">Created From</label>
|
|
<span class="lt-kv-value"><input type="date" id="adv-created-from" class="lt-input lt-input-sm"></span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="adv-created-to">Created To</label>
|
|
<span class="lt-kv-value"><input type="date" id="adv-created-to" class="lt-input lt-input-sm"></span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="adv-updated-from">Updated From</label>
|
|
<span class="lt-kv-value"><input type="date" id="adv-updated-from" class="lt-input lt-input-sm"></span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="adv-updated-to">Updated To</label>
|
|
<span class="lt-kv-value"><input type="date" id="adv-updated-to" class="lt-input lt-input-sm"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<h4 class="lt-subsection-header">Filters</h4>
|
|
<div class="lt-kv-grid">
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="adv-status">Status</label>
|
|
<span class="lt-kv-value">
|
|
<select id="adv-status" class="lt-select" multiple size="4">
|
|
<option value="Open">Open</option>
|
|
<option value="Pending">Pending</option>
|
|
<option value="In Progress">In Progress</option>
|
|
<option value="Closed">Closed</option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="adv-priority-min">Priority</label>
|
|
<span class="lt-kv-value lt-flex lt-flex-gap-sm lt-flex-align-center">
|
|
<select id="adv-priority-min" class="lt-select lt-select-sm">
|
|
<option value="">Any</option>
|
|
<?php foreach (range(1,5) as $p): ?><option value="<?= $p ?>">P<?= $p ?></option><?php endforeach ?>
|
|
</select>
|
|
<span class="lt-text-xs lt-text-muted">to</span>
|
|
<select id="adv-priority-max" class="lt-select lt-select-sm">
|
|
<option value="">Any</option>
|
|
<?php foreach (range(1,5) as $p): ?><option value="<?= $p ?>">P<?= $p ?></option><?php endforeach ?>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<h4 class="lt-subsection-header">Users</h4>
|
|
<div class="lt-kv-grid">
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="adv-created-by">Created By</label>
|
|
<span class="lt-kv-value">
|
|
<select id="adv-created-by" class="lt-select">
|
|
<option value="">Any User</option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
<div class="lt-kv-row">
|
|
<label class="lt-kv-label" for="adv-assigned-to">Assigned To</label>
|
|
<span class="lt-kv-value">
|
|
<select id="adv-assigned-to" class="lt-select">
|
|
<option value="">Any User</option>
|
|
<option value="unassigned">Unassigned</option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /.lt-modal-body -->
|
|
<div class="lt-modal-footer">
|
|
<button type="submit" class="lt-btn lt-btn-primary">SEARCH</button>
|
|
<button type="button" class="lt-btn lt-btn-ghost" data-action="reset-advanced-search">RESET</button>
|
|
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close
|
|
data-action="close-advanced-search">CANCEL</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════
|
|
DASHBOARD INLINE SCRIPT
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<script nonce="<?= $nonce ?>">
|
|
// Initialize keyboard and table navigation
|
|
if (window.lt) {
|
|
lt.keys.initDefaults();
|
|
lt.tableNav.init('tickets-table');
|
|
lt.statsFilter.init();
|
|
}
|
|
|
|
// Helper: get date in server timezone
|
|
function getServerDate() {
|
|
var now = new Date();
|
|
var serverTime = new Date(now.getTime() + (window.APP_TIMEZONE_OFFSET * 60000) + (now.getTimezoneOffset() * 60000));
|
|
return serverTime.getFullYear() + '-' +
|
|
String(serverTime.getMonth() + 1).padStart(2, '0') + '-' +
|
|
String(serverTime.getDate()).padStart(2, '0');
|
|
}
|
|
|
|
// Stat card click-to-filter
|
|
document.querySelectorAll('.lt-stat-card').forEach(function (card) {
|
|
card.addEventListener('click', function () {
|
|
var url = '/?';
|
|
var today = getServerDate();
|
|
if (card.classList.contains('stat-open')) url += 'status=Open,Pending,In+Progress';
|
|
else if (card.classList.contains('stat-critical')) url += 'status=Open,Pending,In+Progress&priority_max=1';
|
|
else if (card.classList.contains('stat-unassigned')) url += 'status=Open,Pending,In+Progress&assigned_to=unassigned';
|
|
else if (card.classList.contains('stat-today')) url += 'status=Open,Pending,In+Progress&created_from=' + today + '&created_to=' + today;
|
|
else if (card.classList.contains('stat-resolved')) url += 'status=Closed&updated_from=' + today + '&updated_to=' + today;
|
|
else return;
|
|
window.location.href = url;
|
|
});
|
|
});
|
|
|
|
// Event delegation for click actions
|
|
document.addEventListener('click', function (e) {
|
|
var target = e.target.closest('[data-action]');
|
|
if (!target) return;
|
|
switch (target.getAttribute('data-action')) {
|
|
case 'open-settings': openSettingsModal(); break;
|
|
case 'close-settings': closeSettingsModal(); break;
|
|
case 'save-settings': saveSettings(); break;
|
|
case 'manual-refresh': if (lt.autoRefresh) lt.autoRefresh.now(); break;
|
|
case 'toggle-sidebar': if (typeof toggleSidebar==='function') toggleSidebar(); break;
|
|
case 'open-advanced-search': openAdvancedSearch(); break;
|
|
case 'close-advanced-search': closeAdvancedSearch(); break;
|
|
case 'reset-advanced-search': resetAdvancedSearch(); break;
|
|
case 'set-view-mode': setViewMode(target.getAttribute('data-mode')); break;
|
|
case 'navigate': window.location.href = target.getAttribute('data-url'); break;
|
|
case 'toggle-export-menu': e.stopPropagation(); toggleExportMenu(e); break;
|
|
case 'export-tickets': e.preventDefault(); exportSelectedTickets(target.getAttribute('data-format')); break;
|
|
case 'bulk-status': showBulkStatusModal(); break;
|
|
case 'bulk-assign': showBulkAssignModal(); break;
|
|
case 'bulk-priority': showBulkPriorityModal(); break;
|
|
case 'clear-selection': clearSelection(); break;
|
|
case 'toggle-select-all': toggleSelectAll(); break;
|
|
case 'toggle-row-checkbox': toggleRowCheckbox(e, target); break;
|
|
case 'view-ticket': e.stopPropagation(); window.location.href = '/ticket/' + target.getAttribute('data-ticket-id'); break;
|
|
case 'quick-status': e.stopPropagation(); quickStatusChange(target.getAttribute('data-ticket-id'), target.getAttribute('data-status')); break;
|
|
case 'quick-assign': e.stopPropagation(); quickAssign(target.getAttribute('data-ticket-id')); break;
|
|
case 'save-filter': saveCurrentFilter(); break;
|
|
case 'delete-filter': deleteSavedFilter(); break;
|
|
case 'remove-filter': removeFilter(target.getAttribute('data-filter-type'), target.getAttribute('data-filter-value')); break;
|
|
case 'clear-all-filters': window.location.href = '/'; break;
|
|
}
|
|
});
|
|
|
|
// Change event delegation
|
|
document.addEventListener('change', function (e) {
|
|
var target = e.target.closest('[data-action]');
|
|
if (!target) return;
|
|
switch (target.getAttribute('data-action')) {
|
|
case 'update-selection': updateSelectionCount(); break;
|
|
case 'load-saved-filter': loadSavedFilter(); break;
|
|
}
|
|
});
|
|
|
|
// Advanced search form submit
|
|
var advForm = document.getElementById('advancedSearchForm');
|
|
if (advForm) advForm.addEventListener('submit', performAdvancedSearch);
|
|
</script>
|
|
|
|
<?php include __DIR__ . '/layout_footer.php'; ?>
|