3 Commits

Author SHA1 Message Date
jared 67a7d769f0 fix: unassigned filter not working + null guards on modal selects
- DashboardController: handle assigned_to='unassigned' before validateUserId()
  which discarded the string, causing the filter to never reach TicketModel;
  model already correctly converts 'unassigned' to IS NULL in SQL
- dashboard.js: add null guards before .value access on dynamically-created
  modal selects (bulkPriority, bulkStatus, quickStatusSelect)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 22:35:04 -04:00
jared 84b104a501 fix: various inline style cleanup, a11y improvements, and bind_param bug
- Replace style="text-align:center" with .lt-text-center utility class in
  WorkflowDesignerView, CustomFieldsView, error_403, error_404, DashboardView JS string
- Replace style="margin-top:..." with .lt-mt-sm utility in WorkflowDesignerView
- Switch comment-edit-raw data-store textareas to .is-hidden class (TicketView PHP
  + JS-rendered; ticket.js template literal) — these are never shown, only read via .value
- Add aria-describedby="visibilityGroupsHint" + id on hint <p> in CreateTicketView
- Fix bind_param type string bug in manage_workflows.php PUT handler: 'ssiiiii' → 'ssiiii'
  (7 type chars for 6 params caused binding error on workflow transition updates)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 22:29:52 -04:00
jared ff109a710c fix: remove CSP-blocked inline event handlers (onerror, onclick)
- Remove all onerror="this.style.display='none'" from avatar imgs in
  layout_header.php, DashboardView.php, and TicketView.php (PHP + JS)
- Replace onclick SLA dismiss with data-action="dismiss-priority-banner"
  attribute; handler wired via existing click delegation in TicketView.php
- Global capture-phase error delegation in layout_footer.php handles all
  avatar image failures by adding .lt-avatar-img-err class (CSS display:none)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 22:15:45 -04:00
12 changed files with 45 additions and 35 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ try {
$stmt = $conn->prepare("UPDATE status_transitions SET
from_status = ?, to_status = ?, requires_comment = ?, requires_admin = ?, is_active = ?
WHERE transition_id = ?");
$stmt->bind_param('ssiiiii',
$stmt->bind_param('ssiiii',
$data['from_status'],
$data['to_status'],
$data['requires_comment'] ?? 0,
+9 -3
View File
@@ -686,7 +686,9 @@ function closeBulkPriorityModal() {
}
function performBulkPriority() {
const priority = document.getElementById('bulkPriority').value;
const priorityEl = document.getElementById('bulkPriority');
if (!priorityEl) return;
const priority = priorityEl.value;
const ticketIds = getSelectedTicketIds();
if (!priority) {
@@ -789,7 +791,9 @@ function closeBulkStatusModal() {
}
function performBulkStatusChange() {
const status = document.getElementById('bulkStatus').value;
const bulkStatusEl = document.getElementById('bulkStatus');
if (!bulkStatusEl) return;
const status = bulkStatusEl.value;
const ticketIds = getSelectedTicketIds();
if (!status) {
@@ -986,7 +990,9 @@ function closeQuickStatusModal() {
}
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 })
.then(data => {
+1 -1
View File
@@ -1528,7 +1528,7 @@ function submitReply(parentCommentId) {
<div class="comment-text" id="comment-text-${data.comment_id}" ${isMarkdownEnabled ? 'data-markdown' : ''}>
${displayText}
</div>
<textarea class="comment-edit-raw" id="comment-raw-${data.comment_id}" style="display:none;">${commentText.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</textarea>
<textarea class="comment-edit-raw is-hidden" id="comment-raw-${data.comment_id}">${commentText.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</textarea>
</div>
`;
+9 -3
View File
@@ -136,10 +136,16 @@ class DashboardController {
// Validate user ID filters
$createdBy = $this->validateUserId($_GET['created_by'] ?? null);
$assignedTo = $this->validateUserId($_GET['assigned_to'] ?? null);
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
$result = $this->ticketModel->getAllTickets($page, $limit, $status, $sortColumn, $sortDirection, $category, $type, $search, $filters, $GLOBALS['currentUser'] ?? []);
+2 -2
View File
@@ -185,7 +185,7 @@ include __DIR__ . '/layout_header.php';
<p id="visibilityHint" class="lt-form-hint">Everyone who is logged in can view this ticket.</p>
</div>
<div id="visibilityGroupsContainer" class="lt-form-group is-hidden" aria-live="polite">
<div id="visibilityGroupsContainer" class="lt-form-group is-hidden" aria-live="polite" aria-describedby="visibilityGroupsHint">
<label class="lt-label lt-text-cyan">Allowed Groups</label>
<div class="visibility-groups-list lt-flex lt-flex-wrap lt-flex-gap-sm">
<?php
@@ -205,7 +205,7 @@ include __DIR__ . '/layout_header.php';
<span class="lt-text-muted lt-text-sm">No groups available</span>
<?php endif ?>
</div>
<p class="lt-form-hint lt-form-hint--warn">Select at least one group for Internal visibility.</p>
<p id="visibilityGroupsHint" class="lt-form-hint lt-form-hint--warn">Select at least one group for Internal visibility.</p>
</div>
</div>
</div>
+2 -2
View File
@@ -331,7 +331,7 @@ include __DIR__ . '/layout_header.php';
<div class="workload-item">
<div class="lt-avatar lt-avatar--sm <?= $avatarColor ?>" aria-hidden="true" title="<?= htmlspecialchars($name) ?>">
<?php if ($userId > 0): ?>
<img src="/api/user_avatar.php?user_id=<?= $userId ?>" alt="" class="lt-avatar-img" onerror="this.style.display='none'">
<img src="/api/user_avatar.php?user_id=<?= $userId ?>" alt="" class="lt-avatar-img">
<?php endif ?>
<span class="lt-avatar-initials"><?= htmlspecialchars($initials) ?></span>
</div>
@@ -1269,7 +1269,7 @@ if (advForm) advForm.addEventListener('submit', performAdvancedSearch);
(age ? '<div class="lt-kv-row"><span class="lt-kv-label">Age</span><span class="lt-kv-value">' + esc(age) + '</span></div>' : '') +
'</div>' +
'</div>' +
'<p class="lt-text-muted lt-text-xs" style="text-align:center">Click "Open Full Ticket" for description, comments &amp; attachments.</p>';
'<p class="lt-text-muted lt-text-xs lt-text-center">Click "Open Full Ticket" for description, comments &amp; attachments.</p>';
lt.rightDrawer.open('ticketPreviewDrawer');
}
+12 -13
View File
@@ -212,12 +212,7 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
</div>
</div>
</div>
<button type="button" class="lt-alert-close" aria-label="Dismiss"
onclick="(function(btn){
var id=btn.closest('[data-alert-id]').dataset.alertId;
try{sessionStorage.setItem('lt_dismissed_'+id,'1');}catch(e){}
btn.closest('.lt-alert').classList.add('dismissed');
})(this)">&#x2715;</button>
<button type="button" class="lt-alert-close" data-action="dismiss-priority-banner" aria-label="Dismiss">&#x2715;</button>
</div>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
(function(){
@@ -564,8 +559,7 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
<?php if ($commentUserId > 0): ?>
<img src="/api/user_avatar.php?user_id=<?= $commentUserId ?>"
alt=""
class="lt-avatar-img"
onerror="this.style.display='none'">
class="lt-avatar-img">
<?php endif ?>
<span class="lt-avatar-initials"><?= htmlspecialchars($initials) ?></span>
</div>
@@ -602,9 +596,8 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
? htmlspecialchars($comment['comment_text'])
: nl2br(htmlspecialchars($comment['comment_text'])) ?>
</div>
<textarea class="lt-input lt-textarea comment-edit-raw"
<textarea class="lt-input lt-textarea comment-edit-raw is-hidden"
id="comment-raw-<?= $commentId ?>"
style="display:none"
aria-hidden="true"><?= htmlspecialchars($comment['comment_text']) ?></textarea>
</div>
<?php if (!empty($comment['replies'])): ?>
@@ -978,7 +971,7 @@ document.addEventListener('DOMContentLoaded', function () {
for (var i = 0; i < (w.display_name || '').length; i++) hash = ((hash << 5) - hash + (w.display_name || '').charCodeAt(i)) | 0;
var color = avatarColors[Math.abs(hash) % 4];
html += '<div class="lt-avatar lt-avatar--xs ' + color + '" title="' + lt.escHtml(w.display_name) + '" aria-label="' + lt.escHtml(w.display_name) + '">' +
'<img src="/api/user_avatar.php?user_id=' + w.user_id + '" alt="" class="lt-avatar-img" onerror="this.style.display=\'none\'">' +
'<img src="/api/user_avatar.php?user_id=' + w.user_id + '" alt="" class="lt-avatar-img">' +
'<span class="lt-avatar-initials">' + lt.escHtml(initials) + '</span>' +
'</div>';
});
@@ -1121,6 +1114,12 @@ document.addEventListener('DOMContentLoaded', function () {
if (typeof editComment === 'function') editComment(parseInt(commentId, 10));
} else if (action === 'delete-comment' && commentId) {
if (typeof deleteComment === 'function') deleteComment(parseInt(commentId, 10));
} else if (action === 'dismiss-priority-banner') {
var banner = target.closest('[data-alert-id]');
if (banner) {
try { sessionStorage.setItem('lt_dismissed_' + banner.dataset.alertId, '1'); } catch(ex) {}
banner.classList.add('dismissed');
}
}
});
@@ -1237,7 +1236,7 @@ document.addEventListener('DOMContentLoaded', function () {
' data-action="delete-comment" data-comment-id="' + commentId + '" aria-label="Delete comment">Del</button>' : '';
var threadLine = parentId ? '<div class="thread-line" aria-hidden="true"></div>' : '';
var avatarImg = userId > 0 ?
'<img src="/api/user_avatar.php?user_id=' + userId + '" alt="" class="lt-avatar-img" onerror="this.style.display=\'none\'">' : '';
'<img src="/api/user_avatar.php?user_id=' + userId + '" alt="" class="lt-avatar-img">' : '';
var div = document.createElement('div');
div.className = 'comment ' + depthClass + ' ' + threadClass;
@@ -1263,7 +1262,7 @@ document.addEventListener('DOMContentLoaded', function () {
'<div class="comment-text" id="comment-text-' + commentId + '"' + (mdEnabled ? ' data-markdown data-rendered="1"' : '') + '>' +
commentText +
'</div>' +
'<textarea class="lt-input lt-textarea comment-edit-raw" id="comment-raw-' + commentId + '" style="display:none" aria-hidden="true">' +
'<textarea class="lt-input lt-textarea comment-edit-raw is-hidden" id="comment-raw-' + commentId + '" aria-hidden="true">' +
escapedRaw +
'</textarea>' +
'</div>';
+1 -1
View File
@@ -50,7 +50,7 @@ include __DIR__ . '/../../views/layout_header.php';
<td data-label="Label"><strong><?= htmlspecialchars($field['field_label']) ?></strong></td>
<td data-label="Type" class="lt-text-xs"><?= ucfirst($field['field_type']) ?></td>
<td data-label="Category" class="lt-text-xs"><?= htmlspecialchars($field['category'] ?? 'All') ?></td>
<td data-label="Required" style="text-align:center">
<td data-label="Required" class="lt-text-center">
<?= $field['is_required'] ? '<span class="lt-text-amber">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
</td>
<td data-label="Status">
+6 -6
View File
@@ -31,13 +31,13 @@ include __DIR__ . '/../../views/layout_header.php';
$toCount = 0;
if (isset($workflows)) { foreach ($workflows as $w) { if ($w['from_status'] === $status) $toCount++; } }
?>
<div class="lt-card" style="text-align:center">
<div class="lt-card lt-text-center">
<span class="lt-status lt-status-<?= $slug ?>"><?= $status ?></span>
<div class="lt-text-xs lt-text-muted lt-mt-sm">→ <?= $toCount ?> transition<?= $toCount !== 1 ? 's' : '' ?></div>
</div>
<?php endforeach ?>
</div>
<p class="lt-text-xs lt-text-muted" style="margin-top:0.5rem">
<p class="lt-text-xs lt-text-muted lt-mt-sm">
Define which status transitions are allowed. This controls what options appear in the status dropdown on tickets.
</p>
</div>
@@ -69,17 +69,17 @@ include __DIR__ . '/../../views/layout_header.php';
<td data-label="From">
<span class="lt-status lt-status-<?= $fromSlug ?>"><?= htmlspecialchars($wf['from_status']) ?></span>
</td>
<td class="lt-text-amber lt-text-xs" style="text-align:center">&rarr;</td>
<td class="lt-text-amber lt-text-xs lt-text-center">&rarr;</td>
<td data-label="To">
<span class="lt-status lt-status-<?= $toSlug ?>"><?= htmlspecialchars($wf['to_status']) ?></span>
</td>
<td data-label="Req. Comment" style="text-align:center">
<td data-label="Req. Comment" class="lt-text-center">
<?= $wf['requires_comment'] ? '<span class="lt-text-cyan">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
</td>
<td data-label="Req. Admin" style="text-align:center">
<td data-label="Req. Admin" class="lt-text-center">
<?= $wf['requires_admin'] ? '<span class="lt-text-amber">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
</td>
<td data-label="Active" style="text-align:center">
<td data-label="Active" class="lt-text-center">
<?= $wf['is_active']
? '<span class="lt-text-cyan">✓</span>'
: '<span class="lt-text-danger">✗</span>' ?>
+1 -1
View File
@@ -10,7 +10,7 @@ include __DIR__ . '/../views/layout_header.php';
<div class="lt-frame" style="max-width:32rem;margin:4rem auto">
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
<div class="lt-section-header lt-text-danger">[ 403 ] ACCESS DENIED</div>
<div class="lt-section-body" style="text-align:center">
<div class="lt-section-body lt-text-center">
<p class="lt-text-muted lt-mb-md">You do not have permission to access this resource.</p>
<a href="/" class="lt-btn lt-btn-primary">&larr; Dashboard</a>
</div>
+1 -1
View File
@@ -10,7 +10,7 @@ include __DIR__ . '/../views/layout_header.php';
<div class="lt-frame" style="max-width:32rem;margin:4rem auto">
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
<div class="lt-section-header lt-text-amber">[ 404 ] NOT FOUND</div>
<div class="lt-section-body" style="text-align:center">
<div class="lt-section-body lt-text-center">
<p class="lt-text-muted lt-mb-md">The page you requested does not exist.</p>
<a href="/" class="lt-btn lt-btn-primary">&larr; Dashboard</a>
</div>
-1
View File
@@ -166,7 +166,6 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
<img src="/api/user_avatar.php?user_id=<?= $_lt_userId ?>"
alt=""
class="lt-avatar-img"
onerror="this.style.display='none'">
<?php endif ?>
<span class="lt-avatar-initials"><?= htmlspecialchars($_lt_initials) ?></span>
</div>