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:
2026-03-27 19:05:42 -04:00
parent 1989bcb8c8
commit 79c2d2b513
16 changed files with 11265 additions and 14094 deletions
+226 -260
View File
@@ -1,278 +1,244 @@
<?php
// Admin view for managing custom fields
// Receives $customFields from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
$nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = 'Custom Fields';
$activeNav = 'admin-custom-fields';
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
$pageScripts = [];
include __DIR__ . '/../../views/layout_header.php';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Fields - Admin</title>
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="/assets/css/base.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260320">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260320">
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js?v=20260320"></script>
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
<div class="user-header">
<div class="user-header-left">
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
<span class="admin-page-title">Admin: Custom Fields</span>
</div>
<div class="user-header-right">
<?php if (isset($GLOBALS['currentUser'])): ?>
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
<span class="admin-badge">[ ADMIN ]</span>
<?php endif; ?>
</div>
<div class="lt-page-header">
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">&larr; Dashboard</a>
<span class="lt-text-muted lt-text-xs">/</span>
<span class="lt-text-muted lt-text-xs">Admin: Custom Fields</span>
</div>
<button type="button" class="lt-btn lt-btn-primary" data-action="show-create-modal">+ NEW FIELD</button>
</div>
<div class="lt-frame">
<span class="lt-frame-bl">╚</span><span class="lt-frame-br"></span>
<div class="lt-section-header">Custom Field Definitions</div>
<div class="lt-section-body">
<p class="lt-text-sm lt-text-muted" style="margin-bottom:0.75rem">
Custom fields extend tickets with additional metadata. Fields appear on the ticket form based on category.
</p>
<div class="lt-table-wrap">
<table class="lt-table lt-table-responsive" aria-label="Custom fields">
<thead>
<tr>
<th scope="col">Order</th>
<th scope="col">Field Name</th>
<th scope="col">Label</th>
<th scope="col">Type</th>
<th scope="col">Category</th>
<th scope="col">Required</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($customFields)): ?>
<tr><td colspan="8" class="lt-empty">No custom fields defined. Create fields to extend ticket metadata.</td></tr>
<?php else: foreach ($customFields as $field): ?>
<tr>
<td data-label="Order" class="lt-text-xs lt-text-muted"><?= (int)$field['display_order'] ?></td>
<td data-label="Field Name"><code class="lt-text-cyan lt-text-xs"><?= htmlspecialchars($field['field_name']) ?></code></td>
<td data-label="Label"><strong><?= htmlspecialchars($field['field_label']) ?></strong></td>
<td data-label="Type" class="lt-text-xs"><?= ucfirst($field['field_type']) ?></td>
<td data-label="Category" class="lt-text-xs"><?= htmlspecialchars($field['category'] ?? 'All') ?></td>
<td data-label="Required" style="text-align:center">
<?= $field['is_required'] ? '<span class="lt-text-amber">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
</td>
<td data-label="Status">
<span class="lt-status <?= $field['is_active'] ? 'lt-status-open' : 'lt-status-closed' ?>">
<?= $field['is_active'] ? 'Active' : 'Inactive' ?>
</span>
</td>
<td data-label="Actions">
<div class="lt-btn-group">
<button type="button" class="lt-btn lt-btn-sm"
data-action="edit-field" data-id="<?= $field['field_id'] ?>">EDIT</button>
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
data-action="delete-field" data-id="<?= $field['field_id'] ?>">DEL</button>
</div>
</td>
</tr>
<?php endforeach; endif ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="ascii-frame-outer admin-container">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Custom Fields Management</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div class="admin-header-row">
<h2>Custom Field Definitions</h2>
<button data-action="show-create-modal" class="btn">+ NEW FIELD</button>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Order</th>
<th>Field Name</th>
<th>Label</th>
<th>Type</th>
<th>Category</th>
<th>Required</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($customFields)): ?>
<tr>
<td colspan="8" class="empty-state">No custom fields defined.</td>
</tr>
<?php else: ?>
<?php foreach ($customFields as $field): ?>
<tr>
<td><?php echo $field['display_order']; ?></td>
<td><code><?php echo htmlspecialchars($field['field_name']); ?></code></td>
<td><?php echo htmlspecialchars($field['field_label']); ?></td>
<td><?php echo ucfirst($field['field_type']); ?></td>
<td><?php echo htmlspecialchars($field['category'] ?? 'All'); ?></td>
<td><?php echo $field['is_required'] ? 'Yes' : 'No'; ?></td>
<td>
<span class="<?php echo $field['is_active'] ? 'text-open' : 'text-closed'; ?>">
<?php echo $field['is_active'] ? 'Active' : 'Inactive'; ?>
</span>
</td>
<td>
<button data-action="edit-field" data-id="<?php echo $field['field_id']; ?>" class="btn btn-small">EDIT</button>
<button data-action="delete-field" data-id="<?php echo $field['field_id']; ?>" class="btn btn-small btn-danger">DELETE</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Create/Edit Modal -->
<div class="lt-modal-overlay" id="fieldModal" aria-hidden="true" role="dialog"
aria-modal="true" aria-labelledby="cfModalTitle">
<div class="lt-modal">
<div class="lt-modal-header">
<span class="lt-modal-title" id="cfModalTitle">Create Custom Field</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<!-- Create/Edit Modal -->
<div class="lt-modal-overlay" id="fieldModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="lt-modal lt-modal-sm">
<div class="lt-modal-header">
<span class="lt-modal-title" id="modalTitle">Create Custom Field</span>
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div>
<form id="fieldForm">
<input type="hidden" id="field_id" name="field_id">
<div class="lt-modal-body">
<div class="setting-row">
<label for="field_name">Field Name * (internal)</label>
<input type="text" id="field_name" name="field_name" required pattern="[a-z_]+" placeholder="e.g., server_name">
</div>
<div class="setting-row">
<label for="field_label">Field Label * (display)</label>
<input type="text" id="field_label" name="field_label" required placeholder="e.g., Server Name">
</div>
<div class="setting-row">
<label for="field_type">Field Type *</label>
<select id="field_type" name="field_type" required data-action="toggle-options-field">
<option value="text">Text</option>
<option value="textarea">Text Area</option>
<option value="select">Dropdown (Select)</option>
<option value="checkbox">Checkbox</option>
<option value="date">Date</option>
<option value="number">Number</option>
</select>
</div>
<div class="setting-row is-hidden" id="options_row">
<label for="field_options">Options (one per line)</label>
<textarea id="field_options" name="field_options" rows="4" placeholder="Option 1&#10;Option 2&#10;Option 3"></textarea>
</div>
<div class="setting-row">
<label for="category">Category (empty = all)</label>
<select id="category" name="category">
<option value="">All Categories</option>
<option value="General">General</option>
<option value="Hardware">Hardware</option>
<option value="Software">Software</option>
<option value="Network">Network</option>
<option value="Security">Security</option>
</select>
</div>
<div class="setting-row">
<label for="display_order">Display Order</label>
<input type="number" id="display_order" name="display_order" value="0" min="0">
</div>
<div class="setting-row">
<label><input type="checkbox" id="is_required" name="is_required"> Required field</label>
</div>
<div class="setting-row">
<label><input type="checkbox" id="is_active" name="is_active" checked> Active</label>
</div>
</div>
<div class="lt-modal-footer">
<button type="submit" class="lt-btn lt-btn-primary">SAVE</button>
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>CANCEL</button>
</div>
</form>
<form id="fieldForm">
<input type="hidden" id="field_id" name="field_id">
<div class="lt-modal-body">
<div class="lt-form-group">
<label class="lt-label" for="field_name">Field Name * <span class="lt-text-muted lt-text-xs">(internal, lowercase_underscore)</span></label>
<input type="text" id="field_name" name="field_name" class="lt-input" required
pattern="[a-z_]+" placeholder="e.g., server_name">
</div>
</div>
<div class="lt-form-group">
<label class="lt-label" for="field_label">Field Label * <span class="lt-text-muted lt-text-xs">(display name)</span></label>
<input type="text" id="field_label" name="field_label" class="lt-input" required
placeholder="e.g., Server Name">
</div>
<div class="lt-form-group">
<label class="lt-label" for="field_type">Field Type *</label>
<select id="field_type" name="field_type" class="lt-select" required
data-action="toggle-options-field">
<option value="text">Text</option>
<option value="textarea">Text Area</option>
<option value="select">Dropdown (Select)</option>
<option value="checkbox">Checkbox</option>
<option value="date">Date</option>
<option value="number">Number</option>
</select>
</div>
<div class="lt-form-group is-hidden" id="options_row">
<label class="lt-label" for="field_options">Options <span class="lt-text-muted lt-text-xs">(one per line)</span></label>
<textarea id="field_options" name="field_options" class="lt-input lt-textarea"
rows="4" placeholder="Option 1&#10;Option 2&#10;Option 3"></textarea>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cf-category">Category <span class="lt-text-muted lt-text-xs">(empty = all categories)</span></label>
<select id="cf-category" name="category" class="lt-select">
<option value="">All Categories</option>
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
<option value="<?= $c ?>"><?= $c ?></option>
<?php endforeach ?>
</select>
</div>
<div class="lt-form-group">
<label class="lt-label" for="display_order">Display Order</label>
<input type="number" id="display_order" name="display_order" class="lt-input"
value="0" min="0" style="max-width:8rem">
</div>
<div class="lt-form-group">
<label class="lt-filter-option">
<input type="checkbox" class="lt-checkbox" id="is_required" name="is_required">
Required field
</label>
</div>
<div class="lt-form-group">
<label class="lt-filter-option">
<input type="checkbox" class="lt-checkbox" id="cf_is_active" name="is_active" checked>
Active
</label>
</div>
</div>
<div class="lt-modal-footer">
<button type="submit" class="lt-btn lt-btn-primary">SAVE</button>
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>CANCEL</button>
</div>
</form>
</div>
</div>
<script nonce="<?php echo $nonce; ?>">
function showCreateModal() {
document.getElementById('modalTitle').textContent = 'Create Custom Field';
document.getElementById('fieldForm').reset();
document.getElementById('field_id').value = '';
document.getElementById('is_active').checked = true;
toggleOptionsField();
lt.modal.open('fieldModal');
}
<script nonce="<?= $nonce ?>">
document.addEventListener('click', function (e) {
var target = e.target.closest('[data-action]');
if (!target) return;
switch (target.getAttribute('data-action')) {
case 'show-create-modal': showCreateModal(); break;
case 'edit-field': editField(target.getAttribute('data-id')); break;
case 'delete-field': deleteField(target.getAttribute('data-id')); break;
}
});
function closeModal() {
lt.modal.close('fieldModal');
}
document.addEventListener('change', function (e) {
var target = e.target.closest('[data-action]');
if (!target) return;
if (target.getAttribute('data-action') === 'toggle-options-field') toggleOptionsField();
});
// Event delegation for data-action handlers
document.addEventListener('click', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
document.getElementById('fieldForm').addEventListener('submit', function (e) {
saveField(e);
});
const action = target.dataset.action;
switch (action) {
case 'show-create-modal':
showCreateModal();
break;
case 'edit-field':
editField(target.dataset.id);
break;
case 'delete-field':
deleteField(target.dataset.id);
break;
}
});
if (window.lt) lt.keys.initDefaults();
document.addEventListener('change', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
function toggleOptionsField() {
var type = document.getElementById('field_type').value;
document.getElementById('options_row').classList.toggle('is-hidden', type !== 'select');
}
if (target.dataset.action === 'toggle-options-field') {
function showCreateModal() {
document.getElementById('cfModalTitle').textContent = 'Create Custom Field';
document.getElementById('fieldForm').reset();
document.getElementById('field_id').value = '';
document.getElementById('cf_is_active').checked = true;
toggleOptionsField();
lt.modal.open('fieldModal');
}
function editField(id) {
lt.api.get('/api/custom_fields.php?id=' + id)
.then(function (data) {
if (data.success && data.field) {
var f = data.field;
document.getElementById('field_id').value = f.field_id;
document.getElementById('field_name').value = f.field_name;
document.getElementById('field_label').value = f.field_label;
document.getElementById('field_type').value = f.field_type;
document.getElementById('cf-category').value = f.category || '';
document.getElementById('display_order').value = f.display_order;
document.getElementById('is_required').checked = f.is_required == 1;
document.getElementById('cf_is_active').checked = f.is_active == 1;
toggleOptionsField();
}
});
// Form submit handler
document.getElementById('fieldForm').addEventListener('submit', function(e) {
saveField(e);
});
if (window.lt) lt.keys.initDefaults();
function toggleOptionsField() {
const type = document.getElementById('field_type').value;
document.getElementById('options_row').classList.toggle('is-hidden', type !== 'select');
}
function saveField(e) {
e.preventDefault();
const form = document.getElementById('fieldForm');
const data = {
field_id: document.getElementById('field_id').value,
field_name: document.getElementById('field_name').value,
field_label: document.getElementById('field_label').value,
field_type: document.getElementById('field_type').value,
category: document.getElementById('category').value || null,
display_order: parseInt(document.getElementById('display_order').value) || 0,
is_required: document.getElementById('is_required').checked ? 1 : 0,
is_active: document.getElementById('is_active').checked ? 1 : 0
};
if (data.field_type === 'select') {
const options = document.getElementById('field_options').value.split('\n').filter(o => o.trim());
data.field_options = { options: options };
}
const url = '/api/custom_fields.php' + (data.field_id ? '?id=' + data.field_id : '');
const apiCall = data.field_id ? lt.api.put(url, data) : lt.api.post(url, data);
apiCall.then(result => {
if (result.success) {
window.location.reload();
} else {
lt.toast.error(result.error || 'Failed to save');
if (f.field_options && f.field_options.options) {
document.getElementById('field_options').value = f.field_options.options.join('\n');
}
}).catch(err => lt.toast.error('Failed to save'));
}
document.getElementById('cfModalTitle').textContent = 'Edit Custom Field';
lt.modal.open('fieldModal');
}
});
}
function editField(id) {
lt.api.get('/api/custom_fields.php?id=' + id)
.then(data => {
if (data.success && data.field) {
const f = data.field;
document.getElementById('field_id').value = f.field_id;
document.getElementById('field_name').value = f.field_name;
document.getElementById('field_label').value = f.field_label;
document.getElementById('field_type').value = f.field_type;
document.getElementById('category').value = f.category || '';
document.getElementById('display_order').value = f.display_order;
document.getElementById('is_required').checked = f.is_required == 1;
document.getElementById('is_active').checked = f.is_active == 1;
toggleOptionsField();
if (f.field_options && f.field_options.options) {
document.getElementById('field_options').value = f.field_options.options.join('\n');
}
document.getElementById('modalTitle').textContent = 'Edit Custom Field';
lt.modal.open('fieldModal');
}
});
}
function deleteField(id) {
showConfirmModal('Delete Custom Field', 'Delete this custom field? All values will be lost.', 'error', function () {
lt.api.delete('/api/custom_fields.php?id=' + id)
.then(function (data) {
if (data.success) window.location.reload();
else lt.toast.error(data.error || 'Failed to delete');
}).catch(function () { lt.toast.error('Failed to delete'); });
});
}
function deleteField(id) {
showConfirmModal('Delete Custom Field', 'Delete this custom field? All values will be lost.', 'error', function() {
lt.api.delete('/api/custom_fields.php?id=' + id)
.then(data => {
if (data.success) window.location.reload();
else lt.toast.error(data.error || 'Failed to delete');
}).catch(err => lt.toast.error('Failed to delete'));
});
}
</script>
</body>
</html>
function saveField(e) {
e.preventDefault();
var data = {
field_id: document.getElementById('field_id').value,
field_name: document.getElementById('field_name').value,
field_label: document.getElementById('field_label').value,
field_type: document.getElementById('field_type').value,
category: document.getElementById('cf-category').value || null,
display_order: parseInt(document.getElementById('display_order').value) || 0,
is_required: document.getElementById('is_required').checked ? 1 : 0,
is_active: document.getElementById('cf_is_active').checked ? 1 : 0,
};
if (data.field_type === 'select') {
var opts = document.getElementById('field_options').value.split('\n').filter(function (o) { return o.trim(); });
data.field_options = { options: opts };
}
var url = '/api/custom_fields.php' + (data.field_id ? '?id=' + data.field_id : '');
var apiCall = data.field_id ? lt.api.put(url, data) : lt.api.post(url, data);
apiCall.then(function (result) {
if (result.success) window.location.reload();
else lt.toast.error(result.error || 'Failed to save');
}).catch(function () { lt.toast.error('Failed to save'); });
}
</script>
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>