Fix dashboard stat tiles and add sidebar date filters

- Created Today tile: no longer limits to open statuses (count is all statuses)
- Closed Today tile: filters by closed_at range, not updated_at
- Add closed_from/closed_to support to TicketModel and DashboardController
- Add Created/Updated/Closed date range inputs to sidebar filter panel
- Apply button collects date inputs; Clear All removes them
- removeFilter handles date chip removal (clears both _from and _to)
- Active filter chips shown for date ranges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 23:03:14 -04:00
parent dd98bfbd49
commit 603ba18067
4 changed files with 91 additions and 38 deletions
+23 -32
View File
@@ -259,6 +259,12 @@ function removeFilter(filterType, filterValue) {
} }
} else if (filterType === 'search') { } else if (filterType === 'search') {
params.delete('search'); params.delete('search');
} else if (filterType === 'created_from') {
params.delete('created_from'); params.delete('created_to');
} else if (filterType === 'updated_from') {
params.delete('updated_from'); params.delete('updated_to');
} else if (filterType === 'closed_from') {
params.delete('closed_from'); params.delete('closed_to');
} else { } else {
params.delete(filterType); params.delete(filterType);
} }
@@ -310,44 +316,33 @@ function initSidebarFilters() {
applyFiltersBtn.addEventListener('click', () => { applyFiltersBtn.addEventListener('click', () => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
// Collect selected statuses // Checkboxes
const selectedStatuses = Array.from( const selectedStatuses = Array.from(
document.querySelectorAll('.lt-filter-group input[name="status"]:checked') document.querySelectorAll('.lt-filter-group input[name="status"]:checked')
).map(cb => cb.value); ).map(cb => cb.value);
// Collect selected categories
const selectedCategories = Array.from( const selectedCategories = Array.from(
document.querySelectorAll('.lt-filter-group input[name="category"]:checked') document.querySelectorAll('.lt-filter-group input[name="category"]:checked')
).map(cb => cb.value); ).map(cb => cb.value);
// Collect selected types
const selectedTypes = Array.from( const selectedTypes = Array.from(
document.querySelectorAll('.lt-filter-group input[name="type"]:checked') document.querySelectorAll('.lt-filter-group input[name="type"]:checked')
).map(cb => cb.value); ).map(cb => cb.value);
// Update URL parameters if (selectedStatuses.length > 0) params.set('status', selectedStatuses.join(','));
if (selectedStatuses.length > 0) { else params.delete('status');
params.set('status', selectedStatuses.join(',')); if (selectedCategories.length > 0) params.set('category', selectedCategories.join(','));
} else { else params.delete('category');
params.delete('status'); if (selectedTypes.length > 0) params.set('type', selectedTypes.join(','));
} else params.delete('type');
if (selectedCategories.length > 0) { // Date inputs
params.set('category', selectedCategories.join(',')); const dateFields = ['created_from','created_to','updated_from','updated_to','closed_from','closed_to'];
} else { dateFields.forEach(name => {
params.delete('category'); const el = document.getElementById('filter-' + name.replace('_', '-'));
} if (el && el.value) params.set(name, el.value);
else params.delete(name);
});
if (selectedTypes.length > 0) {
params.set('type', selectedTypes.join(','));
} else {
params.delete('type');
}
// Reset to page 1 when filters change
params.set('page', '1'); params.set('page', '1');
// Reload with new parameters
window.location.search = params.toString(); window.location.search = params.toString();
}); });
} }
@@ -355,14 +350,10 @@ function initSidebarFilters() {
if (clearFiltersBtn) { if (clearFiltersBtn) {
clearFiltersBtn.addEventListener('click', () => { clearFiltersBtn.addEventListener('click', () => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
['status','category','type',
// Remove filter parameters 'created_from','created_to','updated_from','updated_to','closed_from','closed_to'
params.delete('status'); ].forEach(k => params.delete(k));
params.delete('category');
params.delete('type');
params.set('page', '1'); params.set('page', '1');
// Reload with cleared filters
window.location.search = params.toString(); window.location.search = params.toString();
}); });
} }
+8 -4
View File
@@ -118,14 +118,18 @@ class DashboardController {
// Validate date filters // Validate date filters
$createdFrom = $this->validateDate($_GET['created_from'] ?? null); $createdFrom = $this->validateDate($_GET['created_from'] ?? null);
$createdTo = $this->validateDate($_GET['created_to'] ?? null); $createdTo = $this->validateDate($_GET['created_to'] ?? null);
$updatedFrom = $this->validateDate($_GET['updated_from'] ?? null); $updatedFrom = $this->validateDate($_GET['updated_from'] ?? null);
$updatedTo = $this->validateDate($_GET['updated_to'] ?? null); $updatedTo = $this->validateDate($_GET['updated_to'] ?? null);
$closedFrom = $this->validateDate($_GET['closed_from'] ?? null);
$closedTo = $this->validateDate($_GET['closed_to'] ?? null);
if ($createdFrom) $filters['created_from'] = $createdFrom; if ($createdFrom) $filters['created_from'] = $createdFrom;
if ($createdTo) $filters['created_to'] = $createdTo; if ($createdTo) $filters['created_to'] = $createdTo;
if ($updatedFrom) $filters['updated_from'] = $updatedFrom; if ($updatedFrom) $filters['updated_from'] = $updatedFrom;
if ($updatedTo) $filters['updated_to'] = $updatedTo; if ($updatedTo) $filters['updated_to'] = $updatedTo;
if ($closedFrom) $filters['closed_from'] = $closedFrom;
if ($closedTo) $filters['closed_to'] = $closedTo;
// Validate priority filters // Validate priority filters
$priorityMin = $this->validatePriority($_GET['priority_min'] ?? null); $priorityMin = $this->validatePriority($_GET['priority_min'] ?? null);
+12
View File
@@ -121,6 +121,18 @@ class TicketModel {
$paramTypes .= 's'; $paramTypes .= 's';
} }
// Date range - closed_at
if (!empty($filters['closed_from'])) {
$whereConditions[] = "DATE(t.closed_at) >= ?";
$params[] = $filters['closed_from'];
$paramTypes .= 's';
}
if (!empty($filters['closed_to'])) {
$whereConditions[] = "DATE(t.closed_at) <= ?";
$params[] = $filters['closed_to'];
$paramTypes .= 's';
}
// Priority range // Priority range
if (!empty($filters['priority_min'])) { if (!empty($filters['priority_min'])) {
$whereConditions[] = "t.priority >= ?"; $whereConditions[] = "t.priority >= ?";
+48 -2
View File
@@ -56,6 +56,21 @@ if (!empty($_GET['assigned_to'])) {
$label = $_GET['assigned_to'] === 'unassigned' ? 'Unassigned' : 'User #' . htmlspecialchars($_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]; $activeFilters[] = ['type' => 'assigned_to', 'value' => $_GET['assigned_to'], 'label' => 'Assigned: ' . $label];
} }
if (!empty($_GET['created_from']) || !empty($_GET['created_to'])) {
$from = $_GET['created_from'] ?? ''; $to = $_GET['created_to'] ?? '';
$label = $from === $to && $from ? 'Created: ' . $from : 'Created: ' . ($from ?: '…') . ' ' . ($to ?: '…');
$activeFilters[] = ['type' => 'created_from', 'value' => $from, 'label' => $label];
}
if (!empty($_GET['updated_from']) || !empty($_GET['updated_to'])) {
$from = $_GET['updated_from'] ?? ''; $to = $_GET['updated_to'] ?? '';
$label = $from === $to && $from ? 'Updated: ' . $from : 'Updated: ' . ($from ?: '…') . ' ' . ($to ?: '…');
$activeFilters[] = ['type' => 'updated_from', 'value' => $from, 'label' => $label];
}
if (!empty($_GET['closed_from']) || !empty($_GET['closed_to'])) {
$from = $_GET['closed_from'] ?? ''; $to = $_GET['closed_to'] ?? '';
$label = $from === $to && $from ? 'Closed: ' . $from : 'Closed: ' . ($from ?: '…') . ' ' . ($to ?: '…');
$activeFilters[] = ['type' => 'closed_from', 'value' => $from, 'label' => $label];
}
$_lt_statuses = $GLOBALS['config']['TICKET_STATUSES']; $_lt_statuses = $GLOBALS['config']['TICKET_STATUSES'];
$currentStatus = isset($_GET['status']) ? explode(',', $_GET['status']) : ['Open', 'Pending', 'In Progress']; $currentStatus = isset($_GET['status']) ? explode(',', $_GET['status']) : ['Open', 'Pending', 'In Progress'];
@@ -446,6 +461,37 @@ include __DIR__ . '/layout_header.php';
</fieldset> </fieldset>
<?php endif ?> <?php endif ?>
<!-- Date Filters -->
<fieldset class="lt-filter-group">
<legend class="lt-filter-label">Created</legend>
<div class="lt-flex lt-flex-col lt-flex-gap-xs">
<input type="date" id="filter-created-from" name="created_from" class="lt-input lt-input-sm"
placeholder="From" value="<?= htmlspecialchars($_GET['created_from'] ?? '') ?>">
<input type="date" id="filter-created-to" name="created_to" class="lt-input lt-input-sm"
placeholder="To" value="<?= htmlspecialchars($_GET['created_to'] ?? '') ?>">
</div>
</fieldset>
<fieldset class="lt-filter-group">
<legend class="lt-filter-label">Updated</legend>
<div class="lt-flex lt-flex-col lt-flex-gap-xs">
<input type="date" id="filter-updated-from" name="updated_from" class="lt-input lt-input-sm"
placeholder="From" value="<?= htmlspecialchars($_GET['updated_from'] ?? '') ?>">
<input type="date" id="filter-updated-to" name="updated_to" class="lt-input lt-input-sm"
placeholder="To" value="<?= htmlspecialchars($_GET['updated_to'] ?? '') ?>">
</div>
</fieldset>
<fieldset class="lt-filter-group">
<legend class="lt-filter-label">Closed</legend>
<div class="lt-flex lt-flex-col lt-flex-gap-xs">
<input type="date" id="filter-closed-from" name="closed_from" class="lt-input lt-input-sm"
placeholder="From" value="<?= htmlspecialchars($_GET['closed_from'] ?? '') ?>">
<input type="date" id="filter-closed-to" name="closed_to" class="lt-input lt-input-sm"
placeholder="To" value="<?= htmlspecialchars($_GET['closed_to'] ?? '') ?>">
</div>
</fieldset>
<div class="lt-btn-group lt-flex-col"> <div class="lt-btn-group lt-flex-col">
<button type="button" id="apply-filters-btn" class="lt-btn lt-btn-primary lt-btn-sm">APPLY</button> <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> <button type="button" id="clear-filters-btn" class="lt-btn lt-btn-ghost lt-btn-sm">CLEAR ALL</button>
@@ -1165,8 +1211,8 @@ document.querySelectorAll('.lt-stat-card').forEach(function (card) {
if (card.classList.contains('stat-open')) url += 'status=Open,Pending,In+Progress'; 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-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-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-today')) url += 'created_from=' + today + '&created_to=' + today;
else if (card.classList.contains('stat-resolved')) url += 'status=Closed&updated_from=' + today + '&updated_to=' + today; else if (card.classList.contains('stat-resolved')) url += 'status=Closed&closed_from=' + today + '&closed_to=' + today;
else return; else return;
window.location.href = url; window.location.href = url;
}); });