feat: complete TDS v1.2 redesign across all views
Full application redesign using Terminal Design System v1.2 (lt-* class system). Introduces shared layout_header/footer partials, upgrades base.css/base.js to TDS v1.2, and rewrites all views (Dashboard, Ticket, CreateTicket, and all 7 admin views) with lt-frame, lt-table, lt-modal, lt-stats-grid, lt-kv-grid, and data-action event delegation patterns. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+152
-218
@@ -1,238 +1,172 @@
|
||||
<?php
|
||||
// Admin view for managing API keys
|
||||
// Receives $apiKeys from controller
|
||||
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$pageTitle = 'API Keys';
|
||||
$activeNav = 'admin-api-keys';
|
||||
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
||||
$pageScripts = [];
|
||||
include __DIR__ . '/../../views/layout_header.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Keys - 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: API Keys</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 class="lt-page-header">
|
||||
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
||||
<span class="lt-text-muted lt-text-xs">/</span>
|
||||
<span class="lt-text-muted lt-text-xs">Admin: API Keys</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate new key -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Generate New API Key</div>
|
||||
<div class="lt-section-body">
|
||||
<form id="generateKeyForm" class="lt-flex lt-flex-wrap lt-flex-gap-sm lt-flex-align-end">
|
||||
<div class="lt-form-group" style="flex:2;margin:0">
|
||||
<label class="lt-label" for="keyName">Key Name *</label>
|
||||
<input type="text" id="keyName" required class="lt-input" placeholder="e.g., CI/CD Pipeline">
|
||||
</div>
|
||||
<div class="lt-form-group" style="flex:1;margin:0">
|
||||
<label class="lt-label" for="expiresIn">Expires In</label>
|
||||
<select id="expiresIn" class="lt-select">
|
||||
<option value="">Never</option>
|
||||
<option value="30">30 days</option>
|
||||
<option value="90">90 days</option>
|
||||
<option value="180">180 days</option>
|
||||
<option value="365">1 year</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="lt-btn lt-btn-primary" style="margin-bottom:0">GENERATE KEY</button>
|
||||
</form>
|
||||
|
||||
<!-- New key display (hidden by default) -->
|
||||
<div id="newKeyDisplay" class="lt-frame-inner lt-mt-sm is-hidden">
|
||||
<div class="lt-subsection-header lt-text-amber">⚠ Copy this key now — you won't see it again!</div>
|
||||
<div class="lt-flex lt-flex-gap-sm lt-mt-sm">
|
||||
<input type="text" id="newKeyValue" readonly class="lt-input" style="flex:1;font-family:monospace">
|
||||
<button type="button" class="lt-btn lt-btn-sm" data-action="copy-api-key">COPY</button>
|
||||
</div>
|
||||
</div>
|
||||
</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">API Key Management</div>
|
||||
<div class="ascii-content">
|
||||
<!-- Generate New Key Form -->
|
||||
<div class="ascii-frame-inner">
|
||||
<h3 class="admin-section-title">Generate New API Key</h3>
|
||||
<form id="generateKeyForm" class="admin-form-row">
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="keyName">Key Name *</label>
|
||||
<input type="text" id="keyName" required placeholder="e.g., CI/CD Pipeline" class="admin-input">
|
||||
</div>
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="expiresIn">Expires In</label>
|
||||
<select id="expiresIn" class="admin-input">
|
||||
<option value="">Never</option>
|
||||
<option value="30">30 days</option>
|
||||
<option value="90">90 days</option>
|
||||
<option value="180">180 days</option>
|
||||
<option value="365">1 year</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn">GENERATE KEY</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- New Key Display (hidden by default) -->
|
||||
<div id="newKeyDisplay" class="ascii-frame-inner key-generated-alert is-hidden">
|
||||
<h3 class="admin-section-title">New API Key Generated</h3>
|
||||
<p class="text-danger text-sm mb-1">
|
||||
Copy this key now. You won't be able to see it again!
|
||||
</p>
|
||||
<div class="admin-form-row">
|
||||
<input type="text" id="newKeyValue" readonly class="admin-input">
|
||||
<button data-action="copy-api-key" class="btn" title="Copy to clipboard">COPY</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing Keys Table -->
|
||||
<div class="ascii-frame-inner">
|
||||
<h3 class="admin-section-title">Existing API Keys</h3>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Key Prefix</th>
|
||||
<th>Created By</th>
|
||||
<th>Created At</th>
|
||||
<th>Expires At</th>
|
||||
<th>Last Used</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($apiKeys)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">No API keys found. Generate one above.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($apiKeys as $key): ?>
|
||||
<tr id="key-row-<?php echo $key['api_key_id']; ?>">
|
||||
<td><?php echo htmlspecialchars($key['key_name']); ?></td>
|
||||
<td class="mono">
|
||||
<code><?php echo htmlspecialchars($key['key_prefix']); ?>...</code>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown'); ?></td>
|
||||
<td class="nowrap"><?php echo date('Y-m-d H:i', strtotime($key['created_at'])); ?></td>
|
||||
<td class="nowrap">
|
||||
<?php if ($key['expires_at']): ?>
|
||||
<?php $expired = strtotime($key['expires_at']) < time(); ?>
|
||||
<span class="<?php echo $expired ? 'text-danger' : ''; ?>">
|
||||
<?php echo date('Y-m-d', strtotime($key['expires_at'])); ?>
|
||||
<?php if ($expired): ?> (Expired)<?php endif; ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-cyan">Never</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="nowrap">
|
||||
<?php echo $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never'; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($key['is_active']): ?>
|
||||
<span class="text-open">Active</span>
|
||||
<?php else: ?>
|
||||
<span class="text-closed">Revoked</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($key['is_active']): ?>
|
||||
<button data-action="revoke-key" data-id="<?php echo $key['api_key_id']; ?>" class="btn btn-secondary btn-small">
|
||||
REVOKE
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Usage Info -->
|
||||
<div class="ascii-frame-inner">
|
||||
<h3 class="admin-section-title">API Usage</h3>
|
||||
<p>Include the API key in your requests using the Authorization header:</p>
|
||||
<pre class="admin-code-block"><code>Authorization: Bearer YOUR_API_KEY</code></pre>
|
||||
<p class="text-muted text-sm">
|
||||
API keys provide programmatic access to create and manage tickets. Keep your keys secure and rotate them regularly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Existing keys -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Existing API Keys</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="API keys">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Key Prefix</th>
|
||||
<th scope="col">Created By</th>
|
||||
<th scope="col">Created</th>
|
||||
<th scope="col">Expires</th>
|
||||
<th scope="col">Last Used</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($apiKeys)): ?>
|
||||
<tr><td colspan="8" class="lt-empty">No API keys found. Generate one above.</td></tr>
|
||||
<?php else: foreach ($apiKeys as $key): ?>
|
||||
<?php $expired = $key['expires_at'] && strtotime($key['expires_at']) < time(); ?>
|
||||
<tr id="key-row-<?= $key['api_key_id'] ?>">
|
||||
<td data-label="Name"><strong><?= htmlspecialchars($key['key_name']) ?></strong></td>
|
||||
<td data-label="Prefix" class="lt-text-xs"><code><?= htmlspecialchars($key['key_prefix']) ?>…</code></td>
|
||||
<td data-label="Created By" class="lt-text-xs"><?= htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown') ?></td>
|
||||
<td data-label="Created" class="lt-text-xs lt-text-muted"><?= date('Y-m-d H:i', strtotime($key['created_at'])) ?></td>
|
||||
<td data-label="Expires" class="lt-text-xs <?= $expired ? 'lt-text-danger' : 'lt-text-cyan' ?>">
|
||||
<?= $key['expires_at'] ? date('Y-m-d', strtotime($key['expires_at'])) . ($expired ? ' (Expired)' : '') : 'Never' ?>
|
||||
</td>
|
||||
<td data-label="Last Used" class="lt-text-xs lt-text-muted">
|
||||
<?= $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never' ?>
|
||||
</td>
|
||||
<td data-label="Status">
|
||||
<?php if ($key['is_active']): ?>
|
||||
<span class="lt-status lt-status-open">Active</span>
|
||||
<?php else: ?>
|
||||
<span class="lt-status lt-status-closed">Revoked</span>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<?php if ($key['is_active']): ?>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
|
||||
data-action="revoke-key" data-id="<?= $key['api_key_id'] ?>">REVOKE</button>
|
||||
<?php else: ?>
|
||||
<span class="lt-text-muted lt-text-xs">—</span>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
// Event delegation for data-action handlers
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
<!-- API usage -->
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">API Usage</div>
|
||||
<div class="lt-section-body">
|
||||
<p class="lt-text-sm lt-text-muted">Include the API key in your requests using the Authorization header:</p>
|
||||
<pre class="lt-text-xs lt-text-cyan" style="border:1px solid rgba(0,255,65,0.2);padding:0.5rem;overflow-x:auto"><code>Authorization: Bearer YOUR_API_KEY</code></pre>
|
||||
<p class="lt-text-xs lt-text-muted">API keys provide programmatic access to create and manage tickets. Keep keys secure and rotate them regularly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const action = target.dataset.action;
|
||||
switch (action) {
|
||||
case 'copy-api-key':
|
||||
copyApiKey();
|
||||
break;
|
||||
case 'revoke-key':
|
||||
revokeKey(target.dataset.id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('generateKeyForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const keyName = document.getElementById('keyName').value.trim();
|
||||
const expiresIn = document.getElementById('expiresIn').value;
|
||||
|
||||
if (!keyName) {
|
||||
lt.toast.error('Please enter a key name');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await lt.api.post('/api/generate_api_key.php', {
|
||||
key_name: keyName,
|
||||
expires_in_days: expiresIn || null
|
||||
});
|
||||
<script nonce="<?= $nonce ?>">
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
switch (target.getAttribute('data-action')) {
|
||||
case 'copy-api-key': copyApiKey(); break;
|
||||
case 'revoke-key': revokeKey(target.getAttribute('data-id')); break;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('generateKeyForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var keyName = document.getElementById('keyName').value.trim();
|
||||
var expiresIn = document.getElementById('expiresIn').value;
|
||||
if (!keyName) { lt.toast.error('Please enter a key name'); return; }
|
||||
lt.api.post('/api/generate_api_key.php', { key_name: keyName, expires_in_days: expiresIn || null })
|
||||
.then(function (data) {
|
||||
if (data.success) {
|
||||
// Show the new key
|
||||
document.getElementById('newKeyValue').value = data.api_key;
|
||||
document.getElementById('newKeyDisplay').classList.remove('is-hidden');
|
||||
document.getElementById('keyName').value = '';
|
||||
|
||||
lt.toast.success('API key generated successfully');
|
||||
|
||||
// Reload page after 5 seconds to show new key in table
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
lt.toast.success('API key generated!');
|
||||
setTimeout(function () { location.reload(); }, 5000);
|
||||
} else {
|
||||
lt.toast.error(data.error || 'Failed to generate API key');
|
||||
}
|
||||
} catch (error) {
|
||||
lt.toast.error('Error generating API key: ' + error.message);
|
||||
}
|
||||
}).catch(function (err) { lt.toast.error('Error: ' + err.message); });
|
||||
});
|
||||
|
||||
function copyApiKey() {
|
||||
var input = document.getElementById('newKeyValue');
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
lt.toast.success('Copied to clipboard!');
|
||||
}
|
||||
|
||||
function revokeKey(keyId) {
|
||||
showConfirmModal('Revoke API Key', 'Revoke this API key? This cannot be undone.', 'error', function () {
|
||||
lt.api.post('/api/revoke_api_key.php', { key_id: keyId })
|
||||
.then(function (data) {
|
||||
if (data.success) { lt.toast.success('API key revoked'); location.reload(); }
|
||||
else lt.toast.error(data.error || 'Failed to revoke');
|
||||
}).catch(function (err) { lt.toast.error('Error: ' + err.message); });
|
||||
});
|
||||
}
|
||||
|
||||
function copyApiKey() {
|
||||
const keyInput = document.getElementById('newKeyValue');
|
||||
keyInput.select();
|
||||
document.execCommand('copy');
|
||||
lt.toast.success('API key copied to clipboard');
|
||||
}
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
</script>
|
||||
|
||||
function revokeKey(keyId) {
|
||||
showConfirmModal('Revoke API Key', 'Are you sure you want to revoke this API key? This action cannot be undone.', 'error', function() {
|
||||
lt.api.post('/api/revoke_api_key.php', { key_id: keyId })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
lt.toast.success('API key revoked successfully');
|
||||
location.reload();
|
||||
} else {
|
||||
lt.toast.error(data.error || 'Failed to revoke API key');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Error revoking API key: ' + error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
+122
-159
@@ -1,166 +1,129 @@
|
||||
<?php
|
||||
// Admin view for browsing audit logs
|
||||
// Receives $auditLogs, $totalPages, $page, $filters from controller
|
||||
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$pageTitle = 'Audit Log';
|
||||
$activeNav = 'admin-audit-log';
|
||||
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
||||
$pageScripts = [];
|
||||
include __DIR__ . '/../../views/layout_header.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audit Log - 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; ?>">
|
||||
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: Audit Log</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 class="lt-page-header">
|
||||
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
||||
<span class="lt-text-muted lt-text-xs">/</span>
|
||||
<span class="lt-text-muted lt-text-xs">Admin: Audit Log</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Audit Log Browser</div>
|
||||
<div class="lt-section-body">
|
||||
|
||||
<!-- Filters -->
|
||||
<form method="GET" class="lt-flex lt-flex-wrap lt-flex-gap-sm lt-mb-md" role="search" aria-label="Filter audit logs">
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
<label class="lt-label" for="action_type">Action Type</label>
|
||||
<select name="action_type" id="action_type" class="lt-select lt-select-sm">
|
||||
<option value="">All Actions</option>
|
||||
<?php foreach (['create','update','delete','comment','assign','status_change','login','security'] as $a): ?>
|
||||
<option value="<?= $a ?>" <?= ($filters['action_type'] ?? '') === $a ? 'selected' : '' ?>><?= ucfirst(str_replace('_',' ',$a)) ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
<label class="lt-label" for="user_id">User</label>
|
||||
<select name="user_id" id="user_id" class="lt-select lt-select-sm">
|
||||
<option value="">All Users</option>
|
||||
<?php if (isset($users)): foreach ($users as $u): ?>
|
||||
<option value="<?= $u['user_id'] ?>" <?= ($filters['user_id'] ?? '') == $u['user_id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($u['display_name'] ?? $u['username']) ?>
|
||||
</option>
|
||||
<?php endforeach; endif ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
<label class="lt-label" for="date_from">Date From</label>
|
||||
<input type="date" name="date_from" id="date_from" class="lt-input lt-input-sm"
|
||||
value="<?= htmlspecialchars($filters['date_from'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
<label class="lt-label" for="date_to">Date To</label>
|
||||
<input type="date" name="date_to" id="date_to" class="lt-input lt-input-sm"
|
||||
value="<?= htmlspecialchars($filters['date_to'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="lt-form-group lt-flex lt-flex-align-center lt-flex-gap-sm" style="margin:0;align-self:flex-end">
|
||||
<button type="submit" class="lt-btn lt-btn-primary lt-btn-sm">FILTER</button>
|
||||
<a href="?" class="lt-btn lt-btn-ghost lt-btn-sm">RESET</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Log table -->
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="Audit log entries">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Timestamp</th>
|
||||
<th scope="col">User</th>
|
||||
<th scope="col">Action</th>
|
||||
<th scope="col">Entity</th>
|
||||
<th scope="col">Entity ID</th>
|
||||
<th scope="col">Details</th>
|
||||
<th scope="col">IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($auditLogs)): ?>
|
||||
<tr><td colspan="7" class="lt-empty">No audit log entries found.</td></tr>
|
||||
<?php else: foreach ($auditLogs as $log): ?>
|
||||
<tr>
|
||||
<td data-label="Timestamp" class="lt-text-xs"><?= date('Y-m-d H:i:s', strtotime($log['created_at'])) ?></td>
|
||||
<td data-label="User"><?= htmlspecialchars($log['display_name'] ?? $log['username'] ?? 'System') ?></td>
|
||||
<td data-label="Action"><span class="lt-text-amber"><?= htmlspecialchars($log['action_type']) ?></span></td>
|
||||
<td data-label="Entity" class="lt-text-xs"><?= htmlspecialchars($log['entity_type'] ?? '-') ?></td>
|
||||
<td data-label="Entity ID" class="lt-text-xs">
|
||||
<?php if ($log['entity_type'] === 'ticket' && $log['entity_id']): ?>
|
||||
<a href="/ticket/<?= htmlspecialchars($log['entity_id']) ?>"><?= htmlspecialchars($log['entity_id']) ?></a>
|
||||
<?php else: ?>
|
||||
<?= htmlspecialchars($log['entity_id'] ?? '-') ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Details" class="lt-text-xs lt-text-muted" style="max-width:200px;overflow:hidden;text-overflow:ellipsis">
|
||||
<?php
|
||||
if ($log['details']) {
|
||||
$det = is_string($log['details']) ? json_decode($log['details'], true) : $log['details'];
|
||||
echo '<code>' . htmlspecialchars(is_array($det) ? json_encode($det) : (string)$log['details']) . '</code>';
|
||||
} else {
|
||||
echo '-';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td data-label="IP" class="lt-text-xs lt-text-muted"><?= htmlspecialchars($log['ip_address'] ?? '-') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="ascii-frame-outer admin-container-wide">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header">Audit Log Browser</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<!-- Filters -->
|
||||
<form method="GET" class="admin-form-row">
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="action_type">Action Type</label>
|
||||
<select name="action_type" id="action_type" class="admin-input">
|
||||
<option value="">All Actions</option>
|
||||
<option value="create" <?php echo ($filters['action_type'] ?? '') === 'create' ? 'selected' : ''; ?>>Create</option>
|
||||
<option value="update" <?php echo ($filters['action_type'] ?? '') === 'update' ? 'selected' : ''; ?>>Update</option>
|
||||
<option value="delete" <?php echo ($filters['action_type'] ?? '') === 'delete' ? 'selected' : ''; ?>>Delete</option>
|
||||
<option value="comment" <?php echo ($filters['action_type'] ?? '') === 'comment' ? 'selected' : ''; ?>>Comment</option>
|
||||
<option value="assign" <?php echo ($filters['action_type'] ?? '') === 'assign' ? 'selected' : ''; ?>>Assign</option>
|
||||
<option value="status_change" <?php echo ($filters['action_type'] ?? '') === 'status_change' ? 'selected' : ''; ?>>Status Change</option>
|
||||
<option value="login" <?php echo ($filters['action_type'] ?? '') === 'login' ? 'selected' : ''; ?>>Login</option>
|
||||
<option value="security" <?php echo ($filters['action_type'] ?? '') === 'security' ? 'selected' : ''; ?>>Security</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="user_id">User</label>
|
||||
<select name="user_id" id="user_id" class="admin-input">
|
||||
<option value="">All Users</option>
|
||||
<?php if (isset($users)): foreach ($users as $user): ?>
|
||||
<option value="<?php echo $user['user_id']; ?>" <?php echo ($filters['user_id'] ?? '') == $user['user_id'] ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>
|
||||
</option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="date_from">Date From</label>
|
||||
<input type="date" name="date_from" id="date_from" value="<?php echo htmlspecialchars($filters['date_from'] ?? ''); ?>" class="admin-input">
|
||||
</div>
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="date_to">Date To</label>
|
||||
<input type="date" name="date_to" id="date_to" value="<?php echo htmlspecialchars($filters['date_to'] ?? ''); ?>" class="admin-input">
|
||||
</div>
|
||||
<div class="admin-form-actions">
|
||||
<button type="submit" class="btn">FILTER</button>
|
||||
<a href="?" class="btn btn-secondary">RESET</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Log Table -->
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Entity</th>
|
||||
<th>Entity ID</th>
|
||||
<th>Details</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($auditLogs)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="empty-state">No audit log entries found.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($auditLogs as $log): ?>
|
||||
<tr>
|
||||
<td class="nowrap"><?php echo date('Y-m-d H:i:s', strtotime($log['created_at'])); ?></td>
|
||||
<td><?php echo htmlspecialchars($log['display_name'] ?? $log['username'] ?? 'System'); ?></td>
|
||||
<td>
|
||||
<span class="text-amber"><?php echo htmlspecialchars($log['action_type']); ?></span>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($log['entity_type'] ?? '-'); ?></td>
|
||||
<td>
|
||||
<?php if ($log['entity_type'] === 'ticket' && $log['entity_id']): ?>
|
||||
<a href="/ticket/<?php echo htmlspecialchars($log['entity_id']); ?>" class="text-green">
|
||||
<?php echo htmlspecialchars($log['entity_id']); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?php echo htmlspecialchars($log['entity_id'] ?? '-'); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="td-truncate">
|
||||
<?php
|
||||
if ($log['details']) {
|
||||
$details = is_string($log['details']) ? json_decode($log['details'], true) : $log['details'];
|
||||
if (is_array($details)) {
|
||||
echo '<code>' . htmlspecialchars(json_encode($details)) . '</code>';
|
||||
} else {
|
||||
echo htmlspecialchars($log['details']);
|
||||
}
|
||||
} else {
|
||||
echo '-';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td class="nowrap text-sm"><?php echo htmlspecialchars($log['ip_address'] ?? '-'); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php
|
||||
$params = $_GET;
|
||||
for ($i = 1; $i <= min($totalPages, 10); $i++) {
|
||||
$params['page'] = $i;
|
||||
$activeClass = ($i == $page) ? 'active' : '';
|
||||
$url = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
||||
echo "<a href='$url' class='btn btn-small $activeClass'>$i</a> ";
|
||||
}
|
||||
if ($totalPages > 10) {
|
||||
echo "...";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<?php if (($totalPages ?? 1) > 1): ?>
|
||||
<div class="lt-pagination" role="navigation">
|
||||
<?php
|
||||
$params = $_GET;
|
||||
for ($i = 1; $i <= min($totalPages, 10); $i++) {
|
||||
$params['page'] = $i;
|
||||
$url = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
||||
$class = ($i == $page) ? ' lt-btn-primary' : '';
|
||||
echo "<a href='$url' class='lt-btn lt-btn-sm$class'>$i</a> ";
|
||||
}
|
||||
if ($totalPages > 10) echo '<span class="lt-text-muted lt-text-xs">…</span>';
|
||||
?>
|
||||
</div>
|
||||
<script nonce="<?php echo $nonce; ?>">if (window.lt) lt.keys.initDefaults();</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
+226
-260
@@ -1,278 +1,244 @@
|
||||
<?php
|
||||
// Admin view for managing custom fields
|
||||
// Receives $customFields from controller
|
||||
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$pageTitle = 'Custom Fields';
|
||||
$activeNav = 'admin-custom-fields';
|
||||
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
||||
$pageScripts = [];
|
||||
include __DIR__ . '/../../views/layout_header.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Custom Fields - 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: Custom Fields</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 class="lt-page-header">
|
||||
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
||||
<span class="lt-text-muted lt-text-xs">/</span>
|
||||
<span class="lt-text-muted lt-text-xs">Admin: Custom Fields</span>
|
||||
</div>
|
||||
<button type="button" class="lt-btn lt-btn-primary" data-action="show-create-modal">+ NEW FIELD</button>
|
||||
</div>
|
||||
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Custom Field Definitions</div>
|
||||
<div class="lt-section-body">
|
||||
<p class="lt-text-sm lt-text-muted" style="margin-bottom:0.75rem">
|
||||
Custom fields extend tickets with additional metadata. Fields appear on the ticket form based on category.
|
||||
</p>
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="Custom fields">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Order</th>
|
||||
<th scope="col">Field Name</th>
|
||||
<th scope="col">Label</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Category</th>
|
||||
<th scope="col">Required</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($customFields)): ?>
|
||||
<tr><td colspan="8" class="lt-empty">No custom fields defined. Create fields to extend ticket metadata.</td></tr>
|
||||
<?php else: foreach ($customFields as $field): ?>
|
||||
<tr>
|
||||
<td data-label="Order" class="lt-text-xs lt-text-muted"><?= (int)$field['display_order'] ?></td>
|
||||
<td data-label="Field Name"><code class="lt-text-cyan lt-text-xs"><?= htmlspecialchars($field['field_name']) ?></code></td>
|
||||
<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">
|
||||
<?= $field['is_required'] ? '<span class="lt-text-amber">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
|
||||
</td>
|
||||
<td data-label="Status">
|
||||
<span class="lt-status <?= $field['is_active'] ? 'lt-status-open' : 'lt-status-closed' ?>">
|
||||
<?= $field['is_active'] ? 'Active' : 'Inactive' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="lt-btn-group">
|
||||
<button type="button" class="lt-btn lt-btn-sm"
|
||||
data-action="edit-field" data-id="<?= $field['field_id'] ?>">EDIT</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
|
||||
data-action="delete-field" data-id="<?= $field['field_id'] ?>">DEL</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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">Custom Fields Management</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="admin-header-row">
|
||||
<h2>Custom Field Definitions</h2>
|
||||
<button data-action="show-create-modal" class="btn">+ NEW FIELD</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order</th>
|
||||
<th>Field Name</th>
|
||||
<th>Label</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th>Required</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($customFields)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">No custom fields defined.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($customFields as $field): ?>
|
||||
<tr>
|
||||
<td><?php echo $field['display_order']; ?></td>
|
||||
<td><code><?php echo htmlspecialchars($field['field_name']); ?></code></td>
|
||||
<td><?php echo htmlspecialchars($field['field_label']); ?></td>
|
||||
<td><?php echo ucfirst($field['field_type']); ?></td>
|
||||
<td><?php echo htmlspecialchars($field['category'] ?? 'All'); ?></td>
|
||||
<td><?php echo $field['is_required'] ? 'Yes' : 'No'; ?></td>
|
||||
<td>
|
||||
<span class="<?php echo $field['is_active'] ? 'text-open' : 'text-closed'; ?>">
|
||||
<?php echo $field['is_active'] ? 'Active' : 'Inactive'; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button data-action="edit-field" data-id="<?php echo $field['field_id']; ?>" class="btn btn-small">EDIT</button>
|
||||
<button data-action="delete-field" data-id="<?php echo $field['field_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="fieldModal" aria-hidden="true" role="dialog"
|
||||
aria-modal="true" aria-labelledby="cfModalTitle">
|
||||
<div class="lt-modal">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="cfModalTitle">Create Custom Field</span>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
<div class="lt-modal-overlay" id="fieldModal" 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 Custom Field</span>
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="fieldForm">
|
||||
<input type="hidden" id="field_id" name="field_id">
|
||||
<div class="lt-modal-body">
|
||||
<div class="setting-row">
|
||||
<label for="field_name">Field Name * (internal)</label>
|
||||
<input type="text" id="field_name" name="field_name" required pattern="[a-z_]+" placeholder="e.g., server_name">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="field_label">Field Label * (display)</label>
|
||||
<input type="text" id="field_label" name="field_label" required placeholder="e.g., Server Name">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="field_type">Field Type *</label>
|
||||
<select id="field_type" name="field_type" required data-action="toggle-options-field">
|
||||
<option value="text">Text</option>
|
||||
<option value="textarea">Text Area</option>
|
||||
<option value="select">Dropdown (Select)</option>
|
||||
<option value="checkbox">Checkbox</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="number">Number</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row is-hidden" id="options_row">
|
||||
<label for="field_options">Options (one per line)</label>
|
||||
<textarea id="field_options" name="field_options" rows="4" placeholder="Option 1 Option 2 Option 3"></textarea>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="category">Category (empty = all)</label>
|
||||
<select id="category" name="category">
|
||||
<option value="">All Categories</option>
|
||||
<option value="General">General</option>
|
||||
<option value="Hardware">Hardware</option>
|
||||
<option value="Software">Software</option>
|
||||
<option value="Network">Network</option>
|
||||
<option value="Security">Security</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="display_order">Display Order</label>
|
||||
<input type="number" id="display_order" name="display_order" value="0" min="0">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label><input type="checkbox" id="is_required" name="is_required"> Required field</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>
|
||||
<form id="fieldForm">
|
||||
<input type="hidden" id="field_id" name="field_id">
|
||||
<div class="lt-modal-body">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="field_name">Field Name * <span class="lt-text-muted lt-text-xs">(internal, lowercase_underscore)</span></label>
|
||||
<input type="text" id="field_name" name="field_name" class="lt-input" required
|
||||
pattern="[a-z_]+" placeholder="e.g., server_name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="field_label">Field Label * <span class="lt-text-muted lt-text-xs">(display name)</span></label>
|
||||
<input type="text" id="field_label" name="field_label" class="lt-input" required
|
||||
placeholder="e.g., Server Name">
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="field_type">Field Type *</label>
|
||||
<select id="field_type" name="field_type" class="lt-select" required
|
||||
data-action="toggle-options-field">
|
||||
<option value="text">Text</option>
|
||||
<option value="textarea">Text Area</option>
|
||||
<option value="select">Dropdown (Select)</option>
|
||||
<option value="checkbox">Checkbox</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="number">Number</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group is-hidden" id="options_row">
|
||||
<label class="lt-label" for="field_options">Options <span class="lt-text-muted lt-text-xs">(one per line)</span></label>
|
||||
<textarea id="field_options" name="field_options" class="lt-input lt-textarea"
|
||||
rows="4" placeholder="Option 1 Option 2 Option 3"></textarea>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="cf-category">Category <span class="lt-text-muted lt-text-xs">(empty = all categories)</span></label>
|
||||
<select id="cf-category" name="category" class="lt-select">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
|
||||
<option value="<?= $c ?>"><?= $c ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="display_order">Display Order</label>
|
||||
<input type="number" id="display_order" name="display_order" class="lt-input"
|
||||
value="0" min="0" style="max-width:8rem">
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox" id="is_required" name="is_required">
|
||||
Required field
|
||||
</label>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox" id="cf_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; ?>">
|
||||
function showCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create Custom Field';
|
||||
document.getElementById('fieldForm').reset();
|
||||
document.getElementById('field_id').value = '';
|
||||
document.getElementById('is_active').checked = true;
|
||||
toggleOptionsField();
|
||||
lt.modal.open('fieldModal');
|
||||
}
|
||||
<script nonce="<?= $nonce ?>">
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
switch (target.getAttribute('data-action')) {
|
||||
case 'show-create-modal': showCreateModal(); break;
|
||||
case 'edit-field': editField(target.getAttribute('data-id')); break;
|
||||
case 'delete-field': deleteField(target.getAttribute('data-id')); break;
|
||||
}
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
lt.modal.close('fieldModal');
|
||||
}
|
||||
document.addEventListener('change', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
if (target.getAttribute('data-action') === 'toggle-options-field') toggleOptionsField();
|
||||
});
|
||||
|
||||
// Event delegation for data-action handlers
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
document.getElementById('fieldForm').addEventListener('submit', function (e) {
|
||||
saveField(e);
|
||||
});
|
||||
|
||||
const action = target.dataset.action;
|
||||
switch (action) {
|
||||
case 'show-create-modal':
|
||||
showCreateModal();
|
||||
break;
|
||||
case 'edit-field':
|
||||
editField(target.dataset.id);
|
||||
break;
|
||||
case 'delete-field':
|
||||
deleteField(target.dataset.id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
document.addEventListener('change', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
function toggleOptionsField() {
|
||||
var type = document.getElementById('field_type').value;
|
||||
document.getElementById('options_row').classList.toggle('is-hidden', type !== 'select');
|
||||
}
|
||||
|
||||
if (target.dataset.action === 'toggle-options-field') {
|
||||
function showCreateModal() {
|
||||
document.getElementById('cfModalTitle').textContent = 'Create Custom Field';
|
||||
document.getElementById('fieldForm').reset();
|
||||
document.getElementById('field_id').value = '';
|
||||
document.getElementById('cf_is_active').checked = true;
|
||||
toggleOptionsField();
|
||||
lt.modal.open('fieldModal');
|
||||
}
|
||||
|
||||
function editField(id) {
|
||||
lt.api.get('/api/custom_fields.php?id=' + id)
|
||||
.then(function (data) {
|
||||
if (data.success && data.field) {
|
||||
var f = data.field;
|
||||
document.getElementById('field_id').value = f.field_id;
|
||||
document.getElementById('field_name').value = f.field_name;
|
||||
document.getElementById('field_label').value = f.field_label;
|
||||
document.getElementById('field_type').value = f.field_type;
|
||||
document.getElementById('cf-category').value = f.category || '';
|
||||
document.getElementById('display_order').value = f.display_order;
|
||||
document.getElementById('is_required').checked = f.is_required == 1;
|
||||
document.getElementById('cf_is_active').checked = f.is_active == 1;
|
||||
toggleOptionsField();
|
||||
}
|
||||
});
|
||||
|
||||
// Form submit handler
|
||||
document.getElementById('fieldForm').addEventListener('submit', function(e) {
|
||||
saveField(e);
|
||||
});
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
function toggleOptionsField() {
|
||||
const type = document.getElementById('field_type').value;
|
||||
document.getElementById('options_row').classList.toggle('is-hidden', type !== 'select');
|
||||
}
|
||||
|
||||
function saveField(e) {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('fieldForm');
|
||||
const data = {
|
||||
field_id: document.getElementById('field_id').value,
|
||||
field_name: document.getElementById('field_name').value,
|
||||
field_label: document.getElementById('field_label').value,
|
||||
field_type: document.getElementById('field_type').value,
|
||||
category: document.getElementById('category').value || null,
|
||||
display_order: parseInt(document.getElementById('display_order').value) || 0,
|
||||
is_required: document.getElementById('is_required').checked ? 1 : 0,
|
||||
is_active: document.getElementById('is_active').checked ? 1 : 0
|
||||
};
|
||||
|
||||
if (data.field_type === 'select') {
|
||||
const options = document.getElementById('field_options').value.split('\n').filter(o => o.trim());
|
||||
data.field_options = { options: options };
|
||||
}
|
||||
|
||||
const url = '/api/custom_fields.php' + (data.field_id ? '?id=' + data.field_id : '');
|
||||
const apiCall = data.field_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');
|
||||
if (f.field_options && f.field_options.options) {
|
||||
document.getElementById('field_options').value = f.field_options.options.join('\n');
|
||||
}
|
||||
}).catch(err => lt.toast.error('Failed to save'));
|
||||
}
|
||||
document.getElementById('cfModalTitle').textContent = 'Edit Custom Field';
|
||||
lt.modal.open('fieldModal');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editField(id) {
|
||||
lt.api.get('/api/custom_fields.php?id=' + id)
|
||||
.then(data => {
|
||||
if (data.success && data.field) {
|
||||
const f = data.field;
|
||||
document.getElementById('field_id').value = f.field_id;
|
||||
document.getElementById('field_name').value = f.field_name;
|
||||
document.getElementById('field_label').value = f.field_label;
|
||||
document.getElementById('field_type').value = f.field_type;
|
||||
document.getElementById('category').value = f.category || '';
|
||||
document.getElementById('display_order').value = f.display_order;
|
||||
document.getElementById('is_required').checked = f.is_required == 1;
|
||||
document.getElementById('is_active').checked = f.is_active == 1;
|
||||
toggleOptionsField();
|
||||
if (f.field_options && f.field_options.options) {
|
||||
document.getElementById('field_options').value = f.field_options.options.join('\n');
|
||||
}
|
||||
document.getElementById('modalTitle').textContent = 'Edit Custom Field';
|
||||
lt.modal.open('fieldModal');
|
||||
}
|
||||
});
|
||||
}
|
||||
function deleteField(id) {
|
||||
showConfirmModal('Delete Custom Field', 'Delete this custom field? All values will be lost.', 'error', function () {
|
||||
lt.api.delete('/api/custom_fields.php?id=' + id)
|
||||
.then(function (data) {
|
||||
if (data.success) window.location.reload();
|
||||
else lt.toast.error(data.error || 'Failed to delete');
|
||||
}).catch(function () { lt.toast.error('Failed to delete'); });
|
||||
});
|
||||
}
|
||||
|
||||
function deleteField(id) {
|
||||
showConfirmModal('Delete Custom Field', 'Delete this custom field? All values will be lost.', 'error', function() {
|
||||
lt.api.delete('/api/custom_fields.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>
|
||||
function saveField(e) {
|
||||
e.preventDefault();
|
||||
var data = {
|
||||
field_id: document.getElementById('field_id').value,
|
||||
field_name: document.getElementById('field_name').value,
|
||||
field_label: document.getElementById('field_label').value,
|
||||
field_type: document.getElementById('field_type').value,
|
||||
category: document.getElementById('cf-category').value || null,
|
||||
display_order: parseInt(document.getElementById('display_order').value) || 0,
|
||||
is_required: document.getElementById('is_required').checked ? 1 : 0,
|
||||
is_active: document.getElementById('cf_is_active').checked ? 1 : 0,
|
||||
};
|
||||
if (data.field_type === 'select') {
|
||||
var opts = document.getElementById('field_options').value.split('\n').filter(function (o) { return o.trim(); });
|
||||
data.field_options = { options: opts };
|
||||
}
|
||||
var url = '/api/custom_fields.php' + (data.field_id ? '?id=' + data.field_id : '');
|
||||
var apiCall = data.field_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
||||
apiCall.then(function (result) {
|
||||
if (result.success) window.location.reload();
|
||||
else lt.toast.error(result.error || 'Failed to save');
|
||||
}).catch(function () { lt.toast.error('Failed to save'); });
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
@@ -1,346 +1,297 @@
|
||||
<?php
|
||||
// Admin view for managing recurring tickets
|
||||
// Receives $recurringTickets from controller
|
||||
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$pageTitle = 'Recurring Tickets';
|
||||
$activeNav = 'admin-recurring';
|
||||
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
||||
$pageScripts = [];
|
||||
include __DIR__ . '/../../views/layout_header.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Recurring Tickets - 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: Recurring Tickets</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="lt-page-header">
|
||||
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
||||
<span class="lt-text-muted lt-text-xs">/</span>
|
||||
<span class="lt-text-muted lt-text-xs">Admin: Recurring Tickets</span>
|
||||
</div>
|
||||
<button type="button" class="lt-btn lt-btn-primary" data-action="show-create-modal">+ NEW RECURRING TICKET</button>
|
||||
</div>
|
||||
|
||||
<div class="ascii-section-header">Recurring Tickets Management</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="admin-header-row">
|
||||
<h2>Scheduled Tickets</h2>
|
||||
<button data-action="show-create-modal" class="btn">+ NEW RECURRING TICKET</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Title Template</th>
|
||||
<th>Schedule</th>
|
||||
<th>Category</th>
|
||||
<th>Assigned To</th>
|
||||
<th>Next Run</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recurringTickets)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">No recurring tickets configured.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recurringTickets as $rt): ?>
|
||||
<tr>
|
||||
<td><?php echo $rt['recurring_id']; ?></td>
|
||||
<td><?php echo htmlspecialchars($rt['title_template']); ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$schedule = ucfirst($rt['schedule_type']);
|
||||
if ($rt['schedule_type'] === 'weekly') {
|
||||
$days = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
$schedule .= ' (' . ($days[$rt['schedule_day']] ?? '?') . ')';
|
||||
} elseif ($rt['schedule_type'] === 'monthly') {
|
||||
$schedule .= ' (Day ' . $rt['schedule_day'] . ')';
|
||||
}
|
||||
$schedule .= ' @ ' . substr($rt['schedule_time'], 0, 5);
|
||||
echo htmlspecialchars($schedule);
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($rt['category']); ?></td>
|
||||
<td><?php echo htmlspecialchars($rt['assigned_name'] ?? $rt['assigned_username'] ?? 'Unassigned'); ?></td>
|
||||
<td class="nowrap"><?php echo date('M d, Y H:i', strtotime($rt['next_run_at'])); ?></td>
|
||||
<td>
|
||||
<span class="<?php echo $rt['is_active'] ? 'text-open' : 'text-closed'; ?>">
|
||||
<?php echo $rt['is_active'] ? 'Active' : 'Inactive'; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button data-action="edit-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">EDIT</button>
|
||||
<button data-action="toggle-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">
|
||||
<?php echo $rt['is_active'] ? 'DISABLE' : 'ENABLE'; ?>
|
||||
</button>
|
||||
<button data-action="delete-recurring" data-id="<?php echo $rt['recurring_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="recurringModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||
<div class="lt-modal lt-modal-lg">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="modalTitle">Create Recurring Ticket</span>
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="recurringForm">
|
||||
<input type="hidden" id="recurring_id" name="recurring_id">
|
||||
<div class="lt-modal-body">
|
||||
<div class="setting-row">
|
||||
<label for="title_template">Title Template *</label>
|
||||
<input type="text" id="title_template" name="title_template" required placeholder="Use {{date}}, {{month}}, etc.">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="description_template">Description Template</label>
|
||||
<textarea id="description_template" name="description_template" rows="8"></textarea>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="schedule_type">Schedule Type *</label>
|
||||
<select id="schedule_type" name="schedule_type" required data-action="update-schedule-options">
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row is-hidden" id="schedule_day_row">
|
||||
<label for="schedule_day">Schedule Day</label>
|
||||
<select id="schedule_day" name="schedule_day"></select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="schedule_time">Schedule Time *</label>
|
||||
<input type="time" id="schedule_time" name="schedule_time" value="09:00" required>
|
||||
</div>
|
||||
<div class="setting-grid-2">
|
||||
<div class="setting-row setting-row-compact">
|
||||
<label for="category">Category</label>
|
||||
<select id="category" name="category">
|
||||
<option value="General">General</option>
|
||||
<option value="Hardware">Hardware</option>
|
||||
<option value="Software">Software</option>
|
||||
<option value="Network">Network</option>
|
||||
<option value="Security">Security</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row setting-row-compact">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" name="type">
|
||||
<option value="Issue">Issue</option>
|
||||
<option value="Maintenance">Maintenance</option>
|
||||
<option value="Install">Install</option>
|
||||
<option value="Task">Task</option>
|
||||
<option value="Upgrade">Upgrade</option>
|
||||
<option value="Problem">Problem</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row setting-row-compact">
|
||||
<label for="priority">Priority</label>
|
||||
<select id="priority" name="priority">
|
||||
<option value="1">P1 - Critical</option>
|
||||
<option value="2">P2 - High</option>
|
||||
<option value="3">P3 - Medium</option>
|
||||
<option value="4" selected>P4 - Low</option>
|
||||
<option value="5">P5 - Lowest</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row setting-row-compact">
|
||||
<label for="assigned_to">Assign To</label>
|
||||
<select id="assigned_to" name="assigned_to">
|
||||
<option value="">Unassigned</option>
|
||||
<!-- Populated by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
</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; ?>">
|
||||
function showCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create Recurring Ticket';
|
||||
document.getElementById('recurringForm').reset();
|
||||
document.getElementById('recurring_id').value = '';
|
||||
updateScheduleOptions();
|
||||
lt.modal.open('recurringModal');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
lt.modal.close('recurringModal');
|
||||
}
|
||||
|
||||
// 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-recurring':
|
||||
editRecurring(target.dataset.id);
|
||||
break;
|
||||
case 'toggle-recurring':
|
||||
toggleRecurring(target.dataset.id);
|
||||
break;
|
||||
case 'delete-recurring':
|
||||
deleteRecurring(target.dataset.id);
|
||||
break;
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Scheduled Tickets</div>
|
||||
<div class="lt-section-body">
|
||||
<p class="lt-text-sm lt-text-muted" style="margin-bottom:0.75rem">
|
||||
Recurring tickets are automatically created on a schedule. Use <code>{{date}}</code>, <code>{{month}}</code>, <code>{{year}}</code> in title templates.
|
||||
</p>
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="Recurring tickets">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Title Template</th>
|
||||
<th scope="col">Schedule</th>
|
||||
<th scope="col">Category</th>
|
||||
<th scope="col">Assigned To</th>
|
||||
<th scope="col">Next Run</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recurringTickets)): ?>
|
||||
<tr><td colspan="7" class="lt-empty">No recurring tickets configured. Create schedules to auto-generate tickets.</td></tr>
|
||||
<?php else: foreach ($recurringTickets as $rt): ?>
|
||||
<?php
|
||||
$schedule = ucfirst($rt['schedule_type']);
|
||||
if ($rt['schedule_type'] === 'weekly') {
|
||||
$days = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
$schedule .= ' (' . ($days[$rt['schedule_day']] ?? '?') . ')';
|
||||
} elseif ($rt['schedule_type'] === 'monthly') {
|
||||
$schedule .= ' (Day ' . $rt['schedule_day'] . ')';
|
||||
}
|
||||
$schedule .= ' @ ' . substr($rt['schedule_time'], 0, 5);
|
||||
?>
|
||||
<tr>
|
||||
<td data-label="Title"><strong><?= htmlspecialchars($rt['title_template']) ?></strong></td>
|
||||
<td data-label="Schedule" class="lt-text-xs lt-text-cyan"><?= htmlspecialchars($schedule) ?></td>
|
||||
<td data-label="Category" class="lt-text-xs"><?= htmlspecialchars($rt['category']) ?></td>
|
||||
<td data-label="Assigned To" class="lt-text-xs">
|
||||
<?= htmlspecialchars($rt['assigned_name'] ?? $rt['assigned_username'] ?? 'Unassigned') ?>
|
||||
</td>
|
||||
<td data-label="Next Run" class="lt-text-xs lt-text-muted">
|
||||
<?= date('M d, Y H:i', strtotime($rt['next_run_at'])) ?>
|
||||
</td>
|
||||
<td data-label="Status">
|
||||
<span class="lt-status <?= $rt['is_active'] ? 'lt-status-open' : 'lt-status-closed' ?>">
|
||||
<?= $rt['is_active'] ? 'Active' : 'Inactive' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="lt-btn-group">
|
||||
<button type="button" class="lt-btn lt-btn-sm"
|
||||
data-action="edit-recurring" data-id="<?= $rt['recurring_id'] ?>">EDIT</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm"
|
||||
data-action="toggle-recurring" data-id="<?= $rt['recurring_id'] ?>">
|
||||
<?= $rt['is_active'] ? 'PAUSE' : 'RESUME' ?>
|
||||
</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
|
||||
data-action="delete-recurring" data-id="<?= $rt['recurring_id'] ?>">DEL</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
<div class="lt-modal-overlay" id="recurringModal" aria-hidden="true" role="dialog"
|
||||
aria-modal="true" aria-labelledby="recModalTitle">
|
||||
<div class="lt-modal">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="recModalTitle">Create Recurring Ticket</span>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="recurringForm">
|
||||
<input type="hidden" id="recurring_id" name="recurring_id">
|
||||
<div class="lt-modal-body">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="rec_title_template">Title Template *</label>
|
||||
<input type="text" id="rec_title_template" name="title_template" class="lt-input" required
|
||||
placeholder="Use {{date}}, {{month}}, {{year}}">
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="rec_description_template">Description Template</label>
|
||||
<textarea id="rec_description_template" name="description_template"
|
||||
class="lt-input lt-textarea" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="schedule_type">Schedule Type *</label>
|
||||
<select id="schedule_type" name="schedule_type" class="lt-select" required
|
||||
data-action="update-schedule-options">
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group is-hidden" id="schedule_day_row">
|
||||
<label class="lt-label" for="schedule_day">Schedule Day</label>
|
||||
<select id="schedule_day" name="schedule_day" class="lt-select"></select>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="schedule_time">Schedule Time *</label>
|
||||
<input type="time" id="schedule_time" name="schedule_time" class="lt-input"
|
||||
value="09:00" required style="max-width:12rem">
|
||||
</div>
|
||||
<div class="create-ticket-meta-grid">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="rec-category">Category</label>
|
||||
<select id="rec-category" name="category" class="lt-select">
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
|
||||
<option value="<?= $c ?>"><?= $c ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="rec-type">Type</label>
|
||||
<select id="rec-type" name="type" class="lt-select">
|
||||
<?php foreach (['Issue','Maintenance','Install','Task','Upgrade','Problem'] as $t): ?>
|
||||
<option value="<?= $t ?>"><?= $t ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="rec-priority">Priority</label>
|
||||
<select id="rec-priority" name="priority" class="lt-select">
|
||||
<option value="1">P1 — Critical</option>
|
||||
<option value="2">P2 — High</option>
|
||||
<option value="3">P3 — Medium</option>
|
||||
<option value="4" selected>P4 — Low</option>
|
||||
<option value="5">P5 — Lowest</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="assigned_to">Assign To</label>
|
||||
<select id="assigned_to" name="assigned_to" class="lt-select">
|
||||
<option value="">Unassigned</option>
|
||||
<!-- Populated by JS -->
|
||||
</select>
|
||||
</div>
|
||||
</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="<?= $nonce ?>">
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
switch (target.getAttribute('data-action')) {
|
||||
case 'show-create-modal': showCreateModal(); break;
|
||||
case 'edit-recurring': editRecurring(target.getAttribute('data-id')); break;
|
||||
case 'toggle-recurring': toggleRecurring(target.getAttribute('data-id')); break;
|
||||
case 'delete-recurring': deleteRecurring(target.getAttribute('data-id')); break;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('change', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
if (target.getAttribute('data-action') === 'update-schedule-options') updateScheduleOptions();
|
||||
});
|
||||
|
||||
document.getElementById('recurringForm').addEventListener('submit', function (e) {
|
||||
saveRecurring(e);
|
||||
});
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
function updateScheduleOptions() {
|
||||
var type = document.getElementById('schedule_type').value;
|
||||
var dayRow = document.getElementById('schedule_day_row');
|
||||
var daySelect = document.getElementById('schedule_day');
|
||||
daySelect.innerHTML = '';
|
||||
if (type === 'daily') {
|
||||
dayRow.classList.add('is-hidden');
|
||||
} else if (type === 'weekly') {
|
||||
dayRow.classList.remove('is-hidden');
|
||||
['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'].forEach(function (day, i) {
|
||||
daySelect.innerHTML += '<option value="' + (i + 1) + '">' + day + '</option>';
|
||||
});
|
||||
} else if (type === 'monthly') {
|
||||
dayRow.classList.remove('is-hidden');
|
||||
for (var i = 1; i <= 28; i++) {
|
||||
daySelect.innerHTML += '<option value="' + i + '">Day ' + i + '</option>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('change', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
function showCreateModal() {
|
||||
document.getElementById('recModalTitle').textContent = 'Create Recurring Ticket';
|
||||
document.getElementById('recurringForm').reset();
|
||||
document.getElementById('recurring_id').value = '';
|
||||
updateScheduleOptions();
|
||||
lt.modal.open('recurringModal');
|
||||
}
|
||||
|
||||
if (target.dataset.action === 'update-schedule-options') {
|
||||
function editRecurring(id) {
|
||||
lt.api.get('/api/manage_recurring.php?id=' + id)
|
||||
.then(function (data) {
|
||||
if (data.success && data.recurring) {
|
||||
var rt = data.recurring;
|
||||
document.getElementById('recurring_id').value = rt.recurring_id;
|
||||
document.getElementById('rec_title_template').value = rt.title_template;
|
||||
document.getElementById('rec_description_template').value = rt.description_template || '';
|
||||
document.getElementById('schedule_type').value = rt.schedule_type;
|
||||
updateScheduleOptions();
|
||||
document.getElementById('schedule_day').value = rt.schedule_day || '';
|
||||
document.getElementById('schedule_time').value = rt.schedule_time ? rt.schedule_time.substring(0, 5) : '09:00';
|
||||
document.getElementById('rec-category').value = rt.category || 'General';
|
||||
document.getElementById('rec-type').value = rt.type || 'Issue';
|
||||
document.getElementById('rec-priority').value = rt.priority || 4;
|
||||
document.getElementById('assigned_to').value = rt.assigned_to || '';
|
||||
document.getElementById('recModalTitle').textContent = 'Edit Recurring Ticket';
|
||||
lt.modal.open('recurringModal');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Form submit handler
|
||||
document.getElementById('recurringForm').addEventListener('submit', function(e) {
|
||||
saveRecurring(e);
|
||||
});
|
||||
function toggleRecurring(id) {
|
||||
lt.api.post('/api/manage_recurring.php?action=toggle&id=' + id)
|
||||
.then(function (data) {
|
||||
if (data.success) window.location.reload();
|
||||
else lt.toast.error(data.error || 'Failed to toggle');
|
||||
}).catch(function () { lt.toast.error('Failed to toggle'); });
|
||||
}
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
function updateScheduleOptions() {
|
||||
const type = document.getElementById('schedule_type').value;
|
||||
const dayRow = document.getElementById('schedule_day_row');
|
||||
const daySelect = document.getElementById('schedule_day');
|
||||
|
||||
daySelect.innerHTML = '';
|
||||
|
||||
if (type === 'daily') {
|
||||
dayRow.classList.add('is-hidden');
|
||||
} else if (type === 'weekly') {
|
||||
dayRow.classList.remove('is-hidden');
|
||||
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
days.forEach((day, i) => {
|
||||
daySelect.innerHTML += `<option value="${i + 1}">${day}</option>`;
|
||||
});
|
||||
} else if (type === 'monthly') {
|
||||
dayRow.classList.remove('is-hidden');
|
||||
for (let i = 1; i <= 28; i++) {
|
||||
daySelect.innerHTML += `<option value="${i}">Day ${i}</option>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveRecurring(e) {
|
||||
e.preventDefault();
|
||||
const form = new FormData(document.getElementById('recurringForm'));
|
||||
const data = Object.fromEntries(form);
|
||||
|
||||
const url = '/api/manage_recurring.php' + (data.recurring_id ? '?id=' + data.recurring_id : '');
|
||||
const apiCall = data.recurring_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 toggleRecurring(id) {
|
||||
lt.api.post('/api/manage_recurring.php?action=toggle&id=' + id)
|
||||
.then(data => {
|
||||
function deleteRecurring(id) {
|
||||
showConfirmModal('Delete Schedule', 'Delete this recurring ticket schedule? This cannot be undone.', 'error', function () {
|
||||
lt.api.delete('/api/manage_recurring.php?id=' + id)
|
||||
.then(function (data) {
|
||||
if (data.success) window.location.reload();
|
||||
else lt.toast.error(data.error || 'Failed to toggle');
|
||||
}).catch(err => lt.toast.error('Failed to toggle'));
|
||||
}
|
||||
else lt.toast.error(data.error || 'Failed to delete');
|
||||
}).catch(function () { lt.toast.error('Failed to delete'); });
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRecurring(id) {
|
||||
showConfirmModal('Delete Schedule', 'Delete this recurring ticket schedule?', 'error', function() {
|
||||
lt.api.delete('/api/manage_recurring.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'));
|
||||
});
|
||||
}
|
||||
function saveRecurring(e) {
|
||||
e.preventDefault();
|
||||
var form = new FormData(document.getElementById('recurringForm'));
|
||||
var data = {};
|
||||
form.forEach(function (v, k) { data[k] = v; });
|
||||
var url = '/api/manage_recurring.php' + (data.recurring_id ? '?id=' + data.recurring_id : '');
|
||||
var apiCall = data.recurring_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
||||
apiCall.then(function (result) {
|
||||
if (result.success) window.location.reload();
|
||||
else lt.toast.error(result.error || 'Failed to save');
|
||||
}).catch(function () { lt.toast.error('Failed to save'); });
|
||||
}
|
||||
|
||||
function editRecurring(id) {
|
||||
lt.api.get('/api/manage_recurring.php?id=' + id)
|
||||
.then(data => {
|
||||
if (data.success && data.recurring) {
|
||||
const rt = data.recurring;
|
||||
document.getElementById('recurring_id').value = rt.recurring_id;
|
||||
document.getElementById('title_template').value = rt.title_template;
|
||||
document.getElementById('description_template').value = rt.description_template || '';
|
||||
document.getElementById('schedule_type').value = rt.schedule_type;
|
||||
updateScheduleOptions();
|
||||
document.getElementById('schedule_day').value = rt.schedule_day || '';
|
||||
document.getElementById('schedule_time').value = rt.schedule_time ? rt.schedule_time.substring(0, 5) : '09:00';
|
||||
document.getElementById('category').value = rt.category || 'General';
|
||||
document.getElementById('type').value = rt.type || 'Issue';
|
||||
document.getElementById('priority').value = rt.priority || 4;
|
||||
document.getElementById('assigned_to').value = rt.assigned_to || '';
|
||||
document.getElementById('modalTitle').textContent = 'Edit Recurring Ticket';
|
||||
lt.modal.open('recurringModal');
|
||||
}
|
||||
function loadUsers() {
|
||||
lt.api.get('/api/get_users.php')
|
||||
.then(function (data) {
|
||||
if (data.success && data.users) {
|
||||
var select = document.getElementById('assigned_to');
|
||||
data.users.forEach(function (user) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = user.user_id;
|
||||
opt.textContent = user.display_name || user.username;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load users for assignee dropdown
|
||||
function loadUsers() {
|
||||
lt.api.get('/api/get_users.php')
|
||||
.then(data => {
|
||||
if (data.success && data.users) {
|
||||
const select = document.getElementById('assigned_to');
|
||||
data.users.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.user_id;
|
||||
option.textContent = user.display_name || user.username;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
updateScheduleOptions();
|
||||
loadUsers();
|
||||
</script>
|
||||
|
||||
// Initialize
|
||||
updateScheduleOptions();
|
||||
loadUsers();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
+194
-241
@@ -1,258 +1,211 @@
|
||||
<?php
|
||||
// Admin view for managing ticket templates
|
||||
// Receives $templates from controller
|
||||
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$pageTitle = 'Templates';
|
||||
$activeNav = 'admin-templates';
|
||||
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
||||
$pageScripts = [];
|
||||
include __DIR__ . '/../../views/layout_header.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Template Management - 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: Templates</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 class="lt-page-header">
|
||||
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
||||
<span class="lt-text-muted lt-text-xs">/</span>
|
||||
<span class="lt-text-muted lt-text-xs">Admin: Templates</span>
|
||||
</div>
|
||||
<button type="button" class="lt-btn lt-btn-primary" data-action="show-create-modal">+ NEW TEMPLATE</button>
|
||||
</div>
|
||||
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Ticket Template Management</div>
|
||||
<div class="lt-section-body">
|
||||
<p class="lt-text-sm lt-text-muted" style="margin-bottom:0.75rem">
|
||||
Templates pre-fill ticket creation forms with standard content for common ticket types.
|
||||
</p>
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="Ticket templates">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Template Name</th>
|
||||
<th scope="col">Category</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Priority</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($templates)): ?>
|
||||
<tr><td colspan="6" class="lt-empty">No templates defined. Create templates to speed up ticket creation.</td></tr>
|
||||
<?php else: foreach ($templates as $tpl): ?>
|
||||
<tr>
|
||||
<td data-label="Name"><strong><?= htmlspecialchars($tpl['template_name']) ?></strong></td>
|
||||
<td data-label="Category" class="lt-text-xs"><?= htmlspecialchars($tpl['category'] ?? 'Any') ?></td>
|
||||
<td data-label="Type" class="lt-text-xs"><?= htmlspecialchars($tpl['type'] ?? 'Any') ?></td>
|
||||
<td data-label="Priority" class="lt-text-xs"><span class="lt-p<?= $tpl['default_priority'] ?? 4 ?>">P<?= $tpl['default_priority'] ?? 4 ?></span></td>
|
||||
<td data-label="Status">
|
||||
<span class="lt-status <?= ($tpl['is_active'] ?? 1) ? 'lt-status-open' : 'lt-status-closed' ?>">
|
||||
<?= ($tpl['is_active'] ?? 1) ? 'Active' : 'Inactive' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="lt-btn-group">
|
||||
<button type="button" class="lt-btn lt-btn-sm"
|
||||
data-action="edit-template" data-id="<?= $tpl['template_id'] ?>">EDIT</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
|
||||
data-action="delete-template" data-id="<?= $tpl['template_id'] ?>">DEL</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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">Ticket Template Management</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="admin-header-row">
|
||||
<h2>Ticket Templates</h2>
|
||||
<button data-action="show-create-modal" class="btn">+ NEW TEMPLATE</button>
|
||||
</div>
|
||||
|
||||
<p class="text-muted-green mb-1">
|
||||
Templates pre-fill ticket creation forms with standard content for common ticket types.
|
||||
</p>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Template Name</th>
|
||||
<th>Category</th>
|
||||
<th>Type</th>
|
||||
<th>Priority</th>
|
||||
<th>Active</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($templates)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state">No templates defined. Create templates to speed up ticket creation.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($templates as $tpl): ?>
|
||||
<tr>
|
||||
<td><strong><?php echo htmlspecialchars($tpl['template_name']); ?></strong></td>
|
||||
<td><?php echo htmlspecialchars($tpl['category'] ?? 'Any'); ?></td>
|
||||
<td><?php echo htmlspecialchars($tpl['type'] ?? 'Any'); ?></td>
|
||||
<td>P<?php echo $tpl['default_priority'] ?? '4'; ?></td>
|
||||
<td>
|
||||
<span class="<?php echo ($tpl['is_active'] ?? 1) ? 'text-open' : 'text-closed'; ?>">
|
||||
<?php echo ($tpl['is_active'] ?? 1) ? 'Active' : 'Inactive'; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button data-action="edit-template" data-id="<?php echo $tpl['template_id']; ?>" class="btn btn-small">EDIT</button>
|
||||
<button data-action="delete-template" data-id="<?php echo $tpl['template_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="templateModal" aria-hidden="true" role="dialog"
|
||||
aria-modal="true" aria-labelledby="modalTitle">
|
||||
<div class="lt-modal">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="modalTitle">Create Template</span>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
<div class="lt-modal-overlay" id="templateModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||
<div class="lt-modal lt-modal-lg">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="modalTitle">Create Template</span>
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="templateForm">
|
||||
<input type="hidden" id="template_id" name="template_id">
|
||||
<div class="lt-modal-body">
|
||||
<div class="setting-row">
|
||||
<label for="template_name">Template Name *</label>
|
||||
<input type="text" id="template_name" name="template_name" required>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="title_template">Title Template</label>
|
||||
<input type="text" id="title_template" name="title_template" placeholder="Pre-filled title text">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="description_template">Description Template</label>
|
||||
<textarea id="description_template" name="description_template" rows="10" placeholder="Pre-filled description content"></textarea>
|
||||
</div>
|
||||
<div class="setting-grid-3">
|
||||
<div class="setting-row setting-row-compact">
|
||||
<label for="category">Category</label>
|
||||
<select id="category" name="category">
|
||||
<option value="">Any</option>
|
||||
<option value="General">General</option>
|
||||
<option value="Hardware">Hardware</option>
|
||||
<option value="Software">Software</option>
|
||||
<option value="Network">Network</option>
|
||||
<option value="Security">Security</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row setting-row-compact">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" name="type">
|
||||
<option value="">Any</option>
|
||||
<option value="Maintenance">Maintenance</option>
|
||||
<option value="Install">Install</option>
|
||||
<option value="Task">Task</option>
|
||||
<option value="Upgrade">Upgrade</option>
|
||||
<option value="Issue">Issue</option>
|
||||
<option value="Problem">Problem</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row setting-row-compact">
|
||||
<label for="priority">Priority</label>
|
||||
<select id="priority" name="priority">
|
||||
<option value="1">P1</option>
|
||||
<option value="2">P2</option>
|
||||
<option value="3">P3</option>
|
||||
<option value="4" selected>P4</option>
|
||||
<option value="5">P5</option>
|
||||
</select>
|
||||
</div>
|
||||
</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>
|
||||
<form id="templateForm">
|
||||
<input type="hidden" id="template_id" name="template_id">
|
||||
<div class="lt-modal-body">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="template_name">Template Name *</label>
|
||||
<input type="text" id="template_name" name="template_name" class="lt-input" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="title_template">Title Template</label>
|
||||
<input type="text" id="title_template" name="title_template" class="lt-input"
|
||||
placeholder="Pre-filled title text">
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="description_template">Description Template</label>
|
||||
<textarea id="description_template" name="description_template" class="lt-input lt-textarea"
|
||||
rows="10" placeholder="Pre-filled description content"></textarea>
|
||||
</div>
|
||||
<div class="create-ticket-meta-grid">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="tpl-category">Category</label>
|
||||
<select id="tpl-category" name="category" class="lt-select">
|
||||
<option value="">Any</option>
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
|
||||
<option value="<?= $c ?>"><?= $c ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="tpl-type">Type</label>
|
||||
<select id="tpl-type" name="type" class="lt-select">
|
||||
<option value="">Any</option>
|
||||
<?php foreach (['Maintenance','Install','Task','Upgrade','Issue','Problem'] as $t): ?>
|
||||
<option value="<?= $t ?>"><?= $t ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="tpl-priority">Priority</label>
|
||||
<select id="tpl-priority" name="priority" class="lt-select">
|
||||
<?php foreach ([1=>'P1',2=>'P2',3=>'P3',4=>'P4 (default)',5=>'P5'] as $v=>$l): ?>
|
||||
<option value="<?= $v ?>" <?= $v === 4 ? 'selected' : '' ?>><?= $l ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-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 templates = <?php echo json_encode($templates ?? []); ?>;
|
||||
<script nonce="<?= $nonce ?>">
|
||||
var templates = <?= json_encode($templates ?? []) ?>;
|
||||
|
||||
function showCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create Template';
|
||||
document.getElementById('templateForm').reset();
|
||||
document.getElementById('template_id').value = '';
|
||||
document.getElementById('is_active').checked = true;
|
||||
lt.modal.open('templateModal');
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
switch (target.getAttribute('data-action')) {
|
||||
case 'show-create-modal': showCreateModal(); break;
|
||||
case 'edit-template': editTemplate(target.getAttribute('data-id')); break;
|
||||
case 'delete-template': deleteTemplate(target.getAttribute('data-id')); break;
|
||||
}
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
lt.modal.close('templateModal');
|
||||
}
|
||||
document.getElementById('templateForm').addEventListener('submit', function (e) {
|
||||
saveTemplate(e);
|
||||
});
|
||||
|
||||
// Event delegation for data-action handlers
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
const action = target.dataset.action;
|
||||
switch (action) {
|
||||
case 'show-create-modal':
|
||||
showCreateModal();
|
||||
break;
|
||||
case 'edit-template':
|
||||
editTemplate(target.dataset.id);
|
||||
break;
|
||||
case 'delete-template':
|
||||
deleteTemplate(target.dataset.id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
function showCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create Template';
|
||||
document.getElementById('templateForm').reset();
|
||||
document.getElementById('template_id').value = '';
|
||||
document.getElementById('is_active').checked = true;
|
||||
lt.modal.open('templateModal');
|
||||
}
|
||||
|
||||
// Form submit handler
|
||||
document.getElementById('templateForm').addEventListener('submit', function(e) {
|
||||
saveTemplate(e);
|
||||
});
|
||||
function editTemplate(id) {
|
||||
var tpl = templates.find(function (t) { return t.template_id == id; });
|
||||
if (!tpl) return;
|
||||
document.getElementById('template_id').value = tpl.template_id;
|
||||
document.getElementById('template_name').value = tpl.template_name;
|
||||
document.getElementById('title_template').value = tpl.title_template || '';
|
||||
document.getElementById('description_template').value = tpl.description_template || '';
|
||||
document.getElementById('tpl-category').value = tpl.category || '';
|
||||
document.getElementById('tpl-type').value = tpl.type || '';
|
||||
document.getElementById('tpl-priority').value = tpl.default_priority || 4;
|
||||
document.getElementById('is_active').checked = (tpl.is_active ?? 1) == 1;
|
||||
document.getElementById('modalTitle').textContent = 'Edit Template';
|
||||
lt.modal.open('templateModal');
|
||||
}
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
function deleteTemplate(id) {
|
||||
showConfirmModal('Delete Template', 'Delete this template? This cannot be undone.', 'error', function () {
|
||||
lt.api.delete('/api/manage_templates.php?id=' + id)
|
||||
.then(function (data) {
|
||||
if (data.success) window.location.reload();
|
||||
else lt.toast.error(data.error || 'Failed to delete');
|
||||
}).catch(function () { lt.toast.error('Failed to delete'); });
|
||||
});
|
||||
}
|
||||
|
||||
function saveTemplate(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
template_id: document.getElementById('template_id').value,
|
||||
template_name: document.getElementById('template_name').value,
|
||||
title_template: document.getElementById('title_template').value,
|
||||
description_template: document.getElementById('description_template').value,
|
||||
category: document.getElementById('category').value || null,
|
||||
type: document.getElementById('type').value || null,
|
||||
default_priority: parseInt(document.getElementById('priority').value) || 4,
|
||||
is_active: document.getElementById('is_active').checked ? 1 : 0
|
||||
};
|
||||
function saveTemplate(e) {
|
||||
e.preventDefault();
|
||||
var data = {
|
||||
template_id: document.getElementById('template_id').value,
|
||||
template_name: document.getElementById('template_name').value,
|
||||
title_template: document.getElementById('title_template').value,
|
||||
description_template: document.getElementById('description_template').value,
|
||||
category: document.getElementById('tpl-category').value || null,
|
||||
type: document.getElementById('tpl-type').value || null,
|
||||
default_priority: parseInt(document.getElementById('tpl-priority').value) || 4,
|
||||
is_active: document.getElementById('is_active').checked ? 1 : 0,
|
||||
};
|
||||
var url = '/api/manage_templates.php' + (data.template_id ? '?id=' + data.template_id : '');
|
||||
var apiCall = data.template_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
||||
apiCall.then(function (result) {
|
||||
if (result.success) window.location.reload();
|
||||
else lt.toast.error(result.error || 'Failed to save');
|
||||
}).catch(function () { lt.toast.error('Failed to save'); });
|
||||
}
|
||||
</script>
|
||||
|
||||
const url = '/api/manage_templates.php' + (data.template_id ? '?id=' + data.template_id : '');
|
||||
const apiCall = data.template_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 editTemplate(id) {
|
||||
const tpl = templates.find(t => t.template_id == id);
|
||||
if (!tpl) return;
|
||||
|
||||
document.getElementById('template_id').value = tpl.template_id;
|
||||
document.getElementById('template_name').value = tpl.template_name;
|
||||
document.getElementById('title_template').value = tpl.title_template || '';
|
||||
document.getElementById('description_template').value = tpl.description_template || '';
|
||||
document.getElementById('category').value = tpl.category || '';
|
||||
document.getElementById('type').value = tpl.type || '';
|
||||
document.getElementById('priority').value = tpl.default_priority || 4;
|
||||
document.getElementById('is_active').checked = (tpl.is_active ?? 1) == 1;
|
||||
document.getElementById('modalTitle').textContent = 'Edit Template';
|
||||
lt.modal.open('templateModal');
|
||||
}
|
||||
|
||||
function deleteTemplate(id) {
|
||||
showConfirmModal('Delete Template', 'Delete this template?', 'error', function() {
|
||||
lt.api.delete('/api/manage_templates.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>
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
+107
-126
@@ -1,135 +1,116 @@
|
||||
<?php
|
||||
// Admin view for user activity reports
|
||||
// Receives $userStats, $dateRange from controller
|
||||
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$pageTitle = 'User Activity';
|
||||
$activeNav = 'admin-user-activity';
|
||||
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
||||
$pageScripts = [];
|
||||
include __DIR__ . '/../../views/layout_header.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>User Activity - 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; ?>">
|
||||
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: User Activity</span>
|
||||
|
||||
<div class="lt-page-header">
|
||||
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
||||
<span class="lt-text-muted lt-text-xs">/</span>
|
||||
<span class="lt-text-muted lt-text-xs">Admin: User Activity</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">User Activity Report</div>
|
||||
<div class="lt-section-body">
|
||||
|
||||
<!-- Date filter -->
|
||||
<form method="GET" class="lt-flex lt-flex-wrap lt-flex-gap-sm lt-mb-md" role="search">
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
<label class="lt-label" for="date_from">Date From</label>
|
||||
<input type="date" name="date_from" id="date_from" class="lt-input lt-input-sm"
|
||||
value="<?= htmlspecialchars($dateRange['from'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
<label class="lt-label" for="date_to">Date To</label>
|
||||
<input type="date" name="date_to" id="date_to" class="lt-input lt-input-sm"
|
||||
value="<?= htmlspecialchars($dateRange['to'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center" style="align-self:flex-end">
|
||||
<button type="submit" class="lt-btn lt-btn-primary lt-btn-sm">APPLY</button>
|
||||
<a href="?" class="lt-btn lt-btn-ghost lt-btn-sm">RESET</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Summary stats -->
|
||||
<?php if (!empty($userStats)): ?>
|
||||
<div class="lt-stats-grid lt-mb-md">
|
||||
<div class="lt-stat-card">
|
||||
<div class="lt-stat-icon lt-text-cyan">[ # ]</div>
|
||||
<div class="lt-stat-info">
|
||||
<div class="lt-stat-value"><?= count($userStats) ?></div>
|
||||
<div class="lt-stat-label">Active Users</div>
|
||||
</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 class="lt-stat-card">
|
||||
<div class="lt-stat-icon">[ + ]</div>
|
||||
<div class="lt-stat-info">
|
||||
<div class="lt-stat-value"><?= array_sum(array_column($userStats, 'tickets_created')) ?></div>
|
||||
<div class="lt-stat-label">Total Created</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-stat-card">
|
||||
<div class="lt-stat-icon lt-text-muted">[ OK ]</div>
|
||||
<div class="lt-stat-info">
|
||||
<div class="lt-stat-value"><?= array_sum(array_column($userStats, 'tickets_resolved')) ?></div>
|
||||
<div class="lt-stat-label">Total Resolved</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-stat-card">
|
||||
<div class="lt-stat-icon lt-text-amber">[ > ]</div>
|
||||
<div class="lt-stat-info">
|
||||
<div class="lt-stat-value"><?= array_sum(array_column($userStats, 'comments_added')) ?></div>
|
||||
<div class="lt-stat-label">Total Comments</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="ascii-frame-outer admin-container">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header">User Activity Report</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<!-- Date Range Filter -->
|
||||
<form method="GET" class="admin-form-row">
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="date_from">Date From</label>
|
||||
<input type="date" name="date_from" id="date_from" value="<?php echo htmlspecialchars($dateRange['from'] ?? ''); ?>" class="admin-input">
|
||||
</div>
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="date_to">Date To</label>
|
||||
<input type="date" name="date_to" id="date_to" value="<?php echo htmlspecialchars($dateRange['to'] ?? ''); ?>" class="admin-input">
|
||||
</div>
|
||||
<div class="admin-form-actions">
|
||||
<button type="submit" class="btn">APPLY</button>
|
||||
<a href="?" class="btn btn-secondary">RESET</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- User Activity Table -->
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th class="text-center">Tickets Created</th>
|
||||
<th class="text-center">Tickets Resolved</th>
|
||||
<th class="text-center">Comments Added</th>
|
||||
<th class="text-center">Tickets Assigned</th>
|
||||
<th class="text-center">Last Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($userStats)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state">No user activity data available.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($userStats as $user): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?></strong>
|
||||
<?php if ($user['is_admin']): ?>
|
||||
<span class="admin-badge">[ ADMIN ]</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="text-green fw-bold"><?php echo $user['tickets_created'] ?? 0; ?></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="text-open fw-bold"><?php echo $user['tickets_resolved'] ?? 0; ?></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="text-cyan fw-bold"><?php echo $user['comments_added'] ?? 0; ?></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="text-amber fw-bold"><?php echo $user['tickets_assigned'] ?? 0; ?></span>
|
||||
</td>
|
||||
<td class="text-center text-sm">
|
||||
<?php echo $user['last_activity'] ? date('M d, Y H:i', strtotime($user['last_activity'])) : 'Never'; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<?php if (!empty($userStats)): ?>
|
||||
<div class="admin-stats-grid">
|
||||
<div>
|
||||
<div class="admin-stat-value text-green"><?php echo array_sum(array_column($userStats, 'tickets_created')); ?></div>
|
||||
<div class="admin-stat-label">Total Created</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="admin-stat-value text-open"><?php echo array_sum(array_column($userStats, 'tickets_resolved')); ?></div>
|
||||
<div class="admin-stat-label">Total Resolved</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="admin-stat-value text-cyan"><?php echo array_sum(array_column($userStats, 'comments_added')); ?></div>
|
||||
<div class="admin-stat-label">Total Comments</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="admin-stat-value text-amber"><?php echo count($userStats); ?></div>
|
||||
<div class="admin-stat-label">Active Users</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- User activity table -->
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="User activity">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">User</th>
|
||||
<th scope="col">Tickets Created</th>
|
||||
<th scope="col">Tickets Resolved</th>
|
||||
<th scope="col">Comments</th>
|
||||
<th scope="col">Assigned</th>
|
||||
<th scope="col">Last Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($userStats)): ?>
|
||||
<tr><td colspan="6" class="lt-empty">No user activity data available.</td></tr>
|
||||
<?php else: foreach ($userStats as $u): ?>
|
||||
<tr>
|
||||
<td data-label="User">
|
||||
<strong><?= htmlspecialchars($u['display_name'] ?? $u['username']) ?></strong>
|
||||
<?php if ($u['is_admin']): ?>
|
||||
<span class="lt-badge lt-badge-admin lt-text-xs">ADMIN</span>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Created"><span class="lt-text-cyan"><?= (int)($u['tickets_created'] ?? 0) ?></span></td>
|
||||
<td data-label="Resolved"><span class="lt-text-muted"><?= (int)($u['tickets_resolved'] ?? 0) ?></span></td>
|
||||
<td data-label="Comments"><span class="lt-text-amber"><?= (int)($u['comments_added'] ?? 0) ?></span></td>
|
||||
<td data-label="Assigned"><?= (int)($u['tickets_assigned'] ?? 0) ?></td>
|
||||
<td data-label="Last Activity" class="lt-text-xs lt-text-muted">
|
||||
<?= $u['last_activity'] ? date('M d, Y H:i', strtotime($u['last_activity'])) : 'Never' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script nonce="<?php echo $nonce; ?>">if (window.lt) lt.keys.initDefaults();</script>
|
||||
</body>
|
||||
</html>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
@@ -1,271 +1,227 @@
|
||||
<?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();
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$pageTitle = 'Workflow Designer';
|
||||
$activeNav = 'admin-workflow';
|
||||
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
||||
$pageScripts = [];
|
||||
include __DIR__ . '/../../views/layout_header.php';
|
||||
?>
|
||||
<!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 class="lt-page-header">
|
||||
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
||||
<span class="lt-text-muted lt-text-xs">/</span>
|
||||
<span class="lt-text-muted lt-text-xs">Admin: Workflow</span>
|
||||
</div>
|
||||
<button type="button" class="lt-btn lt-btn-primary" data-action="show-create-modal">+ NEW TRANSITION</button>
|
||||
</div>
|
||||
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Workflow Diagram</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-grid-4">
|
||||
<?php
|
||||
$statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
|
||||
foreach ($statuses as $status):
|
||||
$slug = strtolower(str_replace(' ', '-', $status));
|
||||
$toCount = 0;
|
||||
if (isset($workflows)) { foreach ($workflows as $w) { if ($w['from_status'] === $status) $toCount++; } }
|
||||
?>
|
||||
<div class="lt-card" style="text-align: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">
|
||||
Define which status transitions are allowed. This controls what options appear in the status dropdown on tickets.
|
||||
</p>
|
||||
</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 class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Status Transitions</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="Status transitions">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">From Status</th>
|
||||
<th scope="col">→</th>
|
||||
<th scope="col">To Status</th>
|
||||
<th scope="col">Req. Comment</th>
|
||||
<th scope="col">Req. Admin</th>
|
||||
<th scope="col">Active</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($workflows)): ?>
|
||||
<tr><td colspan="7" class="lt-empty">No transitions defined. Add transitions to enable status changes.</td></tr>
|
||||
<?php else: foreach ($workflows as $wf): ?>
|
||||
<?php $fromSlug = strtolower(str_replace(' ', '-', $wf['from_status'])); $toSlug = strtolower(str_replace(' ', '-', $wf['to_status'])); ?>
|
||||
<tr>
|
||||
<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">→</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">
|
||||
<?= $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">
|
||||
<?= $wf['requires_admin'] ? '<span class="lt-text-amber">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
|
||||
</td>
|
||||
<td data-label="Active" style="text-align:center">
|
||||
<?= $wf['is_active']
|
||||
? '<span class="lt-text-cyan">✓</span>'
|
||||
: '<span class="lt-text-danger">✗</span>' ?>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="lt-btn-group">
|
||||
<button type="button" class="lt-btn lt-btn-sm"
|
||||
data-action="edit-transition" data-id="<?= $wf['transition_id'] ?>">EDIT</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
|
||||
data-action="delete-transition" data-id="<?= $wf['transition_id'] ?>">DEL</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>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
<!-- Create/Edit Modal -->
|
||||
<div class="lt-modal-overlay" id="workflowModal" aria-hidden="true" role="dialog"
|
||||
aria-modal="true" aria-labelledby="wfModalTitle">
|
||||
<div class="lt-modal">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="wfModalTitle">Create Transition</span>
|
||||
<button type="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="lt-form-group">
|
||||
<label class="lt-label" for="from_status">From Status *</label>
|
||||
<select id="from_status" name="from_status" class="lt-select" 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="lt-form-group">
|
||||
<label class="lt-label" for="to_status">To Status *</label>
|
||||
<select id="to_status" name="to_status" class="lt-select" 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="lt-form-group">
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox" id="requires_comment" name="requires_comment">
|
||||
Requires a comment when transitioning
|
||||
</label>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox" id="requires_admin" name="requires_admin">
|
||||
Requires administrator privileges
|
||||
</label>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox" id="wf_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 ?? []); ?>;
|
||||
<script nonce="<?= $nonce ?>">
|
||||
var workflows = <?= 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');
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
switch (target.getAttribute('data-action')) {
|
||||
case 'show-create-modal': showCreateModal(); break;
|
||||
case 'edit-transition': editTransition(target.getAttribute('data-id')); break;
|
||||
case 'delete-transition': deleteTransition(target.getAttribute('data-id')); break;
|
||||
}
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
lt.modal.close('workflowModal');
|
||||
}
|
||||
document.getElementById('workflowForm').addEventListener('submit', function (e) {
|
||||
saveTransition(e);
|
||||
});
|
||||
|
||||
// Event delegation for data-action handlers
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
function showCreateModal() {
|
||||
document.getElementById('wfModalTitle').textContent = 'Create Transition';
|
||||
document.getElementById('workflowForm').reset();
|
||||
document.getElementById('transition_id').value = '';
|
||||
document.getElementById('wf_is_active').checked = true;
|
||||
lt.modal.open('workflowModal');
|
||||
}
|
||||
|
||||
// Form submit handler
|
||||
document.getElementById('workflowForm').addEventListener('submit', function(e) {
|
||||
saveTransition(e);
|
||||
});
|
||||
function editTransition(id) {
|
||||
var wf = workflows.find(function (w) { return 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('wf_is_active').checked = wf.is_active == 1;
|
||||
document.getElementById('wfModalTitle').textContent = 'Edit Transition';
|
||||
lt.modal.open('workflowModal');
|
||||
}
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
function deleteTransition(id) {
|
||||
showConfirmModal('Delete Transition', 'Delete this status transition? This cannot be undone.', 'error', function () {
|
||||
lt.api.delete('/api/manage_workflows.php?id=' + id)
|
||||
.then(function (data) {
|
||||
if (data.success) window.location.reload();
|
||||
else lt.toast.error(data.error || 'Failed to delete');
|
||||
}).catch(function () { lt.toast.error('Failed to delete'); });
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
function saveTransition(e) {
|
||||
e.preventDefault();
|
||||
var 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('wf_is_active').checked ? 1 : 0,
|
||||
};
|
||||
var url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_id : '');
|
||||
var apiCall = data.transition_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
||||
apiCall.then(function (result) {
|
||||
if (result.success) window.location.reload();
|
||||
else lt.toast.error(result.error || 'Failed to save');
|
||||
}).catch(function () { lt.toast.error('Failed to save'); });
|
||||
}
|
||||
</script>
|
||||
|
||||
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>
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user