refactor(ticket): make details lot-based and remove class-detail flow
This commit is contained in:
@@ -1,28 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
|
||||||
|
|
||||||
export function TicketClassActionsSkeleton() {
|
|
||||||
return (
|
|
||||||
<Card aria-label="Loading defect class actions">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Skeleton className="size-4 rounded-lg" />
|
|
||||||
<Skeleton className="h-5 w-32" />
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Skeleton className="h-4 w-20" />
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Skeleton className="h-4 w-12" />
|
|
||||||
<Skeleton className="h-20 w-full" />
|
|
||||||
</div>
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import type { TicketOverviewDetail } from '@/types';
|
|
||||||
|
|
||||||
import { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries';
|
|
||||||
import { TicketClassDetectionPreview } from './TicketClassDetectionPreview';
|
|
||||||
|
|
||||||
interface TicketDefectClassTabsProps {
|
|
||||||
ticketId: string;
|
|
||||||
ticket?: TicketOverviewDetail;
|
|
||||||
assignmentId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TicketDefectClassTabs({
|
|
||||||
ticketId,
|
|
||||||
ticket,
|
|
||||||
assignmentId,
|
|
||||||
}: TicketDefectClassTabsProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const defectClassParam = searchParams.get('defect_class');
|
|
||||||
const defectClassesQuery = useTicketDefectClassesQuery(ticketId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const defectClasses = defectClassesQuery.data?.defect_classes ?? [];
|
|
||||||
|
|
||||||
if (!defectClasses.length) return;
|
|
||||||
|
|
||||||
const hasUrlClass = defectClasses.some(
|
|
||||||
(item) => item.class_name === defectClassParam,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasUrlClass) return;
|
|
||||||
|
|
||||||
const firstClass = defectClasses[0].class_name;
|
|
||||||
const nextParams = new URLSearchParams(searchParams.toString());
|
|
||||||
nextParams.set('defect_class', firstClass);
|
|
||||||
router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false });
|
|
||||||
}, [
|
|
||||||
defectClassParam,
|
|
||||||
defectClassesQuery.data?.defect_classes,
|
|
||||||
pathname,
|
|
||||||
router,
|
|
||||||
searchParams,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const previewVideoId = ticket?.video_id ?? defectClassesQuery.data?.video_id;
|
|
||||||
|
|
||||||
if (!previewVideoId) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardContent className="p-5">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<TicketClassDetectionPreview
|
|
||||||
ticketId={ticketId}
|
|
||||||
videoId={previewVideoId}
|
|
||||||
assignmentId={assignmentId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '../../hooks/useTicketQueries';
|
} from '../../hooks/useTicketQueries';
|
||||||
import { DetectionDiscardDialog } from './DetectionDiscardDialog';
|
import { DetectionDiscardDialog } from './DetectionDiscardDialog';
|
||||||
|
|
||||||
interface TicketClassDetectionPreviewProps {
|
interface TicketDetectionPreviewProps {
|
||||||
ticketId: string;
|
ticketId: string;
|
||||||
videoId?: string | null;
|
videoId?: string | null;
|
||||||
assignmentId: number;
|
assignmentId: number;
|
||||||
@@ -52,7 +52,7 @@ function DetectionMetadataBar({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TicketClassDetectionPreviewSkeleton() {
|
function TicketDetectionPreviewSkeleton() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||||
@@ -91,11 +91,11 @@ function TicketClassDetectionPreviewSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TicketClassDetectionPreview({
|
export function TicketDetectionPreview({
|
||||||
ticketId,
|
ticketId,
|
||||||
videoId,
|
videoId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
}: TicketClassDetectionPreviewProps) {
|
}: TicketDetectionPreviewProps) {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
|
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
|
||||||
|
|
||||||
@@ -130,7 +130,6 @@ export function TicketClassDetectionPreview({
|
|||||||
const discardMutation = useDiscardDetectionMutation(
|
const discardMutation = useDiscardDetectionMutation(
|
||||||
ticketId,
|
ticketId,
|
||||||
videoId ?? undefined,
|
videoId ?? undefined,
|
||||||
activeDetection?.detection.class_name,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -217,7 +216,7 @@ export function TicketClassDetectionPreview({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{detectionsQuery.isLoading && !detectionsQuery.data ? (
|
{detectionsQuery.isLoading && !detectionsQuery.data ? (
|
||||||
<TicketClassDetectionPreviewSkeleton />
|
<TicketDetectionPreviewSkeleton />
|
||||||
) : detectionsQuery.isError ? (
|
) : detectionsQuery.isError ? (
|
||||||
<div className="rounded-lg border border-dashed border-destructive/40 bg-destructive/5 px-4 py-8 text-center">
|
<div className="rounded-lg border border-dashed border-destructive/40 bg-destructive/5 px-4 py-8 text-center">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
|
||||||
|
import { TicketDetectionPreview } from './TicketDetectionPreview';
|
||||||
|
|
||||||
|
interface TicketDetectionPreviewCardProps {
|
||||||
|
ticketId: string;
|
||||||
|
videoId?: string | null;
|
||||||
|
assignmentId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TicketDetectionPreviewCard({
|
||||||
|
ticketId,
|
||||||
|
videoId,
|
||||||
|
assignmentId,
|
||||||
|
}: TicketDetectionPreviewCardProps) {
|
||||||
|
if (!videoId) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardContent className="p-5">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<TicketDetectionPreview
|
||||||
|
ticketId={ticketId}
|
||||||
|
videoId={videoId}
|
||||||
|
assignmentId={assignmentId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
import { PermissionGuard } from '@/guards';
|
import { PermissionGuard } from '@/guards';
|
||||||
import type { TicketAssignmentSummary, TicketDetail } from '@/types';
|
import type { TicketAssignmentSummary } from '@/types';
|
||||||
|
|
||||||
import { NoTicketAction } from './actions/NoTicketAction';
|
import { NoTicketAction } from './actions/NoTicketAction';
|
||||||
import { RequestExtensionAction } from './actions/RequestExtensionAction';
|
import { RequestExtensionAction } from './actions/RequestExtensionAction';
|
||||||
import { ReviewExtensionRequestAction } from './actions/ReviewExtensionRequestAction';
|
import { ReviewExtensionRequestAction } from './actions/ReviewExtensionRequestAction';
|
||||||
|
|
||||||
interface TicketStatusActionsProps {
|
interface TicketStatusActionsProps {
|
||||||
ticket: TicketDetail;
|
ticketId: string;
|
||||||
assignment?: TicketAssignmentSummary;
|
assignment?: TicketAssignmentSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTicketAction(
|
function getTicketAction(
|
||||||
ticket: TicketDetail,
|
ticketId: string,
|
||||||
assignment?: TicketAssignmentSummary,
|
assignment?: TicketAssignmentSummary,
|
||||||
) {
|
) {
|
||||||
if (assignment && !assignment.is_complete) {
|
if (assignment && !assignment.is_complete) {
|
||||||
@@ -27,7 +27,7 @@ function getTicketAction(
|
|||||||
fallback={<NoTicketAction />}
|
fallback={<NoTicketAction />}
|
||||||
>
|
>
|
||||||
<RequestExtensionAction
|
<RequestExtensionAction
|
||||||
ticket={ticket}
|
ticketId={ticketId}
|
||||||
assignmentId={assignment.id}
|
assignmentId={assignment.id}
|
||||||
dueAt={assignment.due_at}
|
dueAt={assignment.due_at}
|
||||||
/>
|
/>
|
||||||
@@ -35,7 +35,7 @@ function getTicketAction(
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ReviewExtensionRequestAction
|
<ReviewExtensionRequestAction
|
||||||
ticket={ticket}
|
ticketId={ticketId}
|
||||||
assignmentId={assignment.id}
|
assignmentId={assignment.id}
|
||||||
/>
|
/>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
@@ -46,10 +46,10 @@ function getTicketAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TicketStatusActions({
|
export function TicketStatusActions({
|
||||||
ticket,
|
ticketId,
|
||||||
assignment,
|
assignment,
|
||||||
}: TicketStatusActionsProps) {
|
}: TicketStatusActionsProps) {
|
||||||
const action = getTicketAction(ticket, assignment);
|
const action = getTicketAction(ticketId, assignment);
|
||||||
|
|
||||||
return action ? <div className="space-y-3">{action}</div> : null;
|
return action ? <div className="space-y-3">{action}</div> : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { CalendarClock, UserCheck } from 'lucide-react';
|
|
||||||
|
|
||||||
import { PersonInfo } from '@/components/person-avatar';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import type { TicketDetail } from '@/types';
|
|
||||||
import { formatDate } from '@/utils/date';
|
|
||||||
|
|
||||||
import { TicketActionCard } from './TicketActionCard';
|
|
||||||
|
|
||||||
function AssignmentField({ label, value }: { label: string; value: string }) {
|
|
||||||
return (
|
|
||||||
<div className="min-w-0 space-y-1">
|
|
||||||
<p className="text-xs text-muted-foreground">{label}</p>
|
|
||||||
<p className="truncate text-sm font-medium text-foreground">{value}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AssignedContractorAction({ ticket }: { ticket: TicketDetail }) {
|
|
||||||
const worker = ticket.worker;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TicketActionCard icon={UserCheck} title="Assigned Contractor">
|
|
||||||
{worker ? (
|
|
||||||
<div className="space-y-5">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<PersonInfo person={worker} size="lg" />
|
|
||||||
<Badge>
|
|
||||||
Assigned
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-5 border-t pt-4">
|
|
||||||
<AssignmentField label="Role" value={worker.role?.name ?? '-'} />
|
|
||||||
<AssignmentField
|
|
||||||
label="Assigned By"
|
|
||||||
value={worker.assigned_by_name ?? '-'}
|
|
||||||
/>
|
|
||||||
<AssignmentField
|
|
||||||
label="Assigned On"
|
|
||||||
value={formatDate(worker.assigned_at)}
|
|
||||||
/>
|
|
||||||
<div className="min-w-0 space-y-1">
|
|
||||||
<p className="text-xs text-muted-foreground">Due Date</p>
|
|
||||||
<p className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
|
||||||
<CalendarClock className="size-4 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="truncate">{formatDate(worker.due_at)}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Assignment details are not available.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</TicketActionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { Lock, MessageSquare } from 'lucide-react';
|
|
||||||
|
|
||||||
import { PersonInfo } from '@/components/person-avatar';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import type { TicketDetail } from '@/types';
|
|
||||||
import { formatDateOnly } from '@/utils/date';
|
|
||||||
|
|
||||||
function MetaField({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
valueClassName,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: ReactNode;
|
|
||||||
valueClassName?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-xs font-medium tracking-wide text-muted-foreground">
|
|
||||||
{label}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
className={cn('text-sm font-semibold text-foreground', valueClassName)}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FinalCommentsPanel({
|
|
||||||
comment,
|
|
||||||
}: {
|
|
||||||
comment: string | null | undefined;
|
|
||||||
}) {
|
|
||||||
if (!comment?.trim()) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-28 flex-col items-center justify-center rounded-lg border border-dashed border-border bg-muted/10 px-4 py-6 text-center">
|
|
||||||
<MessageSquare className="mb-2 size-5 text-muted-foreground" />
|
|
||||||
<p className="text-sm text-muted-foreground">No comments added yet</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border bg-muted/20 px-4 py-3">
|
|
||||||
<p className="text-sm leading-relaxed text-foreground">
|
|
||||||
{comment.trim()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ClosedTicketAction({ ticket }: { ticket: TicketDetail }) {
|
|
||||||
const reviewer = ticket.reviewer;
|
|
||||||
const reviewedAt = reviewer?.reviewed_at ?? ticket.timestamps.updated_at;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-lg">Resolution Details</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Review record for {ticket.ticket_name}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="space-y-5">
|
|
||||||
<div className="space-y-4 rounded-lg border bg-muted/20 p-4">
|
|
||||||
<MetaField
|
|
||||||
label="Reviewed By"
|
|
||||||
value={<PersonInfo person={reviewer} size="lg" />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MetaField label="Reviewed Date" value={formatDateOnly(reviewedAt)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium tracking-wide text-muted-foreground">
|
|
||||||
Review Comment
|
|
||||||
</p>
|
|
||||||
<FinalCommentsPanel comment={reviewer?.review_comment} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
className="w-full text-muted-foreground"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<Lock className="size-4" />
|
|
||||||
Ticket Finalized
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
import { Clock3 } from 'lucide-react';
|
import { Clock3 } from 'lucide-react';
|
||||||
|
|
||||||
import type { TicketDetail } from '@/types';
|
|
||||||
|
|
||||||
import { TicketActionCard } from './TicketActionCard';
|
import { TicketActionCard } from './TicketActionCard';
|
||||||
import { TicketExtensionRequestPanel } from './TicketExtensionRequestPanel';
|
import { TicketExtensionRequestPanel } from './TicketExtensionRequestPanel';
|
||||||
|
|
||||||
export function RequestExtensionAction({
|
export function RequestExtensionAction({
|
||||||
ticket,
|
ticketId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
dueAt,
|
dueAt,
|
||||||
}: {
|
}: {
|
||||||
ticket: TicketDetail;
|
ticketId: string;
|
||||||
assignmentId: number;
|
assignmentId: number;
|
||||||
dueAt: string | null;
|
dueAt: string | null;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TicketActionCard icon={Clock3} title="Request Extension">
|
<TicketActionCard icon={Clock3} title="Request Extension">
|
||||||
<TicketExtensionRequestPanel
|
<TicketExtensionRequestPanel
|
||||||
ticket={ticket}
|
ticketId={ticketId}
|
||||||
assignmentId={assignmentId}
|
assignmentId={assignmentId}
|
||||||
dueAt={dueAt}
|
dueAt={dueAt}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,20 +8,19 @@ import { DatePickerSimple } from '@/components/form/DatePickerSimple';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import type { TicketDetail } from '@/types';
|
|
||||||
|
|
||||||
import { useRequestTicketExtensionMutation } from '../../../hooks/useTicketQueries';
|
import { useRequestTicketExtensionMutation } from '../../../hooks/useTicketQueries';
|
||||||
|
|
||||||
const MAX_REASON_LENGTH = 300;
|
const MAX_REASON_LENGTH = 300;
|
||||||
|
|
||||||
interface RequestExtensionFormProps {
|
interface RequestExtensionFormProps {
|
||||||
ticket: TicketDetail;
|
ticketId: string;
|
||||||
assignmentId: number;
|
assignmentId: number;
|
||||||
currentDueAt: string | null;
|
currentDueAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RequestExtensionForm({
|
export function RequestExtensionForm({
|
||||||
ticket,
|
ticketId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
currentDueAt,
|
currentDueAt,
|
||||||
}: RequestExtensionFormProps) {
|
}: RequestExtensionFormProps) {
|
||||||
@@ -42,9 +41,8 @@ export function RequestExtensionForm({
|
|||||||
const [requestedDueDate, setRequestedDueDate] = useState<Date>();
|
const [requestedDueDate, setRequestedDueDate] = useState<Date>();
|
||||||
const [reason, setReason] = useState('');
|
const [reason, setReason] = useState('');
|
||||||
const requestExtensionMutation = useRequestTicketExtensionMutation(
|
const requestExtensionMutation = useRequestTicketExtensionMutation(
|
||||||
ticket.id,
|
ticketId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
ticket.defect_class,
|
|
||||||
);
|
);
|
||||||
const isPending = requestExtensionMutation.isPending;
|
const isPending = requestExtensionMutation.isPending;
|
||||||
const isRequestedDateValid = Boolean(
|
const isRequestedDateValid = Boolean(
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import type {
|
import type {
|
||||||
TicketDetail,
|
|
||||||
TicketExtensionRequest,
|
TicketExtensionRequest,
|
||||||
TicketExtensionRequestsResponse,
|
TicketExtensionRequestsResponse,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
@@ -86,11 +85,11 @@ function Requester({ request }: { request: TicketExtensionRequest }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ExtensionReviewForm({
|
function ExtensionReviewForm({
|
||||||
ticket,
|
ticketId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
request,
|
request,
|
||||||
}: {
|
}: {
|
||||||
ticket: TicketDetail;
|
ticketId: string;
|
||||||
assignmentId: number;
|
assignmentId: number;
|
||||||
request: TicketExtensionRequest;
|
request: TicketExtensionRequest;
|
||||||
}) {
|
}) {
|
||||||
@@ -102,9 +101,8 @@ function ExtensionReviewForm({
|
|||||||
const [isRejectConfirmationOpen, setIsRejectConfirmationOpen] =
|
const [isRejectConfirmationOpen, setIsRejectConfirmationOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const reviewMutation = useReviewTicketExtensionMutation(
|
const reviewMutation = useReviewTicketExtensionMutation(
|
||||||
ticket.id,
|
ticketId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
ticket.defect_class,
|
|
||||||
);
|
);
|
||||||
const extensionDays = getExtensionDays(request);
|
const extensionDays = getExtensionDays(request);
|
||||||
|
|
||||||
@@ -274,14 +272,14 @@ function ExtensionReviewForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ReviewExtensionRequestAction({
|
export function ReviewExtensionRequestAction({
|
||||||
ticket,
|
ticketId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
}: {
|
}: {
|
||||||
ticket: TicketDetail;
|
ticketId: string;
|
||||||
assignmentId: number;
|
assignmentId: number;
|
||||||
}) {
|
}) {
|
||||||
const extensionRequestQuery = useTicketExtensionRequestQuery(
|
const extensionRequestQuery = useTicketExtensionRequestQuery(
|
||||||
ticket.id,
|
ticketId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -308,7 +306,7 @@ export function ReviewExtensionRequestAction({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtensionReviewForm
|
<ExtensionReviewForm
|
||||||
ticket={ticket}
|
ticketId={ticketId}
|
||||||
assignmentId={assignmentId}
|
assignmentId={assignmentId}
|
||||||
request={pendingRequest}
|
request={pendingRequest}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type {
|
import type {
|
||||||
TicketDetail,
|
|
||||||
TicketExtensionRequest,
|
TicketExtensionRequest,
|
||||||
TicketExtensionRequestsResponse,
|
TicketExtensionRequestsResponse,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
@@ -186,16 +185,16 @@ function ExtensionRequestDetails({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TicketExtensionRequestPanel({
|
export function TicketExtensionRequestPanel({
|
||||||
ticket,
|
ticketId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
dueAt,
|
dueAt,
|
||||||
}: {
|
}: {
|
||||||
ticket: TicketDetail;
|
ticketId: string;
|
||||||
assignmentId: number;
|
assignmentId: number;
|
||||||
dueAt: string | null;
|
dueAt: string | null;
|
||||||
}) {
|
}) {
|
||||||
const extensionRequestQuery = useTicketExtensionRequestQuery(
|
const extensionRequestQuery = useTicketExtensionRequestQuery(
|
||||||
ticket.id,
|
ticketId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -234,7 +233,7 @@ export function TicketExtensionRequestPanel({
|
|||||||
if (!latestRequest) {
|
if (!latestRequest) {
|
||||||
return (
|
return (
|
||||||
<RequestExtensionForm
|
<RequestExtensionForm
|
||||||
ticket={ticket}
|
ticketId={ticketId}
|
||||||
assignmentId={assignmentId}
|
assignmentId={assignmentId}
|
||||||
currentDueAt={dueAt}
|
currentDueAt={dueAt}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -11,24 +11,20 @@ import {
|
|||||||
TicketOverviewCardSkeleton,
|
TicketOverviewCardSkeleton,
|
||||||
TicketOverviewHeaderSkeleton,
|
TicketOverviewHeaderSkeleton,
|
||||||
} from './components/TicketOverviewSkeleton';
|
} from './components/TicketOverviewSkeleton';
|
||||||
import { TicketClassActionsSkeleton } from './components/TicketClassDetailSkeleton';
|
|
||||||
import { TicketAssignmentsCard } from './components/TicketAssignmentsCard';
|
import { TicketAssignmentsCard } from './components/TicketAssignmentsCard';
|
||||||
import { TicketOverviewCard } from './components/TicketOverviewCard';
|
import { TicketOverviewCard } from './components/TicketOverviewCard';
|
||||||
import { TicketStatusActions } from './components/TicketStatusActions';
|
import { TicketStatusActions } from './components/TicketStatusActions';
|
||||||
import { TicketDefectClassTabs } from './components/TicketDefectClassTabs';
|
import { TicketDetectionPreviewCard } from './components/TicketDetectionPreviewCard';
|
||||||
import { TicketHistoryCard } from './components/TicketHistoryCard';
|
import { TicketHistoryCard } from './components/TicketHistoryCard';
|
||||||
import { useTicketDetailEvents } from '../hooks/useTicketDetailEvents';
|
import { useTicketDetailEvents } from '../hooks/useTicketDetailEvents';
|
||||||
import {
|
import {
|
||||||
useTicketAssignmentsQuery,
|
useTicketAssignmentsQuery,
|
||||||
useTicketClassDetailQuery,
|
|
||||||
useTicketOverviewQuery,
|
useTicketOverviewQuery,
|
||||||
} from '../hooks/useTicketQueries';
|
} from '../hooks/useTicketQueries';
|
||||||
|
|
||||||
export default function TicketDetailPage() {
|
export default function TicketDetailPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const { ticketId } = useParams() as { ticketId: string };
|
const { ticketId } = useParams() as { ticketId: string };
|
||||||
const defectClass = searchParams.get('defect_class') ?? undefined;
|
|
||||||
const [selectedAssignmentId, setSelectedAssignmentId] = useState<number>();
|
const [selectedAssignmentId, setSelectedAssignmentId] = useState<number>();
|
||||||
|
|
||||||
const overviewQuery = useTicketOverviewQuery(ticketId);
|
const overviewQuery = useTicketOverviewQuery(ticketId);
|
||||||
@@ -37,8 +33,6 @@ export default function TicketDetailPage() {
|
|||||||
ticketId,
|
ticketId,
|
||||||
overview?.assignments,
|
overview?.assignments,
|
||||||
);
|
);
|
||||||
const classDetailQuery = useTicketClassDetailQuery(ticketId, defectClass);
|
|
||||||
const classDetail = classDetailQuery.data;
|
|
||||||
const assignments = (assignmentsQuery.data?.items ?? []).filter(
|
const assignments = (assignmentsQuery.data?.items ?? []).filter(
|
||||||
(assignment) => assignment.item_count > 0,
|
(assignment) => assignment.item_count > 0,
|
||||||
);
|
);
|
||||||
@@ -46,11 +40,8 @@ export default function TicketDetailPage() {
|
|||||||
assignments.find((assignment) => assignment.id === selectedAssignmentId) ??
|
assignments.find((assignment) => assignment.id === selectedAssignmentId) ??
|
||||||
assignments[0];
|
assignments[0];
|
||||||
const activeAssignmentId = activeAssignment?.id;
|
const activeAssignmentId = activeAssignment?.id;
|
||||||
const isClassDetailLoading = Boolean(
|
|
||||||
defectClass && classDetailQuery.isLoading,
|
|
||||||
);
|
|
||||||
|
|
||||||
useTicketDetailEvents(ticketId, defectClass, Boolean(ticketId));
|
useTicketDetailEvents(ticketId, Boolean(ticketId));
|
||||||
|
|
||||||
if (overviewQuery.isError) {
|
if (overviewQuery.isError) {
|
||||||
return (
|
return (
|
||||||
@@ -94,9 +85,9 @@ export default function TicketDetailPage() {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{activeAssignment ? (
|
{activeAssignment ? (
|
||||||
<TicketDefectClassTabs
|
<TicketDetectionPreviewCard
|
||||||
ticketId={ticketId}
|
ticketId={ticketId}
|
||||||
ticket={overview}
|
videoId={overview?.video_id ?? overview?.video?.id}
|
||||||
assignmentId={activeAssignment.id}
|
assignmentId={activeAssignment.id}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -104,12 +95,9 @@ export default function TicketDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 space-y-3 self-start xl:min-h-0 xl:self-stretch xl:overflow-y-auto xl:overscroll-contain xl:pr-2">
|
<div className="min-w-0 space-y-3 self-start xl:min-h-0 xl:self-stretch xl:overflow-y-auto xl:overscroll-contain xl:pr-2">
|
||||||
{activeAssignment && isClassDetailLoading ? (
|
{activeAssignment ? (
|
||||||
<TicketClassActionsSkeleton />
|
|
||||||
) : null}
|
|
||||||
{activeAssignment && !isClassDetailLoading && classDetail ? (
|
|
||||||
<TicketStatusActions
|
<TicketStatusActions
|
||||||
ticket={classDetail}
|
ticketId={ticketId}
|
||||||
assignment={activeAssignment}
|
assignment={activeAssignment}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { API_ROUTES } from '@/constants/apiRoutes';
|
import { API_ROUTES } from '@/constants/apiRoutes';
|
||||||
@@ -18,15 +18,9 @@ function isHeartbeatEvent(event: { type?: string } | null | undefined) {
|
|||||||
|
|
||||||
export function useTicketDetailEvents(
|
export function useTicketDetailEvents(
|
||||||
ticketId: string | undefined,
|
ticketId: string | undefined,
|
||||||
defectClass: string | undefined,
|
|
||||||
enabled = true,
|
enabled = true,
|
||||||
): void {
|
): void {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const defectClassRef = useRef(defectClass);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
defectClassRef.current = defectClass;
|
|
||||||
}, [defectClass]);
|
|
||||||
|
|
||||||
const refetchTicket = useCallback(() => {
|
const refetchTicket = useCallback(() => {
|
||||||
if (!ticketId) return;
|
if (!ticketId) return;
|
||||||
@@ -38,11 +32,6 @@ export function useTicketDetailEvents(
|
|||||||
queryClient.refetchQueries({
|
queryClient.refetchQueries({
|
||||||
queryKey: ticketKeys.timeline(ticketId),
|
queryKey: ticketKeys.timeline(ticketId),
|
||||||
});
|
});
|
||||||
if (defectClassRef.current) {
|
|
||||||
queryClient.refetchQueries({
|
|
||||||
queryKey: ticketKeys.classDetail(ticketId, defectClassRef.current),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.defectClasses(ticketId),
|
queryKey: ticketKeys.defectClasses(ticketId),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import type {
|
|||||||
RequestTicketExtensionPayload,
|
RequestTicketExtensionPayload,
|
||||||
ReviewDetectionRepairProofPayload,
|
ReviewDetectionRepairProofPayload,
|
||||||
ReviewTicketExtensionPayload,
|
ReviewTicketExtensionPayload,
|
||||||
TicketDetail,
|
|
||||||
TicketAssignmentSummary,
|
TicketAssignmentSummary,
|
||||||
TicketListParams,
|
TicketListParams,
|
||||||
DetectionProofStatus,
|
DetectionProofStatus,
|
||||||
@@ -78,21 +77,6 @@ export function useTicketTimelineQuery(ticketId: string | undefined) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTicketClassDetailQuery(
|
|
||||||
ticketId: string | undefined,
|
|
||||||
defectClass: string | undefined,
|
|
||||||
) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ticketKeys.classDetail(ticketId ?? '', defectClass ?? ''),
|
|
||||||
queryFn: () =>
|
|
||||||
ticketService.getTicketClassDetail(
|
|
||||||
ticketId as string,
|
|
||||||
defectClass as string,
|
|
||||||
),
|
|
||||||
enabled: Boolean(ticketId && defectClass),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTicketDefectClassesQuery(ticketId: string | undefined) {
|
export function useTicketDefectClassesQuery(ticketId: string | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ticketKeys.defectClasses(ticketId ?? ''),
|
queryKey: ticketKeys.defectClasses(ticketId ?? ''),
|
||||||
@@ -224,7 +208,6 @@ export function useTicketReviewDetectionsQuery(
|
|||||||
export function useDiscardDetectionMutation(
|
export function useDiscardDetectionMutation(
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
videoId: string | undefined,
|
videoId: string | undefined,
|
||||||
defectClass?: string,
|
|
||||||
) {
|
) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -248,13 +231,6 @@ export function useDiscardDetectionMutation(
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.overview(ticketId),
|
queryKey: ticketKeys.overview(ticketId),
|
||||||
}),
|
}),
|
||||||
...(defectClass
|
|
||||||
? [
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ticketKeys.classDetail(ticketId, defectClass),
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.defectClasses(ticketId),
|
queryKey: ticketKeys.defectClasses(ticketId),
|
||||||
}),
|
}),
|
||||||
@@ -372,7 +348,6 @@ export function useAssignTicketMutation(ticketId: string, videoId?: string) {
|
|||||||
export function useRequestTicketExtensionMutation(
|
export function useRequestTicketExtensionMutation(
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
assignmentId: number | undefined,
|
assignmentId: number | undefined,
|
||||||
defectClass: string,
|
|
||||||
) {
|
) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -392,9 +367,6 @@ export function useRequestTicketExtensionMutation(
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.extensionRequest(ticketId, assignmentId),
|
queryKey: ticketKeys.extensionRequest(ticketId, assignmentId),
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ticketKeys.classDetail(ticketId, defectClass),
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.assignments(ticketId),
|
queryKey: ticketKeys.assignments(ticketId),
|
||||||
});
|
});
|
||||||
@@ -409,7 +381,6 @@ export function useRequestTicketExtensionMutation(
|
|||||||
export function useReviewTicketExtensionMutation(
|
export function useReviewTicketExtensionMutation(
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
assignmentId: number,
|
assignmentId: number,
|
||||||
defectClass: string,
|
|
||||||
) {
|
) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -425,9 +396,6 @@ export function useReviewTicketExtensionMutation(
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.extensionRequest(ticketId, assignmentId),
|
queryKey: ticketKeys.extensionRequest(ticketId, assignmentId),
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ticketKeys.classDetail(ticketId, defectClass),
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.assignments(ticketId),
|
queryKey: ticketKeys.assignments(ticketId),
|
||||||
});
|
});
|
||||||
@@ -439,18 +407,21 @@ export function useReviewTicketExtensionMutation(
|
|||||||
onError: () => toast.error('Failed to review extension request'),
|
onError: () => toast.error('Failed to review extension request'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCloseTicketMutation(ticketId: string) {
|
export function useCloseTicketMutation(ticketId: string) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: CloseTicketPayload) =>
|
mutationFn: (payload: CloseTicketPayload) =>
|
||||||
ticketService.closeTicket(ticketId, payload),
|
ticketService.closeTicket(ticketId, payload),
|
||||||
onSuccess: (ticket, payload) => {
|
onSuccess: () => {
|
||||||
toast.success('Ticket closed');
|
toast.success('Ticket closed');
|
||||||
queryClient.setQueryData<TicketDetail>(
|
queryClient.invalidateQueries({
|
||||||
ticketKeys.classDetail(ticketId, payload.defect_class),
|
queryKey: ticketKeys.overview(ticketId),
|
||||||
ticket,
|
});
|
||||||
);
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ticketKeys.assignments(ticketId),
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.timeline(ticketId),
|
queryKey: ticketKeys.timeline(ticketId),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { Ticket } from 'lucide-react';
|
import { Ticket } from 'lucide-react';
|
||||||
import type { TicketListItem } from '@/types';
|
import type { TicketListItem } from '@/types';
|
||||||
|
import { ROUTES } from '@/utils/routes';
|
||||||
|
|
||||||
import { TicketFilters } from './components/TicketFilters';
|
import { TicketFilters } from './components/TicketFilters';
|
||||||
import { TicketTable } from './components/TicketTable';
|
import { TicketTable } from './components/TicketTable';
|
||||||
@@ -37,11 +38,7 @@ export default function TicketPage() {
|
|||||||
|
|
||||||
const handleView = useCallback(
|
const handleView = useCallback(
|
||||||
(ticket: TicketListItem) => {
|
(ticket: TicketListItem) => {
|
||||||
const defectClassQuery = ticket.default_defect_class
|
router.push(ROUTES.TICKET_DETAIL(ticket.id));
|
||||||
? `?defect_class=${encodeURIComponent(ticket.default_defect_class)}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
router.push(`/ticket/${ticket.id}${defectClassQuery}`);
|
|
||||||
},
|
},
|
||||||
[router],
|
[router],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export const ticketKeys = {
|
|||||||
[...ticketKeys.details(), ticketId, 'overview'] as const,
|
[...ticketKeys.details(), ticketId, 'overview'] as const,
|
||||||
timeline: (ticketId: string) =>
|
timeline: (ticketId: string) =>
|
||||||
[...ticketKeys.details(), ticketId, 'timeline'] as const,
|
[...ticketKeys.details(), ticketId, 'timeline'] as const,
|
||||||
classDetail: (ticketId: string, defectClass: string) =>
|
|
||||||
[...ticketKeys.details(), ticketId, 'class', defectClass] as const,
|
|
||||||
classDetectionLists: (videoId: string) =>
|
classDetectionLists: (videoId: string) =>
|
||||||
[...ticketKeys.details(), 'video', videoId, 'detections'] as const,
|
[...ticketKeys.details(), 'video', videoId, 'detections'] as const,
|
||||||
classDetections: (videoId: string, params: VideoDetectionsParams) =>
|
classDetections: (videoId: string, params: VideoDetectionsParams) =>
|
||||||
|
|||||||
@@ -66,8 +66,6 @@ export const API_ROUTES = {
|
|||||||
BASE: '/biz/api/v1/tickets',
|
BASE: '/biz/api/v1/tickets',
|
||||||
DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`,
|
DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`,
|
||||||
TIMELINE: (id: string) => `/biz/api/v1/tickets/${id}/timeline`,
|
TIMELINE: (id: string) => `/biz/api/v1/tickets/${id}/timeline`,
|
||||||
CLASS_DETAIL: (id: string, defectClass: string) =>
|
|
||||||
`/biz/api/v1/tickets/${id}/class-detail?defect_class=${defectClass}`,
|
|
||||||
DEFECT_CLASSES: (id: string) => `/biz/api/v1/tickets/${id}/defect-classes`,
|
DEFECT_CLASSES: (id: string) => `/biz/api/v1/tickets/${id}/defect-classes`,
|
||||||
ASSIGNMENTS: (id: string) => `/biz/api/v1/tickets/${id}/assignments`,
|
ASSIGNMENTS: (id: string) => `/biz/api/v1/tickets/${id}/assignments`,
|
||||||
REQUEST_EXTENSION: (ticketId: string, assignmentId: number) =>
|
REQUEST_EXTENSION: (ticketId: string, assignmentId: number) =>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import type {
|
|||||||
RequestTicketExtensionPayload,
|
RequestTicketExtensionPayload,
|
||||||
ReviewTicketExtensionPayload,
|
ReviewTicketExtensionPayload,
|
||||||
SseTokenResponse,
|
SseTokenResponse,
|
||||||
TicketDetail,
|
|
||||||
TicketAssignmentSummary,
|
TicketAssignmentSummary,
|
||||||
TicketAssignmentsResponse,
|
TicketAssignmentsResponse,
|
||||||
TicketExtensionRequestsResponse,
|
TicketExtensionRequestsResponse,
|
||||||
@@ -62,16 +61,6 @@ export const ticketService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTicketClassDetail: async (
|
|
||||||
ticketId: string,
|
|
||||||
defectClass: string,
|
|
||||||
): Promise<TicketDetail> => {
|
|
||||||
const response = await axiosClient.get<TicketDetail>(
|
|
||||||
API_ROUTES.TICKETS.CLASS_DETAIL(ticketId, defectClass),
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTicketDefectClasses: async (
|
getTicketDefectClasses: async (
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
): Promise<TicketDefectClassesResponse> => {
|
): Promise<TicketDefectClassesResponse> => {
|
||||||
@@ -166,11 +155,12 @@ export const ticketService = {
|
|||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
closeTicket: async (
|
closeTicket: async (
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
payload: CloseTicketPayload,
|
payload: CloseTicketPayload,
|
||||||
): Promise<TicketDetail> => {
|
): Promise<unknown> => {
|
||||||
const response = await axiosClient.post<TicketDetail>(
|
const response = await axiosClient.post<unknown>(
|
||||||
API_ROUTES.TICKETS.CLOSE(ticketId),
|
API_ROUTES.TICKETS.CLOSE(ticketId),
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { TicketDefectClassTicketStatus } from './defect-class';
|
|
||||||
import type { TicketStatus } from './status';
|
|
||||||
import type { TicketAssignmentCriteria } from './actions';
|
import type { TicketAssignmentCriteria } from './actions';
|
||||||
|
|
||||||
export interface TicketActor {
|
export interface TicketActor {
|
||||||
@@ -9,15 +7,6 @@ export interface TicketActor {
|
|||||||
avatar_url?: string | null;
|
avatar_url?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketStatusMetadata {
|
|
||||||
reason: string | null;
|
|
||||||
label: string | null;
|
|
||||||
message: string | null;
|
|
||||||
detection_count?: number | null;
|
|
||||||
requires_action?: boolean;
|
|
||||||
review_comment?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TicketVideo {
|
export interface TicketVideo {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
@@ -26,40 +15,6 @@ export interface TicketVideo {
|
|||||||
thumbnail: string | null;
|
thumbnail: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketWorker extends TicketActor {
|
|
||||||
assignment_id: number;
|
|
||||||
assigned_at: string | null;
|
|
||||||
due_at: string | null;
|
|
||||||
assigned_by_user_id: number | null;
|
|
||||||
assigned_by_name: string | null;
|
|
||||||
role: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TicketRepair {
|
|
||||||
notes: string | null;
|
|
||||||
proof_video_url: string | null;
|
|
||||||
proof_image_urls: string[];
|
|
||||||
submitted_at: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TicketReviewer extends TicketActor {
|
|
||||||
review_comment: string | null;
|
|
||||||
reviewed_at: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TicketAiResult {
|
|
||||||
detection_count: number;
|
|
||||||
available_defect_classes: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TicketTenant {
|
|
||||||
root_tenant_id: number;
|
|
||||||
organization_id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TicketTimestamps {
|
export interface TicketTimestamps {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
@@ -137,24 +92,3 @@ export interface TicketOverviewDetail {
|
|||||||
assignments?: TicketAssignmentSummary[];
|
assignments?: TicketAssignmentSummary[];
|
||||||
timestamps?: TicketTimestamps | null;
|
timestamps?: TicketTimestamps | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketDetail {
|
|
||||||
id: string;
|
|
||||||
ticket_name?: string;
|
|
||||||
video_id: string;
|
|
||||||
chainage_id: string | null;
|
|
||||||
chainage_name: string | null;
|
|
||||||
status: TicketStatus;
|
|
||||||
defect_class: string;
|
|
||||||
defect_display_name: string;
|
|
||||||
assignment_status: TicketDefectClassTicketStatus;
|
|
||||||
status_metadata: TicketStatusMetadata | null;
|
|
||||||
video: TicketVideo | null;
|
|
||||||
uploader: TicketActor | null;
|
|
||||||
worker: TicketWorker | null;
|
|
||||||
repair: TicketRepair | null;
|
|
||||||
reviewer: TicketReviewer | null;
|
|
||||||
ai_result: TicketAiResult | null;
|
|
||||||
tenant: TicketTenant | null;
|
|
||||||
timestamps: TicketTimestamps;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { TicketDetail } from './detail';
|
|
||||||
import type { TicketStatus } from './status';
|
import type { TicketStatus } from './status';
|
||||||
|
|
||||||
export interface SseTokenResponse {
|
export interface SseTokenResponse {
|
||||||
@@ -22,7 +21,7 @@ export type TicketTableStatusEvent = {
|
|||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TicketDetailStatusEvent = Partial<TicketDetail> & {
|
export type TicketDetailStatusEvent = {
|
||||||
type: 'ticket_status' | 'heartbeat';
|
type: 'ticket_status' | 'heartbeat';
|
||||||
kind?: 'snapshot' | 'transitioned';
|
kind?: 'snapshot' | 'transitioned';
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user