Files
tinker_tickets/views/admin/TemplatesView.php
T
jared 2e450dc01d Apply web_template gap analysis improvements (P1-P3)
P1-A: Fix CSP - add fonts.googleapis.com to style-src, fonts.gstatic.com to font-src
P1-B: CSRF token rotation - add rotateToken() to CsrfMiddleware; bootstrap.php rotates
      after successful validation and stores in $GLOBALS['_new_csrf_token']; add
      apiRespond() helper to append token to responses; lt.api interceptor in
      layout_footer.php auto-updates window.CSRF_TOKEN from responses
P1-C: Styled 403/404 error views with TDS layout instead of raw text; index.php now
      uses requireAdmin() helper eliminating 7 duplicated guard blocks (P3-D)
P2-A: Remove duplicate JS-generated keyboard help modal from keyboard-shortcuts.js;
      '?' key now routes to static #lt-keys-help modal in footer
P2-B: Asset versioning driven by config ASSET_VERSION key; base.css and base.js get
      ?v= cache-busting in layout_header.php
P2-C: Add data-theme="dark" to <html> tag to prevent FOUC on light-mode users
P2-E: Escape status value in dashboard.js hover preview class attribute via lt.escHtml()
P2-F: Replace bespoke showLoadingOverlay() with lt-spinner / lt-loading-text from
      base.css; add .lt-loading-overlay wrapper CSS to dashboard.css
P2-G: Add keyboard-shortcuts.js to all 7 admin views so J/K nav and ? help work
P3-A: APP_NAME, APP_SUBTITLE, APP_VERSION driven from config.php; layout header/footer
      use config values instead of hardcoded strings
P3-G: Replace custom initTableSorting() with lt.sortTable.init() which manages aria-sort

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

213 lines
10 KiB
PHP

<?php
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 = ['/assets/js/keyboard-shortcuts.js'];
include __DIR__ . '/../../views/layout_header.php';
?>
<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">&larr; Dashboard</a>
<span class="lt-text-muted lt-text-xs">/</span>
<span class="lt-text-muted lt-text-xs">Admin: Templates</span>
</div>
<button type="button" class="lt-btn lt-btn-primary" data-action="show-create-modal">+ NEW TEMPLATE</button>
</div>
<div class="lt-frame">
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
<div class="lt-section-header">Ticket Template Management</div>
<div class="lt-section-body">
<p class="lt-text-sm lt-text-muted" style="margin-bottom:0.75rem">
Templates pre-fill ticket creation forms with standard content for common ticket types.
</p>
<div class="lt-table-wrap">
<table class="lt-table lt-table-responsive" aria-label="Ticket templates">
<thead>
<tr>
<th scope="col">Template Name</th>
<th scope="col">Category</th>
<th scope="col">Type</th>
<th scope="col">Priority</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($templates)): ?>
<tr><td colspan="6" class="lt-empty">No templates defined. Create templates to speed up ticket creation.</td></tr>
<?php else: foreach ($templates as $tpl): ?>
<tr>
<td data-label="Name"><strong><?= htmlspecialchars($tpl['template_name']) ?></strong></td>
<td data-label="Category" class="lt-text-xs"><?= htmlspecialchars($tpl['category'] ?? 'Any') ?></td>
<td data-label="Type" class="lt-text-xs"><?= htmlspecialchars($tpl['type'] ?? 'Any') ?></td>
<?php $tp = (int)($tpl['default_priority'] ?? 4); $tBadge = match($tp) { 1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4' }; ?>
<td data-label="Priority"><span class="lt-badge <?= $tBadge ?>">P<?= $tp ?></span></td>
<td data-label="Status">
<span class="lt-status <?= ($tpl['is_active'] ?? 1) ? 'lt-status-open' : 'lt-status-closed' ?>">
<?= ($tpl['is_active'] ?? 1) ? 'Active' : 'Inactive' ?>
</span>
</td>
<td data-label="Actions">
<div class="lt-btn-group">
<button type="button" class="lt-btn lt-btn-sm"
data-action="edit-template" data-id="<?= $tpl['template_id'] ?>">EDIT</button>
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
data-action="delete-template" data-id="<?= $tpl['template_id'] ?>">DEL</button>
</div>
</td>
</tr>
<?php endforeach; endif ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Create/Edit Modal -->
<div class="lt-modal-overlay" id="templateModal" aria-hidden="true" role="dialog"
aria-modal="true" aria-labelledby="modalTitle">
<div class="lt-modal">
<div class="lt-modal-header">
<span class="lt-modal-title" id="modalTitle">Create Template</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<form id="templateForm">
<input type="hidden" id="template_id" name="template_id">
<div class="lt-modal-body">
<div class="lt-form-group">
<label class="lt-label" for="template_name">Template Name *</label>
<input type="text" id="template_name" name="template_name" class="lt-input" required>
</div>
<div class="lt-form-group">
<label class="lt-label" for="title_template">Title Template</label>
<input type="text" id="title_template" name="title_template" class="lt-input"
placeholder="Pre-filled title text">
</div>
<div class="lt-form-group">
<label class="lt-label" for="description_template">Description Template</label>
<textarea id="description_template" name="description_template" class="lt-input lt-textarea"
rows="10" placeholder="Pre-filled description content"></textarea>
</div>
<div class="create-ticket-meta-grid">
<div class="lt-form-group">
<label class="lt-label" for="tpl-category">Category</label>
<select id="tpl-category" name="category" class="lt-select">
<option value="">Any</option>
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
<option value="<?= $c ?>"><?= $c ?></option>
<?php endforeach ?>
</select>
</div>
<div class="lt-form-group">
<label class="lt-label" for="tpl-type">Type</label>
<select id="tpl-type" name="type" class="lt-select">
<option value="">Any</option>
<?php foreach (['Maintenance','Install','Task','Upgrade','Issue','Problem'] as $t): ?>
<option value="<?= $t ?>"><?= $t ?></option>
<?php endforeach ?>
</select>
</div>
<div class="lt-form-group">
<label class="lt-label" for="tpl-priority">Priority</label>
<select id="tpl-priority" name="priority" class="lt-select">
<?php foreach ([1=>'P1',2=>'P2',3=>'P3',4=>'P4 (default)',5=>'P5'] as $v=>$l): ?>
<option value="<?= $v ?>" <?= $v === 4 ? 'selected' : '' ?>><?= $l ?></option>
<?php endforeach ?>
</select>
</div>
</div>
<div class="lt-form-group">
<label class="lt-filter-option">
<input type="checkbox" class="lt-checkbox" id="is_active" name="is_active" checked>
Active
</label>
</div>
</div>
<div class="lt-modal-footer">
<button type="submit" class="lt-btn lt-btn-primary">SAVE</button>
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>CANCEL</button>
</div>
</form>
</div>
</div>
<script nonce="<?= $nonce ?>">
var templates = <?= json_encode($templates ?? [], JSON_HEX_TAG) ?>;
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';
document.getElementById('templateForm').reset();
document.getElementById('template_id').value = '';
document.getElementById('is_active').checked = true;
lt.modal.open('templateModal');
}
function editTemplate(id) {
var tpl = templates.find(function (t) { return t.template_id == id; });
if (!tpl) return;
document.getElementById('template_id').value = tpl.template_id;
document.getElementById('template_name').value = tpl.template_name;
document.getElementById('title_template').value = tpl.title_template || '';
document.getElementById('description_template').value = tpl.description_template || '';
document.getElementById('tpl-category').value = tpl.category || '';
document.getElementById('tpl-type').value = tpl.type || '';
document.getElementById('tpl-priority').value = tpl.default_priority || 4;
document.getElementById('is_active').checked = (tpl.is_active ?? 1) == 1;
document.getElementById('modalTitle').textContent = 'Edit Template';
lt.modal.open('templateModal');
}
function deleteTemplate(id) {
showConfirmModal('Delete Template', 'Delete this template? This cannot be undone.', 'error', function () {
lt.api.delete('/api/manage_templates.php?id=' + id)
.then(function (data) {
if (data.success) window.location.reload();
else lt.toast.error(data.error || 'Failed to delete');
}).catch(function () { lt.toast.error('Failed to delete'); });
});
}
function saveTemplate(e) {
e.preventDefault();
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>
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>