2025-05-16 20:02:49 -04:00
< ? php
2026-03-27 19:05:42 -04:00
/**
* CreateTicketView.php — New ticket creation form, redesigned for TDS v1.2
* Variables: $templates (array), $allUsers (array), $error (string|null)
*/
2026-01-28 20:27:15 -05:00
require_once __DIR__ . '/../middleware/SecurityHeadersMiddleware.php' ;
require_once __DIR__ . '/../middleware/CsrfMiddleware.php' ;
2026-01-07 10:52:10 -05:00
2026-03-27 19:05:42 -04:00
$nonce = SecurityHeadersMiddleware :: getNonce ();
$pageTitle = 'New Ticket' ;
$activeNav = 'dashboard' ;
2026-03-28 13:30:00 -04:00
$pageStyles = [ '/assets/css/dashboard.css?v=20260327' , '/assets/css/ticket.css?v=20260327' ];
2026-03-27 19:05:42 -04:00
$pageScripts = [
'/assets/js/keyboard-shortcuts.js?v=20260327' ,
];
2026-01-07 10:52:10 -05:00
2026-03-27 19:05:42 -04:00
include __DIR__ . '/layout_header.php' ;
?>
2026-01-07 10:52:10 -05:00
2026-03-27 19:05:42 -04:00
<!-- 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>
<!-- ═══════════════════════════════════════════════════════════
CREATE TICKET FORM
═══════════════════════════════════════════════════════════ -->
<form method="POST"
action="<?= htmlspecialchars($GLOBALS['config']['BASE_URL'], ENT_QUOTES, 'UTF-8') ?>/ticket/create"
class="create-ticket-form"
novalidate>
2026-03-28 21:26:52 -04:00
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars(CsrfMiddleware::getToken(), ENT_QUOTES, 'UTF-8') ?>">
2026-03-27 19:05:42 -04:00
<?php if (isset($error)): ?>
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
<strong>Error:</strong> <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php endif ?>
<!-- ── 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 ?>
</select>
<p class="lt-form-hint">Selecting a template pre-fills the form fields.</p>
</div>
</div>
</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>
<!-- 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>
<!-- ── 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">
<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>
2026-01-07 10:52:10 -05:00
</div>
2026-01-23 10:01:50 -05:00
2026-03-27 19:05:42 -04:00
<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>
2026-01-07 10:52:10 -05:00
2026-03-27 19:05:42 -04:00
<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>
<option value="Security">Security</option>
<option value="General" selected>General</option>
</select>
</div>
2026-01-07 10:52:10 -05:00
2026-03-27 19:05:42 -04:00
<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>
2026-01-07 10:52:10 -05:00
2026-03-27 19:05:42 -04:00
</div><!-- /.create-ticket-meta-grid -->
</div>
</div>
<!-- ── 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 ?>
</select>
<p class="lt-form-hint">Leave blank to create as unassigned.</p>
</div>
</div>
</div>
<!-- ── 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>
</div>
<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
require_once __DIR__ . '/../models/UserModel.php';
$userModel = new UserModel($conn);
$allGroups = $userModel->getAllGroups();
foreach ($allGroups as $group):
?>
<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 if (empty($allGroups)): ?>
<span class="lt-text-muted lt-text-sm">No groups available</span>
<?php endif ?>
</div>
<p class="lt-form-hint lt-form-hint--warn">Select at least one group for Internal visibility.</p>
</div>
2025-05-16 20:02:49 -04:00
</div>
2026-03-27 19:05:42 -04:00
</div>
<!-- ── 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>
<!-- ── 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>
2026-01-20 09:55:01 -05:00
2026-03-27 19:05:42 -04:00
<!-- Page-specific script: duplicate detection + visibility toggle -->
<script nonce="<?= $nonce ?>">
(function () {
'use strict';
2026-01-20 09:55:01 -05:00
2026-03-27 19:05:42 -04:00
// ── Duplicate detection ───────────────────────────────────
var _dupTimer = null;
2026-01-20 09:55:01 -05:00
2026-03-27 19:05:42 -04:00
document.getElementById('title').addEventListener('input', function () {
clearTimeout(_dupTimer);
var title = this.value.trim();
2026-01-20 09:55:01 -05:00
if (title.length < 5) {
2026-03-20 21:18:16 -04:00
document.getElementById('duplicateWarning').classList.add('is-hidden');
2026-01-20 09:55:01 -05:00
return;
}
2026-03-27 19:05:42 -04:00
_dupTimer = setTimeout(function () { checkDuplicates(title); }, 500);
2026-01-20 09:55:01 -05:00
});
2026-03-27 19:05:42 -04:00
function checkDuplicates(title) {
2026-03-28 21:26:52 -04:00
if (!window.lt || typeof lt.api === 'undefined') return;
2026-03-20 20:29:58 -04:00
lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title))
2026-03-27 19:05:42 -04:00
.then(function (data) {
var warn = document.getElementById('duplicateWarning');
var list = document.getElementById('duplicatesList');
2026-01-20 09:55:01 -05:00
if (data.success && data.duplicates && data.duplicates.length > 0) {
2026-03-28 21:26:52 -04:00
var ul = document.createElement('ul');
ul.className = 'duplicate-list lt-text-sm';
2026-03-27 19:05:42 -04:00
data.duplicates.forEach(function (dup) {
2026-03-28 21:26:52 -04:00
var li = document.createElement('li');
var a = document.createElement('a');
a.href = '/ticket/' + encodeURIComponent(dup.ticket_id);
a.target = '_blank';
a.textContent = '#' + dup.ticket_id;
var dash = document.createTextNode(' \u2014 ' + dup.title + ' ');
var badge = document.createElement('span');
badge.className = 'lt-text-muted';
badge.textContent = '(' + dup.similarity + '% match, ' + dup.status + ')';
li.appendChild(a);
li.appendChild(dash);
li.appendChild(badge);
ul.appendChild(li);
2026-01-20 09:55:01 -05:00
});
2026-03-28 21:26:52 -04:00
var hint = document.createElement('p');
hint.className = 'lt-text-xs lt-text-muted lt-mt-sm';
hint.textContent = 'Check these before creating a new ticket.';
list.innerHTML = '';
list.appendChild(ul);
list.appendChild(hint);
2026-03-27 19:05:42 -04:00
warn.classList.remove('is-hidden');
2026-01-20 09:55:01 -05:00
} else {
2026-03-27 19:05:42 -04:00
warn.classList.add('is-hidden');
2026-01-20 09:55:01 -05:00
}
})
2026-03-28 21:26:52 -04:00
.catch(function () { /* silent — duplicate check is non-critical */ });
2026-01-20 09:55:01 -05:00
}
2026-03-27 19:05:42 -04:00
// ── Visibility groups toggle ──────────────────────────────
2026-01-23 10:01:50 -05:00
function toggleVisibilityGroups() {
2026-03-27 19:05:42 -04:00
var vis = document.getElementById('visibility').value;
var container = document.getElementById('visibilityGroupsContainer');
if (vis === 'internal') {
container.classList.remove('is-hidden');
2026-01-23 10:01:50 -05:00
} else {
2026-03-27 19:05:42 -04:00
container.classList.add('is-hidden');
container.querySelectorAll('.visibility-group-checkbox').forEach(function (cb) { cb.checked = false; });
2026-01-23 10:01:50 -05:00
}
}
2026-01-30 13:15:55 -05:00
2026-03-27 19:05:42 -04:00
// ── 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.'); });
}
2026-01-30 13:15:55 -05:00
2026-03-27 19:05:42 -04:00
// ── Event delegation ──────────────────────────────────────
document.addEventListener('change', function (e) {
var target = e.target.closest('[data-action]');
2026-01-30 13:15:55 -05:00
if (!target) return;
2026-03-27 19:05:42 -04:00
switch (target.getAttribute('data-action')) {
case 'load-template': loadTemplate(); break;
case 'toggle-visibility-groups': toggleVisibilityGroups(); break;
2026-01-30 13:15:55 -05:00
}
});
2026-03-20 20:29:58 -04:00
if (window.lt) lt.keys.initDefaults();
2026-03-27 19:05:42 -04:00
}());
</script>
<?php include __DIR__ . '/layout_footer.php'; ?>