feat: add ticket workflow pages
This commit is contained in:
93
frontend/src/features/tickets/TicketDetailPage.tsx
Normal file
93
frontend/src/features/tickets/TicketDetailPage.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { AppEmptyState } from '@/components/app/AppEmptyState'
|
||||
import { AppField } from '@/components/app/AppField'
|
||||
import { AppPageHeader } from '@/components/app/AppPageHeader'
|
||||
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
|
||||
import { AppStatusBadge } from '@/components/app/AppStatusBadge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
|
||||
import { getTicket, transitionTicket } from './api'
|
||||
import { TicketComments } from './TicketComments'
|
||||
import { TicketStatusTimeline } from './TicketStatusTimeline'
|
||||
import type { Ticket, TicketStatus } from './types'
|
||||
|
||||
const NEXT_STATUSES: TicketStatus[] = ['in_progress', 'pending_confirm', 'resolved', 'closed']
|
||||
|
||||
export function TicketDetailPage({ ticketId }: { ticketId: number }) {
|
||||
const [ticket, setTicket] = React.useState<Ticket | null>(null)
|
||||
const [error, setError] = React.useState('')
|
||||
|
||||
React.useEffect(() => {
|
||||
getTicket(ticketId)
|
||||
.then(setTicket)
|
||||
.catch((reason: unknown) => setError(reason instanceof Error ? reason.message : '加载失败'))
|
||||
}, [ticketId])
|
||||
|
||||
async function handleTransition(status: TicketStatus) {
|
||||
if (!ticket) return
|
||||
const updated = await transitionTicket(ticket.id, status, `Move to ${status}`)
|
||||
setTicket(updated)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||||
<AppEmptyState title="Unable to load ticket" description={error} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (!ticket) {
|
||||
return (
|
||||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||||
<AppEmptyState title="Loading ticket" />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-4">
|
||||
<AppPageHeader
|
||||
title={ticket.title}
|
||||
description={ticket.ticket_no}
|
||||
actions={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{NEXT_STATUSES.map((status) => (
|
||||
<Button key={status} variant="secondary" onClick={() => void handleTransition(status)}>
|
||||
{status}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Card>
|
||||
<CardContent className="grid gap-4 md:grid-cols-3">
|
||||
<AppField label="Status">
|
||||
<AppStatusBadge status={ticket.status} />
|
||||
</AppField>
|
||||
<AppField label="Priority">
|
||||
<AppPriorityBadge priority={ticket.priority} />
|
||||
</AppField>
|
||||
<AppField label="System">
|
||||
<span className="text-sm">{ticket.related_system || '-'}</span>
|
||||
</AppField>
|
||||
<AppField label="Environment">
|
||||
<span className="text-sm">{ticket.environment || '-'}</span>
|
||||
</AppField>
|
||||
<AppField label="Impact">
|
||||
<span className="text-sm">{ticket.impact_scope || '-'}</span>
|
||||
</AppField>
|
||||
<AppField label="Description">
|
||||
<span className="text-sm">{ticket.description}</span>
|
||||
</AppField>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TicketStatusTimeline ticket={ticket} />
|
||||
<TicketComments ticketId={ticket.id} />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user