Initial commit: TicketingSystem
Internal ticketing app with CTI routing, severity levels, and n8n integration. Stack: Express + TypeScript + Prisma + PostgreSQL / React + Vite + Tailwind - JWT auth for users, API key auth for service accounts (Goddard/n8n) - CTI hierarchy (Category > Type > Item) for ticket routing - Severity 1-5, auto-close resolved tickets after 14 days - Gitea Actions CI/CD building separate server/client images - Production docker-compose.yml with Traefik integration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import api from '../api/client'
|
||||
import { Category, CTIType, Item } from '../types'
|
||||
|
||||
interface CTISelectProps {
|
||||
value: { categoryId: string; typeId: string; itemId: string }
|
||||
onChange: (value: { categoryId: string; typeId: string; itemId: string }) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function CTISelect({ value, onChange, disabled }: CTISelectProps) {
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [types, setTypes] = useState<CTIType[]>([])
|
||||
const [items, setItems] = useState<Item[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
api.get<Category[]>('/cti/categories').then((r) => setCategories(r.data))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!value.categoryId) {
|
||||
setTypes([])
|
||||
setItems([])
|
||||
return
|
||||
}
|
||||
api
|
||||
.get<CTIType[]>('/cti/types', { params: { categoryId: value.categoryId } })
|
||||
.then((r) => setTypes(r.data))
|
||||
}, [value.categoryId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!value.typeId) {
|
||||
setItems([])
|
||||
return
|
||||
}
|
||||
api
|
||||
.get<Item[]>('/cti/items', { params: { typeId: value.typeId } })
|
||||
.then((r) => setItems(r.data))
|
||||
}, [value.typeId])
|
||||
|
||||
const handleCategory = (categoryId: string) => {
|
||||
onChange({ categoryId, typeId: '', itemId: '' })
|
||||
}
|
||||
|
||||
const handleType = (typeId: string) => {
|
||||
onChange({ ...value, typeId, itemId: '' })
|
||||
}
|
||||
|
||||
const handleItem = (itemId: string) => {
|
||||
onChange({ ...value, itemId })
|
||||
}
|
||||
|
||||
const selectClass =
|
||||
'block w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50 disabled:text-gray-400'
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Category</label>
|
||||
<select
|
||||
value={value.categoryId}
|
||||
onChange={(e) => handleCategory(e.target.value)}
|
||||
disabled={disabled}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
{categories.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Type</label>
|
||||
<select
|
||||
value={value.typeId}
|
||||
onChange={(e) => handleType(e.target.value)}
|
||||
disabled={disabled || !value.categoryId}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
{types.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Item</label>
|
||||
<select
|
||||
value={value.itemId}
|
||||
onChange={(e) => handleItem(e.target.value)}
|
||||
disabled={disabled || !value.typeId}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
{items.map((i) => (
|
||||
<option key={i.id} value={i.id}>
|
||||
{i.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user