feat(tickets): add ticket detail subcomponents
- TicketDetailHeader: Display ticket info with status dropdown - TicketNotesFeed: Chronological list of ticket notes with internal flag - TicketAddNote: Form to add notes (requires linked session) - TicketConfigs: Display related configurations/devices - TicketRelated: List of related tickets as clickable buttons All components use type-safe imports from psaContext and integrations APIs. Styling follows design system (flat dark theme, electric blue accent, Tailwind v4). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { useState } from 'react'
|
||||
import { ticketsApi } from '@/api/tickets'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { PSATicketSearchResult, PSATicketStatusItem } from '@/types/integrations'
|
||||
import type { PSATicketStatusUpdate } from '@/types/tickets'
|
||||
|
||||
interface Props {
|
||||
ticket: PSATicketSearchResult
|
||||
statuses: PSATicketStatusItem[]
|
||||
onStatusUpdated: (ticketId: number, newStatus: string) => void
|
||||
}
|
||||
|
||||
export function TicketDetailHeader({ ticket, statuses, onStatusUpdated }: Props) {
|
||||
const [updating, setUpdating] = useState(false)
|
||||
|
||||
async function handleStatusChange(statusId: number) {
|
||||
if (!ticket.id) return
|
||||
setUpdating(true)
|
||||
try {
|
||||
const result: PSATicketStatusUpdate = await ticketsApi.updateStatus(Number(ticket.id), statusId)
|
||||
onStatusUpdated(result.ticket_id, result.new_status)
|
||||
toast.success(`Status updated to ${result.new_status}`)
|
||||
} catch {
|
||||
toast.error('Failed to update status')
|
||||
} finally {
|
||||
setUpdating(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 border-b border-default space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-accent text-xs font-mono">#{ticket.id}</span>
|
||||
{ticket.board_name && (
|
||||
<span className="text-xs text-muted-foreground">{ticket.board_name}</span>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="font-heading font-semibold text-heading text-base leading-snug">
|
||||
{ticket.summary}
|
||||
</h2>
|
||||
{ticket.company_name && (
|
||||
<p className="text-sm text-muted-foreground mt-0.5">{ticket.company_name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{statuses.length > 0 ? (
|
||||
<select
|
||||
disabled={updating}
|
||||
value={ticket.status_id ?? ''}
|
||||
onChange={e => handleStatusChange(Number(e.target.value))}
|
||||
className="bg-input border border-default rounded-[5px] px-2 py-1 text-xs text-primary focus:border-accent focus:outline-none"
|
||||
>
|
||||
{statuses.map(s => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
ticket.status_name && (
|
||||
<span className="px-2 py-0.5 bg-elevated rounded text-xs text-muted-foreground">
|
||||
{ticket.status_name}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{ticket.priority_name && (
|
||||
<span className="px-2 py-0.5 bg-elevated rounded text-xs text-muted-foreground">
|
||||
{ticket.priority_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user