diff --git a/next-env.d.ts b/next-env.d.ts
index 9edff1c..c4b7818 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/opencode.json b/opencode.json
new file mode 100644
index 0000000..6b8b42d
--- /dev/null
+++ b/opencode.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://opencode.ai/config.json",
+ "mcp": {
+ "dual-graph": {
+ "type": "remote",
+ "url": "http://127.0.0.1:8080/mcp",
+ "enabled": true
+ }
+ }
+}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx
index af2693e..82389e3 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx
@@ -1,12 +1,8 @@
'use client';
-import type { TicketActor, TicketDetail } from '@/types';
+import type { TicketDetail } from '@/types';
import { formatDate } from '@/utils/date';
-import {
- TicketStatusBadge,
-} from '../../components/TicketStatusBadge';
-
function formatCreatedDate(value: string | null | undefined) {
if (!value) return '-';
return new Intl.DateTimeFormat('en-IN', { dateStyle: 'medium' }).format(
@@ -14,20 +10,6 @@ function formatCreatedDate(value: string | null | undefined) {
);
}
-function getInitials(person: TicketActor | null | undefined) {
- const source = person?.name || person?.email || '?';
- return source
- .split(/[\s@._-]+/)
- .filter(Boolean)
- .slice(0, 2)
- .map((part) => part.charAt(0).toUpperCase())
- .join('');
-}
-
-function getPersonLabel(person: TicketActor | null | undefined) {
- return person?.name || person?.email || '-';
-}
-
function MetaLabel({ children }: { children: React.ReactNode }) {
return (
@@ -40,29 +22,6 @@ function MetaValue({ children }: { children: React.ReactNode }) {
return
{children}
;
}
-function PersonMetaValue({
- person,
- accentClassName = 'bg-primary/15 text-primary',
-}: {
- person: TicketActor | null | undefined;
- accentClassName?: string;
-}) {
- if (!person) {
- return -;
- }
-
- return (
-
-
- {getInitials(person)}
-
-
{getPersonLabel(person)}
-
- );
-}
-
function OverviewField({
label,
children,
@@ -80,6 +39,22 @@ function OverviewField({
export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) {
const detectionCount = ticket.ai_result?.detection_count;
+ const assignments = ticket.assignments ?? [];
+ const primaryAssignment = assignments[0] ?? null;
+ const assignedNames = Array.from(
+ new Set(
+ assignments
+ .map((assignment) => assignment.assigned_to_name)
+ .filter(Boolean),
+ ),
+ );
+ const reviewerNames = Array.from(
+ new Set(
+ assignments
+ .map((assignment) => assignment.reviewed_by_name)
+ .filter(Boolean),
+ ),
+ );
return (
@@ -101,7 +76,9 @@ export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) {
-
+
+ {ticket.uploader?.name || ticket.uploader?.email || '-'}
+
@@ -109,21 +86,27 @@ export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) {
-
+
+ {assignedNames.length ? assignedNames.join(', ') : '-'}
+
-
+
+ {reviewerNames.length ? reviewerNames.join(', ') : '-'}
+
- {formatDate(ticket.worker?.assigned_at)}
+ {formatDate(primaryAssignment?.assigned_at)}
+
+
+
+
+ {ticket.coverage
+ ? `${ticket.coverage.assigned_count}/${ticket.coverage.total_count} Classes`
+ : '-'}
+
diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx
index 5db9f75..7eaaa7b 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx
@@ -9,12 +9,23 @@ import { RepairReportPanel } from './repair-report/RepairReportPanel';
import { RepairVideoComparison } from './repair-report/RepairVideoComparison';
export function TicketRepairReportCard({ ticket }: { ticket: TicketDetail }) {
- const repair = ticket.repair;
+ const repairedAssignments = (ticket.assignments ?? []).filter(
+ (assignment) =>
+ assignment.repair_submitted_at ||
+ assignment.repair_notes ||
+ assignment.repair_proof_video_url ||
+ assignment.repair_proof_image_urls.length,
+ );
- if (!repair) {
+ if (!repairedAssignments.length) {
return null;
}
+ const imageUrls = repairedAssignments.flatMap(
+ (assignment) => assignment.repair_proof_image_urls,
+ );
+ const submittedAt = repairedAssignments[0]?.repair_submitted_at ?? null;
+
return (
@@ -28,8 +39,8 @@ export function TicketRepairReportCard({ ticket }: { ticket: TicketDetail }) {
diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx
index e0a08a3..0434728 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx
@@ -9,8 +9,6 @@ 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';
interface TicketStatusActionsProps {
ticket: TicketDetail;
@@ -30,10 +28,18 @@ export function TicketStatusActions({
);
}
- if (ticket.status === 'unassigned') {
+ if (
+ ticket.status === 'unassigned' ||
+ ticket.status === 'assigned' ||
+ ticket.status === 'under_review'
+ ) {
return (
}
>
@@ -41,28 +47,6 @@ export function TicketStatusActions({
);
}
- if (ticket.status === 'assigned') {
- return (
- }
- >
-
-
- );
- }
-
- if (ticket.status === 'under_review') {
- return (
- }
- >
-
-
- );
- }
-
if (ticket.status === 'closed') {
return ;
}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/AssignTicketAction.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/AssignTicketAction.tsx
index d721abd..df092b0 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/actions/AssignTicketAction.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/actions/AssignTicketAction.tsx
@@ -1,57 +1,44 @@
'use client';
import { useState } from 'react';
-import { Send, UserPlus } from 'lucide-react';
+import { UserPlus } from 'lucide-react';
import { Button } from '@/components/ui/button';
-import { Label } from '@/components/ui/label';
-import { Textarea } from '@/components/ui/textarea';
-import { AssignableWorkerSelect } from '@/components/lookups/AssignableWorkerSelect';
import type { TicketDetail } from '@/types';
-import { useAssignTicketMutation } from '../../../hooks/useTicketQueries';
+import { TicketAssignmentSheet } from './TicketAssignmentSheet';
import { TicketActionCard } from './TicketActionCard';
export function AssignTicketAction({ ticket }: { ticket: TicketDetail }) {
- const [selectedUserId, setSelectedUserId] = useState('');
- const [assignNote, setAssignNote] = useState('');
-
- const assignMutation = useAssignTicketMutation(ticket.id);
+ const [open, setOpen] = useState(false);
+ const coverage = ticket.coverage;
return (
-
+
-
-
-
-
-
+
);
}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/ClosedTicketAction.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/ClosedTicketAction.tsx
index d0777c2..2200a5a 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/actions/ClosedTicketAction.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/actions/ClosedTicketAction.tsx
@@ -13,7 +13,7 @@ import {
CardTitle,
} from '@/components/ui/card';
import { cn } from '@/lib/utils';
-import type { TicketDetail } from '@/types';
+import type { TicketAssignmentSummary, TicketDetail } from '@/types';
function getInitials(name?: string | null, email?: string | null) {
const source = name || email || '?';
@@ -25,8 +25,8 @@ function getInitials(name?: string | null, email?: string | null) {
.join('');
}
-function getPersonLabel(person: TicketDetail['reviewer']) {
- return person?.name || person?.email || 'Unknown';
+function getPersonLabel(assignment: TicketAssignmentSummary | null) {
+ return assignment?.reviewed_by_name || 'Unknown';
}
function formatReviewDate(value: string | null | undefined) {
@@ -42,7 +42,8 @@ function getClosedAt(ticket: TicketDetail) {
);
return (
closedEntry?.created_at ??
- ticket.reviewer?.reviewed_at ??
+ (ticket.assignments ?? []).find((assignment) => assignment.reviewed_at)
+ ?.reviewed_at ??
ticket.timestamps.updated_at
);
}
@@ -94,8 +95,13 @@ function FinalCommentsPanel({
}
export function ClosedTicketAction({ ticket }: { ticket: TicketDetail }) {
- const reviewer = ticket.reviewer;
+ const reviewer = (ticket.assignments ?? []).find(
+ (assignment) => assignment.reviewed_by_name || assignment.reviewed_at,
+ ) ?? null;
const reviewedAt = reviewer?.reviewed_at ?? getClosedAt(ticket);
+ const reviewComments = (ticket.assignments ?? [])
+ .map((assignment) => assignment.review_comment)
+ .filter(Boolean);
return (
@@ -112,16 +118,11 @@ export function ClosedTicketAction({ ticket }: { ticket: TicketDetail }) {
- {getInitials(reviewer?.name, reviewer?.email)}
+ {getInitials(reviewer?.reviewed_by_name, null)}
{getPersonLabel(reviewer)}
- {reviewer?.email ? (
-
- {reviewer.email}
-
- ) : null}
}
@@ -137,7 +138,7 @@ export function ClosedTicketAction({ ticket }: { ticket: TicketDetail }) {
Review Comment
-
+
*;
@@ -131,16 +131,23 @@ function ImageSlot({
interface RepairEvidenceFormProps {
ticketId: string;
+ assignmentId: number;
}
-export function RepairEvidenceForm({ ticketId }: RepairEvidenceFormProps) {
+export function RepairEvidenceForm({
+ ticketId,
+ assignmentId,
+}: RepairEvidenceFormProps) {
const imageInputRef = useRef(null);
const [notes, setNotes] = useState('');
const [videoFile, setVideoFile] = useState(null);
const [imageFiles, setImageFiles] = useState([]);
const [imagePreviews, setImagePreviews] = useState([]);
const [error, setError] = useState(null);
- const submitRepairUploadMutation = useSubmitRepairUploadMutation(ticketId);
+ const submitRepairUploadMutation = useSubmitAssignmentRepairUploadMutation(
+ ticketId,
+ assignmentId,
+ );
const isSubmitting = submitRepairUploadMutation.isPending;
const canSubmit = Boolean(notes.trim() && videoFile);
@@ -261,7 +268,7 @@ export function RepairEvidenceForm({ ticketId }: RepairEvidenceFormProps) {
event.target.value = '';
}}
/>
-
+
{imageSlots.map((slot, index) => (
{
- setReviewComment(ticket.reviewer?.review_comment ?? '');
- }, [ticket]);
-
- return (
-
-
-
-
-
-
-
- reviewRepairMutation.mutate({
- action: 'approve',
- comment: reviewComment,
- })
- }
- >
-
- Approve
-
-
- reviewRepairMutation.mutate({
- action: 'reject',
- comment: reviewComment,
- })
- }
- >
-
- Reject
-
-
-
-
- );
+ return ;
}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/SubmitRepairAction.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/SubmitRepairAction.tsx
index 2a973dd..71794cb 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/actions/SubmitRepairAction.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/actions/SubmitRepairAction.tsx
@@ -1,16 +1,7 @@
-'use client';
-
-import { Wrench } from 'lucide-react';
-
import type { TicketDetail } from '@/types';
-import { RepairEvidenceForm } from './RepairEvidenceForm';
-import { TicketActionCard } from './TicketActionCard';
+import { AssignTicketAction } from './AssignTicketAction';
export function SubmitRepairAction({ ticket }: { ticket: TicketDetail }) {
- return (
-
-
-
- );
+ return ;
}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/TicketAssignmentSheet.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/TicketAssignmentSheet.tsx
new file mode 100644
index 0000000..1e3b542
--- /dev/null
+++ b/src/app/(modules)/ticket/[ticketId]/components/actions/TicketAssignmentSheet.tsx
@@ -0,0 +1,465 @@
+'use client';
+
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { Check, ClipboardCheck, Loader2, Send, Wrench, X } from 'lucide-react';
+
+import { AssignableWorkerSelect } from '@/components/lookups/AssignableWorkerSelect';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Label } from '@/components/ui/label';
+import {
+ Sheet,
+ SheetClose,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from '@/components/ui/sheet';
+import { Textarea } from '@/components/ui/textarea';
+import { PERMISSIONS } from '@/constants/permissions';
+import { PermissionGuard } from '@/guards';
+import type {
+ AssignmentStatus,
+ TicketAssignmentSummary,
+ TicketDefectClass,
+ TicketDetail,
+} from '@/types';
+
+import {
+ useReplaceTicketAssignmentsMutation,
+ useReviewAssignmentRepairMutation,
+ useTicketAssignmentsQuery,
+ useTicketDefectClassesQuery,
+} from '../../../hooks/useTicketQueries';
+import { RepairEvidenceForm } from './RepairEvidenceForm';
+
+interface TicketAssignmentSheetProps {
+ ticket: TicketDetail;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+interface AssignmentRowState {
+ defectClass: string;
+ displayName: string;
+ selectedUserId: string;
+ note: string;
+ assignment: TicketAssignmentSummary | null;
+}
+
+const statusStyles: Record = {
+ assigned: 'border-blue-200 bg-blue-50 text-blue-700',
+ under_review: 'border-amber-200 bg-amber-50 text-amber-700',
+ approved: 'border-emerald-200 bg-emerald-50 text-emerald-700',
+ rejected: 'border-red-200 bg-red-50 text-red-700',
+};
+
+const EMPTY_ASSIGNMENTS: TicketAssignmentSummary[] = [];
+
+function titleCase(value: string) {
+ return value
+ .split('_')
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(' ');
+}
+
+function formatStatus(status: AssignmentStatus | null | undefined) {
+ return status ? titleCase(status) : 'Unassigned';
+}
+
+function getFallbackClasses(ticket: TicketDetail): TicketDefectClass[] {
+ const assignments = ticket.assignments ?? [];
+ const names =
+ ticket.coverage?.total_defect_classes ??
+ ticket.ai_result?.available_defect_classes ??
+ assignments.map((assignment) => assignment.defect_class);
+
+ return names.map((className) => {
+ const existing = assignments.find(
+ (assignment) => assignment.defect_class === className,
+ );
+
+ return {
+ class_name: className,
+ display_name: existing?.defect_display_name ?? titleCase(className),
+ };
+ });
+}
+
+function buildRows(
+ classes: TicketDefectClass[],
+ assignments: TicketAssignmentSummary[],
+): AssignmentRowState[] {
+ const classMap = new Map();
+ classes.forEach((item) => classMap.set(item.class_name, item));
+ assignments.forEach((assignment) => {
+ if (!classMap.has(assignment.defect_class)) {
+ classMap.set(assignment.defect_class, {
+ class_name: assignment.defect_class,
+ display_name: assignment.defect_display_name,
+ });
+ }
+ });
+
+ return Array.from(classMap.values()).map((item) => {
+ const assignment = assignments.find(
+ (current) => current.defect_class === item.class_name,
+ );
+
+ return {
+ defectClass: item.class_name,
+ displayName: item.display_name,
+ selectedUserId: assignment?.assigned_to_user_id
+ ? String(assignment.assigned_to_user_id)
+ : '',
+ note: assignment?.note ?? '',
+ assignment: assignment ?? null,
+ };
+ });
+}
+
+function AssignmentStatusBadge({
+ status,
+}: {
+ status: AssignmentStatus | null | undefined;
+}) {
+ if (!status) return Unassigned;
+
+ return (
+
+ {formatStatus(status)}
+
+ );
+}
+
+function AssignmentReviewControls({
+ ticketId,
+ assignment,
+}: {
+ ticketId: string;
+ assignment: TicketAssignmentSummary;
+}) {
+ const [comment, setComment] = useState(assignment.review_comment ?? '');
+ const reviewMutation = useReviewAssignmentRepairMutation(
+ ticketId,
+ assignment.id,
+ );
+
+ useEffect(() => {
+ setComment(assignment.review_comment ?? '');
+ }, [assignment.review_comment]);
+
+ return (
+
+ );
+}
+
+function AssignmentProofSummary({
+ assignment,
+}: {
+ assignment: TicketAssignmentSummary;
+}) {
+ if (!assignment.repair_submitted_at && !assignment.review_comment) {
+ return null;
+ }
+
+ return (
+
+ {assignment.repair_submitted_at ? (
+
+
Repair submitted
+
{assignment.repair_submitted_at}
+
+ {assignment.repair_proof_video_url ? '1 video' : 'No video'} /{' '}
+ {assignment.repair_proof_image_urls.length} images
+
+
+ ) : null}
+ {assignment.review_comment ? (
+
+
Review comment
+
{assignment.review_comment}
+
+ ) : null}
+
+ );
+}
+
+export function TicketAssignmentSheet({
+ ticket,
+ open,
+ onOpenChange,
+}: TicketAssignmentSheetProps) {
+ const sheetContentRef = useRef(null);
+ const fallbackClasses = useMemo(() => getFallbackClasses(ticket), [ticket]);
+ const defectClassesQuery = useTicketDefectClassesQuery(ticket.id, open);
+ const assignmentsQuery = useTicketAssignmentsQuery(ticket.id, open);
+ const replaceAssignmentsMutation = useReplaceTicketAssignmentsMutation(
+ ticket.id,
+ );
+ const [rows, setRows] = useState([]);
+
+ const defectClasses = defectClassesQuery.data?.defect_classes.length
+ ? defectClassesQuery.data.defect_classes
+ : fallbackClasses;
+ const assignments =
+ assignmentsQuery.data?.items ?? ticket.assignments ?? EMPTY_ASSIGNMENTS;
+ const coverage = ticket.coverage;
+ const isLoading = defectClassesQuery.isLoading || assignmentsQuery.isLoading;
+ const assignedRows = rows.filter((row) => row.selectedUserId);
+ const canSave =
+ assignedRows.length > 0 && !replaceAssignmentsMutation.isPending;
+
+ useEffect(() => {
+ if (!open) return;
+ setRows(buildRows(defectClasses, assignments));
+ }, [assignments, defectClasses, open]);
+
+ const updateRow = (
+ defectClass: string,
+ patch: Partial>,
+ ) => {
+ setRows((current) =>
+ current.map((row) =>
+ row.defectClass === defectClass ? { ...row, ...patch } : row,
+ ),
+ );
+ };
+
+ const handleSave = async () => {
+ await replaceAssignmentsMutation.mutateAsync({
+ assignments: assignedRows.map((row) => ({
+ assigned_to_user_id: Number(row.selectedUserId),
+ defect_class: row.defectClass,
+ note: row.note.trim() || undefined,
+ })),
+ note: 'Updated ticket assignments',
+ });
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+
+
+ Manage Assignments
+
+ {coverage ? (
+
+ {coverage.assigned_count}/{coverage.total_count} assigned
+
+ ) : null}
+
+
+ Assign workers, submit repair evidence, and review each defect
+ class.
+
+ {coverage?.unassigned_defect_classes.length ? (
+
+ Still needs a worker:{' '}
+ {coverage.unassigned_defect_classes.join(', ')}
+
+ ) : null}
+
+
+
+ {isLoading && rows.length === 0 ? (
+
+ Loading assignments...
+
+ ) : (
+
+ {rows.map((row) => {
+ const assignment = row.assignment;
+ const canSubmitRepair =
+ assignment?.status === 'assigned' ||
+ assignment?.status === 'rejected';
+ const canReview = assignment?.status === 'under_review';
+
+ return (
+
+
+
+
+
+ {row.displayName}
+
+
+
+
+
+
+ {assignment?.assigned_to_name ?? 'Unassigned'}
+
+ }
+ >
+
+ updateRow(row.defectClass, { selectedUserId })
+ }
+ disabled={replaceAssignmentsMutation.isPending}
+ portalContainer={sheetContentRef}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {assignment ? (
+
+ ) : null}
+
+ {assignment && canSubmitRepair ? (
+
+
+
+
+ Submit repair for {row.displayName}
+
+
+
+
+ ) : null}
+
+ {assignment && canReview ? (
+
+
+
+
+ Review repair for {row.displayName}
+
+
+
+
+ ) : null}
+
+ );
+ })}
+
+ {!rows.length ? (
+
+ No defect classes are available for this ticket.
+
+ ) : null}
+
+ )}
+
+
+
+
+
+ {replaceAssignmentsMutation.isPending ? (
+
+ ) : (
+
+ )}
+ Save Assignments
+
+
+
+
+ Cancel
+
+
+
+
+
+ );
+}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairReportPanel.tsx b/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairReportPanel.tsx
index b884944..3ae7383 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairReportPanel.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairReportPanel.tsx
@@ -1,13 +1,12 @@
'use client';
-import { FileText, MessageSquareQuote, BadgeCheck } from 'lucide-react';
+import { FileText, MessageSquareQuote } from 'lucide-react';
import type { TicketDetail } from '@/types';
import {
EmptyPanel,
formatRepairDate,
- getPersonLabel,
} from './repairReportUtils';
function MetaLabel({ children }: { children: React.ReactNode }) {
@@ -34,9 +33,12 @@ function ReportField({
}
export function RepairReportPanel({ ticket }: { ticket: TicketDetail }) {
- const repair = ticket.repair;
+ const repairs = (ticket.assignments ?? []).filter(
+ (assignment) => assignment.repair_submitted_at || assignment.repair_notes,
+ );
+ const firstRepair = repairs[0] ?? null;
- if (!repair) {
+ if (!firstRepair) {
return (
@@ -60,37 +62,46 @@ export function RepairReportPanel({ ticket }: { ticket: TicketDetail }) {
Repair Report
- {repair.submitted_at ? (
+ {firstRepair.repair_submitted_at ? (
- Completed {formatRepairDate(repair.submitted_at)}
+ Completed {formatRepairDate(firstRepair.repair_submitted_at)}
) : null}
-
+
+
Repair Summary
- {repair.notes?.trim() ? (
- <>
-
- {/*
-
- Self-certified by {getPersonLabel(ticket.worker)}
-
*/}
- >
+ {repairs.some((repair) => repair.repair_notes?.trim()) ? (
+
+ {repairs.map((repair) => (
+
+
+ {repair.defect_display_name}
+
+
+ {repair.repair_notes || '-'}
+
+
+ ))}
+
) : (
assignment.repair_proof_video_url,
+ );
+ const repairVideoUrl = repairedAssignment?.repair_proof_video_url ?? null;
return (
@@ -31,12 +34,12 @@ export function RepairVideoComparison({ ticket }: { ticket: TicketDetail }) {
videoUrl={repairVideoUrl}
meta="Contractor evidence video"
emptyTitle={
- ticket.repair
+ repairedAssignment
? 'No repair video uploaded'
: 'No evidence submitted yet'
}
emptyDescription={
- ticket.repair
+ repairedAssignment
? 'The assigned worker has not uploaded a repair video yet.'
: 'Repair video will appear here once evidence is submitted.'
}
diff --git a/src/app/(modules)/ticket/components/TicketColumns.tsx b/src/app/(modules)/ticket/components/TicketColumns.tsx
index d9ef6e8..8e1ed68 100644
--- a/src/app/(modules)/ticket/components/TicketColumns.tsx
+++ b/src/app/(modules)/ticket/components/TicketColumns.tsx
@@ -9,6 +9,17 @@ import { TicketStatusBadge } from './TicketStatusBadge';
import { formatDate } from '@/utils/date';
export function useTicketColumns(): ColumnDef[] {
+ const getAssignedNames = (ticket: TicketListItem) => {
+ const names = Array.from(
+ new Set(
+ (ticket.assignments ?? [])
+ .map((assignment) => assignment.assigned_to_name)
+ .filter(Boolean),
+ ),
+ );
+ return names.length ? names.join(', ') : 'N/A';
+ };
+
return useMemo(
() => [
{
@@ -37,10 +48,19 @@ export function useTicketColumns(): ColumnDef[] {
size: 120,
},
{
- accessorKey: 'assigned_to_name',
- header: 'Assigned To',
+ accessorKey: 'coverage',
+ header: 'Coverage',
+ size: 120,
+ cell: ({ row }) =>
+ row.original.coverage
+ ? `${row.original.coverage.assigned_count}/${row.original.coverage.total_count}`
+ : 'N/A',
+ },
+ {
+ accessorKey: 'assignments',
+ header: 'Contractors',
size: 220,
- cell: ({ row }) => row.original.assigned_to_name || 'N/A',
+ cell: ({ row }) => getAssignedNames(row.original),
},
{
accessorKey: 'created_by_name',
diff --git a/src/app/(modules)/ticket/hooks/useTenantTicketTableEvents.ts b/src/app/(modules)/ticket/hooks/useTenantTicketTableEvents.ts
index ed7189b..996e47d 100644
--- a/src/app/(modules)/ticket/hooks/useTenantTicketTableEvents.ts
+++ b/src/app/(modules)/ticket/hooks/useTenantTicketTableEvents.ts
@@ -20,18 +20,19 @@ function patchTicketLists(
event: TicketTableStatusEvent,
params?: TicketListParams,
) {
- if (!current || !event.id) return current;
+ const ticketId = event.id ?? event.ticket_id;
+ if (!current || !ticketId) return current;
const nextRow = buildTicketListItem(event);
const existingIndex = current.items.findIndex(
- (ticket) => ticket.id === event.id,
+ (ticket) => ticket.id === ticketId,
);
if (existingIndex >= 0) {
return {
...current,
items: current.items.map((ticket) =>
- ticket.id === event.id ? mergeTicketListItem(ticket, event) : ticket,
+ ticket.id === ticketId ? mergeTicketListItem(ticket, event) : ticket,
),
};
}
@@ -84,8 +85,9 @@ function getTicketListParams(queryKey: readonly unknown[]) {
function buildTicketListItem(
event: TicketTableStatusEvent,
): TicketListItem | null {
+ const ticketId = event.id ?? event.ticket_id;
if (
- !event.id ||
+ !ticketId ||
!event.status ||
event.detection_count === undefined ||
!event.updated_at
@@ -94,11 +96,12 @@ function buildTicketListItem(
}
return {
- id: event.id,
+ id: ticketId,
chainage_id: event.chainage_id ?? null,
chainage_name: event.chainage_name ?? null,
status: event.status,
- assigned_to_name: event.assigned_to_name ?? null,
+ assignments: event.assignments ?? [],
+ coverage: event.coverage ?? null,
detection_count: event.detection_count,
created_by_name: event.created_by_name ?? null,
created_at: event.created_at ?? event.updated_at,
@@ -115,7 +118,8 @@ function mergeTicketListItem(
chainage_id: event.chainage_id ?? current.chainage_id,
chainage_name: event.chainage_name ?? current.chainage_name,
status: event.status ?? current.status,
- assigned_to_name: event.assigned_to_name ?? current.assigned_to_name,
+ assignments: event.assignments ?? current.assignments,
+ coverage: event.coverage ?? current.coverage,
detection_count: event.detection_count ?? current.detection_count,
created_by_name: event.created_by_name ?? current.created_by_name,
created_at: event.created_at ?? current.created_at,
diff --git a/src/app/(modules)/ticket/hooks/useTicketDetailEvents.ts b/src/app/(modules)/ticket/hooks/useTicketDetailEvents.ts
index baf0510..d3ffaab 100644
--- a/src/app/(modules)/ticket/hooks/useTicketDetailEvents.ts
+++ b/src/app/(modules)/ticket/hooks/useTicketDetailEvents.ts
@@ -79,9 +79,9 @@ export function useTicketDetailEvents(
});
refetchTicket();
},
- error: (event: TicketStreamErrorEvent) => {
+ error: (event: TicketStreamErrorEvent | null) => {
setErrorMessage(
- event.message ?? 'Live ticket updates disconnected. Reconnecting...',
+ event?.message ?? 'Live ticket updates disconnected. Reconnecting...',
);
},
heartbeat: () => undefined,
diff --git a/src/app/(modules)/ticket/hooks/useTicketQueries.ts b/src/app/(modules)/ticket/hooks/useTicketQueries.ts
index d53101a..7c66c64 100644
--- a/src/app/(modules)/ticket/hooks/useTicketQueries.ts
+++ b/src/app/(modules)/ticket/hooks/useTicketQueries.ts
@@ -6,10 +6,9 @@ import { toast } from 'sonner';
import { ticketService } from '@/services/api';
import type {
- AssignTicketPayload,
ReviewRepairPayload,
- SubmitRepairPayload,
SubmitRepairUploadPayload,
+ TicketAssignmentsPayload,
TicketDetail,
TicketListParams,
TicketStatus,
@@ -71,6 +70,28 @@ export function useAssignableTicketUsersQuery(enabled: boolean) {
});
}
+export function useTicketDefectClassesQuery(
+ ticketId: string | undefined,
+ enabled: boolean,
+) {
+ return useQuery({
+ queryKey: ticketKeys.defectClasses(ticketId ?? ''),
+ queryFn: () => ticketService.getTicketDefectClasses(ticketId as string),
+ enabled: Boolean(enabled && ticketId),
+ });
+}
+
+export function useTicketAssignmentsQuery(
+ ticketId: string | undefined,
+ enabled: boolean,
+) {
+ return useQuery({
+ queryKey: ticketKeys.assignments(ticketId ?? ''),
+ queryFn: () => ticketService.getTicketAssignments(ticketId as string),
+ enabled: Boolean(enabled && ticketId),
+ });
+}
+
function mergeTicketDetailResponse(
current: TicketDetail | undefined,
next: TicketDetail,
@@ -84,21 +105,24 @@ function mergeTicketDetailResponse(
};
}
-export function useAssignTicketMutation(ticketId: string) {
+export function useReplaceTicketAssignmentsMutation(ticketId: string) {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (payload: AssignTicketPayload) =>
- ticketService.assignTicket(ticketId, payload),
+ mutationFn: (payload: TicketAssignmentsPayload) =>
+ ticketService.replaceTicketAssignments(ticketId, payload),
onSuccess: (ticket) => {
- toast.success('Ticket assigned');
+ toast.success('Assignments saved');
queryClient.setQueryData(
ticketKeys.detail(ticketId),
(current) => mergeTicketDetailResponse(current, ticket),
);
+ queryClient.invalidateQueries({
+ queryKey: ticketKeys.assignments(ticketId),
+ });
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
},
- onError: () => toast.error('Failed to assign ticket'),
+ onError: () => toast.error('Failed to save assignments'),
});
}
@@ -119,48 +143,39 @@ export function useStartTicketMutation(ticketId: string) {
});
}
-export function useSubmitRepairMutation(ticketId: string) {
+export function useSubmitAssignmentRepairUploadMutation(
+ ticketId: string,
+ assignmentId: number,
+) {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (payload: SubmitRepairPayload) =>
- ticketService.submitRepair(ticketId, payload),
+ mutationFn: (payload: SubmitRepairUploadPayload) =>
+ ticketService.submitAssignmentRepairUpload(ticketId, assignmentId, payload),
onSuccess: (ticket) => {
toast.success('Repair submitted');
queryClient.setQueryData(
ticketKeys.detail(ticketId),
(current) => mergeTicketDetailResponse(current, ticket),
);
+ queryClient.invalidateQueries({
+ queryKey: ticketKeys.assignments(ticketId),
+ });
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
},
onError: () => toast.error('Failed to submit repair'),
});
}
-export function useSubmitRepairUploadMutation(ticketId: string) {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (payload: SubmitRepairUploadPayload) =>
- ticketService.submitRepairUpload(ticketId, payload),
- onSuccess: (ticket) => {
- toast.success('Repair evidence uploaded');
- queryClient.setQueryData(
- ticketKeys.detail(ticketId),
- (current) => mergeTicketDetailResponse(current, ticket),
- );
- queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
- },
- onError: () => toast.error('Failed to upload repair evidence'),
- });
-}
-
-export function useReviewRepairMutation(ticketId: string) {
+export function useReviewAssignmentRepairMutation(
+ ticketId: string,
+ assignmentId: number,
+) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: ReviewRepairPayload) =>
- ticketService.reviewRepair(ticketId, payload),
+ ticketService.reviewRepair(ticketId, assignmentId, payload),
onSuccess: (ticket, payload) => {
toast.success(
payload.action === 'approve' ? 'Repair approved' : 'Repair rejected',
@@ -169,6 +184,9 @@ export function useReviewRepairMutation(ticketId: string) {
ticketKeys.detail(ticketId),
(current) => mergeTicketDetailResponse(current, ticket),
);
+ queryClient.invalidateQueries({
+ queryKey: ticketKeys.assignments(ticketId),
+ });
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
},
onError: () => toast.error('Failed to review repair'),
diff --git a/src/app/(modules)/ticket/queries/ticketKeys.ts b/src/app/(modules)/ticket/queries/ticketKeys.ts
index 6d8c458..be4fe8f 100644
--- a/src/app/(modules)/ticket/queries/ticketKeys.ts
+++ b/src/app/(modules)/ticket/queries/ticketKeys.ts
@@ -7,4 +7,8 @@ export const ticketKeys = {
details: () => [...ticketKeys.all, 'detail'] as const,
detail: (ticketId: string) => [...ticketKeys.details(), ticketId] as const,
assignableUsers: () => [...ticketKeys.all, 'assignable-users'] as const,
+ defectClasses: (ticketId: string) =>
+ [...ticketKeys.detail(ticketId), 'defect-classes'] as const,
+ assignments: (ticketId: string) =>
+ [...ticketKeys.detail(ticketId), 'assignments'] as const,
};
diff --git a/src/components/lookups/AssignableWorkerSelect.tsx b/src/components/lookups/AssignableWorkerSelect.tsx
index 1f95c52..971b8bb 100644
--- a/src/components/lookups/AssignableWorkerSelect.tsx
+++ b/src/components/lookups/AssignableWorkerSelect.tsx
@@ -21,6 +21,7 @@ export function AssignableWorkerSelect({
value,
onValueChange,
disabled = false,
+ portalContainer,
}: AssignableWorkerSelectProps) {
const lookup = usePaginatedSelect({
selectedValue: value,
@@ -66,6 +67,7 @@ export function AssignableWorkerSelect({
placeholder="Select contractor"
searchPlaceholder="Search contractors"
emptyMessage="No assignable contractors found."
+ portalContainer={portalContainer}
/>
);
}
diff --git a/src/components/lookups/AssignableWorkerSelect.types.ts b/src/components/lookups/AssignableWorkerSelect.types.ts
index 09dda04..313051d 100644
--- a/src/components/lookups/AssignableWorkerSelect.types.ts
+++ b/src/components/lookups/AssignableWorkerSelect.types.ts
@@ -1,5 +1,8 @@
+import type { RefObject } from 'react';
+
export interface AssignableWorkerSelectProps {
value: string;
onValueChange: (value: string) => void;
disabled?: boolean;
+ portalContainer?: RefObject;
}
diff --git a/src/constants/apiRoutes.ts b/src/constants/apiRoutes.ts
index c9c6d14..ac0a4d0 100644
--- a/src/constants/apiRoutes.ts
+++ b/src/constants/apiRoutes.ts
@@ -55,12 +55,19 @@ export const API_ROUTES = {
TICKETS: {
BASE: '/biz/api/v1/tickets',
DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`,
- ASSIGN: (id: string) => `/biz/api/v1/tickets/${id}/assign`,
+ DEFECT_CLASSES: (id: string) =>
+ `/biz/api/v1/tickets/${id}/defect-classes`,
+ ASSIGNMENTS: (id: string) => `/biz/api/v1/tickets/${id}/assignments`,
+ ASSIGNMENT_DETAIL: (ticketId: string, assignmentId: number) =>
+ `/biz/api/v1/tickets/${ticketId}/assignments/${assignmentId}`,
START: (id: string) => `/biz/api/v1/tickets/${id}/start`,
- SUBMIT_REPAIR: (id: string) => `/biz/api/v1/tickets/${id}/submit-repair`,
- SUBMIT_REPAIR_UPLOAD: (id: string) =>
- `/biz/api/v1/tickets/${id}/submit-repair/upload`,
- REVIEW: (id: string) => `/biz/api/v1/tickets/${id}/review`,
+ SUBMIT_ASSIGNMENT_REPAIR_UPLOAD: (
+ ticketId: string,
+ assignmentId: number,
+ ) =>
+ `/biz/api/v1/tickets/${ticketId}/assignments/${assignmentId}/submit-repair/upload`,
+ REVIEW_ASSIGNMENT: (ticketId: string, assignmentId: number) =>
+ `/biz/api/v1/tickets/${ticketId}/assignments/${assignmentId}/review`,
CLOSE: (id: string) => `/biz/api/v1/tickets/${id}/close`,
DETAIL_EVENTS_TOKEN: (id: string) =>
`/biz/api/v1/tickets/${id}/events/token`,
diff --git a/src/services/api/ticket.service.ts b/src/services/api/ticket.service.ts
index 6df5d56..8b40682 100644
--- a/src/services/api/ticket.service.ts
+++ b/src/services/api/ticket.service.ts
@@ -1,16 +1,19 @@
import axiosClient from '../axios/axios';
import { API_ROUTES } from '@/constants/apiRoutes';
import type {
- AssignTicketPayload,
AssignableTicketUsersResponse,
AssignableTicketUsersParams,
ReviewRepairPayload,
SseTokenResponse,
- SubmitRepairPayload,
SubmitRepairUploadPayload,
+ TicketAssignmentListResponse,
+ TicketAssignmentsPayload,
+ TicketAssignmentSummary,
+ TicketDefectClassesResponse,
TicketDetail,
TicketListParams,
TicketListResponse,
+ UpdateTicketAssignmentPayload,
} from '@/types';
export const ticketService = {
@@ -54,17 +57,78 @@ export const ticketService = {
return response.data;
},
- assignTicket: async (
+ getTicketDefectClasses: async (
ticketId: string,
- payload: AssignTicketPayload,
+ ): Promise => {
+ const response = await axiosClient.get(
+ API_ROUTES.TICKETS.DEFECT_CLASSES(ticketId),
+ );
+ return response.data;
+ },
+
+ getTicketAssignments: async (
+ ticketId: string,
+ ): Promise => {
+ const response = await axiosClient.get(
+ API_ROUTES.TICKETS.ASSIGNMENTS(ticketId),
+ );
+ return response.data;
+ },
+
+ getTicketAssignment: async (
+ ticketId: string,
+ assignmentId: number,
+ ): Promise => {
+ const response = await axiosClient.get(
+ API_ROUTES.TICKETS.ASSIGNMENT_DETAIL(ticketId, assignmentId),
+ );
+ return response.data;
+ },
+
+ addTicketAssignments: async (
+ ticketId: string,
+ payload: TicketAssignmentsPayload,
): Promise => {
const response = await axiosClient.post(
- API_ROUTES.TICKETS.ASSIGN(ticketId),
+ API_ROUTES.TICKETS.ASSIGNMENTS(ticketId),
payload,
);
return response.data;
},
+ replaceTicketAssignments: async (
+ ticketId: string,
+ payload: TicketAssignmentsPayload,
+ ): Promise => {
+ const response = await axiosClient.put(
+ API_ROUTES.TICKETS.ASSIGNMENTS(ticketId),
+ payload,
+ );
+ return response.data;
+ },
+
+ updateTicketAssignment: async (
+ ticketId: string,
+ assignmentId: number,
+ payload: UpdateTicketAssignmentPayload,
+ ): Promise => {
+ const response = await axiosClient.patch(
+ API_ROUTES.TICKETS.ASSIGNMENT_DETAIL(ticketId, assignmentId),
+ payload,
+ );
+ return response.data;
+ },
+
+ deleteTicketAssignment: async (
+ ticketId: string,
+ assignmentId: number,
+ ): Promise => {
+ const response = await axiosClient.delete(
+ API_ROUTES.TICKETS.ASSIGNMENT_DETAIL(ticketId, assignmentId),
+ );
+ return response.data;
+ },
+
startTicket: async (ticketId: string): Promise => {
const response = await axiosClient.post(
API_ROUTES.TICKETS.START(ticketId),
@@ -72,19 +136,9 @@ export const ticketService = {
return response.data;
},
- submitRepair: async (
- ticketId: string,
- payload: SubmitRepairPayload,
- ): Promise => {
- const response = await axiosClient.post(
- API_ROUTES.TICKETS.SUBMIT_REPAIR(ticketId),
- payload,
- );
- return response.data;
- },
-
- submitRepairUpload: async (
+ submitAssignmentRepairUpload: async (
ticketId: string,
+ assignmentId: number,
payload: SubmitRepairUploadPayload,
): Promise => {
const formData = new FormData();
@@ -95,7 +149,10 @@ export const ticketService = {
});
const response = await axiosClient.post(
- API_ROUTES.TICKETS.SUBMIT_REPAIR_UPLOAD(ticketId),
+ API_ROUTES.TICKETS.SUBMIT_ASSIGNMENT_REPAIR_UPLOAD(
+ ticketId,
+ assignmentId,
+ ),
formData,
{
headers: {
@@ -108,10 +165,11 @@ export const ticketService = {
reviewRepair: async (
ticketId: string,
+ assignmentId: number,
payload: ReviewRepairPayload,
): Promise => {
const response = await axiosClient.post(
- API_ROUTES.TICKETS.REVIEW(ticketId),
+ API_ROUTES.TICKETS.REVIEW_ASSIGNMENT(ticketId, assignmentId),
payload,
);
return response.data;
diff --git a/src/services/axios/axios.ts b/src/services/axios/axios.ts
index b5820ea..fbcb5a8 100644
--- a/src/services/axios/axios.ts
+++ b/src/services/axios/axios.ts
@@ -8,7 +8,7 @@ const BASE_URL = ENV_CONSTANT.BASE_API_URL;
const axiosClient = axios.create({
baseURL: BASE_URL,
headers: {
- 'Content-Type': 'application/json',
+ // 'Content-Type': 'application/json',
},
});
diff --git a/src/types/ticket/actions.ts b/src/types/ticket/actions.ts
index c10e381..f21dfa4 100644
--- a/src/types/ticket/actions.ts
+++ b/src/types/ticket/actions.ts
@@ -1,5 +1,11 @@
import type { PaginationParams } from '../common';
+export type AssignmentStatus =
+ | 'assigned'
+ | 'under_review'
+ | 'approved'
+ | 'rejected';
+
export interface AssignableTicketUser {
id: number;
name: string;
@@ -16,15 +22,70 @@ export interface AssignableTicketUsersResponse {
total: number;
}
-export interface AssignTicketPayload {
+export interface TicketDefectClass {
+ class_name: string;
+ display_name: string;
+}
+
+export interface TicketDefectClassesResponse {
+ ticket_id: string;
+ video_id: string;
+ defect_classes: TicketDefectClass[];
+}
+
+export interface TicketCoverage {
+ total_defect_classes: string[];
+ assigned_defect_classes: string[];
+ unassigned_defect_classes: string[];
+ is_fully_assigned: boolean;
+ assigned_count: number;
+ total_count: number;
+}
+
+export interface TicketAssignmentSummary {
+ id: number;
+ assigned_to_user_id: number;
+ assigned_to_email: string | null;
+ assigned_to_name: string | null;
+ defect_class: string;
+ defect_display_name: string;
+ status: AssignmentStatus;
+ note: string | null;
+ assigned_by_user_id: number | null;
+ assigned_by_name: string | null;
+ assigned_at: string | null;
+ repair_notes: string | null;
+ repair_proof_video_url: string | null;
+ repair_proof_image_urls: string[];
+ repair_submitted_at: string | null;
+ reviewed_by_user_id: number | null;
+ reviewed_by_name: string | null;
+ review_comment: string | null;
+ reviewed_at: string | null;
+}
+
+export interface TicketAssignmentListResponse {
+ items: TicketAssignmentSummary[];
+ total: number;
+}
+
+export interface TicketAssignmentInput {
assigned_to_user_id: number;
assigned_to_email?: string;
+ defect_class: string;
note?: string;
}
-export interface SubmitRepairPayload {
- notes: string;
- proof_path: string;
+export interface TicketAssignmentsPayload {
+ assignments: TicketAssignmentInput[];
+ note?: string;
+}
+
+export interface UpdateTicketAssignmentPayload {
+ assigned_to_user_id?: number;
+ assigned_to_email?: string;
+ defect_class?: string;
+ note?: string;
}
export interface SubmitRepairUploadPayload {
diff --git a/src/types/ticket/detail.ts b/src/types/ticket/detail.ts
index 74764df..dd90752 100644
--- a/src/types/ticket/detail.ts
+++ b/src/types/ticket/detail.ts
@@ -1,3 +1,4 @@
+import type { TicketAssignmentSummary, TicketCoverage } from './actions';
import type { TicketStatus } from './status';
export interface TicketActor {
@@ -32,26 +33,10 @@ export interface TicketVideo {
thumbnail: string | null;
}
-export interface TicketWorker extends TicketActor {
- assigned_at: string | null;
- assigned_by: TicketActor | 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;
completed_at: string | null;
+ available_defect_classes?: string[];
}
export interface TicketTenant {
@@ -68,13 +53,13 @@ export interface TicketDetail {
id: string;
video_id: string;
chainage_id: string | null;
+ chainage_name?: string | null;
status: TicketStatus;
status_metadata: TicketStatusMetadata | null;
video: TicketVideo | null;
uploader: TicketActor | null;
- worker: TicketWorker | null;
- repair: TicketRepair | null;
- reviewer: TicketReviewer | null;
+ assignments: TicketAssignmentSummary[];
+ coverage: TicketCoverage | null;
ai_result: TicketAiResult | null;
tenant: TicketTenant | null;
timestamps: TicketTimestamps;
diff --git a/src/types/ticket/events.ts b/src/types/ticket/events.ts
index a16bab5..c02b9d4 100644
--- a/src/types/ticket/events.ts
+++ b/src/types/ticket/events.ts
@@ -1,4 +1,5 @@
import type { TicketDetail } from './detail';
+import type { TicketAssignmentSummary, TicketCoverage } from './actions';
import type { TicketStatus } from './status';
export interface SseTokenResponse {
@@ -8,12 +9,15 @@ export interface SseTokenResponse {
export type TicketTableStatusEvent = {
type?: 'ticket_status';
- kind?: 'created' | 'transitioned';
- id: string;
+ kind?: 'created' | 'transitioned' | 'snapshot';
+ id?: string;
+ ticket_id?: string;
chainage_id?: string | null;
chainage_name?: string | null;
status?: TicketStatus;
- assigned_to_name?: string | null;
+ assignments?: TicketAssignmentSummary[];
+ coverage?: TicketCoverage | null;
+ assignment_summary?: Partial>;
detection_count?: number;
created_by_name?: string | null;
created_at?: string;
diff --git a/src/types/ticket/list.ts b/src/types/ticket/list.ts
index 4b2ad4d..923f8df 100644
--- a/src/types/ticket/list.ts
+++ b/src/types/ticket/list.ts
@@ -1,4 +1,5 @@
import type { PaginationParams } from '../common';
+import type { TicketAssignmentSummary, TicketCoverage } from './actions';
import type { TicketStatus } from './status';
export interface TicketListParams extends PaginationParams {
@@ -11,7 +12,8 @@ export interface TicketListItem {
chainage_id: string | null;
chainage_name: string | null;
status: TicketStatus;
- assigned_to_name: string | null;
+ assignments: TicketAssignmentSummary[];
+ coverage: TicketCoverage | null;
detection_count: number;
created_by_name: string | null;
created_at: string;