2d6b2b8058
- NotificationHelper::notifyWatchers: excludeUserId parameter was accepted but never used; actors were notified of their own actions. Fix: add AND tw.user_id != ? clause to watcher query when exclusion is requested. - TicketView.php: formatAction() default case returned raw $event['action_type'] unescaped into HTML context. Fix: wrap with htmlspecialchars(). - Admin views: field_id, recurring_id, template_id, transition_id in data-id attributes were uncast; field_type was unescaped in CustomFieldsView; from/to_status slugs derived from DB values were used directly in class attributes in WorkflowDesignerView. Fix: (int) cast for IDs, htmlspecialchars for field_type, preg_replace to sanitize DB-derived CSS class slugs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
248 lines
12 KiB
PHP
248 lines
12 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
|
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
|
$nonce = SecurityHeadersMiddleware::getNonce();
|
|
$pageTitle = 'Custom Fields';
|
|
$activeNav = 'admin-custom-fields';
|
|
$_v = $GLOBALS['config']['ASSET_VERSION'] ?? '1';
|
|
$pageStyles = ["/assets/css/dashboard.css?v={$_v}"];
|
|
$pageScripts = ["/assets/js/keyboard-shortcuts.js?v={$_v}"];
|
|
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">← 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"><?= htmlspecialchars(ucfirst($field['field_type'])) ?></td>
|
|
<td data-label="Category" class="lt-text-xs"><?= htmlspecialchars($field['category'] ?? 'All') ?></td>
|
|
<td data-label="Required" class="lt-text-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="<?= (int)$field['field_id'] ?>">EDIT</button>
|
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
|
|
data-action="delete-field" data-id="<?= (int)$field['field_id'] ?>">DEL</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; 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">✕</button>
|
|
</div>
|
|
<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 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 Option 2 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="<?= $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;
|
|
}
|
|
});
|
|
|
|
document.addEventListener('change', function (e) {
|
|
var target = e.target.closest('[data-action]');
|
|
if (!target) return;
|
|
if (target.getAttribute('data-action') === 'toggle-options-field') toggleOptionsField();
|
|
});
|
|
|
|
document.getElementById('fieldForm').addEventListener('submit', function (e) {
|
|
saveField(e);
|
|
});
|
|
|
|
if (window.lt) lt.keys.initDefaults();
|
|
|
|
function toggleOptionsField() {
|
|
var type = document.getElementById('field_type').value;
|
|
document.getElementById('options_row').classList.toggle('is-hidden', type !== 'select');
|
|
}
|
|
|
|
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();
|
|
if (f.field_options && f.field_options.options) {
|
|
document.getElementById('field_options').value = f.field_options.options.join('\n');
|
|
}
|
|
document.getElementById('cfModalTitle').textContent = 'Edit Custom Field';
|
|
lt.modal.open('fieldModal');
|
|
} else {
|
|
lt.toast.error(data.error || 'Failed to load field');
|
|
}
|
|
}).catch(function () { lt.toast.error('Failed to load field'); });
|
|
}
|
|
|
|
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 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'; ?>
|