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:
+294
-323
@@ -1,358 +1,329 @@
|
||||
<?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();
|
||||
|
||||
$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; ?>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<?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>
|
||||
|
||||
<!-- OUTER FRAME: Create Ticket Form Container -->
|
||||
<div class="ascii-frame-outer">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
<!-- ── 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 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>
|
||||
|
||||
<?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>
|
||||
</div>
|
||||
</div>
|
||||
<?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']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<p class="form-hint">
|
||||
Select a template to auto-fill form fields
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></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>
|
||||
<div id="duplicatesList" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
<div class="detail-quarter">
|
||||
<label for="category">Category</label>
|
||||
<select id="category" name="category" class="editable">
|
||||
<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>
|
||||
<div class="detail-quarter">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" name="type" class="editable">
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</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']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<p class="form-hint">
|
||||
Select a user to assign this ticket to
|
||||
</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>
|
||||
</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">
|
||||
<?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>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($allGroups)): ?>
|
||||
<span class="text-muted">No groups available</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p class="form-hint-warning">
|
||||
Select which groups can view this ticket
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<!-- 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>
|
||||
<!-- END OUTER FRAME -->
|
||||
</div>
|
||||
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
// Duplicate detection with debounce
|
||||
let duplicateCheckTimeout = null;
|
||||
<!-- ── 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">
|
||||
|
||||
document.getElementById('title').addEventListener('input', function() {
|
||||
clearTimeout(duplicateCheckTimeout);
|
||||
const title = this.value.trim();
|
||||
<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="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="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>
|
||||
|
||||
<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><!-- /.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>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 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(_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>
|
||||
}());
|
||||
</script>
|
||||
|
||||
<?php include __DIR__ . '/layout_footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user