CSS fixes: - Fix [ ] brackets appearing below button text by replacing display:inline-flex with display:inline-block + white-space:nowrap on .btn — removes cross-browser flex pseudo-element inconsistency as root cause - Remove conflicting .btn::before ripple block (position:absolute was overriding bracket content positioning) - Remove overflow:hidden from .btn which was clipping bracket content - Fix body::after duplicate rule causing GPU layer blink (second position:fixed rule re-created compositor layer, overriding display:none suppression) - Replace all transition:all with scoped property transitions in dashboard.css, ticket.css, base.css (prevents full CSS property evaluation on every hover) - Convert pulse-warning/pulse-critical keyframes from box-shadow to opacity animation (GPU-composited, eliminates CPU repaints at 60fps) - Fix mobile *::before/*::after blanket content:none rule — now targets only decorative frame glyphs, preserving button brackets and status indicators - Remove --terminal-green-dim override that broke .lt-btn hover backgrounds JS fixes: - Fix all lt.lt.toast.* double-prefix instances in dashboard.js - Add null guard before .appendChild() on bulkAssignUser select - Replace all remaining emoji with terminal bracket notation (dashboard.js, ticket.js, markdown.js) - Migrate all toast.*() shim calls to lt.toast.* across all JS files View fixes: - Remove hardcoded [ ] brackets from .btn buttons (CSS now adds them) - Replace all emoji with terminal bracket notation in all views and admin views - Add missing CSP nonces to AuditLogView.php and UserActivityView.php script tags - Bump CSS version strings to ?v=20260319b for cache busting Security fixes: - update_ticket.php: add authorization check (non-admins can only edit their own or assigned tickets) - add_comment.php: validate and cast ticket_id to integer with 400 response - clone_ticket.php: fix unconditional session_start(), add ticket ID validation, add internal ticket access check - bulk_operation.php: add HTTP 401/403 status codes on auth failures - upload_attachment.php: fix missing $conn arg in AttachmentModel constructor - assign_ticket.php: add ticket existence check and permission verification Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
284 lines
14 KiB
PHP
284 lines
14 KiB
PHP
<?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">
|
||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></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 style="margin-left: 1rem; color: var(--terminal-amber);">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" style="max-width: 1200px; margin: 2rem auto;">
|
||
<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 style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||
<h2 style="margin: 0;">Status Transitions</h2>
|
||
<button data-action="show-create-modal" class="btn">+ New Transition</button>
|
||
</div>
|
||
|
||
<p style="color: var(--terminal-green-dim); margin-bottom: 1rem;">
|
||
Define which status transitions are allowed. This controls what options appear in the status dropdown.
|
||
</p>
|
||
|
||
<!-- Visual Workflow Diagram -->
|
||
<div style="margin-bottom: 2rem; padding: 1rem; border: 1px solid var(--terminal-green); background: var(--bg-secondary);">
|
||
<h4 style="color: var(--terminal-amber); margin-bottom: 1rem;">Workflow Diagram</h4>
|
||
<div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap;">
|
||
<?php
|
||
$statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
|
||
foreach ($statuses as $status):
|
||
$statusClass = 'status-' . str_replace(' ', '-', strtolower($status));
|
||
?>
|
||
<div style="text-align: center;">
|
||
<div class="<?php echo $statusClass; ?>" style="padding: 0.5rem 1rem; display: inline-block;">
|
||
<?php echo $status; ?>
|
||
</div>
|
||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim); margin-top: 0.5rem;">
|
||
<?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 -->
|
||
<table style="width: 100%;">
|
||
<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" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
||
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 style="text-align: center; color: var(--terminal-amber);">→</td>
|
||
<td>
|
||
<span class="status-<?php echo str_replace(' ', '-', strtolower($wf['to_status'])); ?>">
|
||
<?php echo htmlspecialchars($wf['to_status']); ?>
|
||
</span>
|
||
</td>
|
||
<td style="text-align: center;"><?php echo $wf['requires_comment'] ? '✓' : '−'; ?></td>
|
||
<td style="text-align: center;"><?php echo $wf['requires_admin'] ? '✓' : '−'; ?></td>
|
||
<td style="text-align: center;">
|
||
<span style="color: <?php echo $wf['is_active'] ? 'var(--status-open)' : 'var(--status-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>
|
||
|
||
<!-- Create/Edit Modal -->
|
||
<div class="lt-modal-overlay" id="workflowModal" aria-hidden="true">
|
||
<div class="lt-modal" style="max-width: 450px;">
|
||
<div class="lt-modal-header">
|
||
<span class="lt-modal-title" id="modalTitle">Create Transition</span>
|
||
<button class="lt-modal-close" data-modal-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; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||
<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 method = data.transition_id ? 'PUT' : 'POST';
|
||
const url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_id : '');
|
||
|
||
fetch(url, {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-Token': window.CSRF_TOKEN
|
||
},
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(r => r.json())
|
||
.then(result => {
|
||
if (result.success) {
|
||
window.location.reload();
|
||
} else {
|
||
lt.toast.error(result.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) {
|
||
if (!confirm('Delete this status transition?')) return;
|
||
fetch('/api/manage_workflows.php?id=' + id, {
|
||
method: 'DELETE',
|
||
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.success) window.location.reload();
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|