refactor: simplify ticket lifecycle flow, remove processing and completed state

This commit is contained in:
2026-06-30 13:01:08 +05:30
parent adff2e2c41
commit ec77696157
20 changed files with 36 additions and 326 deletions

View File

@@ -88,7 +88,7 @@ export function TicketDefectClassTabs({
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span>Issues</span>
<Badge className="min-w-5 justify-center rounded-full px-1.5 py-0.5 text-xs tabular-nums">
<Badge variant="secondary" className="min-w-5 min-h-5 justify-center rounded-full">
{defectClasses.length}
</Badge>
</CardTitle>

View File

@@ -9,26 +9,19 @@ import { PERMISSIONS } from '@/constants/permissions';
import { PermissionGuard } from '@/guards';
import type { TicketDetail } from '@/types';
import { TicketStatusBadge } from '../../components/TicketStatusBadge';
export function TicketDetailHeader({ ticket }: { ticket: TicketDetail }) {
const router = useRouter();
const ticketLabel = ticket.ticket_name
const ticketLabel = ticket.ticket_name;
return (
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h1 className="text-2xl font-semibold tracking-tight">
Ticket Detail
</h1>
<TicketStatusBadge status={ticket.status} />
</div>
<h1 className="text-2xl font-semibold tracking-tight">Ticket Detail</h1>
<p className="text-xs ">Ticket: {ticketLabel}</p>
</div>
<div className="flex shrink-0 items-center gap-2">
{ticket.video_id && ticket.status !== 'processing' ? (
{ticket.video_id ? (
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
<SparkleButton
className="h-10 min-w-36 px-5"

View File

@@ -7,22 +7,19 @@ import { cn } from '@/lib/utils';
import type { TicketDetail, TicketHistoryItem, TicketStatus } from '@/types';
import { formatDate } from '@/utils/date';
import { formatTicketStatus } from '../../components/TicketStatusBadge';
function getActorLabel(item: TicketHistoryItem) {
return item.actor?.name || item.actor?.email || 'System';
}
function getLifecycleTitle(status: TicketStatus) {
const titles: Record<TicketStatus, string> = {
processing: 'Incident Ticket Created',
unassigned: 'Awaiting Assignment',
assigned: 'Dispatched to Maintenance',
under_review: 'Maintenance Finalized',
closed: 'Ticket Closed',
};
return titles[status] ?? formatTicketStatus(status);
return titles[status];
}
function HistoryEntry({
@@ -39,7 +36,7 @@ function HistoryEntry({
{!isLast ? (
<span
aria-hidden
className="absolute top-6 left-[11px] h-[calc(100%-10px)] w-px bg-border"
className="absolute top-6 left-2.75 h-[calc(100%-10px)] w-px bg-border"
/>
) : null}
@@ -56,7 +53,7 @@ function HistoryEntry({
className={cn(
'size-2 rounded-full bg-primary',
isClosed &&
'h-1.5 w-2.5 rotate-[-45deg] rounded-none border-b-2 border-l-2 border-current bg-transparent',
'h-1.5 w-2.5 -rotate-45 rounded-none border-b-2 border-l-2 border-current bg-transparent',
)}
/>
</span>

View File

@@ -1,14 +1,7 @@
'use client';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { TicketActor, TicketDetail } from '@/types';
import { formatDate } from '@/utils/date';
function formatCreatedDate(value: string | null | undefined) {
if (!value) return '-';
@@ -111,12 +104,6 @@ export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) {
</MetaValue>
</OverviewField>
</CardContent>
{ticket.ai_result?.completed_at ? (
<CardFooter className="text-xs text-muted-foreground">
AI analysis completed {formatDate(ticket.ai_result.completed_at)}
</CardFooter>
) : null}
</Card>
);
}

View File

@@ -3,39 +3,24 @@
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionGuard } from '@/guards';
import type { TicketDetail } from '@/types';
import type { TicketDetailLiveState } from '../../hooks/useTicketDetailEvents';
import { AssignTicketAction } from './actions/AssignTicketAction';
import { ClosedTicketAction } from './actions/ClosedTicketAction';
import { NoTicketAction } from './actions/NoTicketAction';
import { ProcessingTicketAction } from './actions/ProcessingTicketAction';
import { ReviewRepairAction } from './actions/ReviewRepairAction';
import { SubmitRepairAction } from './actions/SubmitRepairAction';
import { TicketDefectClassTabs } from './TicketDefectClassTabs';
interface TicketStatusActionsProps {
ticket: TicketDetail;
liveState: TicketDetailLiveState;
}
function getTicketAction(
ticket: TicketDetail,
liveState: TicketDetailLiveState,
) {
if (ticket.status === 'processing') {
return (
<ProcessingTicketAction
progress={liveState.progress}
errorMessage={liveState.errorMessage}
/>
);
}
function getTicketAction(ticket: TicketDetail) {
if (ticket.assignment_status === 'unassigned') {
return (
<PermissionGuard
permissions={PERMISSIONS.TICKET.ASSIGN}
fallback={<NoTicketAction status={ticket.status} />}
fallback={<NoTicketAction />}
>
<AssignTicketAction ticket={ticket} />
</PermissionGuard>
@@ -46,7 +31,7 @@ function getTicketAction(
return (
<PermissionGuard
permissions={PERMISSIONS.TICKET.WORK}
fallback={<NoTicketAction status={ticket.status} />}
fallback={<NoTicketAction />}
>
<SubmitRepairAction ticket={ticket} />
</PermissionGuard>
@@ -57,7 +42,7 @@ function getTicketAction(
return (
<PermissionGuard
permissions={PERMISSIONS.TICKET.REVIEW}
fallback={<NoTicketAction status={ticket.status} />}
fallback={<NoTicketAction />}
>
<ReviewRepairAction ticket={ticket} />
</PermissionGuard>
@@ -68,17 +53,14 @@ function getTicketAction(
return <ClosedTicketAction ticket={ticket} />;
}
return <NoTicketAction status={ticket.status} />;
return <NoTicketAction />;
}
export function TicketStatusActions({
ticket,
liveState,
}: TicketStatusActionsProps) {
export function TicketStatusActions({ ticket }: TicketStatusActionsProps) {
return (
<div className="space-y-3">
<TicketDefectClassTabs ticketId={ticket.id} />
{getTicketAction(ticket, liveState)}
{getTicketAction(ticket)}
</div>
);
}

View File

@@ -1,37 +1,15 @@
'use client';
import { CircleOff, Clock } from 'lucide-react';
import type { TicketStatus } from '@/types';
import { CircleOff } from 'lucide-react';
import { TicketActionCard } from './TicketActionCard';
const statusConfig: Record<
'processing',
{ icon: typeof Clock; title: string; message: string }
> = {
processing: {
icon: Clock,
title: 'Processing',
message: 'This ticket is still being processed. Check back shortly.',
},
};
export function NoTicketAction({ status }: { status: TicketStatus }) {
const config =
status === 'processing'
? statusConfig[status]
: {
icon: CircleOff,
title: 'No Actions',
message: 'No action available for this ticket.',
};
const Icon = config.icon;
export function NoTicketAction() {
return (
<TicketActionCard icon={Icon} title={config.title}>
<p className="text-sm text-muted-foreground">{config.message}</p>
<TicketActionCard icon={CircleOff} title="No Actions">
<p className="text-sm text-muted-foreground">
No action available for this ticket.
</p>
</TicketActionCard>
);
}

View File

@@ -1,58 +0,0 @@
'use client';
import { AlertCircle, Loader } from 'lucide-react';
import { Progress } from '@/components/ui/progress';
import type { TicketProcessingProgress } from '../../../hooks/useTicketDetailEvents';
import { TicketActionCard } from './TicketActionCard';
interface ProcessingTicketActionProps {
progress: TicketProcessingProgress | null;
errorMessage: string | null;
}
function getProgressMessage(progress: TicketProcessingProgress | null) {
if (!progress) return 'Waiting for analysis updates.';
if (progress.progress >= 95) {
return progress.message ?? 'Rendering annotated video...';
}
return progress.message ?? 'Processing video.';
}
export function ProcessingTicketAction({
progress,
errorMessage,
}: ProcessingTicketActionProps) {
const progressValue = progress ? progress.progress : 0;
const hasProgress = Boolean(progress);
return (
<TicketActionCard
icon={Loader}
iconClassName="animate-spin"
title="Analyzing Video"
>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="font-medium text-foreground">
{getProgressMessage(progress)}
</span>
<span className="shrink-0 text-muted-foreground">
{hasProgress ? `${Math.round(progressValue)}%` : '--'}
</span>
</div>
<Progress value={hasProgress ? progressValue : 0} />
</div>
{errorMessage && (
<div className="flex gap-2 rounded-md border border-destructive/25 bg-destructive/10 p-3 text-sm text-destructive">
<AlertCircle className="mt-0.5 size-4 shrink-0" />
<span>{errorMessage}</span>
</div>
)}
</div>
</TicketActionCard>
);
}