Accessibility pass: ARIA roles, label associations, CSS class migrations
- Add role=dialog/aria-modal/aria-labelledby to all 12 modal overlays (JS + PHP)
- Add aria-label="Close" to all 14 modal close buttons
- Add full ARIA combobox pattern to @mention autocomplete (listbox, option, aria-selected, aria-expanded)
- Add for= attributes to admin filter form labels (AuditLog, UserActivity, ApiKeys)
- Remove dead closeOnAdvancedSearchBackdropClick() from advanced-search.js
CSS/JS style cleanup:
- Move .ascii-banner static styles from JS inline to CSS class; add .ascii-banner--glow
- Add .ascii-banner-cursor, .loading-overlay--hiding, .has-overlay, tr[data-clickable]
- Add .animate-fadein/.animate-fadeout/.comment--deleting to ticket.css
- Add .lt-toast--hiding to base.css; remove opacity/transition inline JS
- Remove redundant cursor:pointer JS (already in th{} CSS rule)
- Remove trailing space in lt-select class attributes
Bug fixes:
- base.js: boot overlay opacity inline style was overriding .fade-out class opacity via
specificity (1000 vs 20), preventing the fade-out animation — removed
- ascii-banner.js: cursor used blink-caret (border-color only) instead of blink-cursor
(opacity-based), so the █ cursor never actually blinked — fixed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
10
README.md
10
README.md
@@ -259,8 +259,8 @@ tinker_tickets/
|
|||||||
│ │ ├── markdown.js # Markdown rendering + ticket linking (XSS-safe)
|
│ │ ├── markdown.js # Markdown rendering + ticket linking (XSS-safe)
|
||||||
│ │ ├── settings.js # User preferences
|
│ │ ├── settings.js # User preferences
|
||||||
│ │ ├── ticket.js # Ticket + comments + visibility
|
│ │ ├── ticket.js # Ticket + comments + visibility
|
||||||
│ │ ├── toast.js # Backwards-compat shim → delegates to lt.toast
|
│ │ ├── toast.js # Deprecated shim (no longer loaded — all callers use lt.toast directly)
|
||||||
│ │ └── utils.js # escapeHtml (→ lt.escHtml) + getTicketIdFromUrl
|
│ │ └── utils.js # escapeHtml (→ lt.escHtml), getTicketIdFromUrl, showConfirmModal
|
||||||
│ └── images/
|
│ └── images/
|
||||||
│ └── favicon.png
|
│ └── favicon.png
|
||||||
├── config/
|
├── config/
|
||||||
@@ -397,6 +397,12 @@ Key conventions and gotchas for working with this codebase:
|
|||||||
17. **Rate limiting**: Both session-based AND IP-based limits are enforced
|
17. **Rate limiting**: Both session-based AND IP-based limits are enforced
|
||||||
18. **Relative timestamps**: Dashboard dates use `lt.time.ago()` and refresh every 60s; full date is always in the `title` attribute for hover
|
18. **Relative timestamps**: Dashboard dates use `lt.time.ago()` and refresh every 60s; full date is always in the `title` attribute for hover
|
||||||
19. **Boot sequence**: Shows ASCII banner then boot messages on first page visit per session (`sessionStorage.getItem('booted')`); removed on subsequent loads
|
19. **Boot sequence**: Shows ASCII banner then boot messages on first page visit per session (`sessionStorage.getItem('booted')`); removed on subsequent loads
|
||||||
|
20. **No raw `fetch()`**: All AJAX calls use `lt.api.get/post/put/delete()` — never use raw `fetch()`. The wrapper auto-adds `Content-Type: application/json` and `X-CSRF-Token`, auto-parses JSON, and throws on non-2xx.
|
||||||
|
21. **Confirm dialogs**: Never use browser `confirm()`. Use `showConfirmModal(title, message, type, onConfirm)` (defined in `utils.js`, available on all pages). Types: `'warning'` | `'error'` | `'info'`.
|
||||||
|
22. **`utils.js` on all pages**: `utils.js` is loaded by all views (including admin). It provides `escapeHtml()`, `getTicketIdFromUrl()`, and `showConfirmModal()`.
|
||||||
|
23. **No `toast.js`**: `toast.js` is deprecated and no longer loaded by any view. Use `lt.toast.success/error/warning/info()` directly from `base.js`.
|
||||||
|
24. **CSS utility classes** (dashboard.css): Use `.text-green`, `.text-amber`, `.text-cyan`, `.text-danger`, `.text-open`, `.text-closed`, `.text-muted`, `.text-muted-green`, `.text-sm`, `.text-center`, `.nowrap`, `.mono`, `.fw-bold`, `.mb-1` for typography. Use `.admin-container` (1200px) or `.admin-container-wide` (1400px) for admin page max-widths. Use `.admin-header-row` for title+button header rows. Use `.admin-form-row`, `.admin-form-field`, `.admin-label`, `.admin-input` for filter forms. Use `.admin-form-actions` for filter form button groups. Use `.table-wrapper` + `td.empty-state` for tables. Use `.setting-grid-2` and `.setting-grid-3` for modal form grids. Use `.lt-modal-sm` or `.lt-modal-lg` for modal sizing.
|
||||||
|
25. **CSS utility classes** (ticket.css): Use `.form-hint` (green helper text), `.form-hint-warning` (amber helper text), `.visibility-groups-list` (checkbox row), `.group-checkbox-label` (checkbox label) for ticket forms.
|
||||||
|
|
||||||
## File Reference
|
## File Reference
|
||||||
|
|
||||||
|
|||||||
@@ -922,7 +922,8 @@ pre {
|
|||||||
.lt-table-wrap { overflow-x: auto; border: 2px solid var(--terminal-green); }
|
.lt-table-wrap { overflow-x: auto; border: 2px solid var(--terminal-green); }
|
||||||
|
|
||||||
/* Sortable column header */
|
/* Sortable column header */
|
||||||
.lt-table th[data-sort] { cursor: pointer; }
|
.lt-table th[data-sort],
|
||||||
|
th[data-sort-key] { cursor: pointer; }
|
||||||
.lt-table th[data-sort]:hover { color: var(--terminal-green); text-shadow: var(--glow-green); }
|
.lt-table th[data-sort]:hover { color: var(--terminal-green); text-shadow: var(--glow-green); }
|
||||||
.lt-table th[data-sort="asc"]::after { content: ' ▲'; color: var(--terminal-green); }
|
.lt-table th[data-sort="asc"]::after { content: ' ▲'; color: var(--terminal-green); }
|
||||||
.lt-table th[data-sort="desc"]::after { content: ' ▼'; color: var(--terminal-green); }
|
.lt-table th[data-sort="desc"]::after { content: ' ▼'; color: var(--terminal-green); }
|
||||||
@@ -1177,6 +1178,7 @@ pre {
|
|||||||
.lt-toast-close::before,
|
.lt-toast-close::before,
|
||||||
.lt-toast-close::after { content: ''; }
|
.lt-toast-close::after { content: ''; }
|
||||||
.lt-toast-close:hover { opacity: 1; transform: none; }
|
.lt-toast-close:hover { opacity: 1; transform: none; }
|
||||||
|
.lt-toast--hiding { opacity: 0; transition: opacity 0.3s ease; }
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
15. TAB NAVIGATION
|
15. TAB NAVIGATION
|
||||||
|
|||||||
@@ -284,6 +284,8 @@ tbody tr {
|
|||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr[data-clickable="true"] { cursor: pointer; }
|
||||||
|
|
||||||
/* Button press effect */
|
/* Button press effect */
|
||||||
.btn {
|
.btn {
|
||||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
@@ -1405,8 +1407,12 @@ h1 {
|
|||||||
z-index: var(--z-dropdown);
|
z-index: var(--z-dropdown);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--terminal-green);
|
color: var(--terminal-green);
|
||||||
|
transition: opacity 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-overlay--hiding { opacity: 0; }
|
||||||
|
.has-overlay { position: relative; }
|
||||||
|
|
||||||
.loading-overlay .loading-text {
|
.loading-overlay .loading-text {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
animation: blink-cursor 1s step-end infinite;
|
animation: blink-cursor 1s step-end infinite;
|
||||||
@@ -1504,6 +1510,27 @@ h1 {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inside table cells: compact version — no ASCII art, reduced padding */
|
||||||
|
td.empty-state {
|
||||||
|
padding: 2rem;
|
||||||
|
color: var(--terminal-green-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
td.empty-state::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin views max-width container */
|
||||||
|
.admin-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-container-wide {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== LAYOUT COMPONENTS ===== */
|
/* ===== LAYOUT COMPONENTS ===== */
|
||||||
.dashboard-header {
|
.dashboard-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1580,11 +1607,6 @@ button {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-glow-box {
|
|
||||||
0%, 100% { box-shadow: 0 0 8px currentColor, 0 0 16px currentColor; }
|
|
||||||
50% { box-shadow: 0 0 12px currentColor, 0 0 24px currentColor, 0 0 32px currentColor; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Terminal prompt for primary action buttons */
|
/* Terminal prompt for primary action buttons */
|
||||||
.btn.create-ticket::before,
|
.btn.create-ticket::before,
|
||||||
.btn.primary::before {
|
.btn.primary::before {
|
||||||
@@ -2217,6 +2239,19 @@ input[type="checkbox"]:checked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ===== COLLAPSIBLE ASCII BANNER ===== */
|
/* ===== COLLAPSIBLE ASCII BANNER ===== */
|
||||||
|
.ascii-banner {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--terminal-green);
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: visible;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-banner--glow { text-shadow: var(--glow-green); }
|
||||||
|
.ascii-banner-cursor { margin-left: 5px; animation: blink-cursor 0.75s step-end infinite; }
|
||||||
|
|
||||||
.ascii-banner-wrapper {
|
.ascii-banner-wrapper {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0 1rem 1rem 1rem;
|
margin: 0 1rem 1rem 1rem;
|
||||||
@@ -2774,6 +2809,8 @@ input[type="checkbox"]:checked {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination button {
|
.pagination button {
|
||||||
@@ -3905,6 +3942,279 @@ body.modal-open {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
ADMIN FORM UTILITIES
|
||||||
|
Shared across all admin views (WorkflowDesigner, CustomFields,
|
||||||
|
Templates, RecurringTickets, ApiKeys).
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.admin-section-title {
|
||||||
|
color: var(--terminal-amber);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal flex row for form fields */
|
||||||
|
.admin-form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Growable field within .admin-form-row */
|
||||||
|
.admin-form-field {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact label above admin inputs */
|
||||||
|
.admin-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--terminal-amber);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styled input / select / textarea for admin forms */
|
||||||
|
.admin-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 2px solid var(--terminal-green);
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--terminal-green);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--terminal-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin page title in user-header (breadcrumb-style label) */
|
||||||
|
.admin-page-title {
|
||||||
|
margin-left: 1rem;
|
||||||
|
color: var(--terminal-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility: prevent text wrap (e.g., date columns in tables) */
|
||||||
|
.nowrap { white-space: nowrap; }
|
||||||
|
|
||||||
|
/* Utility: monospace font */
|
||||||
|
.mono { font-family: var(--font-mono); }
|
||||||
|
|
||||||
|
/* Utility: color helpers for status / accent text */
|
||||||
|
.text-green { color: var(--terminal-green); }
|
||||||
|
.text-amber { color: var(--terminal-amber); }
|
||||||
|
.text-cyan { color: var(--terminal-cyan); }
|
||||||
|
.text-danger { color: var(--priority-1); }
|
||||||
|
.text-open { color: var(--status-open); }
|
||||||
|
.text-closed { color: var(--status-closed); }
|
||||||
|
.text-muted-green { color: var(--terminal-green-dim); }
|
||||||
|
|
||||||
|
/* Pre / code display block in admin UI */
|
||||||
|
.admin-code-block {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--terminal-green);
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary stats grid (4 columns, used in UserActivityView) */
|
||||||
|
.admin-stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--terminal-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--terminal-green-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row with title on left and action button on right */
|
||||||
|
.admin-header-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header-row h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Font weight utility */
|
||||||
|
.fw-bold { font-weight: bold; }
|
||||||
|
|
||||||
|
/* Small button variant (shared with ticket.css) */
|
||||||
|
.btn-small {
|
||||||
|
padding: 0.4rem 0.75rem !important;
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boot banner centering */
|
||||||
|
#boot-banner {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state for dashboard table (PHP-generated) */
|
||||||
|
.dashboard-empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixed-width table columns */
|
||||||
|
.col-checkbox { width: 40px; }
|
||||||
|
.col-actions { width: 100px; }
|
||||||
|
|
||||||
|
.dashboard-empty-pre {
|
||||||
|
color: var(--terminal-green);
|
||||||
|
text-shadow: var(--glow-green);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid layouts for modal form bodies */
|
||||||
|
.setting-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
|
||||||
|
.setting-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
||||||
|
|
||||||
|
/* Workflow status diagram container */
|
||||||
|
.workflow-diagram {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--terminal-green);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-diagram-nodes {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-diagram-node {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-diagram-node-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline paragraph spacing utility */
|
||||||
|
.mb-1 { margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
/* Smaller modal size variants (complement lt-modal-lg from base.css) */
|
||||||
|
.lt-modal-sm { width: 460px; }
|
||||||
|
|
||||||
|
/* Half-rem margin-bottom utility */
|
||||||
|
.mb-half { margin-bottom: 0.5rem; }
|
||||||
|
|
||||||
|
/* Modal form label (block display with bottom spacing) */
|
||||||
|
.lt-modal-body label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--terminal-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal form select/input top spacing */
|
||||||
|
.lt-modal-body .lt-select,
|
||||||
|
.lt-modal-body .lt-input {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra-small modal size (400px) */
|
||||||
|
.lt-modal-xs { width: 400px; }
|
||||||
|
|
||||||
|
/* Confirmation modal body message */
|
||||||
|
.modal-message {
|
||||||
|
color: var(--terminal-green);
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger modal header variant */
|
||||||
|
.lt-modal-header--danger {
|
||||||
|
color: var(--status-closed) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger action button */
|
||||||
|
.lt-btn-danger {
|
||||||
|
background: var(--status-closed);
|
||||||
|
border-color: var(--status-closed);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lt-btn-danger:hover {
|
||||||
|
background: var(--priority-1);
|
||||||
|
border-color: var(--priority-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal body text sizing */
|
||||||
|
.modal-warning-text {
|
||||||
|
color: var(--terminal-amber);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyboard shortcuts modal */
|
||||||
|
.kb-section-heading {
|
||||||
|
color: var(--terminal-amber);
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-shortcuts-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-shortcuts-table.no-margin {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-shortcuts-table td {
|
||||||
|
padding: 0.4rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-shortcuts-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button/action group aligned to bottom (used in filter forms) */
|
||||||
|
.admin-form-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Truncated table cell for long content */
|
||||||
|
.td-truncate {
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small text utility */
|
||||||
|
.text-sm { font-size: 0.85rem; }
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
MARKDOWN FORMATTING STYLES
|
MARKDOWN FORMATTING STYLES
|
||||||
======================================== */
|
======================================== */
|
||||||
|
|||||||
@@ -466,6 +466,50 @@ textarea[data-field="description"]:not(:disabled)::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Form Elements */
|
/* Form Elements */
|
||||||
|
/* Helper text below form fields */
|
||||||
|
.form-hint {
|
||||||
|
color: var(--terminal-green);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint-warning {
|
||||||
|
color: var(--terminal-amber);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Visibility group checkbox row */
|
||||||
|
.visibility-groups-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Duplicate warning box and visibility groups (JS-toggled, need margin when visible) */
|
||||||
|
#duplicateWarning {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#visibilityGroupsContainer {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Duplicate found heading */
|
||||||
|
.duplicate-heading {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-group {
|
.detail-group {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
@@ -944,6 +988,10 @@ textarea.editable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animate-fadein { animation: fadeIn 0.3s ease; }
|
||||||
|
.animate-fadeout { animation: fadeIn 0.2s ease reverse; }
|
||||||
|
.comment--deleting { opacity: 0; transform: translateX(-20px); transition: opacity 0.3s, transform 0.3s; }
|
||||||
|
|
||||||
.reply-form-container .reply-header {
|
.reply-form-container .reply-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -19,14 +19,6 @@ function closeAdvancedSearch() {
|
|||||||
lt.modal.close('advancedSearchModal');
|
lt.modal.close('advancedSearchModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal when clicking on backdrop
|
|
||||||
function closeOnAdvancedSearchBackdropClick(event) {
|
|
||||||
const modal = document.getElementById('advancedSearchModal');
|
|
||||||
if (event.target === modal) {
|
|
||||||
closeAdvancedSearch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load users for dropdown
|
// Load users for dropdown
|
||||||
async function loadUsersForSearch() {
|
async function loadUsersForSearch() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -65,20 +65,8 @@ function renderASCIIBanner(bannerId, containerSelector, speed = 5, addGlow = tru
|
|||||||
|
|
||||||
// Create pre element for ASCII art
|
// Create pre element for ASCII art
|
||||||
const pre = document.createElement('pre');
|
const pre = document.createElement('pre');
|
||||||
pre.className = 'ascii-banner';
|
pre.className = addGlow ? 'ascii-banner ascii-banner--glow' : 'ascii-banner';
|
||||||
pre.style.margin = '0';
|
|
||||||
pre.style.fontFamily = 'var(--font-mono)';
|
|
||||||
pre.style.color = 'var(--terminal-green)';
|
|
||||||
|
|
||||||
if (addGlow) {
|
|
||||||
pre.style.textShadow = 'var(--glow-green)';
|
|
||||||
}
|
|
||||||
|
|
||||||
pre.style.fontSize = getBannerFontSize(bannerId);
|
pre.style.fontSize = getBannerFontSize(bannerId);
|
||||||
pre.style.lineHeight = '1.2';
|
|
||||||
pre.style.whiteSpace = 'pre';
|
|
||||||
pre.style.overflow = 'visible';
|
|
||||||
pre.style.textAlign = 'center';
|
|
||||||
|
|
||||||
container.appendChild(pre);
|
container.appendChild(pre);
|
||||||
|
|
||||||
@@ -178,8 +166,7 @@ function animatedWelcome(containerSelector) {
|
|||||||
banner.addEventListener('bannerComplete', () => {
|
banner.addEventListener('bannerComplete', () => {
|
||||||
const cursor = document.createElement('span');
|
const cursor = document.createElement('span');
|
||||||
cursor.textContent = '█';
|
cursor.textContent = '█';
|
||||||
cursor.style.animation = 'blink-caret 0.75s step-end infinite';
|
cursor.className = 'ascii-banner-cursor';
|
||||||
cursor.style.marginLeft = '5px';
|
|
||||||
banner.appendChild(cursor);
|
banner.appendChild(cursor);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,8 +124,7 @@
|
|||||||
function _dismissToast(toast) {
|
function _dismissToast(toast) {
|
||||||
if (!toast || !toast.parentNode) return;
|
if (!toast || !toast.parentNode) return;
|
||||||
clearTimeout(toast._lt_timer);
|
clearTimeout(toast._lt_timer);
|
||||||
toast.style.opacity = '0';
|
toast.classList.add('lt-toast--hiding');
|
||||||
toast.style.transition = 'opacity 0.3s ease';
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
||||||
_toastActive = false;
|
_toastActive = false;
|
||||||
@@ -176,11 +175,11 @@
|
|||||||
lt.modal.closeAll();
|
lt.modal.closeAll();
|
||||||
|
|
||||||
HTML contract:
|
HTML contract:
|
||||||
<div id="my-modal-id" class="lt-modal-overlay">
|
<div id="my-modal-id" class="lt-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="myModalTitle">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">Title</span>
|
<span class="lt-modal-title" id="myModalTitle">Title</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">…</div>
|
<div class="lt-modal-body">…</div>
|
||||||
<div class="lt-modal-footer">…</div>
|
<div class="lt-modal-footer">…</div>
|
||||||
@@ -295,7 +294,6 @@
|
|||||||
if (!overlay || !pre) return;
|
if (!overlay || !pre) return;
|
||||||
|
|
||||||
overlay.style.display = 'flex';
|
overlay.style.display = 'flex';
|
||||||
overlay.style.opacity = '1';
|
|
||||||
|
|
||||||
const name = (appName || 'TERMINAL').toUpperCase();
|
const name = (appName || 'TERMINAL').toUpperCase();
|
||||||
const titleStr = name + ' v1.0';
|
const titleStr = name + ' v1.0';
|
||||||
@@ -653,7 +651,6 @@
|
|||||||
|
|
||||||
const ths = Array.from(table.querySelectorAll('th[data-sort-key]'));
|
const ths = Array.from(table.querySelectorAll('th[data-sort-key]'));
|
||||||
ths.forEach((th, colIdx) => {
|
ths.forEach((th, colIdx) => {
|
||||||
th.style.cursor = 'pointer';
|
|
||||||
let dir = 'asc';
|
let dir = 'asc';
|
||||||
|
|
||||||
th.addEventListener('click', () => {
|
th.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -261,7 +261,6 @@ function clearAllFilters() {
|
|||||||
function initTableSorting() {
|
function initTableSorting() {
|
||||||
const tableHeaders = document.querySelectorAll('th');
|
const tableHeaders = document.querySelectorAll('th');
|
||||||
tableHeaders.forEach((header, index) => {
|
tableHeaders.forEach((header, index) => {
|
||||||
header.style.cursor = 'pointer';
|
|
||||||
header.addEventListener('click', () => {
|
header.addEventListener('click', () => {
|
||||||
const table = header.closest('table');
|
const table = header.closest('table');
|
||||||
sortTable(table, index);
|
sortTable(table, index);
|
||||||
@@ -764,15 +763,15 @@ function showBulkAssignModal() {
|
|||||||
|
|
||||||
// Create modal HTML
|
// Create modal HTML
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="bulkAssignModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="bulkAssignModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="bulkAssignModalTitle">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">Assign ${ticketIds.length} Ticket(s)</span>
|
<span class="lt-modal-title" id="bulkAssignModalTitle">Assign ${ticketIds.length} Ticket(s)</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<label for="bulkAssignUser">Assign to:</label>
|
<label for="bulkAssignUser">Assign to:</label>
|
||||||
<select id="bulkAssignUser" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
<select id="bulkAssignUser" class="lt-select">
|
||||||
<option value="">Select User...</option>
|
<option value="">Select User...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -852,15 +851,15 @@ function showBulkPriorityModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="bulkPriorityModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="bulkPriorityModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="bulkPriorityModalTitle">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">Change Priority for ${ticketIds.length} Ticket(s)</span>
|
<span class="lt-modal-title" id="bulkPriorityModalTitle">Change Priority for ${ticketIds.length} Ticket(s)</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<label for="bulkPriority">Priority:</label>
|
<label for="bulkPriority">Priority:</label>
|
||||||
<select id="bulkPriority" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
<select id="bulkPriority" class="lt-select">
|
||||||
<option value="">Select Priority...</option>
|
<option value="">Select Priority...</option>
|
||||||
<option value="1">P1 - Critical Impact</option>
|
<option value="1">P1 - Critical Impact</option>
|
||||||
<option value="2">P2 - High Impact</option>
|
<option value="2">P2 - High Impact</option>
|
||||||
@@ -927,7 +926,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (row.dataset.clickable) return;
|
if (row.dataset.clickable) return;
|
||||||
|
|
||||||
row.dataset.clickable = 'true';
|
row.dataset.clickable = 'true';
|
||||||
row.style.cursor = 'pointer';
|
|
||||||
|
|
||||||
row.addEventListener('click', function(e) {
|
row.addEventListener('click', function(e) {
|
||||||
// Don't navigate if clicking on a link, button, checkbox, or select
|
// Don't navigate if clicking on a link, button, checkbox, or select
|
||||||
@@ -960,15 +958,15 @@ function showBulkStatusModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="bulkStatusModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="bulkStatusModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="bulkStatusModalTitle">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">Change Status for ${ticketIds.length} Ticket(s)</span>
|
<span class="lt-modal-title" id="bulkStatusModalTitle">Change Status for ${ticketIds.length} Ticket(s)</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<label for="bulkStatus">New Status:</label>
|
<label for="bulkStatus">New Status:</label>
|
||||||
<select id="bulkStatus" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
<select id="bulkStatus" class="lt-select">
|
||||||
<option value="">Select Status...</option>
|
<option value="">Select Status...</option>
|
||||||
<option value="Open">Open</option>
|
<option value="Open">Open</option>
|
||||||
<option value="Pending">Pending</option>
|
<option value="Pending">Pending</option>
|
||||||
@@ -1036,18 +1034,18 @@ function showBulkDeleteModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="bulkDeleteModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="bulkDeleteModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="bulkDeleteModalTitle">
|
||||||
<div class="lt-modal">
|
<div class="lt-modal lt-modal-sm">
|
||||||
<div class="lt-modal-header" style="color: var(--status-closed);">
|
<div class="lt-modal-header lt-modal-header--danger">
|
||||||
<span class="lt-modal-title">[ ! ] DELETE ${ticketIds.length} TICKET(S)</span>
|
<span class="lt-modal-title" id="bulkDeleteModalTitle">[ ! ] DELETE ${ticketIds.length} TICKET(S)</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body" style="text-align:center;">
|
<div class="lt-modal-body text-center">
|
||||||
<p style="color: var(--terminal-amber); font-size: 1.1rem; margin-bottom: 1rem;">This action cannot be undone!</p>
|
<p class="modal-warning-text">This action cannot be undone!</p>
|
||||||
<p style="color: var(--terminal-green);">You are about to permanently delete ${ticketIds.length} ticket(s).<br>All associated comments and history will be lost.</p>
|
<p class="text-green">You are about to permanently delete ${ticketIds.length} ticket(s).<br>All associated comments and history will be lost.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-footer">
|
<div class="lt-modal-footer">
|
||||||
<button data-action="perform-bulk-delete" class="lt-btn lt-btn-primary" style="background: var(--status-closed); border-color: var(--status-closed);">DELETE PERMANENTLY</button>
|
<button data-action="perform-bulk-delete" class="lt-btn lt-btn-danger">DELETE PERMANENTLY</button>
|
||||||
<button data-action="close-bulk-delete-modal" class="lt-btn lt-btn-ghost">CANCEL</button>
|
<button data-action="close-bulk-delete-modal" class="lt-btn lt-btn-ghost">CANCEL</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1121,14 +1119,14 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
|
|||||||
const safeMessage = lt.escHtml(message);
|
const safeMessage = lt.escHtml(message);
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
|
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="${modalId}_title">
|
||||||
<div class="lt-modal" style="max-width: 500px;">
|
<div class="lt-modal lt-modal-sm">
|
||||||
<div class="lt-modal-header" style="color: ${color};">
|
<div class="lt-modal-header" style="color: ${color};">
|
||||||
<span class="lt-modal-title">${icon} ${safeTitle}</span>
|
<span class="lt-modal-title" id="${modalId}_title">${icon} ${safeTitle}</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body" style="text-align: center;">
|
<div class="lt-modal-body text-center">
|
||||||
<p style="color: var(--terminal-green); white-space: pre-line;">${safeMessage}</p>
|
<p class="modal-message">${safeMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-footer">
|
<div class="lt-modal-footer">
|
||||||
<button class="lt-btn lt-btn-primary" id="${modalId}_confirm">CONFIRM</button>
|
<button class="lt-btn lt-btn-primary" id="${modalId}_confirm">CONFIRM</button>
|
||||||
@@ -1172,15 +1170,15 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
|
|||||||
const safePlaceholder = lt.escHtml(placeholder);
|
const safePlaceholder = lt.escHtml(placeholder);
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
|
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="${modalId}_title">
|
||||||
<div class="lt-modal" style="max-width: 500px;">
|
<div class="lt-modal lt-modal-sm">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">${safeTitle}</span>
|
<span class="lt-modal-title" id="${modalId}_title">${safeTitle}</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<label for="${inputId}" style="display: block; margin-bottom: 0.5rem; color: var(--terminal-green);">${safeLabel}</label>
|
<label for="${inputId}">${safeLabel}</label>
|
||||||
<input type="text" id="${inputId}" class="lt-input" placeholder="${safePlaceholder}" style="width: 100%;" />
|
<input type="text" id="${inputId}" class="lt-input" placeholder="${safePlaceholder}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-footer">
|
<div class="lt-modal-footer">
|
||||||
<button class="lt-btn lt-btn-primary" id="${modalId}_submit">SAVE</button>
|
<button class="lt-btn lt-btn-primary" id="${modalId}_submit">SAVE</button>
|
||||||
@@ -1224,17 +1222,17 @@ function quickStatusChange(ticketId, currentStatus) {
|
|||||||
const otherStatuses = statuses.filter(s => s !== currentStatus);
|
const otherStatuses = statuses.filter(s => s !== currentStatus);
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="quickStatusModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="quickStatusModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="quickStatusModalTitle">
|
||||||
<div class="lt-modal" style="max-width:400px;">
|
<div class="lt-modal lt-modal-xs">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">Quick Status Change</span>
|
<span class="lt-modal-title" id="quickStatusModalTitle">Quick Status Change</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<p style="margin-bottom:0.5rem;">Ticket #${lt.escHtml(ticketId)}</p>
|
<p class="mb-half">Ticket #${lt.escHtml(ticketId)}</p>
|
||||||
<p style="margin-bottom:0.5rem;color:var(--terminal-amber);">Current: ${lt.escHtml(currentStatus)}</p>
|
<p class="text-amber mb-half">Current: ${lt.escHtml(currentStatus)}</p>
|
||||||
<label for="quickStatusSelect">New Status:</label>
|
<label for="quickStatusSelect">New Status:</label>
|
||||||
<select id="quickStatusSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
<select id="quickStatusSelect" class="lt-select">
|
||||||
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
|
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1280,16 +1278,16 @@ function performQuickStatusChange(ticketId) {
|
|||||||
*/
|
*/
|
||||||
function quickAssign(ticketId) {
|
function quickAssign(ticketId) {
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="quickAssignModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="quickAssignModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="quickAssignModalTitle">
|
||||||
<div class="lt-modal" style="max-width:400px;">
|
<div class="lt-modal lt-modal-xs">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">Quick Assign</span>
|
<span class="lt-modal-title" id="quickAssignModalTitle">Quick Assign</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<p style="margin-bottom:0.5rem;">Ticket #${lt.escHtml(ticketId)}</p>
|
<p class="mb-half">Ticket #${lt.escHtml(ticketId)}</p>
|
||||||
<label for="quickAssignSelect">Assign to:</label>
|
<label for="quickAssignSelect">Assign to:</label>
|
||||||
<select id="quickAssignSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
<select id="quickAssignSelect" class="lt-select">
|
||||||
<option value="">Unassigned</option>
|
<option value="">Unassigned</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1697,7 +1695,7 @@ function showLoadingOverlay(element, message = 'Loading...') {
|
|||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
<div class="loading-text">${message}</div>
|
<div class="loading-text">${message}</div>
|
||||||
`;
|
`;
|
||||||
element.style.position = 'relative';
|
element.classList.add('has-overlay');
|
||||||
element.appendChild(overlay);
|
element.appendChild(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1707,9 +1705,11 @@ function showLoadingOverlay(element, message = 'Loading...') {
|
|||||||
function hideLoadingOverlay(element) {
|
function hideLoadingOverlay(element) {
|
||||||
const overlay = element.querySelector('.loading-overlay');
|
const overlay = element.querySelector('.loading-overlay');
|
||||||
if (overlay) {
|
if (overlay) {
|
||||||
overlay.style.opacity = '0';
|
overlay.classList.add('loading-overlay--hiding');
|
||||||
overlay.style.transition = 'opacity 0.3s';
|
setTimeout(() => {
|
||||||
setTimeout(() => overlay.remove(), 300);
|
overlay.remove();
|
||||||
|
element.classList.remove('has-overlay');
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,43 +33,46 @@ function showKeyboardHelp() {
|
|||||||
modal.id = 'keyboardHelpModal';
|
modal.id = 'keyboardHelpModal';
|
||||||
modal.className = 'lt-modal-overlay';
|
modal.className = 'lt-modal-overlay';
|
||||||
modal.setAttribute('aria-hidden', 'true');
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
modal.setAttribute('role', 'dialog');
|
||||||
|
modal.setAttribute('aria-modal', 'true');
|
||||||
|
modal.setAttribute('aria-labelledby', 'keyboardHelpModalTitle');
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="lt-modal" style="max-width: 500px;">
|
<div class="lt-modal lt-modal-sm">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title">KEYBOARD SHORTCUTS</span>
|
<span class="lt-modal-title" id="keyboardHelpModalTitle">KEYBOARD SHORTCUTS</span>
|
||||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<h4 style="color: var(--terminal-amber); margin: 0 0 0.5rem 0; font-size: 0.9rem;">Navigation</h4>
|
<h4 class="kb-section-heading">Navigation</h4>
|
||||||
<table style="width: 100%; border-collapse: collapse; margin-bottom: 1rem;">
|
<table class="kb-shortcuts-table">
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>J</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Next ticket in list</td></tr>
|
<tr><td><kbd>J</kbd></td><td>Next ticket in list</td></tr>
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>K</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Previous ticket in list</td></tr>
|
<tr><td><kbd>K</kbd></td><td>Previous ticket in list</td></tr>
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>Enter</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Open selected ticket</td></tr>
|
<tr><td><kbd>Enter</kbd></td><td>Open selected ticket</td></tr>
|
||||||
<tr><td style="padding: 0.4rem;"><kbd>G</kbd> then <kbd>D</kbd></td><td style="padding: 0.4rem;">Go to Dashboard</td></tr>
|
<tr><td><kbd>G</kbd> then <kbd>D</kbd></td><td>Go to Dashboard</td></tr>
|
||||||
</table>
|
</table>
|
||||||
<h4 style="color: var(--terminal-amber); margin: 0 0 0.5rem 0; font-size: 0.9rem;">Actions</h4>
|
<h4 class="kb-section-heading">Actions</h4>
|
||||||
<table style="width: 100%; border-collapse: collapse; margin-bottom: 1rem;">
|
<table class="kb-shortcuts-table">
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>N</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">New ticket</td></tr>
|
<tr><td><kbd>N</kbd></td><td>New ticket</td></tr>
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>C</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Focus comment box</td></tr>
|
<tr><td><kbd>C</kbd></td><td>Focus comment box</td></tr>
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>Ctrl/Cmd+E</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Toggle Edit Mode</td></tr>
|
<tr><td><kbd>Ctrl/Cmd+E</kbd></td><td>Toggle Edit Mode</td></tr>
|
||||||
<tr><td style="padding: 0.4rem;"><kbd>Ctrl/Cmd+S</kbd></td><td style="padding: 0.4rem;">Save Changes</td></tr>
|
<tr><td><kbd>Ctrl/Cmd+S</kbd></td><td>Save Changes</td></tr>
|
||||||
</table>
|
</table>
|
||||||
<h4 style="color: var(--terminal-amber); margin: 0 0 0.5rem 0; font-size: 0.9rem;">Quick Status (Ticket Page)</h4>
|
<h4 class="kb-section-heading">Quick Status (Ticket Page)</h4>
|
||||||
<table style="width: 100%; border-collapse: collapse; margin-bottom: 1rem;">
|
<table class="kb-shortcuts-table">
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>1</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Set Open</td></tr>
|
<tr><td><kbd>1</kbd></td><td>Set Open</td></tr>
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>2</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Set Pending</td></tr>
|
<tr><td><kbd>2</kbd></td><td>Set Pending</td></tr>
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>3</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Set In Progress</td></tr>
|
<tr><td><kbd>3</kbd></td><td>Set In Progress</td></tr>
|
||||||
<tr><td style="padding: 0.4rem;"><kbd>4</kbd></td><td style="padding: 0.4rem;">Set Closed</td></tr>
|
<tr><td><kbd>4</kbd></td><td>Set Closed</td></tr>
|
||||||
</table>
|
</table>
|
||||||
<h4 style="color: var(--terminal-amber); margin: 0 0 0.5rem 0; font-size: 0.9rem;">Other</h4>
|
<h4 class="kb-section-heading">Other</h4>
|
||||||
<table style="width: 100%; border-collapse: collapse;">
|
<table class="kb-shortcuts-table no-margin">
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>Ctrl/Cmd+K</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Focus Search</td></tr>
|
<tr><td><kbd>Ctrl/Cmd+K</kbd></td><td>Focus Search</td></tr>
|
||||||
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>ESC</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Close Modal / Cancel</td></tr>
|
<tr><td><kbd>ESC</kbd></td><td>Close Modal / Cancel</td></tr>
|
||||||
<tr><td style="padding: 0.4rem;"><kbd>?</kbd></td><td style="padding: 0.4rem;">Show This Help</td></tr>
|
<tr><td><kbd>?</kbd></td><td>Show This Help</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-footer">
|
<div class="lt-modal-footer">
|
||||||
<button class="lt-btn lt-btn-ghost" data-modal-close>Close</button>
|
<button class="lt-btn lt-btn-ghost" data-modal-close>CLOSE</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function saveTicket() {
|
|||||||
// Use the correct API path
|
// Use the correct API path
|
||||||
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, ...data })
|
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, ...data })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if(data.success) {
|
if (data.success) {
|
||||||
const statusDisplay = document.getElementById('statusDisplay');
|
const statusDisplay = document.getElementById('statusDisplay');
|
||||||
if (statusDisplay) {
|
if (statusDisplay) {
|
||||||
statusDisplay.className = `status-${data.status}`;
|
statusDisplay.className = `status-${data.status}`;
|
||||||
@@ -58,9 +58,11 @@ function saveTicket() {
|
|||||||
}
|
}
|
||||||
lt.toast.success('Ticket updated successfully');
|
lt.toast.success('Ticket updated successfully');
|
||||||
} else {
|
} else {
|
||||||
|
lt.toast.error('Error saving ticket: ' + (data.error || 'Unknown error'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
lt.toast.error('Error saving ticket: ' + error.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,10 +513,10 @@ function showDependencyError(message) {
|
|||||||
const dependentsList = document.getElementById('dependentsList');
|
const dependentsList = document.getElementById('dependentsList');
|
||||||
|
|
||||||
if (dependenciesList) {
|
if (dependenciesList) {
|
||||||
dependenciesList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
|
dependenciesList.innerHTML = `<p class="text-amber">${lt.escHtml(message)}</p>`;
|
||||||
}
|
}
|
||||||
if (dependentsList) {
|
if (dependentsList) {
|
||||||
dependentsList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
|
dependentsList.innerHTML = `<p class="text-amber">${lt.escHtml(message)}</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +559,7 @@ function renderDependencies(dependencies) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasAny) {
|
if (!hasAny) {
|
||||||
html = '<p style="color: var(--terminal-green-dim);">No dependencies configured.</p>';
|
html = '<p class="text-muted-green">No dependencies configured.</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
@@ -568,21 +570,21 @@ function renderDependents(dependents) {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
if (dependents.length === 0) {
|
if (dependents.length === 0) {
|
||||||
container.innerHTML = '<p style="color: var(--terminal-green-dim);">No tickets depend on this one.</p>';
|
container.innerHTML = '<p class="text-muted-green">No tickets depend on this one.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
dependents.forEach(dep => {
|
dependents.forEach(dep => {
|
||||||
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
|
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
|
||||||
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
|
html += `<div class="dependency-item">
|
||||||
<div>
|
<div>
|
||||||
<a href="/ticket/${lt.escHtml(dep.ticket_id)}" style="color: var(--terminal-green);">
|
<a href="/ticket/${lt.escHtml(dep.ticket_id)}">
|
||||||
#${lt.escHtml(dep.ticket_id)}
|
#${lt.escHtml(dep.ticket_id)}
|
||||||
</a>
|
</a>
|
||||||
<span style="margin-left: 0.5rem;">${lt.escHtml(dep.title)}</span>
|
<span class="dependency-title">${lt.escHtml(dep.title)}</span>
|
||||||
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${lt.escHtml(dep.status)}</span>
|
<span class="status-badge ${statusClass}">${lt.escHtml(dep.status)}</span>
|
||||||
<span style="margin-left: 0.5rem; color: var(--terminal-amber);">(${lt.escHtml(dep.dependency_type)})</span>
|
<span class="dependency-title text-amber">(${lt.escHtml(dep.dependency_type)})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
@@ -623,22 +625,25 @@ function addDependency() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeDependency(dependencyId) {
|
function removeDependency(dependencyId) {
|
||||||
if (!confirm('Are you sure you want to remove this dependency?')) {
|
showConfirmModal(
|
||||||
return;
|
'Remove Dependency',
|
||||||
}
|
'Are you sure you want to remove this dependency?',
|
||||||
|
'warning',
|
||||||
lt.api.delete('/api/ticket_dependencies.php', { dependency_id: dependencyId })
|
function() {
|
||||||
.then(data => {
|
lt.api.delete('/api/ticket_dependencies.php', { dependency_id: dependencyId })
|
||||||
if (data.success) {
|
.then(data => {
|
||||||
lt.toast.success('Dependency removed', 3000);
|
if (data.success) {
|
||||||
loadDependencies();
|
lt.toast.success('Dependency removed', 3000);
|
||||||
} else {
|
loadDependencies();
|
||||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
} else {
|
||||||
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
lt.toast.error('Error removing dependency', 4000);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
);
|
||||||
.catch(error => {
|
|
||||||
lt.toast.error('Error removing dependency', 4000);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -789,11 +794,11 @@ function loadAttachments() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
renderAttachments(data.attachments || []);
|
renderAttachments(data.attachments || []);
|
||||||
} else {
|
} else {
|
||||||
container.innerHTML = '<p style="color: var(--terminal-green-dim);">Error loading attachments.</p>';
|
container.innerHTML = '<p class="text-muted-green">Error loading attachments.</p>';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
container.innerHTML = '<p style="color: var(--terminal-green-dim);">Error loading attachments.</p>';
|
container.innerHTML = '<p class="text-muted-green">Error loading attachments.</p>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,7 +807,7 @@ function renderAttachments(attachments) {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
if (attachments.length === 0) {
|
if (attachments.length === 0) {
|
||||||
container.innerHTML = '<p style="color: var(--terminal-green-dim);">No files attached to this ticket.</p>';
|
container.innerHTML = '<p class="text-muted-green">No files attached to this ticket.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,7 +828,7 @@ function renderAttachments(attachments) {
|
|||||||
<div class="attachment-icon">${lt.escHtml(att.icon || '[ f ]')}</div>
|
<div class="attachment-icon">${lt.escHtml(att.icon || '[ f ]')}</div>
|
||||||
<div class="attachment-info">
|
<div class="attachment-info">
|
||||||
<div class="attachment-name" title="${lt.escHtml(att.original_filename)}">
|
<div class="attachment-name" title="${lt.escHtml(att.original_filename)}">
|
||||||
<a href="/api/download_attachment.php?id=${att.attachment_id}" target="_blank" style="color: var(--terminal-green);">
|
<a href="/api/download_attachment.php?id=${att.attachment_id}" target="_blank">
|
||||||
${lt.escHtml(att.original_filename)}
|
${lt.escHtml(att.original_filename)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -855,22 +860,25 @@ function formatFileSize(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteAttachment(attachmentId) {
|
function deleteAttachment(attachmentId) {
|
||||||
if (!confirm('Are you sure you want to delete this attachment?')) {
|
showConfirmModal(
|
||||||
return;
|
'Delete Attachment',
|
||||||
}
|
'Are you sure you want to delete this attachment?',
|
||||||
|
'warning',
|
||||||
lt.api.post('/api/delete_attachment.php', { attachment_id: attachmentId })
|
function() {
|
||||||
.then(data => {
|
lt.api.post('/api/delete_attachment.php', { attachment_id: attachmentId })
|
||||||
if (data.success) {
|
.then(data => {
|
||||||
lt.toast.success('Attachment deleted', 3000);
|
if (data.success) {
|
||||||
loadAttachments();
|
lt.toast.success('Attachment deleted', 3000);
|
||||||
} else {
|
loadAttachments();
|
||||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
} else {
|
||||||
|
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
lt.toast.error('Error deleting attachment', 4000);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
);
|
||||||
.catch(error => {
|
|
||||||
lt.toast.error('Error deleting attachment', 4000);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -893,7 +901,12 @@ function initMentionAutocomplete() {
|
|||||||
mentionAutocomplete = document.createElement('div');
|
mentionAutocomplete = document.createElement('div');
|
||||||
mentionAutocomplete.className = 'mention-autocomplete';
|
mentionAutocomplete.className = 'mention-autocomplete';
|
||||||
mentionAutocomplete.id = 'mentionAutocomplete';
|
mentionAutocomplete.id = 'mentionAutocomplete';
|
||||||
textarea.parentElement.style.position = 'relative';
|
mentionAutocomplete.setAttribute('role', 'listbox');
|
||||||
|
mentionAutocomplete.setAttribute('aria-label', 'User suggestions');
|
||||||
|
textarea.setAttribute('aria-autocomplete', 'list');
|
||||||
|
textarea.setAttribute('aria-controls', 'mentionAutocomplete');
|
||||||
|
textarea.setAttribute('aria-expanded', 'false');
|
||||||
|
textarea.parentElement.classList.add('has-overlay');
|
||||||
textarea.parentElement.appendChild(mentionAutocomplete);
|
textarea.parentElement.appendChild(mentionAutocomplete);
|
||||||
|
|
||||||
// Fetch users list
|
// Fetch users list
|
||||||
@@ -990,7 +1003,9 @@ function handleMentionKeydown(e) {
|
|||||||
*/
|
*/
|
||||||
function updateMentionSelection(options) {
|
function updateMentionSelection(options) {
|
||||||
options.forEach((opt, i) => {
|
options.forEach((opt, i) => {
|
||||||
opt.classList.toggle('selected', i === selectedMentionIndex);
|
const isSelected = i === selectedMentionIndex;
|
||||||
|
opt.classList.toggle('selected', isSelected);
|
||||||
|
opt.setAttribute('aria-selected', isSelected ? 'true' : 'false');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1012,7 +1027,8 @@ function showMentionSuggestions(query, textarea) {
|
|||||||
let html = '';
|
let html = '';
|
||||||
filtered.forEach((user, index) => {
|
filtered.forEach((user, index) => {
|
||||||
const isSelected = index === 0 ? 'selected' : '';
|
const isSelected = index === 0 ? 'selected' : '';
|
||||||
html += `<div class="mention-option ${isSelected}" data-username="${lt.escHtml(user.username)}" data-action="select-mention">
|
const ariaSelected = index === 0 ? 'true' : 'false';
|
||||||
|
html += `<div class="mention-option ${isSelected}" role="option" aria-selected="${ariaSelected}" data-username="${lt.escHtml(user.username)}" data-action="select-mention">
|
||||||
<span class="mention-username">@${lt.escHtml(user.username)}</span>
|
<span class="mention-username">@${lt.escHtml(user.username)}</span>
|
||||||
${user.display_name ? `<span class="mention-displayname">${lt.escHtml(user.display_name)}</span>` : ''}
|
${user.display_name ? `<span class="mention-displayname">${lt.escHtml(user.display_name)}</span>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -1020,6 +1036,8 @@ function showMentionSuggestions(query, textarea) {
|
|||||||
|
|
||||||
mentionAutocomplete.innerHTML = html;
|
mentionAutocomplete.innerHTML = html;
|
||||||
mentionAutocomplete.classList.add('active');
|
mentionAutocomplete.classList.add('active');
|
||||||
|
const textarea = document.getElementById('newComment');
|
||||||
|
if (textarea) textarea.setAttribute('aria-expanded', 'true');
|
||||||
selectedMentionIndex = 0;
|
selectedMentionIndex = 0;
|
||||||
|
|
||||||
// Position dropdown below cursor
|
// Position dropdown below cursor
|
||||||
@@ -1034,6 +1052,8 @@ function showMentionSuggestions(query, textarea) {
|
|||||||
function hideMentionAutocomplete() {
|
function hideMentionAutocomplete() {
|
||||||
if (mentionAutocomplete) {
|
if (mentionAutocomplete) {
|
||||||
mentionAutocomplete.classList.remove('active');
|
mentionAutocomplete.classList.remove('active');
|
||||||
|
const textarea = document.getElementById('newComment');
|
||||||
|
if (textarea) textarea.setAttribute('aria-expanded', 'false');
|
||||||
}
|
}
|
||||||
mentionStartPos = -1;
|
mentionStartPos = -1;
|
||||||
}
|
}
|
||||||
@@ -1257,29 +1277,29 @@ function cancelEditComment(commentId) {
|
|||||||
* Delete a comment
|
* Delete a comment
|
||||||
*/
|
*/
|
||||||
function deleteComment(commentId) {
|
function deleteComment(commentId) {
|
||||||
if (!confirm('Are you sure you want to delete this comment? This cannot be undone.')) {
|
showConfirmModal(
|
||||||
return;
|
'Delete Comment',
|
||||||
}
|
'Are you sure you want to delete this comment? This cannot be undone.',
|
||||||
|
'warning',
|
||||||
lt.api.post('/api/delete_comment.php', { comment_id: commentId })
|
function() {
|
||||||
.then(data => {
|
lt.api.post('/api/delete_comment.php', { comment_id: commentId })
|
||||||
if (data.success) {
|
.then(data => {
|
||||||
// Remove the comment from the DOM
|
if (data.success) {
|
||||||
const commentDiv = document.querySelector(`.comment[data-comment-id="${commentId}"]`);
|
const commentDiv = document.querySelector(`.comment[data-comment-id="${commentId}"]`);
|
||||||
if (commentDiv) {
|
if (commentDiv) {
|
||||||
commentDiv.style.transition = 'opacity 0.3s, transform 0.3s';
|
commentDiv.classList.add('comment--deleting');
|
||||||
commentDiv.style.opacity = '0';
|
setTimeout(() => commentDiv.remove(), 300);
|
||||||
commentDiv.style.transform = 'translateX(-20px)';
|
}
|
||||||
setTimeout(() => commentDiv.remove(), 300);
|
lt.toast.success('Comment deleted successfully');
|
||||||
}
|
} else {
|
||||||
lt.toast.success('Comment deleted successfully');
|
lt.toast.error(data.error || 'Failed to delete comment');
|
||||||
} else {
|
}
|
||||||
lt.toast.error(data.error || 'Failed to delete comment');
|
})
|
||||||
|
.catch(error => {
|
||||||
|
lt.toast.error('Failed to delete comment');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
);
|
||||||
.catch(error => {
|
|
||||||
lt.toast.error('Failed to delete comment');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -1331,7 +1351,7 @@ function showReplyForm(commentId, userName) {
|
|||||||
*/
|
*/
|
||||||
function closeReplyForm() {
|
function closeReplyForm() {
|
||||||
document.querySelectorAll('.reply-form-container').forEach(form => {
|
document.querySelectorAll('.reply-form-container').forEach(form => {
|
||||||
form.style.animation = 'fadeIn 0.2s ease reverse';
|
form.classList.add('animate-fadeout');
|
||||||
setTimeout(() => form.remove(), 200);
|
setTimeout(() => form.remove(), 200);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1420,7 +1440,7 @@ function submitReply(parentCommentId) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Add animation
|
// Add animation
|
||||||
replyDiv.style.animation = 'fadeIn 0.3s ease';
|
replyDiv.classList.add('animate-fadein');
|
||||||
repliesContainer.appendChild(replyDiv);
|
repliesContainer.appendChild(replyDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,3 +10,49 @@ function getTicketIdFromUrl() {
|
|||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
return params.get('id');
|
return params.get('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a terminal-style confirmation modal using the lt.modal system.
|
||||||
|
* Falls back gracefully if dashboard.js has already defined this function.
|
||||||
|
* @param {string} title - Modal title
|
||||||
|
* @param {string} message - Confirmation message
|
||||||
|
* @param {string} type - 'warning' | 'error' | 'info'
|
||||||
|
* @param {Function} onConfirm - Called when user confirms
|
||||||
|
* @param {Function|null} onCancel - Called when user cancels
|
||||||
|
*/
|
||||||
|
if (typeof showConfirmModal === 'undefined') {
|
||||||
|
window.showConfirmModal = function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel = null) {
|
||||||
|
const modalId = 'confirmModal' + Date.now();
|
||||||
|
const colors = { warning: 'var(--terminal-amber)', error: 'var(--status-closed)', info: 'var(--terminal-cyan)' };
|
||||||
|
const icons = { warning: '[ ! ]', error: '[ X ]', info: '[ i ]' };
|
||||||
|
const color = colors[type] || colors.warning;
|
||||||
|
const icon = icons[type] || icons.warning;
|
||||||
|
const safeTitle = lt.escHtml(title);
|
||||||
|
const safeMessage = lt.escHtml(message);
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML('beforeend', `
|
||||||
|
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="${modalId}_title">
|
||||||
|
<div class="lt-modal lt-modal-sm">
|
||||||
|
<div class="lt-modal-header" style="color:${color};">
|
||||||
|
<span class="lt-modal-title" id="${modalId}_title">${icon} ${safeTitle}</span>
|
||||||
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="lt-modal-body text-center">
|
||||||
|
<p class="modal-message">${safeMessage}</p>
|
||||||
|
</div>
|
||||||
|
<div class="lt-modal-footer">
|
||||||
|
<button class="lt-btn lt-btn-primary" id="${modalId}_confirm">CONFIRM</button>
|
||||||
|
<button class="lt-btn lt-btn-ghost" id="${modalId}_cancel">CANCEL</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
lt.modal.open(modalId);
|
||||||
|
const cleanup = (cb) => { lt.modal.close(modalId); setTimeout(() => modal.remove(), 300); if (cb) cb(); };
|
||||||
|
document.getElementById(`${modalId}_confirm`).addEventListener('click', () => cleanup(onConfirm));
|
||||||
|
document.getElementById(`${modalId}_cancel`).addEventListener('click', () => cleanup(onCancel));
|
||||||
|
modal.querySelector('[data-modal-close]').addEventListener('click', () => cleanup(onCancel));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Create New Ticket</title>
|
<title>Create New Ticket</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260319d">
|
<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=20260319d">
|
<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="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.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=20260205"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
// CSRF Token for AJAX requests
|
// CSRF Token for AJAX requests
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
@@ -48,7 +48,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<div class="ticket-header">
|
<div class="ticket-header">
|
||||||
<h2>New Ticket Form</h2>
|
<h2>New Ticket Form</h2>
|
||||||
<p style="color: var(--terminal-green); font-family: var(--font-mono); font-size: 0.9rem; margin-top: 0.5rem;">
|
<p class="form-hint">
|
||||||
Complete the form below to create a new ticket
|
Complete the form below to create a new ticket
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +90,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</select>
|
</select>
|
||||||
<p style="color: var(--terminal-green); font-size: 0.85rem; margin-top: 0.5rem; font-family: var(--font-mono);">
|
<p class="form-hint">
|
||||||
Select a template to auto-fill form fields
|
Select a template to auto-fill form fields
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,8 +109,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<input type="text" id="title" name="title" class="editable" required placeholder="Enter a descriptive title for this ticket">
|
<input type="text" id="title" name="title" class="editable" required placeholder="Enter a descriptive title for this ticket">
|
||||||
</div>
|
</div>
|
||||||
<!-- Duplicate Warning Area -->
|
<!-- Duplicate Warning Area -->
|
||||||
<div id="duplicateWarning" class="inline-warning" role="alert" aria-live="polite" aria-atomic="true" style="display: none; margin-top: 1rem;">
|
<div id="duplicateWarning" class="inline-warning" role="alert" aria-live="polite" aria-atomic="true" style="display: none;">
|
||||||
<div style="color: var(--terminal-amber); font-weight: bold; margin-bottom: 0.5rem;">
|
<div class="text-amber fw-bold duplicate-heading">
|
||||||
Possible Duplicates Found
|
Possible Duplicates Found
|
||||||
</div>
|
</div>
|
||||||
<div id="duplicatesList" aria-live="polite"></div>
|
<div id="duplicatesList" aria-live="polite"></div>
|
||||||
@@ -185,7 +185,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</select>
|
</select>
|
||||||
<p style="color: var(--terminal-green); font-size: 0.85rem; margin-top: 0.5rem; font-family: var(--font-mono);">
|
<p class="form-hint">
|
||||||
Select a user to assign this ticket to
|
Select a user to assign this ticket to
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,13 +206,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<option value="internal">Internal - Specific groups only</option>
|
<option value="internal">Internal - Specific groups only</option>
|
||||||
<option value="confidential">Confidential - Creator, assignee, admins only</option>
|
<option value="confidential">Confidential - Creator, assignee, admins only</option>
|
||||||
</select>
|
</select>
|
||||||
<p style="color: var(--terminal-green); font-size: 0.85rem; margin-top: 0.5rem; font-family: var(--font-mono);">
|
<p class="form-hint">
|
||||||
Controls who can view this ticket
|
Controls who can view this ticket
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="visibilityGroupsContainer" class="detail-group" style="display: none; margin-top: 1rem;">
|
<div id="visibilityGroupsContainer" class="detail-group" style="display: none;">
|
||||||
<label>Allowed Groups</label>
|
<label>Allowed Groups</label>
|
||||||
<div class="visibility-groups-list" style="display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 0.5rem;">
|
<div class="visibility-groups-list">
|
||||||
<?php
|
<?php
|
||||||
// Get all available groups
|
// Get all available groups
|
||||||
require_once __DIR__ . '/../models/UserModel.php';
|
require_once __DIR__ . '/../models/UserModel.php';
|
||||||
@@ -220,16 +220,16 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
$allGroups = $userModel->getAllGroups();
|
$allGroups = $userModel->getAllGroups();
|
||||||
foreach ($allGroups as $group):
|
foreach ($allGroups as $group):
|
||||||
?>
|
?>
|
||||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
<label class="group-checkbox-label">
|
||||||
<input type="checkbox" name="visibility_groups[]" value="<?php echo htmlspecialchars($group); ?>" class="visibility-group-checkbox">
|
<input type="checkbox" name="visibility_groups[]" value="<?php echo htmlspecialchars($group); ?>" class="visibility-group-checkbox">
|
||||||
<span class="group-badge"><?php echo htmlspecialchars($group); ?></span>
|
<span class="group-badge"><?php echo htmlspecialchars($group); ?></span>
|
||||||
</label>
|
</label>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($allGroups)): ?>
|
<?php if (empty($allGroups)): ?>
|
||||||
<span style="color: var(--text-muted);">No groups available</span>
|
<span class="text-muted">No groups available</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<p style="color: var(--terminal-amber); font-size: 0.85rem; margin-top: 0.5rem; font-family: var(--font-mono);">
|
<p class="form-hint-warning">
|
||||||
Select which groups can view this ticket
|
Select which groups can view this ticket
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,8 +288,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
});
|
});
|
||||||
|
|
||||||
function checkForDuplicates(title) {
|
function checkForDuplicates(title) {
|
||||||
fetch('/api/check_duplicates.php?title=' + encodeURIComponent(title))
|
lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title))
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const warningDiv = document.getElementById('duplicateWarning');
|
const warningDiv = document.getElementById('duplicateWarning');
|
||||||
const listDiv = document.getElementById('duplicatesList');
|
const listDiv = document.getElementById('duplicatesList');
|
||||||
@@ -352,6 +351,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
toggleVisibilityGroups();
|
toggleVisibilityGroups();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (window.lt) lt.keys.initDefaults();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,13 +13,12 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Ticket Dashboard</title>
|
<title>Ticket Dashboard</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260319d">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260320">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.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/utils.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260131e"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260205"></script>
|
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
// CSRF Token for AJAX requests
|
// CSRF Token for AJAX requests
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
@@ -33,7 +32,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
|
|
||||||
<!-- Terminal Boot Sequence -->
|
<!-- Terminal Boot Sequence -->
|
||||||
<div id="boot-sequence" class="boot-overlay">
|
<div id="boot-sequence" class="boot-overlay">
|
||||||
<div id="boot-banner" style="text-align:center; margin-bottom: 1rem;"></div>
|
<div id="boot-banner"></div>
|
||||||
<pre id="boot-text"></pre>
|
<pre id="boot-text"></pre>
|
||||||
</div>
|
</div>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
@@ -102,7 +101,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button class="btn" style="font-size:0.75rem; padding: 0.2rem 0.5rem;" data-action="manual-refresh" title="Refresh now (auto-refreshes every 5 min)" aria-label="Refresh dashboard">REFRESH</button>
|
<button class="btn btn-small" data-action="manual-refresh" title="Refresh now (auto-refreshes every 5 min)" aria-label="Refresh dashboard">REFRESH</button>
|
||||||
<button class="settings-icon" title="Settings (Alt+S)" data-action="open-settings" aria-label="Settings">[ CFG ]</button>
|
<button class="settings-icon" title="Settings (Alt+S)" data-action="open-settings" aria-label="Settings">[ CFG ]</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -390,7 +389,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<?php if ($GLOBALS['currentUser']['is_admin'] ?? false): ?>
|
<?php if ($GLOBALS['currentUser']['is_admin'] ?? false): ?>
|
||||||
<th style="width: 40px;" scope="col"><input type="checkbox" id="selectAllCheckbox" data-action="toggle-select-all" aria-label="Select all tickets"></th>
|
<th class="col-checkbox" scope="col"><input type="checkbox" id="selectAllCheckbox" data-action="toggle-select-all" aria-label="Select all tickets"></th>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php
|
<?php
|
||||||
$currentSort = isset($_GET['sort']) ? $_GET['sort'] : 'ticket_id';
|
$currentSort = isset($_GET['sort']) ? $_GET['sort'] : 'ticket_id';
|
||||||
@@ -412,7 +411,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
|
|
||||||
foreach($columns as $col => $label) {
|
foreach($columns as $col => $label) {
|
||||||
if ($col === '_actions') {
|
if ($col === '_actions') {
|
||||||
echo "<th scope='col' style='width: 100px; text-align: center;'>$label</th>";
|
echo "<th scope='col' class='col-actions text-center'>$label</th>";
|
||||||
} else {
|
} else {
|
||||||
$newDir = ($currentSort === $col && $currentDir === 'asc') ? 'desc' : 'asc';
|
$newDir = ($currentSort === $col && $currentDir === 'asc') ? 'desc' : 'asc';
|
||||||
$sortClass = ($currentSort === $col) ? "sort-$currentDir" : '';
|
$sortClass = ($currentSort === $col) ? "sort-$currentDir" : '';
|
||||||
@@ -460,8 +459,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$colspan = ($GLOBALS['currentUser']['is_admin'] ?? false) ? '12' : '11';
|
$colspan = ($GLOBALS['currentUser']['is_admin'] ?? false) ? '12' : '11';
|
||||||
echo "<tr><td colspan='$colspan' style='text-align: center; padding: 3rem;'>";
|
echo "<tr><td colspan='$colspan' class='dashboard-empty-state'>";
|
||||||
echo "<pre style='color: var(--terminal-green); text-shadow: var(--glow-green); font-size: 0.8rem; line-height: 1.2;'>";
|
echo "<pre class='dashboard-empty-pre'>";
|
||||||
echo "╔════════════════════════════════════════╗\n";
|
echo "╔════════════════════════════════════════╗\n";
|
||||||
echo "║ ║\n";
|
echo "║ ║\n";
|
||||||
echo "║ NO TICKETS FOUND ║\n";
|
echo "║ NO TICKETS FOUND ║\n";
|
||||||
@@ -832,9 +831,9 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/settings.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/settings.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/keyboard-shortcuts.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/keyboard-shortcuts.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/advanced-search.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/advanced-search.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
// Initialize lt keyboard defaults (ESC closes modals, Ctrl+K focuses search, ? shows help)
|
// Initialize lt keyboard defaults (ESC closes modals, Ctrl+K focuses search, ? shows help)
|
||||||
if (window.lt) lt.keys.initDefaults();
|
if (window.lt) lt.keys.initDefaults();
|
||||||
|
|||||||
@@ -51,14 +51,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Ticket #<?php echo $ticket['ticket_id']; ?></title>
|
<title>Ticket #<?php echo $ticket['ticket_id']; ?></title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260319d">
|
<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=20260319d">
|
<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="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.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/utils.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260131e"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260205"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.js?v=20260131e"></script>
|
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
// CSRF Token for AJAX requests
|
// CSRF Token for AJAX requests
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
@@ -428,7 +427,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<p>Drag and drop files here or click to browse</p>
|
<p>Drag and drop files here or click to browse</p>
|
||||||
<p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p>
|
<p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p>
|
||||||
<input type="file" id="fileInput" multiple class="sr-only" aria-label="Upload files">
|
<input type="file" id="fileInput" multiple class="sr-only" aria-label="Upload files">
|
||||||
<button type="button" id="browseFilesBtn" class="btn upload-browse-btn">Browse Files</button>
|
<button type="button" id="browseFilesBtn" class="btn upload-browse-btn">BROWSE FILES</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="uploadProgress" class="upload-progress" style="display: none;">
|
<div id="uploadProgress" class="upload-progress" style="display: none;">
|
||||||
@@ -561,39 +560,36 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
var cloneBtn = document.getElementById('cloneButton');
|
var cloneBtn = document.getElementById('cloneButton');
|
||||||
if (cloneBtn) {
|
if (cloneBtn) {
|
||||||
cloneBtn.addEventListener('click', function() {
|
cloneBtn.addEventListener('click', function() {
|
||||||
if (confirm('Create a copy of this ticket? The new ticket will have the same title, description, priority, category, and type.')) {
|
showConfirmModal(
|
||||||
cloneBtn.disabled = true;
|
'Clone Ticket',
|
||||||
cloneBtn.textContent = 'Cloning...';
|
'Create a copy of this ticket? The new ticket will have the same title, description, priority, category, and type.',
|
||||||
|
'warning',
|
||||||
|
function() {
|
||||||
|
cloneBtn.disabled = true;
|
||||||
|
cloneBtn.textContent = 'Cloning...';
|
||||||
|
|
||||||
fetch('/api/clone_ticket.php', {
|
lt.api.post('/api/clone_ticket.php', {
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRF-Token': window.CSRF_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
ticket_id: window.ticketData.ticket_id
|
ticket_id: window.ticketData.ticket_id
|
||||||
})
|
})
|
||||||
})
|
.then(data => {
|
||||||
.then(response => response.json())
|
if (data.success) {
|
||||||
.then(data => {
|
lt.toast.success('Ticket cloned successfully!');
|
||||||
if (data.success) {
|
setTimeout(function() {
|
||||||
toast.success('Ticket cloned successfully!');
|
window.location.href = '/ticket/' + data.new_ticket_id;
|
||||||
setTimeout(function() {
|
}, 1000);
|
||||||
window.location.href = '/ticket/' + data.new_ticket_id;
|
} else {
|
||||||
}, 1000);
|
lt.toast.error('Failed to clone ticket: ' + (data.error || 'Unknown error'));
|
||||||
} else {
|
cloneBtn.disabled = false;
|
||||||
toast.error('Failed to clone ticket: ' + (data.error || 'Unknown error'));
|
cloneBtn.textContent = 'Clone';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
lt.toast.error('Failed to clone ticket: ' + error.message);
|
||||||
cloneBtn.disabled = false;
|
cloneBtn.disabled = false;
|
||||||
cloneBtn.textContent = 'Clone';
|
cloneBtn.textContent = 'Clone';
|
||||||
}
|
});
|
||||||
})
|
}
|
||||||
.catch(function(error) {
|
);
|
||||||
toast.error('Failed to clone ticket: ' + error.message);
|
|
||||||
cloneBtn.disabled = false;
|
|
||||||
cloneBtn.textContent = 'Clone';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,8 +813,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/keyboard-shortcuts.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/keyboard-shortcuts.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/settings.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/settings.js?v=20260320"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">if (window.lt) lt.keys.initDefaults();</script>
|
<script nonce="<?php echo $nonce; ?>">if (window.lt) lt.keys.initDefaults();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -13,10 +13,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>API Keys - Admin</title>
|
<title>API Keys - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.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">
|
<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="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.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; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
</script>
|
</script>
|
||||||
@@ -25,7 +25,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: API Keys</span>
|
<span class="admin-page-title">Admin: API Keys</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
@@ -35,24 +35,23 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ascii-frame-outer" style="max-width: 1200px; margin: 2rem auto;">
|
<div class="ascii-frame-outer admin-container">
|
||||||
<span class="bottom-left-corner">╚</span>
|
<span class="bottom-left-corner">╚</span>
|
||||||
<span class="bottom-right-corner">╝</span>
|
<span class="bottom-right-corner">╝</span>
|
||||||
|
|
||||||
<div class="ascii-section-header">API Key Management</div>
|
<div class="ascii-section-header">API Key Management</div>
|
||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<!-- Generate New Key Form -->
|
<!-- Generate New Key Form -->
|
||||||
<div class="ascii-frame-inner" style="margin-bottom: 1.5rem;">
|
<div class="ascii-frame-inner">
|
||||||
<h3 style="color: var(--terminal-amber); margin-bottom: 1rem;">Generate New API Key</h3>
|
<h3 class="admin-section-title">Generate New API Key</h3>
|
||||||
<form id="generateKeyForm" style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-end;">
|
<form id="generateKeyForm" class="admin-form-row">
|
||||||
<div style="flex: 1; min-width: 200px;">
|
<div class="admin-form-field">
|
||||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber); margin-bottom: 0.25rem;">Key Name *</label>
|
<label class="admin-label" for="keyName">Key Name *</label>
|
||||||
<input type="text" id="keyName" required placeholder="e.g., CI/CD Pipeline"
|
<input type="text" id="keyName" required placeholder="e.g., CI/CD Pipeline" class="admin-input">
|
||||||
style="width: 100%; padding: 0.5rem; border: 2px solid var(--terminal-green); background: var(--bg-primary); color: var(--terminal-green); font-family: var(--font-mono);">
|
|
||||||
</div>
|
</div>
|
||||||
<div style="min-width: 150px;">
|
<div class="admin-form-field">
|
||||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber); margin-bottom: 0.25rem;">Expires In</label>
|
<label class="admin-label" for="expiresIn">Expires In</label>
|
||||||
<select id="expiresIn" style="width: 100%; padding: 0.5rem; border: 2px solid var(--terminal-green); background: var(--bg-primary); color: var(--terminal-green); font-family: var(--font-mono);">
|
<select id="expiresIn" class="admin-input">
|
||||||
<option value="">Never</option>
|
<option value="">Never</option>
|
||||||
<option value="30">30 days</option>
|
<option value="30">30 days</option>
|
||||||
<option value="90">90 days</option>
|
<option value="90">90 days</option>
|
||||||
@@ -67,22 +66,22 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Key Display (hidden by default) -->
|
<!-- New Key Display (hidden by default) -->
|
||||||
<div id="newKeyDisplay" class="ascii-frame-inner key-generated-alert" style="display: none; margin-bottom: 1.5rem;">
|
<div id="newKeyDisplay" class="ascii-frame-inner key-generated-alert" style="display: none;">
|
||||||
<h3 style="color: var(--terminal-amber); margin-bottom: 0.5rem;">New API Key Generated</h3>
|
<h3 class="admin-section-title">New API Key Generated</h3>
|
||||||
<p style="color: var(--priority-1); margin-bottom: 1rem; font-size: 0.9rem;">
|
<p class="text-danger text-sm mb-1">
|
||||||
Copy this key now. You won't be able to see it again!
|
Copy this key now. You won't be able to see it again!
|
||||||
</p>
|
</p>
|
||||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
<div class="admin-form-row">
|
||||||
<input type="text" id="newKeyValue" readonly
|
<input type="text" id="newKeyValue" readonly class="admin-input">
|
||||||
style="flex: 1; padding: 0.75rem; font-family: var(--font-mono); font-size: 0.85rem; background: var(--bg-primary); border: 2px solid var(--terminal-green); color: var(--terminal-green);">
|
|
||||||
<button data-action="copy-api-key" class="btn" title="Copy to clipboard">COPY</button>
|
<button data-action="copy-api-key" class="btn" title="Copy to clipboard">COPY</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Existing Keys Table -->
|
<!-- Existing Keys Table -->
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<h3 style="color: var(--terminal-amber); margin-bottom: 1rem;">Existing API Keys</h3>
|
<h3 class="admin-section-title">Existing API Keys</h3>
|
||||||
<table style="width: 100%; font-size: 0.9rem;">
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
@@ -98,50 +97,45 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($apiKeys)): ?>
|
<?php if (empty($apiKeys)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
<td colspan="8" class="empty-state">No API keys found. Generate one above.</td>
|
||||||
No API keys found. Generate one above.
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($apiKeys as $key): ?>
|
<?php foreach ($apiKeys as $key): ?>
|
||||||
<tr id="key-row-<?php echo $key['api_key_id']; ?>">
|
<tr id="key-row-<?php echo $key['api_key_id']; ?>">
|
||||||
<td><?php echo htmlspecialchars($key['key_name']); ?></td>
|
<td><?php echo htmlspecialchars($key['key_name']); ?></td>
|
||||||
<td style="font-family: var(--font-mono);">
|
<td class="mono">
|
||||||
<code><?php echo htmlspecialchars($key['key_prefix']); ?>...</code>
|
<code><?php echo htmlspecialchars($key['key_prefix']); ?>...</code>
|
||||||
</td>
|
</td>
|
||||||
<td><?php echo htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown'); ?></td>
|
<td><?php echo htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown'); ?></td>
|
||||||
<td style="white-space: nowrap;"><?php echo date('Y-m-d H:i', strtotime($key['created_at'])); ?></td>
|
<td class="nowrap"><?php echo date('Y-m-d H:i', strtotime($key['created_at'])); ?></td>
|
||||||
<td style="white-space: nowrap;">
|
<td class="nowrap">
|
||||||
<?php if ($key['expires_at']): ?>
|
<?php if ($key['expires_at']): ?>
|
||||||
<?php
|
<?php $expired = strtotime($key['expires_at']) < time(); ?>
|
||||||
$expired = strtotime($key['expires_at']) < time();
|
<span class="<?php echo $expired ? 'text-danger' : ''; ?>">
|
||||||
$color = $expired ? 'var(--priority-1)' : 'var(--terminal-green)';
|
|
||||||
?>
|
|
||||||
<span style="color: <?php echo $color; ?>;">
|
|
||||||
<?php echo date('Y-m-d', strtotime($key['expires_at'])); ?>
|
<?php echo date('Y-m-d', strtotime($key['expires_at'])); ?>
|
||||||
<?php if ($expired): ?> (Expired)<?php endif; ?>
|
<?php if ($expired): ?> (Expired)<?php endif; ?>
|
||||||
</span>
|
</span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span style="color: var(--terminal-cyan);">Never</span>
|
<span class="text-cyan">Never</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td style="white-space: nowrap;">
|
<td class="nowrap">
|
||||||
<?php echo $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never'; ?>
|
<?php echo $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never'; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?php if ($key['is_active']): ?>
|
<?php if ($key['is_active']): ?>
|
||||||
<span style="color: var(--status-open);">Active</span>
|
<span class="text-open">Active</span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span style="color: var(--status-closed);">Revoked</span>
|
<span class="text-closed">Revoked</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?php if ($key['is_active']): ?>
|
<?php if ($key['is_active']): ?>
|
||||||
<button data-action="revoke-key" data-id="<?php echo $key['api_key_id']; ?>" class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">
|
<button data-action="revoke-key" data-id="<?php echo $key['api_key_id']; ?>" class="btn btn-secondary btn-small">
|
||||||
Revoke
|
REVOKE
|
||||||
</button>
|
</button>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span style="color: var(--text-muted);">-</span>
|
<span class="text-muted">-</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -149,14 +143,15 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Usage Info -->
|
<!-- API Usage Info -->
|
||||||
<div class="ascii-frame-inner" style="margin-top: 1.5rem;">
|
<div class="ascii-frame-inner">
|
||||||
<h3 style="color: var(--terminal-amber); margin-bottom: 1rem;">API Usage</h3>
|
<h3 class="admin-section-title">API Usage</h3>
|
||||||
<p style="margin-bottom: 0.5rem;">Include the API key in your requests using the Authorization header:</p>
|
<p>Include the API key in your requests using the Authorization header:</p>
|
||||||
<pre style="background: var(--bg-primary); padding: 1rem; border: 1px solid var(--terminal-green); overflow-x: auto;"><code>Authorization: Bearer YOUR_API_KEY</code></pre>
|
<pre class="admin-code-block"><code>Authorization: Bearer YOUR_API_KEY</code></pre>
|
||||||
<p style="margin-top: 1rem; font-size: 0.9rem; color: var(--text-muted);">
|
<p class="text-muted text-sm">
|
||||||
API keys provide programmatic access to create and manage tickets. Keep your keys secure and rotate them regularly.
|
API keys provide programmatic access to create and manage tickets. Keep your keys secure and rotate them regularly.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,20 +187,11 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/generate_api_key.php', {
|
const data = await lt.api.post('/api/generate_api_key.php', {
|
||||||
method: 'POST',
|
key_name: keyName,
|
||||||
headers: {
|
expires_in_days: expiresIn || null
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRF-Token': window.CSRF_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key_name: keyName,
|
|
||||||
expires_in_days: expiresIn || null
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Show the new key
|
// Show the new key
|
||||||
document.getElementById('newKeyValue').value = data.api_key;
|
document.getElementById('newKeyValue').value = data.api_key;
|
||||||
@@ -231,32 +217,21 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
lt.toast.success('API key copied to clipboard');
|
lt.toast.success('API key copied to clipboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revokeKey(keyId) {
|
function revokeKey(keyId) {
|
||||||
if (!confirm('Are you sure you want to revoke this API key? This action cannot be undone.')) {
|
showConfirmModal('Revoke API Key', 'Are you sure you want to revoke this API key? This action cannot be undone.', 'error', function() {
|
||||||
return;
|
lt.api.post('/api/revoke_api_key.php', { key_id: keyId })
|
||||||
}
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
try {
|
lt.toast.success('API key revoked successfully');
|
||||||
const response = await fetch('/api/revoke_api_key.php', {
|
location.reload();
|
||||||
method: 'POST',
|
} else {
|
||||||
headers: {
|
lt.toast.error(data.error || 'Failed to revoke API key');
|
||||||
'Content-Type': 'application/json',
|
}
|
||||||
'X-CSRF-Token': window.CSRF_TOKEN
|
})
|
||||||
},
|
.catch(error => {
|
||||||
body: JSON.stringify({ key_id: keyId })
|
lt.toast.error('Error revoking API key: ' + error.message);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
lt.toast.success('API key revoked successfully');
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
lt.toast.error(data.error || 'Failed to revoke API key');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
lt.toast.error('Error revoking API key: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Audit Log - Admin</title>
|
<title>Audit Log - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.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">
|
<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="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
@@ -24,7 +24,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Audit Log</span>
|
<span class="admin-page-title">Admin: Audit Log</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
@@ -34,7 +34,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ascii-frame-outer" style="max-width: 1400px; margin: 2rem auto;">
|
<div class="ascii-frame-outer admin-container-wide">
|
||||||
<span class="bottom-left-corner">╚</span>
|
<span class="bottom-left-corner">╚</span>
|
||||||
<span class="bottom-right-corner">╝</span>
|
<span class="bottom-right-corner">╝</span>
|
||||||
|
|
||||||
@@ -42,10 +42,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<form method="GET" style="display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;">
|
<form method="GET" class="admin-form-row">
|
||||||
<div>
|
<div class="admin-form-field">
|
||||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Action Type</label>
|
<label class="admin-label" for="action_type">Action Type</label>
|
||||||
<select name="action_type" class="setting-select">
|
<select name="action_type" id="action_type" class="admin-input">
|
||||||
<option value="">All Actions</option>
|
<option value="">All Actions</option>
|
||||||
<option value="create" <?php echo ($filters['action_type'] ?? '') === 'create' ? 'selected' : ''; ?>>Create</option>
|
<option value="create" <?php echo ($filters['action_type'] ?? '') === 'create' ? 'selected' : ''; ?>>Create</option>
|
||||||
<option value="update" <?php echo ($filters['action_type'] ?? '') === 'update' ? 'selected' : ''; ?>>Update</option>
|
<option value="update" <?php echo ($filters['action_type'] ?? '') === 'update' ? 'selected' : ''; ?>>Update</option>
|
||||||
@@ -57,9 +57,9 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<option value="security" <?php echo ($filters['action_type'] ?? '') === 'security' ? 'selected' : ''; ?>>Security</option>
|
<option value="security" <?php echo ($filters['action_type'] ?? '') === 'security' ? 'selected' : ''; ?>>Security</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="admin-form-field">
|
||||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">User</label>
|
<label class="admin-label" for="user_id">User</label>
|
||||||
<select name="user_id" class="setting-select">
|
<select name="user_id" id="user_id" class="admin-input">
|
||||||
<option value="">All Users</option>
|
<option value="">All Users</option>
|
||||||
<?php if (isset($users)): foreach ($users as $user): ?>
|
<?php if (isset($users)): foreach ($users as $user): ?>
|
||||||
<option value="<?php echo $user['user_id']; ?>" <?php echo ($filters['user_id'] ?? '') == $user['user_id'] ? 'selected' : ''; ?>>
|
<option value="<?php echo $user['user_id']; ?>" <?php echo ($filters['user_id'] ?? '') == $user['user_id'] ? 'selected' : ''; ?>>
|
||||||
@@ -68,22 +68,23 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endforeach; endif; ?>
|
<?php endforeach; endif; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="admin-form-field">
|
||||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Date From</label>
|
<label class="admin-label" for="date_from">Date From</label>
|
||||||
<input type="date" name="date_from" value="<?php echo htmlspecialchars($filters['date_from'] ?? ''); ?>" class="setting-select">
|
<input type="date" name="date_from" id="date_from" value="<?php echo htmlspecialchars($filters['date_from'] ?? ''); ?>" class="admin-input">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="admin-form-field">
|
||||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Date To</label>
|
<label class="admin-label" for="date_to">Date To</label>
|
||||||
<input type="date" name="date_to" value="<?php echo htmlspecialchars($filters['date_to'] ?? ''); ?>" class="setting-select">
|
<input type="date" name="date_to" id="date_to" value="<?php echo htmlspecialchars($filters['date_to'] ?? ''); ?>" class="admin-input">
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: flex-end;">
|
<div class="admin-form-actions">
|
||||||
<button type="submit" class="btn">FILTER</button>
|
<button type="submit" class="btn">FILTER</button>
|
||||||
<a href="?" class="btn btn-secondary" style="margin-left: 0.5rem;">RESET</a>
|
<a href="?" class="btn btn-secondary">RESET</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Log Table -->
|
<!-- Log Table -->
|
||||||
<table style="width: 100%; font-size: 0.9rem;">
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Timestamp</th>
|
<th>Timestamp</th>
|
||||||
@@ -98,34 +99,32 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($auditLogs)): ?>
|
<?php if (empty($auditLogs)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
<td colspan="7" class="empty-state">No audit log entries found.</td>
|
||||||
No audit log entries found.
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($auditLogs as $log): ?>
|
<?php foreach ($auditLogs as $log): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="white-space: nowrap;"><?php echo date('Y-m-d H:i:s', strtotime($log['created_at'])); ?></td>
|
<td class="nowrap"><?php echo date('Y-m-d H:i:s', strtotime($log['created_at'])); ?></td>
|
||||||
<td><?php echo htmlspecialchars($log['display_name'] ?? $log['username'] ?? 'System'); ?></td>
|
<td><?php echo htmlspecialchars($log['display_name'] ?? $log['username'] ?? 'System'); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<span style="color: var(--terminal-amber);"><?php echo htmlspecialchars($log['action_type']); ?></span>
|
<span class="text-amber"><?php echo htmlspecialchars($log['action_type']); ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td><?php echo htmlspecialchars($log['entity_type'] ?? '-'); ?></td>
|
<td><?php echo htmlspecialchars($log['entity_type'] ?? '-'); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?php if ($log['entity_type'] === 'ticket' && $log['entity_id']): ?>
|
<?php if ($log['entity_type'] === 'ticket' && $log['entity_id']): ?>
|
||||||
<a href="/ticket/<?php echo htmlspecialchars($log['entity_id']); ?>" style="color: var(--terminal-green);">
|
<a href="/ticket/<?php echo htmlspecialchars($log['entity_id']); ?>" class="text-green">
|
||||||
<?php echo htmlspecialchars($log['entity_id']); ?>
|
<?php echo htmlspecialchars($log['entity_id']); ?>
|
||||||
</a>
|
</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php echo htmlspecialchars($log['entity_id'] ?? '-'); ?>
|
<?php echo htmlspecialchars($log['entity_id'] ?? '-'); ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">
|
<td class="td-truncate">
|
||||||
<?php
|
<?php
|
||||||
if ($log['details']) {
|
if ($log['details']) {
|
||||||
$details = is_string($log['details']) ? json_decode($log['details'], true) : $log['details'];
|
$details = is_string($log['details']) ? json_decode($log['details'], true) : $log['details'];
|
||||||
if (is_array($details)) {
|
if (is_array($details)) {
|
||||||
echo '<code style="font-size: 0.8rem;">' . htmlspecialchars(json_encode($details)) . '</code>';
|
echo '<code>' . htmlspecialchars(json_encode($details)) . '</code>';
|
||||||
} else {
|
} else {
|
||||||
echo htmlspecialchars($log['details']);
|
echo htmlspecialchars($log['details']);
|
||||||
}
|
}
|
||||||
@@ -134,16 +133,17 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</td>
|
</td>
|
||||||
<td style="white-space: nowrap; font-size: 0.85rem;"><?php echo htmlspecialchars($log['ip_address'] ?? '-'); ?></td>
|
<td class="nowrap text-sm"><?php echo htmlspecialchars($log['ip_address'] ?? '-'); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<?php if ($totalPages > 1): ?>
|
<?php if ($totalPages > 1): ?>
|
||||||
<div class="pagination" style="margin-top: 1rem; text-align: center;">
|
<div class="pagination">
|
||||||
<?php
|
<?php
|
||||||
$params = $_GET;
|
$params = $_GET;
|
||||||
for ($i = 1; $i <= min($totalPages, 10); $i++) {
|
for ($i = 1; $i <= min($totalPages, 10); $i++) {
|
||||||
@@ -161,6 +161,6 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>if (window.lt) lt.keys.initDefaults();</script>
|
<script nonce="<?php echo $nonce; ?>">if (window.lt) lt.keys.initDefaults();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Custom Fields - Admin</title>
|
<title>Custom Fields - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.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">
|
<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="/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; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
</script>
|
</script>
|
||||||
@@ -24,7 +25,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Custom Fields</span>
|
<span class="admin-page-title">Admin: Custom Fields</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
@@ -34,19 +35,20 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ascii-frame-outer" style="max-width: 1200px; margin: 2rem auto;">
|
<div class="ascii-frame-outer admin-container">
|
||||||
<span class="bottom-left-corner">╚</span>
|
<span class="bottom-left-corner">╚</span>
|
||||||
<span class="bottom-right-corner">╝</span>
|
<span class="bottom-right-corner">╝</span>
|
||||||
|
|
||||||
<div class="ascii-section-header">Custom Fields Management</div>
|
<div class="ascii-section-header">Custom Fields Management</div>
|
||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
<div class="admin-header-row">
|
||||||
<h2 style="margin: 0;">Custom Field Definitions</h2>
|
<h2>Custom Field Definitions</h2>
|
||||||
<button data-action="show-create-modal" class="btn">+ NEW FIELD</button>
|
<button data-action="show-create-modal" class="btn">+ NEW FIELD</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table style="width: 100%;">
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Order</th>
|
<th>Order</th>
|
||||||
@@ -62,9 +64,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($customFields)): ?>
|
<?php if (empty($customFields)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
<td colspan="8" class="empty-state">No custom fields defined.</td>
|
||||||
No custom fields defined.
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($customFields as $field): ?>
|
<?php foreach ($customFields as $field): ?>
|
||||||
@@ -76,7 +76,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<td><?php echo htmlspecialchars($field['category'] ?? 'All'); ?></td>
|
<td><?php echo htmlspecialchars($field['category'] ?? 'All'); ?></td>
|
||||||
<td><?php echo $field['is_required'] ? 'Yes' : 'No'; ?></td>
|
<td><?php echo $field['is_required'] ? 'Yes' : 'No'; ?></td>
|
||||||
<td>
|
<td>
|
||||||
<span style="color: <?php echo $field['is_active'] ? 'var(--status-open)' : 'var(--status-closed)'; ?>;">
|
<span class="<?php echo $field['is_active'] ? 'text-open' : 'text-closed'; ?>">
|
||||||
<?php echo $field['is_active'] ? 'Active' : 'Inactive'; ?>
|
<?php echo $field['is_active'] ? 'Active' : 'Inactive'; ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -89,16 +89,17 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create/Edit Modal -->
|
<!-- Create/Edit Modal -->
|
||||||
<div class="lt-modal-overlay" id="fieldModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="fieldModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||||
<div class="lt-modal" style="max-width: 500px;">
|
<div class="lt-modal lt-modal-sm">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title" id="modalTitle">Create Custom Field</span>
|
<span class="lt-modal-title" id="modalTitle">Create Custom Field</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="fieldForm">
|
<form id="fieldForm">
|
||||||
<input type="hidden" id="field_id" name="field_id">
|
<input type="hidden" id="field_id" name="field_id">
|
||||||
@@ -156,7 +157,6 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
function showCreateModal() {
|
function showCreateModal() {
|
||||||
document.getElementById('modalTitle').textContent = 'Create Custom Field';
|
document.getElementById('modalTitle').textContent = 'Create Custom Field';
|
||||||
@@ -230,30 +230,19 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
data.field_options = { options: options };
|
data.field_options = { options: options };
|
||||||
}
|
}
|
||||||
|
|
||||||
const method = data.field_id ? 'PUT' : 'POST';
|
|
||||||
const url = '/api/custom_fields.php' + (data.field_id ? '?id=' + data.field_id : '');
|
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);
|
||||||
fetch(url, {
|
apiCall.then(result => {
|
||||||
method: method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRF-Token': window.CSRF_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(result => {
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
lt.toast.error(result.error || 'Failed to save');
|
lt.toast.error(result.error || 'Failed to save');
|
||||||
}
|
}
|
||||||
});
|
}).catch(err => lt.toast.error('Failed to save'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function editField(id) {
|
function editField(id) {
|
||||||
fetch('/api/custom_fields.php?id=' + id)
|
lt.api.get('/api/custom_fields.php?id=' + id)
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.field) {
|
if (data.success && data.field) {
|
||||||
const f = data.field;
|
const f = data.field;
|
||||||
@@ -276,14 +265,12 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteField(id) {
|
function deleteField(id) {
|
||||||
if (!confirm('Delete this custom field? All values will be lost.')) return;
|
showConfirmModal('Delete Custom Field', 'Delete this custom field? All values will be lost.', 'error', function() {
|
||||||
fetch('/api/custom_fields.php?id=' + id, {
|
lt.api.delete('/api/custom_fields.php?id=' + id)
|
||||||
method: 'DELETE',
|
.then(data => {
|
||||||
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
|
if (data.success) window.location.reload();
|
||||||
})
|
else lt.toast.error(data.error || 'Failed to delete');
|
||||||
.then(r => r.json())
|
}).catch(err => lt.toast.error('Failed to delete'));
|
||||||
.then(data => {
|
|
||||||
if (data.success) window.location.reload();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Recurring Tickets - Admin</title>
|
<title>Recurring Tickets - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.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">
|
<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="/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; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
</script>
|
</script>
|
||||||
@@ -24,7 +25,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Recurring Tickets</span>
|
<span class="admin-page-title">Admin: Recurring Tickets</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
@@ -34,19 +35,20 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ascii-frame-outer" style="max-width: 1200px; margin: 2rem auto;">
|
<div class="ascii-frame-outer admin-container">
|
||||||
<span class="bottom-left-corner">╚</span>
|
<span class="bottom-left-corner">╚</span>
|
||||||
<span class="bottom-right-corner">╝</span>
|
<span class="bottom-right-corner">╝</span>
|
||||||
|
|
||||||
<div class="ascii-section-header">Recurring Tickets Management</div>
|
<div class="ascii-section-header">Recurring Tickets Management</div>
|
||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
<div class="admin-header-row">
|
||||||
<h2 style="margin: 0;">Scheduled Tickets</h2>
|
<h2>Scheduled Tickets</h2>
|
||||||
<button data-action="show-create-modal" class="btn">+ NEW RECURRING TICKET</button>
|
<button data-action="show-create-modal" class="btn">+ NEW RECURRING TICKET</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table style="width: 100%;">
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
@@ -62,9 +64,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($recurringTickets)): ?>
|
<?php if (empty($recurringTickets)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
<td colspan="8" class="empty-state">No recurring tickets configured.</td>
|
||||||
No recurring tickets configured.
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($recurringTickets as $rt): ?>
|
<?php foreach ($recurringTickets as $rt): ?>
|
||||||
@@ -86,16 +86,16 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</td>
|
</td>
|
||||||
<td><?php echo htmlspecialchars($rt['category']); ?></td>
|
<td><?php echo htmlspecialchars($rt['category']); ?></td>
|
||||||
<td><?php echo htmlspecialchars($rt['assigned_name'] ?? $rt['assigned_username'] ?? 'Unassigned'); ?></td>
|
<td><?php echo htmlspecialchars($rt['assigned_name'] ?? $rt['assigned_username'] ?? 'Unassigned'); ?></td>
|
||||||
<td><?php echo date('M d, Y H:i', strtotime($rt['next_run_at'])); ?></td>
|
<td class="nowrap"><?php echo date('M d, Y H:i', strtotime($rt['next_run_at'])); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<span style="color: <?php echo $rt['is_active'] ? 'var(--status-open)' : 'var(--status-closed)'; ?>;">
|
<span class="<?php echo $rt['is_active'] ? 'text-open' : 'text-closed'; ?>">
|
||||||
<?php echo $rt['is_active'] ? 'Active' : 'Inactive'; ?>
|
<?php echo $rt['is_active'] ? 'Active' : 'Inactive'; ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button data-action="edit-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">EDIT</button>
|
<button data-action="edit-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">EDIT</button>
|
||||||
<button data-action="toggle-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">
|
<button data-action="toggle-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">
|
||||||
<?php echo $rt['is_active'] ? 'Disable' : 'Enable'; ?>
|
<?php echo $rt['is_active'] ? 'DISABLE' : 'ENABLE'; ?>
|
||||||
</button>
|
</button>
|
||||||
<button data-action="delete-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small btn-danger">DELETE</button>
|
<button data-action="delete-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small btn-danger">DELETE</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -104,27 +104,28 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create/Edit Modal -->
|
<!-- Create/Edit Modal -->
|
||||||
<div class="lt-modal-overlay" id="recurringModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="recurringModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||||
<div class="lt-modal" style="max-width: 800px; width: 90%;">
|
<div class="lt-modal lt-modal-lg">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title" id="modalTitle">Create Recurring Ticket</span>
|
<span class="lt-modal-title" id="modalTitle">Create Recurring Ticket</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="recurringForm">
|
<form id="recurringForm">
|
||||||
<input type="hidden" id="recurring_id" name="recurring_id">
|
<input type="hidden" id="recurring_id" name="recurring_id">
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<label for="title_template">Title Template *</label>
|
<label for="title_template">Title Template *</label>
|
||||||
<input type="text" id="title_template" name="title_template" required style="width: 100%;" placeholder="Use {{date}}, {{month}}, etc.">
|
<input type="text" id="title_template" name="title_template" required placeholder="Use {{date}}, {{month}}, etc.">
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<label for="description_template">Description Template</label>
|
<label for="description_template">Description Template</label>
|
||||||
<textarea id="description_template" name="description_template" rows="8" style="width: 100%; min-height: 150px;"></textarea>
|
<textarea id="description_template" name="description_template" rows="8"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<label for="schedule_type">Schedule Type *</label>
|
<label for="schedule_type">Schedule Type *</label>
|
||||||
@@ -142,7 +143,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<label for="schedule_time">Schedule Time *</label>
|
<label for="schedule_time">Schedule Time *</label>
|
||||||
<input type="time" id="schedule_time" name="schedule_time" value="09:00" required>
|
<input type="time" id="schedule_time" name="schedule_time" value="09:00" required>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem;">
|
<div class="setting-grid-2">
|
||||||
<div class="setting-row setting-row-compact">
|
<div class="setting-row setting-row-compact">
|
||||||
<label for="category">Category</label>
|
<label for="category">Category</label>
|
||||||
<select id="category" name="category">
|
<select id="category" name="category">
|
||||||
@@ -191,7 +192,6 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
function showCreateModal() {
|
function showCreateModal() {
|
||||||
document.getElementById('modalTitle').textContent = 'Create Recurring Ticket';
|
document.getElementById('modalTitle').textContent = 'Create Recurring Ticket';
|
||||||
@@ -271,53 +271,37 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
const form = new FormData(document.getElementById('recurringForm'));
|
const form = new FormData(document.getElementById('recurringForm'));
|
||||||
const data = Object.fromEntries(form);
|
const data = Object.fromEntries(form);
|
||||||
|
|
||||||
const method = data.recurring_id ? 'PUT' : 'POST';
|
|
||||||
const url = '/api/manage_recurring.php' + (data.recurring_id ? '?id=' + data.recurring_id : '');
|
const url = '/api/manage_recurring.php' + (data.recurring_id ? '?id=' + data.recurring_id : '');
|
||||||
|
const apiCall = data.recurring_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
||||||
fetch(url, {
|
apiCall.then(result => {
|
||||||
method: method,
|
if (result.success) {
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRF-Token': window.CSRF_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
lt.toast.error(data.error || 'Failed to save');
|
lt.toast.error(result.error || 'Failed to save');
|
||||||
}
|
}
|
||||||
});
|
}).catch(err => lt.toast.error('Failed to save'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleRecurring(id) {
|
function toggleRecurring(id) {
|
||||||
fetch('/api/manage_recurring.php?action=toggle&id=' + id, {
|
lt.api.post('/api/manage_recurring.php?action=toggle&id=' + id)
|
||||||
method: 'POST',
|
|
||||||
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) window.location.reload();
|
if (data.success) window.location.reload();
|
||||||
});
|
else lt.toast.error(data.error || 'Failed to toggle');
|
||||||
|
}).catch(err => lt.toast.error('Failed to toggle'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteRecurring(id) {
|
function deleteRecurring(id) {
|
||||||
if (!confirm('Delete this recurring ticket schedule?')) return;
|
showConfirmModal('Delete Schedule', 'Delete this recurring ticket schedule?', 'error', function() {
|
||||||
fetch('/api/manage_recurring.php?id=' + id, {
|
lt.api.delete('/api/manage_recurring.php?id=' + id)
|
||||||
method: 'DELETE',
|
.then(data => {
|
||||||
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
|
if (data.success) window.location.reload();
|
||||||
})
|
else lt.toast.error(data.error || 'Failed to delete');
|
||||||
.then(r => r.json())
|
}).catch(err => lt.toast.error('Failed to delete'));
|
||||||
.then(data => {
|
|
||||||
if (data.success) window.location.reload();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function editRecurring(id) {
|
function editRecurring(id) {
|
||||||
fetch('/api/manage_recurring.php?id=' + id)
|
lt.api.get('/api/manage_recurring.php?id=' + id)
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.recurring) {
|
if (data.success && data.recurring) {
|
||||||
const rt = data.recurring;
|
const rt = data.recurring;
|
||||||
@@ -340,8 +324,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
|
|
||||||
// Load users for assignee dropdown
|
// Load users for assignee dropdown
|
||||||
function loadUsers() {
|
function loadUsers() {
|
||||||
fetch('/api/get_users.php')
|
lt.api.get('/api/get_users.php')
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.users) {
|
if (data.success && data.users) {
|
||||||
const select = document.getElementById('assigned_to');
|
const select = document.getElementById('assigned_to');
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Template Management - Admin</title>
|
<title>Template Management - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.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">
|
<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="/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; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
</script>
|
</script>
|
||||||
@@ -24,7 +25,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Templates</span>
|
<span class="admin-page-title">Admin: Templates</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
@@ -34,23 +35,24 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ascii-frame-outer" style="max-width: 1200px; margin: 2rem auto;">
|
<div class="ascii-frame-outer admin-container">
|
||||||
<span class="bottom-left-corner">╚</span>
|
<span class="bottom-left-corner">╚</span>
|
||||||
<span class="bottom-right-corner">╝</span>
|
<span class="bottom-right-corner">╝</span>
|
||||||
|
|
||||||
<div class="ascii-section-header">Ticket Template Management</div>
|
<div class="ascii-section-header">Ticket Template Management</div>
|
||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
<div class="admin-header-row">
|
||||||
<h2 style="margin: 0;">Ticket Templates</h2>
|
<h2>Ticket Templates</h2>
|
||||||
<button data-action="show-create-modal" class="btn">+ NEW TEMPLATE</button>
|
<button data-action="show-create-modal" class="btn">+ NEW TEMPLATE</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="color: var(--terminal-green-dim); margin-bottom: 1rem;">
|
<p class="text-muted-green mb-1">
|
||||||
Templates pre-fill ticket creation forms with standard content for common ticket types.
|
Templates pre-fill ticket creation forms with standard content for common ticket types.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table style="width: 100%;">
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Template Name</th>
|
<th>Template Name</th>
|
||||||
@@ -64,9 +66,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($templates)): ?>
|
<?php if (empty($templates)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
<td colspan="6" class="empty-state">No templates defined. Create templates to speed up ticket creation.</td>
|
||||||
No templates defined. Create templates to speed up ticket creation.
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($templates as $tpl): ?>
|
<?php foreach ($templates as $tpl): ?>
|
||||||
@@ -76,7 +76,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<td><?php echo htmlspecialchars($tpl['type'] ?? 'Any'); ?></td>
|
<td><?php echo htmlspecialchars($tpl['type'] ?? 'Any'); ?></td>
|
||||||
<td>P<?php echo $tpl['default_priority'] ?? '4'; ?></td>
|
<td>P<?php echo $tpl['default_priority'] ?? '4'; ?></td>
|
||||||
<td>
|
<td>
|
||||||
<span style="color: <?php echo ($tpl['is_active'] ?? 1) ? 'var(--status-open)' : 'var(--status-closed)'; ?>;">
|
<span class="<?php echo ($tpl['is_active'] ?? 1) ? 'text-open' : 'text-closed'; ?>">
|
||||||
<?php echo ($tpl['is_active'] ?? 1) ? 'Active' : 'Inactive'; ?>
|
<?php echo ($tpl['is_active'] ?? 1) ? 'Active' : 'Inactive'; ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -89,33 +89,34 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create/Edit Modal -->
|
<!-- Create/Edit Modal -->
|
||||||
<div class="lt-modal-overlay" id="templateModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="templateModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||||
<div class="lt-modal" style="max-width: 800px; width: 90%;">
|
<div class="lt-modal lt-modal-lg">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title" id="modalTitle">Create Template</span>
|
<span class="lt-modal-title" id="modalTitle">Create Template</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="templateForm">
|
<form id="templateForm">
|
||||||
<input type="hidden" id="template_id" name="template_id">
|
<input type="hidden" id="template_id" name="template_id">
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<label for="template_name">Template Name *</label>
|
<label for="template_name">Template Name *</label>
|
||||||
<input type="text" id="template_name" name="template_name" required style="width: 100%;">
|
<input type="text" id="template_name" name="template_name" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<label for="title_template">Title Template</label>
|
<label for="title_template">Title Template</label>
|
||||||
<input type="text" id="title_template" name="title_template" style="width: 100%;" placeholder="Pre-filled title text">
|
<input type="text" id="title_template" name="title_template" placeholder="Pre-filled title text">
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<label for="description_template">Description Template</label>
|
<label for="description_template">Description Template</label>
|
||||||
<textarea id="description_template" name="description_template" rows="10" style="width: 100%; min-height: 200px;" placeholder="Pre-filled description content"></textarea>
|
<textarea id="description_template" name="description_template" rows="10" placeholder="Pre-filled description content"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
|
<div class="setting-grid-3">
|
||||||
<div class="setting-row setting-row-compact">
|
<div class="setting-row setting-row-compact">
|
||||||
<label for="category">Category</label>
|
<label for="category">Category</label>
|
||||||
<select id="category" name="category">
|
<select id="category" name="category">
|
||||||
@@ -162,7 +163,6 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
const templates = <?php echo json_encode($templates ?? []); ?>;
|
const templates = <?php echo json_encode($templates ?? []); ?>;
|
||||||
|
|
||||||
@@ -217,25 +217,15 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
is_active: document.getElementById('is_active').checked ? 1 : 0
|
is_active: document.getElementById('is_active').checked ? 1 : 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const method = data.template_id ? 'PUT' : 'POST';
|
|
||||||
const url = '/api/manage_templates.php' + (data.template_id ? '?id=' + data.template_id : '');
|
const url = '/api/manage_templates.php' + (data.template_id ? '?id=' + data.template_id : '');
|
||||||
|
const apiCall = data.template_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
||||||
fetch(url, {
|
apiCall.then(result => {
|
||||||
method: method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRF-Token': window.CSRF_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(result => {
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
lt.toast.error(result.error || 'Failed to save');
|
lt.toast.error(result.error || 'Failed to save');
|
||||||
}
|
}
|
||||||
});
|
}).catch(err => lt.toast.error('Failed to save'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function editTemplate(id) {
|
function editTemplate(id) {
|
||||||
@@ -255,14 +245,12 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteTemplate(id) {
|
function deleteTemplate(id) {
|
||||||
if (!confirm('Delete this template?')) return;
|
showConfirmModal('Delete Template', 'Delete this template?', 'error', function() {
|
||||||
fetch('/api/manage_templates.php?id=' + id, {
|
lt.api.delete('/api/manage_templates.php?id=' + id)
|
||||||
method: 'DELETE',
|
.then(data => {
|
||||||
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
|
if (data.success) window.location.reload();
|
||||||
})
|
else lt.toast.error(data.error || 'Failed to delete');
|
||||||
.then(r => r.json())
|
}).catch(err => lt.toast.error('Failed to delete'));
|
||||||
.then(data => {
|
|
||||||
if (data.success) window.location.reload();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>User Activity - Admin</title>
|
<title>User Activity - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.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">
|
<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="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
@@ -24,7 +24,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: User Activity</span>
|
<span class="admin-page-title">Admin: User Activity</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
@@ -34,7 +34,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ascii-frame-outer" style="max-width: 1200px; margin: 2rem auto;">
|
<div class="ascii-frame-outer admin-container">
|
||||||
<span class="bottom-left-corner">╚</span>
|
<span class="bottom-left-corner">╚</span>
|
||||||
<span class="bottom-right-corner">╝</span>
|
<span class="bottom-right-corner">╝</span>
|
||||||
|
|
||||||
@@ -42,37 +42,38 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<!-- Date Range Filter -->
|
<!-- Date Range Filter -->
|
||||||
<form method="GET" style="display: flex; gap: 1rem; margin-bottom: 1.5rem; align-items: flex-end;">
|
<form method="GET" class="admin-form-row">
|
||||||
<div>
|
<div class="admin-form-field">
|
||||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Date From</label>
|
<label class="admin-label" for="date_from">Date From</label>
|
||||||
<input type="date" name="date_from" value="<?php echo htmlspecialchars($dateRange['from'] ?? ''); ?>" class="setting-select">
|
<input type="date" name="date_from" id="date_from" value="<?php echo htmlspecialchars($dateRange['from'] ?? ''); ?>" class="admin-input">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="admin-form-field">
|
||||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Date To</label>
|
<label class="admin-label" for="date_to">Date To</label>
|
||||||
<input type="date" name="date_to" value="<?php echo htmlspecialchars($dateRange['to'] ?? ''); ?>" class="setting-select">
|
<input type="date" name="date_to" id="date_to" value="<?php echo htmlspecialchars($dateRange['to'] ?? ''); ?>" class="admin-input">
|
||||||
|
</div>
|
||||||
|
<div class="admin-form-actions">
|
||||||
|
<button type="submit" class="btn">APPLY</button>
|
||||||
|
<a href="?" class="btn btn-secondary">RESET</a>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn">APPLY</button>
|
|
||||||
<a href="?" class="btn btn-secondary">RESET</a>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- User Activity Table -->
|
<!-- User Activity Table -->
|
||||||
<table style="width: 100%;">
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th style="text-align: center;">Tickets Created</th>
|
<th class="text-center">Tickets Created</th>
|
||||||
<th style="text-align: center;">Tickets Resolved</th>
|
<th class="text-center">Tickets Resolved</th>
|
||||||
<th style="text-align: center;">Comments Added</th>
|
<th class="text-center">Comments Added</th>
|
||||||
<th style="text-align: center;">Tickets Assigned</th>
|
<th class="text-center">Tickets Assigned</th>
|
||||||
<th style="text-align: center;">Last Activity</th>
|
<th class="text-center">Last Activity</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($userStats)): ?>
|
<?php if (empty($userStats)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
<td colspan="6" class="empty-state">No user activity data available.</td>
|
||||||
No user activity data available.
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($userStats as $user): ?>
|
<?php foreach ($userStats as $user): ?>
|
||||||
@@ -80,22 +81,22 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<td>
|
<td>
|
||||||
<strong><?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?></strong>
|
<strong><?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?></strong>
|
||||||
<?php if ($user['is_admin']): ?>
|
<?php if ($user['is_admin']): ?>
|
||||||
<span class="admin-badge" style="font-size: 0.7rem;">[ ADMIN ]</span>
|
<span class="admin-badge">[ ADMIN ]</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;">
|
<td class="text-center">
|
||||||
<span style="color: var(--terminal-green); font-weight: bold;"><?php echo $user['tickets_created'] ?? 0; ?></span>
|
<span class="text-green fw-bold"><?php echo $user['tickets_created'] ?? 0; ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;">
|
<td class="text-center">
|
||||||
<span style="color: var(--status-open); font-weight: bold;"><?php echo $user['tickets_resolved'] ?? 0; ?></span>
|
<span class="text-open fw-bold"><?php echo $user['tickets_resolved'] ?? 0; ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;">
|
<td class="text-center">
|
||||||
<span style="color: var(--terminal-cyan); font-weight: bold;"><?php echo $user['comments_added'] ?? 0; ?></span>
|
<span class="text-cyan fw-bold"><?php echo $user['comments_added'] ?? 0; ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;">
|
<td class="text-center">
|
||||||
<span style="color: var(--terminal-amber); font-weight: bold;"><?php echo $user['tickets_assigned'] ?? 0; ?></span>
|
<span class="text-amber fw-bold"><?php echo $user['tickets_assigned'] ?? 0; ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center; font-size: 0.9rem;">
|
<td class="text-center text-sm">
|
||||||
<?php echo $user['last_activity'] ? date('M d, Y H:i', strtotime($user['last_activity'])) : 'Never'; ?>
|
<?php echo $user['last_activity'] ? date('M d, Y H:i', strtotime($user['last_activity'])) : 'Never'; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -103,42 +104,32 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Summary Stats -->
|
<!-- Summary Stats -->
|
||||||
<?php if (!empty($userStats)): ?>
|
<?php if (!empty($userStats)): ?>
|
||||||
<div style="margin-top: 2rem; padding: 1rem; border: 1px solid var(--terminal-green);">
|
<div class="admin-stats-grid">
|
||||||
<h4 style="color: var(--terminal-amber); margin-bottom: 1rem;">Summary</h4>
|
<div>
|
||||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; text-align: center;">
|
<div class="admin-stat-value text-green"><?php echo array_sum(array_column($userStats, 'tickets_created')); ?></div>
|
||||||
<div>
|
<div class="admin-stat-label">Total Created</div>
|
||||||
<div style="font-size: 1.5rem; color: var(--terminal-green); font-weight: bold;">
|
</div>
|
||||||
<?php echo array_sum(array_column($userStats, 'tickets_created')); ?>
|
<div>
|
||||||
</div>
|
<div class="admin-stat-value text-open"><?php echo array_sum(array_column($userStats, 'tickets_resolved')); ?></div>
|
||||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim);">Total Created</div>
|
<div class="admin-stat-label">Total Resolved</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size: 1.5rem; color: var(--status-open); font-weight: bold;">
|
<div class="admin-stat-value text-cyan"><?php echo array_sum(array_column($userStats, 'comments_added')); ?></div>
|
||||||
<?php echo array_sum(array_column($userStats, 'tickets_resolved')); ?>
|
<div class="admin-stat-label">Total Comments</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim);">Total Resolved</div>
|
<div>
|
||||||
</div>
|
<div class="admin-stat-value text-amber"><?php echo count($userStats); ?></div>
|
||||||
<div>
|
<div class="admin-stat-label">Active Users</div>
|
||||||
<div style="font-size: 1.5rem; color: var(--terminal-cyan); font-weight: bold;">
|
|
||||||
<?php echo array_sum(array_column($userStats, 'comments_added')); ?>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim);">Total Comments</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="font-size: 1.5rem; color: var(--terminal-amber); font-weight: bold;">
|
|
||||||
<?php echo count($userStats); ?>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim);">Active Users</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>if (window.lt) lt.keys.initDefaults();</script>
|
<script nonce="<?php echo $nonce; ?>">if (window.lt) lt.keys.initDefaults();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<title>Workflow Designer - Admin</title>
|
<title>Workflow Designer - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<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="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.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">
|
<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="/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; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
</script>
|
</script>
|
||||||
@@ -24,7 +25,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<div class="user-header-left">
|
<div class="user-header-left">
|
||||||
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
|
||||||
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Workflow Designer</span>
|
<span class="admin-page-title">Admin: Workflow Designer</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-header-right">
|
<div class="user-header-right">
|
||||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||||
@@ -34,36 +35,36 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ascii-frame-outer" style="max-width: 1200px; margin: 2rem auto;">
|
<div class="ascii-frame-outer admin-container">
|
||||||
<span class="bottom-left-corner">╚</span>
|
<span class="bottom-left-corner">╚</span>
|
||||||
<span class="bottom-right-corner">╝</span>
|
<span class="bottom-right-corner">╝</span>
|
||||||
|
|
||||||
<div class="ascii-section-header">Status Workflow Designer</div>
|
<div class="ascii-section-header">Status Workflow Designer</div>
|
||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
<div class="admin-header-row">
|
||||||
<h2 style="margin: 0;">Status Transitions</h2>
|
<h2>Status Transitions</h2>
|
||||||
<button data-action="show-create-modal" class="btn">+ NEW TRANSITION</button>
|
<button data-action="show-create-modal" class="btn">+ NEW TRANSITION</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="color: var(--terminal-green-dim); margin-bottom: 1rem;">
|
<p class="text-muted-green mb-1">
|
||||||
Define which status transitions are allowed. This controls what options appear in the status dropdown.
|
Define which status transitions are allowed. This controls what options appear in the status dropdown.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Visual Workflow Diagram -->
|
<!-- Visual Workflow Diagram -->
|
||||||
<div style="margin-bottom: 2rem; padding: 1rem; border: 1px solid var(--terminal-green); background: var(--bg-secondary);">
|
<div class="workflow-diagram">
|
||||||
<h4 style="color: var(--terminal-amber); margin-bottom: 1rem;">Workflow Diagram</h4>
|
<h4 class="admin-section-title">Workflow Diagram</h4>
|
||||||
<div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap;">
|
<div class="workflow-diagram-nodes">
|
||||||
<?php
|
<?php
|
||||||
$statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
|
$statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
|
||||||
foreach ($statuses as $status):
|
foreach ($statuses as $status):
|
||||||
$statusClass = 'status-' . str_replace(' ', '-', strtolower($status));
|
$statusClass = 'status-' . str_replace(' ', '-', strtolower($status));
|
||||||
?>
|
?>
|
||||||
<div style="text-align: center;">
|
<div class="workflow-diagram-node">
|
||||||
<div class="<?php echo $statusClass; ?>" style="padding: 0.5rem 1rem; display: inline-block;">
|
<div class="<?php echo $statusClass; ?>">
|
||||||
<?php echo $status; ?>
|
<?php echo $status; ?>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim); margin-top: 0.5rem;">
|
<div class="text-muted-green workflow-diagram-node-label">
|
||||||
<?php
|
<?php
|
||||||
$toCount = 0;
|
$toCount = 0;
|
||||||
if (isset($workflows)) {
|
if (isset($workflows)) {
|
||||||
@@ -80,7 +81,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Transitions Table -->
|
<!-- Transitions Table -->
|
||||||
<table style="width: 100%;">
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>From Status</th>
|
<th>From Status</th>
|
||||||
@@ -95,9 +97,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($workflows)): ?>
|
<?php if (empty($workflows)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
<td colspan="7" class="empty-state">No transitions defined. Add transitions to enable status changes.</td>
|
||||||
No transitions defined. Add transitions to enable status changes.
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($workflows as $wf): ?>
|
<?php foreach ($workflows as $wf): ?>
|
||||||
@@ -107,16 +107,16 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php echo htmlspecialchars($wf['from_status']); ?>
|
<?php echo htmlspecialchars($wf['from_status']); ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center; color: var(--terminal-amber);">→</td>
|
<td class="text-amber text-center">→</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="status-<?php echo str_replace(' ', '-', strtolower($wf['to_status'])); ?>">
|
<span class="status-<?php echo str_replace(' ', '-', strtolower($wf['to_status'])); ?>">
|
||||||
<?php echo htmlspecialchars($wf['to_status']); ?>
|
<?php echo htmlspecialchars($wf['to_status']); ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;"><?php echo $wf['requires_comment'] ? '✓' : '−'; ?></td>
|
<td class="text-center"><?php echo $wf['requires_comment'] ? '✓' : '−'; ?></td>
|
||||||
<td style="text-align: center;"><?php echo $wf['requires_admin'] ? '✓' : '−'; ?></td>
|
<td class="text-center"><?php echo $wf['requires_admin'] ? '✓' : '−'; ?></td>
|
||||||
<td style="text-align: center;">
|
<td class="text-center">
|
||||||
<span style="color: <?php echo $wf['is_active'] ? 'var(--status-open)' : 'var(--status-closed)'; ?>;">
|
<span class="<?php echo $wf['is_active'] ? 'text-open' : 'text-closed'; ?>">
|
||||||
<?php echo $wf['is_active'] ? '✓' : '✗'; ?>
|
<?php echo $wf['is_active'] ? '✓' : '✗'; ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -129,16 +129,17 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create/Edit Modal -->
|
<!-- Create/Edit Modal -->
|
||||||
<div class="lt-modal-overlay" id="workflowModal" aria-hidden="true">
|
<div class="lt-modal-overlay" id="workflowModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||||
<div class="lt-modal" style="max-width: 450px;">
|
<div class="lt-modal lt-modal-sm">
|
||||||
<div class="lt-modal-header">
|
<div class="lt-modal-header">
|
||||||
<span class="lt-modal-title" id="modalTitle">Create Transition</span>
|
<span class="lt-modal-title" id="modalTitle">Create Transition</span>
|
||||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="workflowForm">
|
<form id="workflowForm">
|
||||||
<input type="hidden" id="transition_id" name="transition_id">
|
<input type="hidden" id="transition_id" name="transition_id">
|
||||||
@@ -179,7 +180,6 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
const workflows = <?php echo json_encode($workflows ?? []); ?>;
|
const workflows = <?php echo json_encode($workflows ?? []); ?>;
|
||||||
|
|
||||||
@@ -232,25 +232,15 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
is_active: document.getElementById('is_active').checked ? 1 : 0
|
is_active: document.getElementById('is_active').checked ? 1 : 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const method = data.transition_id ? 'PUT' : 'POST';
|
|
||||||
const url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_id : '');
|
const url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_id : '');
|
||||||
|
const apiCall = data.transition_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
||||||
fetch(url, {
|
apiCall.then(result => {
|
||||||
method: method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRF-Token': window.CSRF_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(result => {
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
lt.toast.error(result.error || 'Failed to save');
|
lt.toast.error(result.error || 'Failed to save');
|
||||||
}
|
}
|
||||||
});
|
}).catch(err => lt.toast.error('Failed to save'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function editTransition(id) {
|
function editTransition(id) {
|
||||||
@@ -268,14 +258,12 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteTransition(id) {
|
function deleteTransition(id) {
|
||||||
if (!confirm('Delete this status transition?')) return;
|
showConfirmModal('Delete Transition', 'Delete this status transition?', 'error', function() {
|
||||||
fetch('/api/manage_workflows.php?id=' + id, {
|
lt.api.delete('/api/manage_workflows.php?id=' + id)
|
||||||
method: 'DELETE',
|
.then(data => {
|
||||||
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
|
if (data.success) window.location.reload();
|
||||||
})
|
else lt.toast.error(data.error || 'Failed to delete');
|
||||||
.then(r => r.json())
|
}).catch(err => lt.toast.error('Failed to delete'));
|
||||||
.then(data => {
|
|
||||||
if (data.success) window.location.reload();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user