Files
tinker_tickets/views/layout_footer.php
T
jared c0dfbdbc26 feat: status dots, priority banners, lt-tags, command palette, activity timeline improvements
- Fix DashboardView asset version (was hardcoded 20260327, now uses config ASSET_VERSION)
- Add lt-dot status indicators on dashboard table rows and ticket view toolbar
- Add lt-tag display for Category/Type in ticket read mode (swaps to select in edit mode)
- Add P1/P2 SLA alert banner with elapsed time, progress bar, per-session dismiss
- Wire command palette (Ctrl+K): global nav + admin links via lt.cmdPalette.init()
- Fix cmdPalette.init() call format (flat array, not nested group objects)
- Improve activity timeline: richer formatAction(), better color coding by event type,
  inline status transitions shown in meta row, icon column added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:54:26 -04:00

194 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* layout_footer.php — Shared bottom-of-page partial for all views.
*
* Expected variables available from the including view (set before require):
* string $nonce CSP nonce from SecurityHeadersMiddleware::getNonce()
* array|null $pageScripts Optional array of extra JS paths to load after base.js
* string|null $pageInlineScript Optional raw JS string to run after all scripts load
*
* Globals used:
* $GLOBALS['currentUser'] — user array (user_id, username, is_admin)
* $GLOBALS['config'] — app config array (TIMEZONE, TIMEZONE_ABBREV)
* CsrfMiddleware::getToken() — returns current CSRF token string
*/
// layout_footer.php — JS globals + runtime scripts are loaded here
?>
</main><!-- /#main-content / .lt-main -->
<!-- ================================================================
FOOTER — keyboard hint bar + version
================================================================ -->
<?php
// Context-sensitive keyboard hints based on active nav
$_ltf_nav = $activeNav ?? 'dashboard';
$_ltf_isTicket = str_starts_with($pageTitle ?? '', 'Ticket #');
?>
<footer class="lt-footer" role="contentinfo" aria-label="Keyboard shortcuts and app info">
<nav class="lt-footer-hints" aria-label="Keyboard shortcuts">
<?php if ($_ltf_isTicket): ?>
<a href="/" class="lt-footer-hint" title="Go to dashboard"><span class="lt-footer-key">[ ← ]</span> BACK</a>
<span class="lt-footer-sep">|</span>
<span class="lt-footer-hint" title="Press 14 to change status"><span class="lt-footer-key">[ 1-4 ]</span> STATUS</span>
<span class="lt-footer-sep">|</span>
<span class="lt-footer-hint" title="Press C to jump to comment box"><span class="lt-footer-key">[ C ]</span> COMMENT</span>
<span class="lt-footer-sep">|</span>
<button type="button" class="lt-footer-hint" data-action="open-settings" title="Open settings"><span class="lt-footer-key">[ * ]</span> CFG</button>
<?php elseif (str_starts_with($_ltf_nav, 'admin')): ?>
<a href="/" class="lt-footer-hint" title="Go to dashboard"><span class="lt-footer-key">[ ~ ]</span> HOME</a>
<span class="lt-footer-sep">|</span>
<button type="button" class="lt-footer-hint" data-action="open-settings" title="Open settings"><span class="lt-footer-key">[ * ]</span> CFG</button>
<?php else: ?>
<a href="/" class="lt-footer-hint" title="Go to dashboard (G then D)"><span class="lt-footer-key">[ ~ ]</span> HOME</a>
<span class="lt-footer-sep">|</span>
<span class="lt-footer-hint" title="Press / or Ctrl+K to search"><span class="lt-footer-key">[ / ]</span> SEARCH</span>
<span class="lt-footer-sep">|</span>
<a href="/ticket/create" class="lt-footer-hint" title="Create new ticket (N)"><span class="lt-footer-key">[ + ]</span> NEW</a>
<span class="lt-footer-sep">|</span>
<button type="button" class="lt-footer-hint" data-action="open-settings" title="Open settings"><span class="lt-footer-key">[ * ]</span> CFG</button>
<?php endif ?>
<span class="lt-footer-sep">|</span>
<button type="button" class="lt-footer-hint" data-action="show-keyboard-help" title="Show keyboard shortcuts (?)"><span class="lt-footer-key">[ ? ]</span> HELP</button>
</nav>
<span aria-label="Application version"><?= htmlspecialchars($GLOBALS['config']['APP_NAME'] ?? 'TINKER TICKETS', ENT_QUOTES, 'UTF-8') ?> &mdash; TDS v<?= htmlspecialchars($GLOBALS['config']['APP_VERSION'] ?? '1.2', ENT_QUOTES, 'UTF-8') ?></span>
</footer>
<!-- ================================================================
KEYBOARD SHORTCUTS HELP MODAL — opened by ? key or footer [?] hint
================================================================ -->
<div id="lt-keys-help" class="lt-modal-overlay" aria-hidden="true">
<div class="lt-modal" role="dialog" aria-modal="true" aria-labelledby="keys-help-title">
<div class="lt-modal-header">
<span class="lt-modal-title" id="keys-help-title">Keyboard Shortcuts</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<div class="lt-modal-body">
<table class="lt-data-table" style="width:100%">
<thead>
<tr><th scope="col">Shortcut</th><th scope="col">Action</th></tr>
</thead>
<tbody>
<tr><td>Ctrl / &#x2318; + K</td><td>Focus search box</td></tr>
<tr><td>Ctrl / &#x2318; + E</td><td>Toggle edit mode (ticket page)</td></tr>
<tr><td>Ctrl / &#x2318; + S</td><td>Save changes (ticket page)</td></tr>
<tr><td>j / &#x2193;</td><td>Select next row</td></tr>
<tr><td>k / &#x2191;</td><td>Select previous row</td></tr>
<tr><td>Enter</td><td>Open selected ticket</td></tr>
<tr><td>n</td><td>New ticket</td></tr>
<tr><td>1&ndash;4</td><td>Change ticket status (ticket page)</td></tr>
<tr><td>c</td><td>Jump to comment box (ticket page)</td></tr>
<tr><td>?</td><td>Show this help</td></tr>
<tr><td>ESC</td><td>Close modal / cancel</td></tr>
</tbody>
</table>
</div>
<div class="lt-modal-footer">
<button type="button" class="lt-btn" data-modal-close>Close</button>
</div>
</div>
</div>
<!-- ================================================================
COMMAND PALETTE — Ctrl+K opens when no search input focused
================================================================ -->
<div id="lt-cmd-overlay" class="lt-cmd-overlay" role="dialog" aria-modal="true" aria-label="Command palette" aria-hidden="true">
<div class="lt-cmd-palette" id="lt-cmd-palette">
<div class="lt-cmd-input-wrap">
<span class="lt-cmd-prompt">&gt;</span>
<input id="lt-cmd-input" class="lt-cmd-input" type="text"
placeholder="Search commands&hellip;" autocomplete="off"
spellcheck="false" aria-label="Search commands">
</div>
<div class="lt-cmd-results" id="lt-cmd-results">
<div class="lt-cmd-empty">Start typing to search&hellip;</div>
</div>
<div class="lt-cmd-footer">
<span><kbd>&#x2191;</kbd><kbd>&#x2193;</kbd> Navigate</span>
<span><kbd>Enter</kbd> Select</span>
<span><kbd>Esc</kbd> Close</span>
</div>
</div>
</div>
<!-- base.js + utils.js + globals already loaded in <head> via layout_header.php -->
<?php if (!empty($pageScripts)): ?>
<!-- PAGE-SPECIFIC SCRIPTS -->
<?php foreach ($pageScripts as $_ltf_script): ?>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="<?= htmlspecialchars($_ltf_script, ENT_QUOTES, 'UTF-8') ?>"></script>
<?php endforeach; ?>
<?php endif; ?>
<?php if (!empty($pageInlineScript)): ?>
<!-- PAGE INLINE SCRIPT -->
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
<?= $pageInlineScript ?>
</script>
<?php endif; ?>
<!-- LT INIT — boot animation + global UI init (base.js handles keys/nav automatically) -->
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
if (window.lt) {
lt.init({ bootName: <?= json_encode($GLOBALS['config']['APP_NAME'] ?? 'TINKER TICKETS', JSON_HEX_TAG) ?> });
// Theme toggle button
var themeBtn = document.getElementById('lt-theme-btn');
if (themeBtn) themeBtn.addEventListener('click', function() { lt.theme.toggle(); });
// Command palette — global navigation commands available on all pages
var _cpCmds = [
{ id: 'nav-dashboard', group: 'Navigation', icon: '~', label: 'Dashboard', kbd: 'G D', action: function() { window.location.href = '/'; } },
{ id: 'nav-new-ticket', group: 'Navigation', icon: '+', label: 'New Ticket', kbd: 'N', action: function() { window.location.href = '/ticket/create'; } },
{ id: 'help-shortcuts', group: 'Help', icon: '?', label: 'Keyboard Shortcuts', kbd: '?', action: function() { lt.modal.open('lt-keys-help'); } },
{ id: 'help-theme', group: 'Help', icon: '*', label: 'Toggle Theme', action: function() { lt.theme.toggle(); } },
];
<?php if (!empty($GLOBALS['currentUser']['is_admin'])): ?>
_cpCmds = _cpCmds.concat([
{ id: 'admin-templates', group: 'Admin', icon: 'T', label: 'Templates', action: function() { window.location.href = '/admin/templates'; } },
{ id: 'admin-workflow', group: 'Admin', icon: 'W', label: 'Workflow', action: function() { window.location.href = '/admin/workflow'; } },
{ id: 'admin-recurring', group: 'Admin', icon: 'R', label: 'Recurring Tickets',action: function() { window.location.href = '/admin/recurring-tickets'; } },
{ id: 'admin-fields', group: 'Admin', icon: 'F', label: 'Custom Fields', action: function() { window.location.href = '/admin/custom-fields'; } },
{ id: 'admin-activity', group: 'Admin', icon: 'A', label: 'User Activity', action: function() { window.location.href = '/admin/user-activity'; } },
{ id: 'admin-audit', group: 'Admin', icon: 'L', label: 'Audit Log', action: function() { window.location.href = '/admin/audit-log'; } },
{ id: 'admin-api-keys', group: 'Admin', icon: 'K', label: 'API Keys', action: function() { window.location.href = '/admin/api-keys'; } },
]);
<?php endif ?>
lt.cmdPalette.init(_cpCmds);
}
// Patch lt.api mutating methods to auto-rotate CSRF token when server returns a new one
if (window.lt && lt.api) {
['post', 'put', 'patch', 'delete'].forEach(function(method) {
if (typeof lt.api[method] !== 'function') return;
var _orig = lt.api[method];
lt.api[method] = function(url, body) {
return _orig.call(lt.api, url, body).then(function(data) {
if (data && data.csrf_token) window.CSRF_TOKEN = data.csrf_token;
return data;
});
};
});
}
// Footer hint bar actions (keyboard help + settings — work on all pages)
document.addEventListener('click', function(e) {
var btn = e.target.closest('[data-action]');
if (!btn) return;
var action = btn.getAttribute('data-action');
if (action === 'show-keyboard-help') {
if (window.lt) lt.modal.open('lt-keys-help');
} else if (action === 'open-settings' || action === 'open-settings-modal') {
if (typeof openSettingsModal === 'function') {
openSettingsModal();
} else if (window.lt) {
lt.toast.info('Settings available on the Dashboard');
}
}
});
</script>
</body>
</html>