Dark theme, roles overhaul, modal New Ticket, My Tickets page, and more
- Dark UI across all pages and components (gray-950/900/800 palette) - New Ticket is now a centered modal (triggered from sidebar), not a separate page - Add USER role: view and comment only; AGENT and SERVICE can create/edit tickets - Only admins can set ticket status to CLOSED (enforced server + UI) - Add My Tickets page (/my-tickets) showing tickets assigned to current user - Add queue (category) filter to Dashboard - Audit log entries are clickable to expand detail; comment body shown as markdown - Resolved date now includes time (HH:mm) in ticket sidebar - Store comment body in audit log detail for COMMENT_ADDED and COMMENT_DELETED - Clarify role descriptions in Admin Users modal - Remove CI/CD section from README; add full API reference documentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import api from '../api/client'
|
||||
import Layout from '../components/Layout'
|
||||
import Modal from '../components/Modal'
|
||||
import CTISelect from '../components/CTISelect'
|
||||
import { User } from '../types'
|
||||
|
||||
export default function NewTicket() {
|
||||
interface NewTicketModalProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function NewTicketModal({ onClose }: NewTicketModalProps) {
|
||||
const navigate = useNavigate()
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [error, setError] = useState('')
|
||||
@@ -49,6 +53,7 @@ export default function NewTicket() {
|
||||
if (form.assigneeId) payload.assigneeId = form.assigneeId
|
||||
|
||||
const res = await api.post('/tickets', payload)
|
||||
onClose()
|
||||
navigate(`/tickets/${res.data.displayId}`)
|
||||
} catch {
|
||||
setError('Failed to create ticket')
|
||||
@@ -58,104 +63,103 @@ export default function NewTicket() {
|
||||
}
|
||||
|
||||
const inputClass =
|
||||
'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||||
const labelClass = 'block text-sm font-medium text-gray-700 mb-1'
|
||||
'w-full bg-gray-800 border border-gray-700 text-gray-100 placeholder-gray-500 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent'
|
||||
const labelClass = 'block text-sm font-medium text-gray-300 mb-1'
|
||||
|
||||
return (
|
||||
<Layout title="New Ticket">
|
||||
<div className="max-w-2xl">
|
||||
<form onSubmit={handleSubmit} className="bg-white border border-gray-200 rounded-xl p-6 space-y-5">
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 text-sm px-4 py-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<Modal title="New Ticket" onClose={onClose} size="lg">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm px-4 py-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.title}
|
||||
onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}
|
||||
required
|
||||
className={inputClass}
|
||||
placeholder="Brief description of the issue"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Overview</label>
|
||||
<textarea
|
||||
value={form.overview}
|
||||
onChange={(e) => setForm((f) => ({ ...f, overview: e.target.value }))}
|
||||
required
|
||||
rows={4}
|
||||
className={inputClass}
|
||||
placeholder="Detailed description... Markdown supported"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.title}
|
||||
onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}
|
||||
required
|
||||
<label className={labelClass}>Severity</label>
|
||||
<select
|
||||
value={form.severity}
|
||||
onChange={(e) => setForm((f) => ({ ...f, severity: Number(e.target.value) }))}
|
||||
className={inputClass}
|
||||
placeholder="Brief description of the issue"
|
||||
/>
|
||||
>
|
||||
<option value={1}>SEV 1 — Critical</option>
|
||||
<option value={2}>SEV 2 — High</option>
|
||||
<option value={3}>SEV 3 — Medium</option>
|
||||
<option value={4}>SEV 4 — Low</option>
|
||||
<option value={5}>SEV 5 — Minimal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Overview</label>
|
||||
<textarea
|
||||
value={form.overview}
|
||||
onChange={(e) => setForm((f) => ({ ...f, overview: e.target.value }))}
|
||||
required
|
||||
rows={4}
|
||||
<label className={labelClass}>Assignee</label>
|
||||
<select
|
||||
value={form.assigneeId}
|
||||
onChange={(e) => setForm((f) => ({ ...f, assigneeId: e.target.value }))}
|
||||
className={inputClass}
|
||||
placeholder="Detailed description..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Severity</label>
|
||||
<select
|
||||
value={form.severity}
|
||||
onChange={(e) => setForm((f) => ({ ...f, severity: Number(e.target.value) }))}
|
||||
className={inputClass}
|
||||
>
|
||||
<option value={1}>SEV 1 — Critical</option>
|
||||
<option value={2}>SEV 2 — High</option>
|
||||
<option value={3}>SEV 3 — Medium</option>
|
||||
<option value={4}>SEV 4 — Low</option>
|
||||
<option value={5}>SEV 5 — Minimal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Assignee</label>
|
||||
<select
|
||||
value={form.assigneeId}
|
||||
onChange={(e) => setForm((f) => ({ ...f, assigneeId: e.target.value }))}
|
||||
className={inputClass}
|
||||
>
|
||||
<option value="">Unassigned</option>
|
||||
{users
|
||||
.filter((u) => u.role !== 'SERVICE')
|
||||
.map((u) => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Routing (CTI)</label>
|
||||
<CTISelect
|
||||
value={{ categoryId: form.categoryId, typeId: form.typeId, itemId: form.itemId }}
|
||||
onChange={handleCTI}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
className="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{submitting ? 'Creating...' : 'Create Ticket'}
|
||||
</button>
|
||||
<option value="">Unassigned</option>
|
||||
{users
|
||||
.filter((u) => u.role !== 'SERVICE')
|
||||
.map((u) => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Routing (CTI)</label>
|
||||
<CTISelect
|
||||
value={{ categoryId: form.categoryId, typeId: form.typeId, itemId: form.itemId }}
|
||||
onChange={handleCTI}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm text-gray-400 border border-gray-700 rounded-lg hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{submitting ? 'Creating...' : 'Create Ticket'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user