Files
tinker_tickets/views/admin/WorkflowDesignerView.php
Jared Vititoe 7695c6134c Accessibility pass: ARIA roles, label associations, CSS class migrations
- Add role=dialog/aria-modal/aria-labelledby to all 12 modal overlays (JS + PHP)
- Add aria-label="Close" to all 14 modal close buttons
- Add full ARIA combobox pattern to @mention autocomplete (listbox, option, aria-selected, aria-expanded)
- Add for= attributes to admin filter form labels (AuditLog, UserActivity, ApiKeys)
- Remove dead closeOnAdvancedSearchBackdropClick() from advanced-search.js

CSS/JS style cleanup:
- Move .ascii-banner static styles from JS inline to CSS class; add .ascii-banner--glow
- Add .ascii-banner-cursor, .loading-overlay--hiding, .has-overlay, tr[data-clickable]
- Add .animate-fadein/.animate-fadeout/.comment--deleting to ticket.css
- Add .lt-toast--hiding to base.css; remove opacity/transition inline JS
- Remove redundant cursor:pointer JS (already in th{} CSS rule)
- Remove trailing space in lt-select class attributes

Bug fixes:
- base.js: boot overlay opacity inline style was overriding .fade-out class opacity via
  specificity (1000 vs 20), preventing the fade-out animation — removed
- ascii-banner.js: cursor used blink-caret (border-color only) instead of blink-cursor
  (opacity-based), so the █ cursor never actually blinked — fixed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 20:29:58 -04:00

272 lines
13 KiB
PHP
Raw Permalink 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
// Admin view for workflow/status transitions designer
// Receives $workflows from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workflow Designer - Admin</title>
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="/assets/css/base.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260320">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260320">
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js?v=20260320"></script>
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
<div class="user-header">
<div class="user-header-left">
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
<span class="admin-page-title">Admin: Workflow Designer</span>
</div>
<div class="user-header-right">
<?php if (isset($GLOBALS['currentUser'])): ?>
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
<span class="admin-badge">[ ADMIN ]</span>
<?php endif; ?>
</div>
</div>
<div class="ascii-frame-outer admin-container">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Status Workflow Designer</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div class="admin-header-row">
<h2>Status Transitions</h2>
<button data-action="show-create-modal" class="btn">+ NEW TRANSITION</button>
</div>
<p class="text-muted-green mb-1">
Define which status transitions are allowed. This controls what options appear in the status dropdown.
</p>
<!-- Visual Workflow Diagram -->
<div class="workflow-diagram">
<h4 class="admin-section-title">Workflow Diagram</h4>
<div class="workflow-diagram-nodes">
<?php
$statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
foreach ($statuses as $status):
$statusClass = 'status-' . str_replace(' ', '-', strtolower($status));
?>
<div class="workflow-diagram-node">
<div class="<?php echo $statusClass; ?>">
<?php echo $status; ?>
</div>
<div class="text-muted-green workflow-diagram-node-label">
<?php
$toCount = 0;
if (isset($workflows)) {
foreach ($workflows as $w) {
if ($w['from_status'] === $status) $toCount++;
}
}
echo "→ $toCount transitions";
?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Transitions Table -->
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>From Status</th>
<th>→</th>
<th>To Status</th>
<th>Requires Comment</th>
<th>Requires Admin</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($workflows)): ?>
<tr>
<td colspan="7" class="empty-state">No transitions defined. Add transitions to enable status changes.</td>
</tr>
<?php else: ?>
<?php foreach ($workflows as $wf): ?>
<tr>
<td>
<span class="status-<?php echo str_replace(' ', '-', strtolower($wf['from_status'])); ?>">
<?php echo htmlspecialchars($wf['from_status']); ?>
</span>
</td>
<td class="text-amber text-center">→</td>
<td>
<span class="status-<?php echo str_replace(' ', '-', strtolower($wf['to_status'])); ?>">
<?php echo htmlspecialchars($wf['to_status']); ?>
</span>
</td>
<td class="text-center"><?php echo $wf['requires_comment'] ? '✓' : ''; ?></td>
<td class="text-center"><?php echo $wf['requires_admin'] ? '✓' : ''; ?></td>
<td class="text-center">
<span class="<?php echo $wf['is_active'] ? 'text-open' : 'text-closed'; ?>">
<?php echo $wf['is_active'] ? '✓' : '✗'; ?>
</span>
</td>
<td>
<button data-action="edit-transition" data-id="<?php echo $wf['transition_id']; ?>" class="btn btn-small">EDIT</button>
<button data-action="delete-transition" data-id="<?php echo $wf['transition_id']; ?>" class="btn btn-small btn-danger">DELETE</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Create/Edit Modal -->
<div class="lt-modal-overlay" id="workflowModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="lt-modal lt-modal-sm">
<div class="lt-modal-header">
<span class="lt-modal-title" id="modalTitle">Create Transition</span>
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div>
<form id="workflowForm">
<input type="hidden" id="transition_id" name="transition_id">
<div class="lt-modal-body">
<div class="setting-row">
<label for="from_status">From Status *</label>
<select id="from_status" name="from_status" required>
<option value="Open">Open</option>
<option value="Pending">Pending</option>
<option value="In Progress">In Progress</option>
<option value="Closed">Closed</option>
</select>
</div>
<div class="setting-row">
<label for="to_status">To Status *</label>
<select id="to_status" name="to_status" required>
<option value="Open">Open</option>
<option value="Pending">Pending</option>
<option value="In Progress">In Progress</option>
<option value="Closed">Closed</option>
</select>
</div>
<div class="setting-row">
<label><input type="checkbox" id="requires_comment" name="requires_comment"> Requires comment</label>
</div>
<div class="setting-row">
<label><input type="checkbox" id="requires_admin" name="requires_admin"> Requires admin privileges</label>
</div>
<div class="setting-row">
<label><input type="checkbox" id="is_active" name="is_active" checked> Active</label>
</div>
</div>
<div class="lt-modal-footer">
<button type="submit" class="lt-btn lt-btn-primary">SAVE</button>
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>CANCEL</button>
</div>
</form>
</div>
</div>
<script nonce="<?php echo $nonce; ?>">
const workflows = <?php echo json_encode($workflows ?? []); ?>;
function showCreateModal() {
document.getElementById('modalTitle').textContent = 'Create Transition';
document.getElementById('workflowForm').reset();
document.getElementById('transition_id').value = '';
document.getElementById('is_active').checked = true;
lt.modal.open('workflowModal');
}
function closeModal() {
lt.modal.close('workflowModal');
}
// Event delegation for data-action handlers
document.addEventListener('click', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
switch (action) {
case 'show-create-modal':
showCreateModal();
break;
case 'edit-transition':
editTransition(target.dataset.id);
break;
case 'delete-transition':
deleteTransition(target.dataset.id);
break;
}
});
// Form submit handler
document.getElementById('workflowForm').addEventListener('submit', function(e) {
saveTransition(e);
});
if (window.lt) lt.keys.initDefaults();
function saveTransition(e) {
e.preventDefault();
const data = {
transition_id: document.getElementById('transition_id').value,
from_status: document.getElementById('from_status').value,
to_status: document.getElementById('to_status').value,
requires_comment: document.getElementById('requires_comment').checked ? 1 : 0,
requires_admin: document.getElementById('requires_admin').checked ? 1 : 0,
is_active: document.getElementById('is_active').checked ? 1 : 0
};
const url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_id : '');
const apiCall = data.transition_id ? lt.api.put(url, data) : lt.api.post(url, data);
apiCall.then(result => {
if (result.success) {
window.location.reload();
} else {
lt.toast.error(result.error || 'Failed to save');
}
}).catch(err => lt.toast.error('Failed to save'));
}
function editTransition(id) {
const wf = workflows.find(w => w.transition_id == id);
if (!wf) return;
document.getElementById('transition_id').value = wf.transition_id;
document.getElementById('from_status').value = wf.from_status;
document.getElementById('to_status').value = wf.to_status;
document.getElementById('requires_comment').checked = wf.requires_comment == 1;
document.getElementById('requires_admin').checked = wf.requires_admin == 1;
document.getElementById('is_active').checked = wf.is_active == 1;
document.getElementById('modalTitle').textContent = 'Edit Transition';
lt.modal.open('workflowModal');
}
function deleteTransition(id) {
showConfirmModal('Delete Transition', 'Delete this status transition?', 'error', function() {
lt.api.delete('/api/manage_workflows.php?id=' + id)
.then(data => {
if (data.success) window.location.reload();
else lt.toast.error(data.error || 'Failed to delete');
}).catch(err => lt.toast.error('Failed to delete'));
});
}
</script>
</body>
</html>