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:
+4857
-1068
File diff suppressed because it is too large
Load Diff
+147
-6133
File diff suppressed because it is too large
Load Diff
+199
-2700
File diff suppressed because it is too large
Load Diff
+2535
-381
File diff suppressed because it is too large
Load Diff
+223
-252
@@ -1,150 +1,124 @@
|
||||
<?php
|
||||
// This file contains the HTML template for creating a new ticket
|
||||
/**
|
||||
* CreateTicketView.php — New ticket creation form, redesigned for TDS v1.2
|
||||
* Variables: $templates (array), $allUsers (array), $error (string|null)
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../middleware/SecurityHeadersMiddleware.php';
|
||||
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
|
||||
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$pageTitle = 'New Ticket';
|
||||
$activeNav = 'dashboard';
|
||||
$pageStyles = ['/assets/css/ticket.css?v=20260327'];
|
||||
$pageScripts = [
|
||||
'/assets/js/keyboard-shortcuts.js?v=20260327',
|
||||
];
|
||||
|
||||
include __DIR__ . '/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>Create New Ticket</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; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260320"></script>
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
// CSRF Token for AJAX requests
|
||||
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>
|
||||
</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>
|
||||
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
||||
<span class="admin-badge">[ ADMIN ]</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Page header -->
|
||||
<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">New Ticket</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OUTER FRAME: Create Ticket Form Container -->
|
||||
<div class="ascii-frame-outer">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<!-- SECTION 1: Form Header -->
|
||||
<div class="ascii-section-header">Create New Ticket</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="ticket-header">
|
||||
<h2>New Ticket Form</h2>
|
||||
<p class="form-hint">
|
||||
Complete the form below to create a new ticket
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
CREATE TICKET FORM
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
<form method="POST"
|
||||
action="<?= htmlspecialchars($GLOBALS['config']['BASE_URL'], ENT_QUOTES, 'UTF-8') ?>/ticket/create"
|
||||
class="create-ticket-form"
|
||||
novalidate>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<!-- ERROR SECTION -->
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="error-message inline-error">
|
||||
<strong>[ ! ] ERROR:</strong> <?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?>
|
||||
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
|
||||
<strong>Error:</strong> <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<form method="POST" action="<?php echo $GLOBALS['config']['BASE_URL']; ?>/ticket/create" class="ticket-form">
|
||||
|
||||
<!-- SECTION 2: Template Selection -->
|
||||
<div class="ascii-section-header">Template Selection</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="detail-group">
|
||||
<label for="templateSelect">Use Template (Optional)</label>
|
||||
<select id="templateSelect" class="editable" data-action="load-template">
|
||||
<option value="">-- No Template --</option>
|
||||
<?php if (isset($templates) && !empty($templates)): ?>
|
||||
<?php foreach ($templates as $template): ?>
|
||||
<option value="<?php echo $template['template_id']; ?>">
|
||||
<?php echo htmlspecialchars($template['template_name']); ?>
|
||||
<!-- ── SECTION 1: Template ───────────────────────────────── -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Template (Optional)</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="templateSelect">Use a Template</label>
|
||||
<select id="templateSelect" class="lt-select" data-action="load-template">
|
||||
<option value="">— No Template —</option>
|
||||
<?php if (!empty($templates)): ?>
|
||||
<?php foreach ($templates as $tpl): ?>
|
||||
<option value="<?= (int)$tpl['template_id'] ?>">
|
||||
<?= htmlspecialchars($tpl['template_name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</select>
|
||||
<p class="form-hint">
|
||||
Select a template to auto-fill form fields
|
||||
</p>
|
||||
<p class="lt-form-hint">Selecting a template pre-fills the form fields.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
<!-- ── SECTION 2: Title ─────────────────────────────────── -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Title *</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label lt-sr-only" for="title">Ticket Title</label>
|
||||
<input type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
class="lt-input"
|
||||
required
|
||||
autocomplete="off"
|
||||
placeholder="Enter a clear, concise title for this ticket"
|
||||
aria-required="true"
|
||||
aria-describedby="duplicateWarning">
|
||||
</div>
|
||||
|
||||
<!-- SECTION 3: Basic Information -->
|
||||
<div class="ascii-section-header">Basic Information</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="detail-group">
|
||||
<label for="title">Ticket Title *</label>
|
||||
<input type="text" id="title" name="title" class="editable" required placeholder="Enter a descriptive title for this ticket">
|
||||
</div>
|
||||
<!-- Duplicate Warning Area -->
|
||||
<div id="duplicateWarning" class="inline-warning is-hidden" role="alert" aria-live="polite" aria-atomic="true">
|
||||
<div class="text-amber fw-bold duplicate-heading">
|
||||
Possible Duplicates Found
|
||||
</div>
|
||||
<!-- Duplicate warning (shown by JS when similar tickets exist) -->
|
||||
<div id="duplicateWarning" class="lt-msg lt-msg-warning is-hidden"
|
||||
role="alert" aria-live="polite" aria-atomic="true">
|
||||
<strong class="lt-text-amber">Possible Duplicates Found</strong>
|
||||
<div id="duplicatesList" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
<!-- ── SECTION 3: Metadata ──────────────────────────────── -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Metadata</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="create-ticket-meta-grid">
|
||||
|
||||
<!-- SECTION 4: Ticket Metadata -->
|
||||
<div class="ascii-section-header">Ticket Metadata</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="detail-group status-priority-row">
|
||||
<div class="detail-quarter">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status" class="editable">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="status">Status</label>
|
||||
<select id="status" name="status" class="lt-select">
|
||||
<option value="Open" selected>Open</option>
|
||||
<option value="Closed">Closed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="detail-quarter">
|
||||
<label for="priority">Priority</label>
|
||||
<select id="priority" name="priority" class="editable">
|
||||
<option value="1">P1 - Critical Impact</option>
|
||||
<option value="2">P2 - High Impact</option>
|
||||
<option value="3">P3 - Medium Impact</option>
|
||||
<option value="4" selected>P4 - Low Impact</option>
|
||||
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="priority">Priority</label>
|
||||
<select id="priority" name="priority" class="lt-select">
|
||||
<option value="1">P1 — Critical Impact</option>
|
||||
<option value="2">P2 — High Impact</option>
|
||||
<option value="3">P3 — Medium Impact</option>
|
||||
<option value="4" selected>P4 — Low Impact</option>
|
||||
<option value="5">P5 — Minimal Impact</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="detail-quarter">
|
||||
<label for="category">Category</label>
|
||||
<select id="category" name="category" class="editable">
|
||||
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="category">Category</label>
|
||||
<select id="category" name="category" class="lt-select">
|
||||
<option value="Hardware">Hardware</option>
|
||||
<option value="Software">Software</option>
|
||||
<option value="Network">Network</option>
|
||||
@@ -152,207 +126,204 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
<option value="General" selected>General</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="detail-quarter">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" name="type" class="editable">
|
||||
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="type">Type</label>
|
||||
<select id="type" name="type" class="lt-select">
|
||||
<option value="Maintenance">Maintenance</option>
|
||||
<option value="Install">Install</option>
|
||||
<option value="Task">Task</option>
|
||||
<option value="Upgrade">Upgrade</option>
|
||||
<option value="Issue" selected>Issue</option>
|
||||
<option value="Problem">Problem</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.create-ticket-meta-grid -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<!-- SECTION 4b: Assignment -->
|
||||
<div class="ascii-section-header">Assignment</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="detail-group">
|
||||
<label for="assigned_to">Assign To (Optional)</label>
|
||||
<select id="assigned_to" name="assigned_to" class="editable">
|
||||
<option value="">-- Unassigned --</option>
|
||||
<?php if (isset($allUsers) && !empty($allUsers)): ?>
|
||||
<?php foreach ($allUsers as $user): ?>
|
||||
<option value="<?php echo $user['user_id']; ?>">
|
||||
<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>
|
||||
<!-- ── SECTION 4: Assignment ────────────────────────────── -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Assignment</div>
|
||||
<div class="lt-section-body">
|
||||
<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>
|
||||
<?php if (!empty($allUsers)): ?>
|
||||
<?php foreach ($allUsers as $u): ?>
|
||||
<option value="<?= (int)$u['user_id'] ?>">
|
||||
<?= htmlspecialchars($u['display_name'] ?? $u['username']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</select>
|
||||
<p class="form-hint">
|
||||
Select a user to assign this ticket to
|
||||
</p>
|
||||
<p class="lt-form-hint">Leave blank to create as unassigned.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<!-- SECTION 5: Visibility Settings -->
|
||||
<div class="ascii-section-header">Visibility Settings</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="detail-group">
|
||||
<label for="visibility">Ticket Visibility</label>
|
||||
<select id="visibility" name="visibility" class="editable" data-action="toggle-visibility-groups">
|
||||
<option value="public" selected>Public - All authenticated users</option>
|
||||
<option value="internal">Internal - Specific groups only</option>
|
||||
<option value="confidential">Confidential - Creator, assignee, admins only</option>
|
||||
<!-- ── SECTION 5: Visibility ────────────────────────────── -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Visibility</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="visibility">Who can see this ticket?</label>
|
||||
<select id="visibility" name="visibility" class="lt-select" data-action="toggle-visibility-groups">
|
||||
<option value="public" selected>Public — All authenticated users</option>
|
||||
<option value="internal">Internal — Specific groups only</option>
|
||||
<option value="confidential">Confidential — Creator, assignee, and admins only</option>
|
||||
</select>
|
||||
<p class="form-hint">
|
||||
Controls who can view this ticket
|
||||
</p>
|
||||
</div>
|
||||
<div id="visibilityGroupsContainer" class="detail-group is-hidden">
|
||||
<label>Allowed Groups</label>
|
||||
<div class="visibility-groups-list">
|
||||
|
||||
<div id="visibilityGroupsContainer" class="lt-form-group is-hidden" aria-live="polite">
|
||||
<label class="lt-label lt-text-cyan">Allowed Groups</label>
|
||||
<div class="visibility-groups-list lt-flex lt-flex-wrap lt-flex-gap-sm">
|
||||
<?php
|
||||
// Get all available groups
|
||||
require_once __DIR__ . '/../models/UserModel.php';
|
||||
$userModel = new UserModel($conn);
|
||||
$allGroups = $userModel->getAllGroups();
|
||||
foreach ($allGroups as $group):
|
||||
?>
|
||||
<label class="group-checkbox-label">
|
||||
<input type="checkbox" name="visibility_groups[]" value="<?php echo htmlspecialchars($group); ?>" class="visibility-group-checkbox">
|
||||
<span class="group-badge"><?php echo htmlspecialchars($group); ?></span>
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox visibility-group-checkbox"
|
||||
name="visibility_groups[]"
|
||||
value="<?= htmlspecialchars($group, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<span class="lt-badge"><?= htmlspecialchars($group) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach ?>
|
||||
<?php if (empty($allGroups)): ?>
|
||||
<span class="text-muted">No groups available</span>
|
||||
<?php endif; ?>
|
||||
<span class="lt-text-muted lt-text-sm">No groups available</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<p class="form-hint-warning">
|
||||
Select which groups can view this ticket
|
||||
</p>
|
||||
<p class="lt-form-hint lt-form-hint--warn">Select at least one group for Internal visibility.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<!-- SECTION 6: Detailed Description -->
|
||||
<div class="ascii-section-header">Detailed Description</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="detail-group full-width">
|
||||
<label for="description">Description *</label>
|
||||
<textarea id="description" name="description" class="editable" rows="15" required placeholder="Provide a detailed description of the ticket..."></textarea>
|
||||
<!-- ── SECTION 6: Description ───────────────────────────── -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Description *</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-sr-only lt-label" for="description">Description</label>
|
||||
<textarea id="description"
|
||||
name="description"
|
||||
class="lt-input lt-textarea"
|
||||
rows="16"
|
||||
required
|
||||
aria-required="true"
|
||||
placeholder="Provide a detailed description of the issue, steps to reproduce, expected vs. actual behavior, and any relevant context…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<!-- SECTION 6: Form Actions -->
|
||||
<div class="ascii-section-header">Form Actions</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="ticket-footer">
|
||||
<button type="submit" class="btn primary">CREATE TICKET</button>
|
||||
<button type="button" data-action="navigate" data-url="<?php echo $GLOBALS['config']['BASE_URL']; ?>/" class="btn back-btn">CANCEL</button>
|
||||
<!-- ── SECTION 7: Actions ───────────────────────────────── -->
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Actions</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-btn-group">
|
||||
<button type="submit" class="lt-btn lt-btn-primary">CREATE TICKET</button>
|
||||
<a href="/" class="lt-btn lt-btn-ghost">CANCEL</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<!-- END OUTER FRAME -->
|
||||
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
// Duplicate detection with debounce
|
||||
let duplicateCheckTimeout = null;
|
||||
<!-- Page-specific script: duplicate detection + visibility toggle -->
|
||||
<script nonce="<?= $nonce ?>">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── Duplicate detection ───────────────────────────────────
|
||||
var _dupTimer = null;
|
||||
|
||||
document.getElementById('title').addEventListener('input', function () {
|
||||
clearTimeout(duplicateCheckTimeout);
|
||||
const title = this.value.trim();
|
||||
|
||||
clearTimeout(_dupTimer);
|
||||
var title = this.value.trim();
|
||||
if (title.length < 5) {
|
||||
document.getElementById('duplicateWarning').classList.add('is-hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce: wait 500ms after user stops typing
|
||||
duplicateCheckTimeout = setTimeout(() => {
|
||||
checkForDuplicates(title);
|
||||
}, 500);
|
||||
_dupTimer = setTimeout(function () { checkDuplicates(title); }, 500);
|
||||
});
|
||||
|
||||
function checkForDuplicates(title) {
|
||||
function checkDuplicates(title) {
|
||||
lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title))
|
||||
.then(data => {
|
||||
const warningDiv = document.getElementById('duplicateWarning');
|
||||
const listDiv = document.getElementById('duplicatesList');
|
||||
|
||||
.then(function (data) {
|
||||
var warn = document.getElementById('duplicateWarning');
|
||||
var list = document.getElementById('duplicatesList');
|
||||
if (data.success && data.duplicates && data.duplicates.length > 0) {
|
||||
let html = '<ul class="duplicate-list">';
|
||||
data.duplicates.forEach(dup => {
|
||||
html += `<li>
|
||||
<a href="/ticket/${escapeHtml(dup.ticket_id)}" target="_blank">
|
||||
#${escapeHtml(dup.ticket_id)}
|
||||
</a>
|
||||
- ${escapeHtml(dup.title)}
|
||||
<span class="duplicate-meta">(${dup.similarity}% match, ${escapeHtml(dup.status)})</span>
|
||||
</li>`;
|
||||
var html = '<ul class="duplicate-list lt-text-sm">';
|
||||
data.duplicates.forEach(function (dup) {
|
||||
html += '<li><a href="/ticket/' + lt.escHtml(dup.ticket_id) + '" target="_blank">#' +
|
||||
lt.escHtml(dup.ticket_id) + '</a> — ' + lt.escHtml(dup.title) +
|
||||
' <span class="lt-text-muted">(' + dup.similarity + '% match, ' +
|
||||
lt.escHtml(dup.status) + ')</span></li>';
|
||||
});
|
||||
html += '</ul>';
|
||||
html += '<p class="duplicate-hint">Consider checking these tickets before creating a new one.</p>';
|
||||
|
||||
listDiv.innerHTML = html;
|
||||
warningDiv.classList.remove('is-hidden');
|
||||
html += '</ul><p class="lt-text-xs lt-text-muted lt-mt-sm">Check these before creating a new ticket.</p>';
|
||||
list.innerHTML = html;
|
||||
warn.classList.remove('is-hidden');
|
||||
} else {
|
||||
warningDiv.classList.add('is-hidden');
|
||||
warn.classList.add('is-hidden');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking duplicates:', error);
|
||||
});
|
||||
.catch(function () { /* silent */ });
|
||||
}
|
||||
|
||||
// ── Visibility groups toggle ──────────────────────────────
|
||||
function toggleVisibilityGroups() {
|
||||
const visibility = document.getElementById('visibility').value;
|
||||
const groupsContainer = document.getElementById('visibilityGroupsContainer');
|
||||
if (visibility === 'internal') {
|
||||
groupsContainer.classList.remove('is-hidden');
|
||||
var vis = document.getElementById('visibility').value;
|
||||
var container = document.getElementById('visibilityGroupsContainer');
|
||||
if (vis === 'internal') {
|
||||
container.classList.remove('is-hidden');
|
||||
} else {
|
||||
groupsContainer.classList.add('is-hidden');
|
||||
document.querySelectorAll('.visibility-group-checkbox').forEach(cb => cb.checked = false);
|
||||
container.classList.add('is-hidden');
|
||||
container.querySelectorAll('.visibility-group-checkbox').forEach(function (cb) { cb.checked = false; });
|
||||
}
|
||||
}
|
||||
|
||||
// Event delegation for data-action handlers
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
// ── Template loader ───────────────────────────────────────
|
||||
function loadTemplate() {
|
||||
var tplId = document.getElementById('templateSelect').value;
|
||||
if (!tplId) return;
|
||||
lt.api.get('/api/get_template.php?template_id=' + encodeURIComponent(tplId))
|
||||
.then(function (data) {
|
||||
if (!data.success || !data.template) {
|
||||
lt.toast.error('Failed to load template.');
|
||||
return;
|
||||
}
|
||||
var t = data.template;
|
||||
if (t.title) document.getElementById('title').value = t.title;
|
||||
if (t.description) document.getElementById('description').value = t.description;
|
||||
if (t.priority) document.getElementById('priority').value = t.priority;
|
||||
if (t.category) document.getElementById('category').value = t.category;
|
||||
if (t.type) document.getElementById('type').value = t.type;
|
||||
// Trigger duplicate check after template fill
|
||||
document.getElementById('title').dispatchEvent(new Event('input'));
|
||||
lt.toast.success('Template applied.');
|
||||
})
|
||||
.catch(function () { lt.toast.error('Could not load template.'); });
|
||||
}
|
||||
|
||||
// ── Event delegation ──────────────────────────────────────
|
||||
document.addEventListener('change', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
const action = target.dataset.action;
|
||||
if (action === 'navigate') {
|
||||
window.location.href = target.dataset.url;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('change', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
const action = target.dataset.action;
|
||||
if (action === 'load-template') {
|
||||
loadTemplate();
|
||||
} else if (action === 'toggle-visibility-groups') {
|
||||
toggleVisibilityGroups();
|
||||
switch (target.getAttribute('data-action')) {
|
||||
case 'load-template': loadTemplate(); break;
|
||||
case 'toggle-visibility-groups': toggleVisibilityGroups(); break;
|
||||
}
|
||||
});
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php include __DIR__ . '/layout_footer.php'; ?>
|
||||
|
||||
+720
-858
File diff suppressed because it is too large
Load Diff
+590
-631
File diff suppressed because it is too large
Load Diff
+105
-171
@@ -1,57 +1,35 @@
|
||||
<?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();
|
||||
$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 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>
|
||||
|
||||
<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">
|
||||
<!-- 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="admin-form-field">
|
||||
<label class="admin-label" for="expiresIn">Expires In</label>
|
||||
<select id="expiresIn" class="admin-input">
|
||||
<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>
|
||||
@@ -59,180 +37,136 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
<option value="365">1 year</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn">GENERATE KEY</button>
|
||||
</div>
|
||||
<button type="submit" class="lt-btn lt-btn-primary" style="margin-bottom:0">GENERATE KEY</button>
|
||||
</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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Existing Keys Table -->
|
||||
<div class="ascii-frame-inner">
|
||||
<h3 class="admin-section-title">Existing API Keys</h3>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<!-- 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>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>
|
||||
<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="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>
|
||||
<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><?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 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 class="nowrap">
|
||||
<?php echo $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never'; ?>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Status">
|
||||
<?php if ($key['is_active']): ?>
|
||||
<span class="text-open">Active</span>
|
||||
<span class="lt-status lt-status-open">Active</span>
|
||||
<?php else: ?>
|
||||
<span class="text-closed">Revoked</span>
|
||||
<?php endif; ?>
|
||||
<span class="lt-status lt-status-closed">Revoked</span>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Actions">
|
||||
<?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>
|
||||
<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="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
<span class="lt-text-muted lt-text-xs">—</span>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
// Event delegation for data-action handlers
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
<script nonce="<?= $nonce ?>">
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
const action = target.dataset.action;
|
||||
switch (action) {
|
||||
case 'copy-api-key':
|
||||
copyApiKey();
|
||||
break;
|
||||
case 'revoke-key':
|
||||
revokeKey(target.dataset.id);
|
||||
break;
|
||||
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', async function(e) {
|
||||
document.getElementById('generateKeyForm').addEventListener('submit', 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
|
||||
});
|
||||
|
||||
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() {
|
||||
const keyInput = document.getElementById('newKeyValue');
|
||||
keyInput.select();
|
||||
var input = document.getElementById('newKeyValue');
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
lt.toast.success('API key copied to clipboard');
|
||||
lt.toast.success('Copied to clipboard!');
|
||||
}
|
||||
|
||||
function revokeKey(keyId) {
|
||||
showConfirmModal('Revoke API Key', 'Are you sure you want to revoke this API key? This action cannot be undone.', 'error', function() {
|
||||
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(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);
|
||||
});
|
||||
.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); });
|
||||
});
|
||||
}
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
+74
-111
@@ -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();
|
||||
$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 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="ascii-frame-outer admin-container-wide">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
<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">
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
<?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="admin-form-field">
|
||||
<label class="admin-label" for="user_id">User</label>
|
||||
<select name="user_id" id="user_id" class="admin-input">
|
||||
<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 $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']); ?>
|
||||
<?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; ?>
|
||||
<?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 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="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 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="admin-form-actions">
|
||||
<button type="submit" class="btn">FILTER</button>
|
||||
<a href="?" class="btn btn-secondary">RESET</a>
|
||||
<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="table-wrapper">
|
||||
<table>
|
||||
<!-- Log table -->
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="Audit log entries">
|
||||
<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>
|
||||
<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 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>
|
||||
<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/<?php echo htmlspecialchars($log['entity_id']); ?>" class="text-green">
|
||||
<?php echo htmlspecialchars($log['entity_id']); ?>
|
||||
</a>
|
||||
<a href="/ticket/<?= htmlspecialchars($log['entity_id']) ?>"><?= htmlspecialchars($log['entity_id']) ?></a>
|
||||
<?php else: ?>
|
||||
<?php echo htmlspecialchars($log['entity_id'] ?? '-'); ?>
|
||||
<?php endif; ?>
|
||||
<?= htmlspecialchars($log['entity_id'] ?? '-') ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td class="td-truncate">
|
||||
<td data-label="Details" class="lt-text-xs lt-text-muted" style="max-width:200px;overflow:hidden;text-overflow:ellipsis">
|
||||
<?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']);
|
||||
}
|
||||
$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 class="nowrap text-sm"><?php echo htmlspecialchars($log['ip_address'] ?? '-'); ?></td>
|
||||
<td data-label="IP" class="lt-text-xs lt-text-muted"><?= htmlspecialchars($log['ip_address'] ?? '-') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="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;
|
||||
$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 "...";
|
||||
$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>
|
||||
<?php endif; ?>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script nonce="<?php echo $nonce; ?>">if (window.lt) lt.keys.initDefaults();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
+142
-176
@@ -1,120 +1,103 @@
|
||||
<?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();
|
||||
$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 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="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>
|
||||
<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>Order</th>
|
||||
<th>Field Name</th>
|
||||
<th>Label</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th>Required</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
<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 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'; ?>
|
||||
<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>
|
||||
<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 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; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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-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="modalTitle">Create Custom Field</span>
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
<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>
|
||||
<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 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 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 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="setting-row">
|
||||
<label for="field_type">Field Type *</label>
|
||||
<select id="field_type" name="field_type" required data-action="toggle-options-field">
|
||||
<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>
|
||||
@@ -123,30 +106,36 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
<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 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="setting-row">
|
||||
<label for="category">Category (empty = all)</label>
|
||||
<select id="category" name="category">
|
||||
<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>
|
||||
<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>
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
|
||||
<option value="<?= $c ?>"><?= $c ?></option>
|
||||
<?php endforeach ?>
|
||||
</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 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="setting-row">
|
||||
<label><input type="checkbox" id="is_required" name="is_required"> Required field</label>
|
||||
<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="setting-row">
|
||||
<label><input type="checkbox" id="is_active" name="is_active" checked> Active</label>
|
||||
<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">
|
||||
@@ -157,49 +146,23 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
</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');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
lt.modal.close('fieldModal');
|
||||
}
|
||||
|
||||
// Event delegation for data-action handlers
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
<script nonce="<?= $nonce ?>">
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('change', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
document.addEventListener('change', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
if (target.dataset.action === 'toggle-options-field') {
|
||||
toggleOptionsField();
|
||||
}
|
||||
if (target.getAttribute('data-action') === 'toggle-options-field') toggleOptionsField();
|
||||
});
|
||||
|
||||
// Form submit handler
|
||||
document.getElementById('fieldForm').addEventListener('submit', function (e) {
|
||||
saveField(e);
|
||||
});
|
||||
@@ -207,58 +170,37 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
function toggleOptionsField() {
|
||||
const type = document.getElementById('field_type').value;
|
||||
var 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');
|
||||
}
|
||||
}).catch(err => lt.toast.error('Failed to save'));
|
||||
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(data => {
|
||||
.then(function (data) {
|
||||
if (data.success && data.field) {
|
||||
const f = 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('category').value = f.category || '';
|
||||
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('is_active').checked = f.is_active == 1;
|
||||
document.getElementById('cf_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';
|
||||
document.getElementById('cfModalTitle').textContent = 'Edit Custom Field';
|
||||
lt.modal.open('fieldModal');
|
||||
}
|
||||
});
|
||||
@@ -267,12 +209,36 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
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 => {
|
||||
.then(function (data) {
|
||||
if (data.success) window.location.reload();
|
||||
else lt.toast.error(data.error || 'Failed to delete');
|
||||
}).catch(err => lt.toast.error('Failed to delete'));
|
||||
}).catch(function () { lt.toast.error('Failed to delete'); });
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
@@ -1,77 +1,47 @@
|
||||
<?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();
|
||||
$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 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-frame-outer admin-container">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<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>
|
||||
<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>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>
|
||||
<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="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>
|
||||
<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') {
|
||||
@@ -81,105 +51,113 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$schedule .= ' (Day ' . $rt['schedule_day'] . ')';
|
||||
}
|
||||
$schedule .= ' @ ' . substr($rt['schedule_time'], 0, 5);
|
||||
echo htmlspecialchars($schedule);
|
||||
?>
|
||||
<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><?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'; ?>
|
||||
<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>
|
||||
<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'; ?>
|
||||
<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 data-action="delete-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small btn-danger">DELETE</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; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; 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-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="modalTitle">Create Recurring Ticket</span>
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
<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="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 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="setting-row">
|
||||
<label for="description_template">Description Template</label>
|
||||
<textarea id="description_template" name="description_template" rows="8"></textarea>
|
||||
<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="setting-row">
|
||||
<label for="schedule_type">Schedule Type *</label>
|
||||
<select id="schedule_type" name="schedule_type" required data-action="update-schedule-options">
|
||||
<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="setting-row is-hidden" id="schedule_day_row">
|
||||
<label for="schedule_day">Schedule Day</label>
|
||||
<select id="schedule_day" name="schedule_day"></select>
|
||||
<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="setting-row">
|
||||
<label for="schedule_time">Schedule Time *</label>
|
||||
<input type="time" id="schedule_time" name="schedule_time" value="09:00" required>
|
||||
<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="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>
|
||||
<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="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>
|
||||
<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="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>
|
||||
<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="setting-row setting-row-compact">
|
||||
<label for="assigned_to">Assign To</label>
|
||||
<select id="assigned_to" name="assigned_to">
|
||||
<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 JavaScript -->
|
||||
<!-- Populated by JS -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,51 +170,24 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
</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]');
|
||||
<script nonce="<?= $nonce ?>">
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.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;
|
||||
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(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
document.addEventListener('change', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
if (target.dataset.action === 'update-schedule-options') {
|
||||
updateScheduleOptions();
|
||||
}
|
||||
if (target.getAttribute('data-action') === 'update-schedule-options') updateScheduleOptions();
|
||||
});
|
||||
|
||||
// Form submit handler
|
||||
document.getElementById('recurringForm').addEventListener('submit', function (e) {
|
||||
saveRecurring(e);
|
||||
});
|
||||
@@ -244,103 +195,103 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
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');
|
||||
|
||||
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');
|
||||
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
days.forEach((day, i) => {
|
||||
daySelect.innerHTML += `<option value="${i + 1}">${day}</option>`;
|
||||
['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 (let i = 1; i <= 28; i++) {
|
||||
daySelect.innerHTML += `<option value="${i}">Day ${i}</option>`;
|
||||
for (var 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 => {
|
||||
if (data.success) window.location.reload();
|
||||
else lt.toast.error(data.error || 'Failed to toggle');
|
||||
}).catch(err => lt.toast.error('Failed to toggle'));
|
||||
}
|
||||
|
||||
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 showCreateModal() {
|
||||
document.getElementById('recModalTitle').textContent = 'Create Recurring Ticket';
|
||||
document.getElementById('recurringForm').reset();
|
||||
document.getElementById('recurring_id').value = '';
|
||||
updateScheduleOptions();
|
||||
lt.modal.open('recurringModal');
|
||||
}
|
||||
|
||||
function editRecurring(id) {
|
||||
lt.api.get('/api/manage_recurring.php?id=' + id)
|
||||
.then(data => {
|
||||
.then(function (data) {
|
||||
if (data.success && data.recurring) {
|
||||
const rt = data.recurring;
|
||||
var 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('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('category').value = rt.category || 'General';
|
||||
document.getElementById('type').value = rt.type || 'Issue';
|
||||
document.getElementById('priority').value = rt.priority || 4;
|
||||
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('modalTitle').textContent = 'Edit Recurring Ticket';
|
||||
document.getElementById('recModalTitle').textContent = 'Edit Recurring Ticket';
|
||||
lt.modal.open('recurringModal');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load users for assignee dropdown
|
||||
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'); });
|
||||
}
|
||||
|
||||
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 delete');
|
||||
}).catch(function () { 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 loadUsers() {
|
||||
lt.api.get('/api/get_users.php')
|
||||
.then(data => {
|
||||
.then(function (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);
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
updateScheduleOptions();
|
||||
loadUsers();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
+128
-175
@@ -1,158 +1,130 @@
|
||||
<?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();
|
||||
$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 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="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">
|
||||
<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="table-wrapper">
|
||||
<table>
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="Ticket templates">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Template Name</th>
|
||||
<th>Category</th>
|
||||
<th>Type</th>
|
||||
<th>Priority</th>
|
||||
<th>Active</th>
|
||||
<th>Actions</th>
|
||||
<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 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'; ?>
|
||||
<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>
|
||||
<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 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; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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 lt-modal-lg">
|
||||
<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 class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
<button type="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 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 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 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="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 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="setting-grid-3">
|
||||
<div class="setting-row setting-row-compact">
|
||||
<label for="category">Category</label>
|
||||
<select id="category" name="category">
|
||||
<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>
|
||||
<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>
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
|
||||
<option value="<?= $c ?>"><?= $c ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row setting-row-compact">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" name="type">
|
||||
<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>
|
||||
<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>
|
||||
<?php foreach (['Maintenance','Install','Task','Upgrade','Issue','Problem'] as $t): ?>
|
||||
<option value="<?= $t ?>"><?= $t ?></option>
|
||||
<?php endforeach ?>
|
||||
</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>
|
||||
<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="setting-row">
|
||||
<label><input type="checkbox" id="is_active" name="is_active" checked> Active</label>
|
||||
<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">
|
||||
@@ -163,8 +135,24 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
const templates = <?php echo json_encode($templates ?? []); ?>;
|
||||
<script nonce="<?= $nonce ?>">
|
||||
var templates = <?= json_encode($templates ?? []) ?>;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('templateForm').addEventListener('submit', function (e) {
|
||||
saveTemplate(e);
|
||||
});
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
function showCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create Template';
|
||||
@@ -174,85 +162,50 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
lt.modal.open('templateModal');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
lt.modal.close('templateModal');
|
||||
}
|
||||
|
||||
// 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-template':
|
||||
editTemplate(target.dataset.id);
|
||||
break;
|
||||
case 'delete-template':
|
||||
deleteTemplate(target.dataset.id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Form submit handler
|
||||
document.getElementById('templateForm').addEventListener('submit', function(e) {
|
||||
saveTemplate(e);
|
||||
});
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
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);
|
||||
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('category').value = tpl.category || '';
|
||||
document.getElementById('type').value = tpl.type || '';
|
||||
document.getElementById('priority').value = tpl.default_priority || 4;
|
||||
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');
|
||||
}
|
||||
|
||||
function deleteTemplate(id) {
|
||||
showConfirmModal('Delete Template', 'Delete this template?', 'error', function() {
|
||||
showConfirmModal('Delete Template', 'Delete this template? This cannot be undone.', 'error', function () {
|
||||
lt.api.delete('/api/manage_templates.php?id=' + id)
|
||||
.then(data => {
|
||||
.then(function (data) {
|
||||
if (data.success) window.location.reload();
|
||||
else lt.toast.error(data.error || 'Failed to delete');
|
||||
}).catch(err => lt.toast.error('Failed to delete'));
|
||||
}).catch(function () { lt.toast.error('Failed to delete'); });
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
@@ -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();
|
||||
$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>
|
||||
<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: User Activity</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ascii-frame-outer admin-container">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
<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">
|
||||
|
||||
<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">
|
||||
<!-- 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="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 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="admin-form-actions">
|
||||
<button type="submit" class="btn">APPLY</button>
|
||||
<a href="?" class="btn btn-secondary">RESET</a>
|
||||
<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>
|
||||
|
||||
<!-- User Activity Table -->
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<!-- 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>
|
||||
<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 ?>
|
||||
|
||||
<!-- User activity table -->
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="User activity">
|
||||
<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>
|
||||
<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 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 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 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 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; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
<script nonce="<?php echo $nonce; ?>">if (window.lt) lt.keys.initDefaults();</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
@@ -1,175 +1,150 @@
|
||||
<?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();
|
||||
$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="ascii-frame-outer admin-container">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header">Status Workflow Designer</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="admin-header-row">
|
||||
<h2>Status Transitions</h2>
|
||||
<button data-action="show-create-modal" class="btn">+ NEW TRANSITION</button>
|
||||
</div>
|
||||
|
||||
<p class="text-muted-green mb-1">
|
||||
Define which status transitions are allowed. This controls what options appear in the status dropdown.
|
||||
</p>
|
||||
|
||||
<!-- Visual Workflow Diagram -->
|
||||
<div class="workflow-diagram">
|
||||
<h4 class="admin-section-title">Workflow Diagram</h4>
|
||||
<div class="workflow-diagram-nodes">
|
||||
<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):
|
||||
$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
|
||||
$slug = strtolower(str_replace(' ', '-', $status));
|
||||
$toCount = 0;
|
||||
if (isset($workflows)) {
|
||||
foreach ($workflows as $w) {
|
||||
if ($w['from_status'] === $status) $toCount++;
|
||||
}
|
||||
}
|
||||
echo "→ $toCount transitions";
|
||||
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>
|
||||
<?php endforeach; ?>
|
||||
<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>
|
||||
|
||||
<!-- Transitions Table -->
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<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>From Status</th>
|
||||
<th>→</th>
|
||||
<th>To Status</th>
|
||||
<th>Requires Comment</th>
|
||||
<th>Requires Admin</th>
|
||||
<th>Active</th>
|
||||
<th>Actions</th>
|
||||
<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 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 data-label="From">
|
||||
<span class="lt-status lt-status-<?= $fromSlug ?>"><?= 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 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 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 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>
|
||||
<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 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>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
<div class="lt-modal-overlay" id="workflowModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||
<div class="lt-modal lt-modal-sm">
|
||||
<div class="lt-modal-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="modalTitle">Create Transition</span>
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
<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="setting-row">
|
||||
<label for="from_status">From Status *</label>
|
||||
<select id="from_status" name="from_status" required>
|
||||
<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="setting-row">
|
||||
<label for="to_status">To Status *</label>
|
||||
<select id="to_status" name="to_status" required>
|
||||
<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="setting-row">
|
||||
<label><input type="checkbox" id="requires_comment" name="requires_comment"> Requires comment</label>
|
||||
<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="setting-row">
|
||||
<label><input type="checkbox" id="requires_admin" name="requires_admin"> Requires admin privileges</label>
|
||||
<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="setting-row">
|
||||
<label><input type="checkbox" id="is_active" name="is_active" checked> Active</label>
|
||||
<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">
|
||||
@@ -180,92 +155,73 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
</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');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
lt.modal.close('workflowModal');
|
||||
}
|
||||
|
||||
// Event delegation for data-action handlers
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
const action = target.dataset.action;
|
||||
switch (action) {
|
||||
case 'show-create-modal':
|
||||
showCreateModal();
|
||||
break;
|
||||
case 'edit-transition':
|
||||
editTransition(target.dataset.id);
|
||||
break;
|
||||
case 'delete-transition':
|
||||
deleteTransition(target.dataset.id);
|
||||
break;
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// Form submit handler
|
||||
document.getElementById('workflowForm').addEventListener('submit', function (e) {
|
||||
saveTransition(e);
|
||||
});
|
||||
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
|
||||
function saveTransition(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
transition_id: document.getElementById('transition_id').value,
|
||||
from_status: document.getElementById('from_status').value,
|
||||
to_status: document.getElementById('to_status').value,
|
||||
requires_comment: document.getElementById('requires_comment').checked ? 1 : 0,
|
||||
requires_admin: document.getElementById('requires_admin').checked ? 1 : 0,
|
||||
is_active: document.getElementById('is_active').checked ? 1 : 0
|
||||
};
|
||||
|
||||
const url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_id : '');
|
||||
const apiCall = data.transition_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
||||
apiCall.then(result => {
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
lt.toast.error(result.error || 'Failed to save');
|
||||
}
|
||||
}).catch(err => lt.toast.error('Failed to save'));
|
||||
function 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');
|
||||
}
|
||||
|
||||
function editTransition(id) {
|
||||
const wf = workflows.find(w => w.transition_id == 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('is_active').checked = wf.is_active == 1;
|
||||
document.getElementById('modalTitle').textContent = 'Edit Transition';
|
||||
document.getElementById('wf_is_active').checked = wf.is_active == 1;
|
||||
document.getElementById('wfModalTitle').textContent = 'Edit Transition';
|
||||
lt.modal.open('workflowModal');
|
||||
}
|
||||
|
||||
function deleteTransition(id) {
|
||||
showConfirmModal('Delete Transition', 'Delete this status transition?', 'error', function() {
|
||||
showConfirmModal('Delete Transition', 'Delete this status transition? This cannot be undone.', 'error', function () {
|
||||
lt.api.delete('/api/manage_workflows.php?id=' + id)
|
||||
.then(data => {
|
||||
.then(function (data) {
|
||||
if (data.success) window.location.reload();
|
||||
else lt.toast.error(data.error || 'Failed to delete');
|
||||
}).catch(err => lt.toast.error('Failed to delete'));
|
||||
}).catch(function () { lt.toast.error('Failed to delete'); });
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* layout_footer.php — Shared bottom-of-page partial for all views.
|
||||
*
|
||||
* Expected variables available from the including view (set before require):
|
||||
* string $nonce CSP nonce from SecurityHeadersMiddleware::getNonce()
|
||||
* array|null $pageScripts Optional array of extra JS paths to load after base.js
|
||||
* string|null $pageInlineScript Optional raw JS string to run after all scripts load
|
||||
*
|
||||
* Globals used:
|
||||
* $GLOBALS['currentUser'] — user array (user_id, username, is_admin)
|
||||
* $GLOBALS['config'] — app config array (TIMEZONE, TIMEZONE_ABBREV)
|
||||
* CsrfMiddleware::getToken() — returns current CSRF token string
|
||||
*/
|
||||
|
||||
// layout_footer.php — JS globals + runtime scripts are loaded here
|
||||
?>
|
||||
|
||||
</main><!-- /#main-content / .lt-main -->
|
||||
|
||||
<!-- base.js + utils.js + globals already loaded in <head> via layout_header.php -->
|
||||
|
||||
<?php if (!empty($pageScripts)): ?>
|
||||
<!-- PAGE-SPECIFIC SCRIPTS -->
|
||||
<?php foreach ($pageScripts as $_ltf_script): ?>
|
||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="<?= htmlspecialchars($_ltf_script, ENT_QUOTES, 'UTF-8') ?>"></script>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($pageInlineScript)): ?>
|
||||
<!-- PAGE INLINE SCRIPT -->
|
||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= $pageInlineScript ?>
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- LT INIT — runs boot animation once per session, then sets up UI handlers -->
|
||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
||||
if (window.lt) {
|
||||
lt.init({ bootName: 'TINKER TICKETS', skipBoot: false });
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
/**
|
||||
* layout_header.php — Shared top-of-page partial for all views.
|
||||
*
|
||||
* Expected variables set by the including view before require:
|
||||
* string $pageTitle Page title suffix (e.g. "Dashboard", "Ticket #42")
|
||||
* string $activeNav Active nav key: 'dashboard', 'tickets', 'admin-*'
|
||||
* array|null $pageStyles Optional extra CSS hrefs to load
|
||||
* string $nonce CSP nonce from SecurityHeadersMiddleware::getNonce()
|
||||
*
|
||||
* Globals used:
|
||||
* $GLOBALS['currentUser'] — user array (username, display_name, is_admin, groups)
|
||||
* $GLOBALS['config'] — app config array
|
||||
* CsrfMiddleware::getToken() — returns current CSRF token string
|
||||
*/
|
||||
|
||||
$_lt_user = $GLOBALS['currentUser'] ?? [];
|
||||
$_lt_isAdmin = !empty($_lt_user['is_admin']);
|
||||
$_lt_navActive = $activeNav ?? 'dashboard';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#030508">
|
||||
<title><?= htmlspecialchars($pageTitle ?? 'Dashboard', ENT_QUOTES, 'UTF-8') ?> — Tinker Tickets</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/css/base.css">
|
||||
<?php if (!empty($pageStyles)): ?>
|
||||
<?php foreach ($pageStyles as $_lt_css): ?>
|
||||
<link rel="stylesheet" href="<?= htmlspecialchars($_lt_css, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<link rel="icon" href="/assets/images/favicon.png" type="image/png">
|
||||
<!-- Base JS loaded in head so lt.* is available for inline view scripts -->
|
||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/base.js"></script>
|
||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/utils.js"></script>
|
||||
<!-- Inline JS globals (CSRF, timezone, user) available immediately -->
|
||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
||||
window.CSRF_TOKEN = <?= json_encode(CsrfMiddleware::getToken(), JSON_UNESCAPED_UNICODE) ?>;
|
||||
window.APP_TIMEZONE = <?= json_encode($GLOBALS['config']['TIMEZONE'] ?? 'UTC', JSON_UNESCAPED_UNICODE) ?>;
|
||||
window.APP_TIMEZONE_ABBREV = <?= json_encode($GLOBALS['config']['TIMEZONE_ABBREV'] ?? 'UTC', JSON_UNESCAPED_UNICODE) ?>;
|
||||
window.APP_TIMEZONE_OFFSET = <?= (int)($GLOBALS['config']['TIMEZONE_OFFSET'] ?? 0) ?>;
|
||||
window.CURRENT_USER = <?= json_encode([
|
||||
'id' => (int)($GLOBALS['currentUser']['user_id'] ?? 0),
|
||||
'username'=> $GLOBALS['currentUser']['username'] ?? '',
|
||||
'isAdmin' => !empty($GLOBALS['currentUser']['is_admin']),
|
||||
], JSON_UNESCAPED_UNICODE) ?>;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- SKIP LINK -->
|
||||
<a class="lt-skip-link" href="#main-content">Skip to main content</a>
|
||||
|
||||
<!-- BOOT OVERLAY — controlled by lt.boot() in base.js; shown once per session -->
|
||||
<div id="lt-boot" class="lt-boot-overlay" data-app-name="TINKER TICKETS" style="display:none" aria-hidden="true">
|
||||
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE NAV DRAWER -->
|
||||
<div class="lt-nav-drawer" id="lt-nav-drawer" role="dialog" aria-modal="true" aria-label="Mobile navigation">
|
||||
<div class="lt-nav-drawer-backdrop" id="lt-nav-drawer-backdrop" data-action="close-nav-drawer" aria-hidden="true"></div>
|
||||
<nav class="lt-nav-drawer-panel" aria-label="Mobile main navigation">
|
||||
<div class="lt-nav-drawer-header">
|
||||
<span class="lt-nav-drawer-title">TINKER TICKETS</span>
|
||||
<button type="button"
|
||||
class="lt-nav-drawer-close"
|
||||
id="lt-nav-drawer-close"
|
||||
data-action="close-nav-drawer"
|
||||
aria-label="Close navigation">✕</button>
|
||||
</div>
|
||||
<ul class="lt-nav-drawer-list" role="list">
|
||||
<li>
|
||||
<a href="/"
|
||||
class="lt-nav-drawer-link<?= $_lt_navActive === 'dashboard' ? ' active' : '' ?>"
|
||||
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<?php if ($_lt_isAdmin): ?>
|
||||
<li class="lt-nav-drawer-section-label" aria-hidden="true">Admin</li>
|
||||
<li><a href="/admin/templates" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-templates' ? ' active' : '' ?>">Templates</a></li>
|
||||
<li><a href="/admin/workflow" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-workflow' ? ' active' : '' ?>">Workflow</a></li>
|
||||
<li><a href="/admin/recurring-tickets" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-recurring' ? ' active' : '' ?>">Recurring</a></li>
|
||||
<li><a href="/admin/custom-fields" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-custom-fields' ? ' active' : '' ?>">Custom Fields</a></li>
|
||||
<li><a href="/admin/user-activity" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-user-activity' ? ' active' : '' ?>">User Activity</a></li>
|
||||
<li><a href="/admin/audit-log" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-audit-log' ? ' active' : '' ?>">Audit Log</a></li>
|
||||
<li><a href="/admin/api-keys" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-api-keys' ? ' active' : '' ?>">API Keys</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</div><!-- /.lt-nav-drawer -->
|
||||
|
||||
<!-- PRIMARY HEADER -->
|
||||
<header class="lt-header" role="banner">
|
||||
<div class="lt-header-left">
|
||||
|
||||
<!-- Hamburger — opens mobile nav drawer -->
|
||||
<button type="button"
|
||||
class="lt-menu-btn"
|
||||
id="lt-menu-btn"
|
||||
data-action="open-nav-drawer"
|
||||
aria-label="Open navigation menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="lt-nav-drawer">
|
||||
<span class="lt-menu-btn-bar"></span>
|
||||
<span class="lt-menu-btn-bar"></span>
|
||||
<span class="lt-menu-btn-bar"></span>
|
||||
</button>
|
||||
|
||||
<!-- Brand -->
|
||||
<div class="lt-brand">
|
||||
<a href="/"
|
||||
class="lt-brand-title lt-glitch"
|
||||
data-text="TINKER TICKETS"
|
||||
style="text-decoration:none"
|
||||
aria-label="Tinker Tickets home">TINKER TICKETS</a>
|
||||
<span class="lt-brand-subtitle">LotusGuild Infrastructure</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop navigation -->
|
||||
<nav class="lt-nav" aria-label="Main navigation">
|
||||
<a href="/"
|
||||
class="lt-nav-link<?= $_lt_navActive === 'dashboard' ? ' active' : '' ?>"
|
||||
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>
|
||||
Dashboard
|
||||
</a>
|
||||
<?php if ($_lt_isAdmin): ?>
|
||||
<div class="lt-nav-dropdown" data-action="toggle-nav-dropdown">
|
||||
<a href="#"
|
||||
class="lt-nav-link<?= str_starts_with($_lt_navActive, 'admin') ? ' active' : '' ?>"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-controls="lt-admin-dropdown-menu">
|
||||
Admin ▾
|
||||
</a>
|
||||
<ul class="lt-nav-dropdown-menu"
|
||||
id="lt-admin-dropdown-menu"
|
||||
role="menu"
|
||||
aria-label="Admin menu">
|
||||
<li role="none"><a href="/admin/templates" role="menuitem" class="<?= $_lt_navActive === 'admin-templates' ? 'active' : '' ?>">Templates</a></li>
|
||||
<li role="none"><a href="/admin/workflow" role="menuitem" class="<?= $_lt_navActive === 'admin-workflow' ? 'active' : '' ?>">Workflow</a></li>
|
||||
<li role="none"><a href="/admin/recurring-tickets" role="menuitem" class="<?= $_lt_navActive === 'admin-recurring' ? 'active' : '' ?>">Recurring</a></li>
|
||||
<li role="none"><a href="/admin/custom-fields" role="menuitem" class="<?= $_lt_navActive === 'admin-custom-fields' ? 'active' : '' ?>">Custom Fields</a></li>
|
||||
<li role="none"><a href="/admin/user-activity" role="menuitem" class="<?= $_lt_navActive === 'admin-user-activity' ? 'active' : '' ?>">User Activity</a></li>
|
||||
<li role="none"><a href="/admin/audit-log" role="menuitem" class="<?= $_lt_navActive === 'admin-audit-log' ? 'active' : '' ?>">Audit Log</a></li>
|
||||
<li role="none"><a href="/admin/api-keys" role="menuitem" class="<?= $_lt_navActive === 'admin-api-keys' ? 'active' : '' ?>">API Keys</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</nav><!-- /.lt-nav -->
|
||||
|
||||
</div><!-- /.lt-header-left -->
|
||||
|
||||
<div class="lt-header-right">
|
||||
<?php if (!empty($_lt_user)): ?>
|
||||
<span class="lt-header-username">
|
||||
<?= htmlspecialchars($_lt_user['display_name'] ?? $_lt_user['username'] ?? '', ENT_QUOTES, 'UTF-8') ?>
|
||||
</span>
|
||||
<?php if ($_lt_isAdmin): ?>
|
||||
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div><!-- /.lt-header-right -->
|
||||
|
||||
</header><!-- /.lt-header -->
|
||||
|
||||
<main class="lt-main lt-container" id="main-content">
|
||||
Reference in New Issue
Block a user