feat(ticket): scope issue preview to selected assignment lot
This commit is contained in:
@@ -6,14 +6,17 @@ import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PERMISSIONS } from '@/constants/permissions';
|
||||
import { PermissionGuard } from '@/guards';
|
||||
import type { TicketOverviewDetail } from '@/types';
|
||||
import type { TicketDetail, TicketOverviewDetail } from '@/types';
|
||||
|
||||
import { AssignTicketAction } from './actions/AssignTicketAction';
|
||||
import { OpenRepairReviewAction } from './actions/OpenRepairReviewAction';
|
||||
|
||||
export function TicketDetailHeader({
|
||||
ticket,
|
||||
classDetail,
|
||||
}: {
|
||||
ticket: TicketOverviewDetail;
|
||||
classDetail?: TicketDetail;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const ticketLabel = ticket.ticket_name || ticket.id;
|
||||
@@ -26,6 +29,11 @@ export function TicketDetailHeader({
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{classDetail?.assignment_status === 'under_review' ? (
|
||||
<PermissionGuard permissions={PERMISSIONS.TICKET.REVIEW}>
|
||||
<OpenRepairReviewAction ticket={classDetail} />
|
||||
</PermissionGuard>
|
||||
) : null}
|
||||
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
|
||||
<AssignTicketAction ticketId={ticket.id} />
|
||||
</PermissionGuard>
|
||||
|
||||
@@ -2,25 +2,24 @@
|
||||
|
||||
import { PERMISSIONS } from '@/constants/permissions';
|
||||
import { PermissionGuard } from '@/guards';
|
||||
import type { TicketDetail } from '@/types';
|
||||
import type { TicketAssignmentSummary, TicketDetail } from '@/types';
|
||||
|
||||
import { AssignedContractorAction } from './actions/AssignedContractorAction';
|
||||
import { ClosedTicketAction } from './actions/ClosedTicketAction';
|
||||
import { NoTicketAction } from './actions/NoTicketAction';
|
||||
import { OpenRepairReviewAction } from './actions/OpenRepairReviewAction';
|
||||
import { RequestExtensionAction } from './actions/RequestExtensionAction';
|
||||
import { ReviewExtensionRequestAction } from './actions/ReviewExtensionRequestAction';
|
||||
|
||||
interface TicketStatusActionsProps {
|
||||
ticket: TicketDetail;
|
||||
assignment?: TicketAssignmentSummary;
|
||||
}
|
||||
|
||||
function getTicketAction(ticket: TicketDetail) {
|
||||
if (ticket.assignment_status === 'unassigned') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ticket.assignment_status === 'assigned') {
|
||||
function getTicketAction(
|
||||
ticket: TicketDetail,
|
||||
assignment?: TicketAssignmentSummary,
|
||||
) {
|
||||
if (assignment && !assignment.is_complete) {
|
||||
return (
|
||||
<PermissionGuard
|
||||
permissions={PERMISSIONS.TICKET.ASSIGN}
|
||||
@@ -29,29 +28,46 @@ function getTicketAction(ticket: TicketDetail) {
|
||||
permissions={PERMISSIONS.TICKET.WORK}
|
||||
fallback={<NoTicketAction />}
|
||||
>
|
||||
<RequestExtensionAction ticket={ticket} />
|
||||
<RequestExtensionAction
|
||||
ticket={ticket}
|
||||
assignmentId={assignment.id}
|
||||
dueAt={assignment.due_at}
|
||||
/>
|
||||
</PermissionGuard>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{ticket.assignment_status === 'assigned' ? (
|
||||
<AssignedContractorAction ticket={ticket} />
|
||||
<ReviewExtensionRequestAction ticket={ticket} />
|
||||
) : null}
|
||||
<ReviewExtensionRequestAction
|
||||
ticket={ticket}
|
||||
assignmentId={assignment.id}
|
||||
/>
|
||||
</>
|
||||
</PermissionGuard>
|
||||
);
|
||||
}
|
||||
|
||||
if (ticket.assignment_status === 'under_review') {
|
||||
if (ticket.assignment_status === 'unassigned') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ticket.assignment_status === 'assigned') {
|
||||
return (
|
||||
<PermissionGuard
|
||||
permissions={PERMISSIONS.TICKET.REVIEW}
|
||||
permissions={PERMISSIONS.TICKET.ASSIGN}
|
||||
fallback={<NoTicketAction />}
|
||||
>
|
||||
<OpenRepairReviewAction ticket={ticket} />
|
||||
<AssignedContractorAction ticket={ticket} />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}
|
||||
|
||||
if (ticket.assignment_status === 'under_review') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
ticket.assignment_status === 'approved' ||
|
||||
ticket.assignment_status === 'rejected'
|
||||
@@ -62,8 +78,11 @@ function getTicketAction(ticket: TicketDetail) {
|
||||
return <NoTicketAction />;
|
||||
}
|
||||
|
||||
export function TicketStatusActions({ ticket }: TicketStatusActionsProps) {
|
||||
const action = getTicketAction(ticket);
|
||||
export function TicketStatusActions({
|
||||
ticket,
|
||||
assignment,
|
||||
}: TicketStatusActionsProps) {
|
||||
const action = getTicketAction(ticket, assignment);
|
||||
|
||||
return action ? <div className="space-y-3">{action}</div> : null;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowRight, ClipboardCheck } from 'lucide-react';
|
||||
import { ClipboardCheck } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { TicketDetail } from '@/types';
|
||||
import { ROUTES } from '@/utils/routes';
|
||||
|
||||
import { TicketActionCard } from './TicketActionCard';
|
||||
|
||||
export function OpenRepairReviewAction({ ticket }: { ticket: TicketDetail }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<TicketActionCard icon={ClipboardCheck} title="Review Submitted Repairs">
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
Review every submitted repair separately using its detection image,
|
||||
location, and repair proof.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
ROUTES.TICKET_CLASS_REVIEW(ticket.id, ticket.defect_class),
|
||||
)
|
||||
router.push(ROUTES.TICKET_CLASS_REVIEW(ticket.id, ticket.defect_class))
|
||||
}
|
||||
>
|
||||
Open Issue Review
|
||||
<ArrowRight />
|
||||
<ClipboardCheck />
|
||||
Review Repairs
|
||||
</Button>
|
||||
</div>
|
||||
</TicketActionCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,22 @@ import type { TicketDetail } from '@/types';
|
||||
import { TicketActionCard } from './TicketActionCard';
|
||||
import { TicketExtensionRequestPanel } from './TicketExtensionRequestPanel';
|
||||
|
||||
export function RequestExtensionAction({ ticket }: { ticket: TicketDetail }) {
|
||||
export function RequestExtensionAction({
|
||||
ticket,
|
||||
assignmentId,
|
||||
dueAt,
|
||||
}: {
|
||||
ticket: TicketDetail;
|
||||
assignmentId: number;
|
||||
dueAt: string | null;
|
||||
}) {
|
||||
return (
|
||||
<TicketActionCard icon={Clock3} title="Request Extension">
|
||||
<TicketExtensionRequestPanel ticket={ticket} />
|
||||
<TicketExtensionRequestPanel
|
||||
ticket={ticket}
|
||||
assignmentId={assignmentId}
|
||||
dueAt={dueAt}
|
||||
/>
|
||||
</TicketActionCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,10 +16,15 @@ const MAX_REASON_LENGTH = 300;
|
||||
|
||||
interface RequestExtensionFormProps {
|
||||
ticket: TicketDetail;
|
||||
assignmentId: number;
|
||||
currentDueAt: string | null;
|
||||
}
|
||||
|
||||
export function RequestExtensionForm({ ticket }: RequestExtensionFormProps) {
|
||||
const currentDueAt = ticket.worker?.due_at;
|
||||
export function RequestExtensionForm({
|
||||
ticket,
|
||||
assignmentId,
|
||||
currentDueAt,
|
||||
}: RequestExtensionFormProps) {
|
||||
const currentDueDate = useMemo(
|
||||
() => (currentDueAt ? new Date(currentDueAt) : undefined),
|
||||
[currentDueAt],
|
||||
@@ -36,7 +41,6 @@ export function RequestExtensionForm({ ticket }: RequestExtensionFormProps) {
|
||||
);
|
||||
const [requestedDueDate, setRequestedDueDate] = useState<Date>();
|
||||
const [reason, setReason] = useState('');
|
||||
const assignmentId = ticket.worker?.assignment_id;
|
||||
const requestExtensionMutation = useRequestTicketExtensionMutation(
|
||||
ticket.id,
|
||||
assignmentId,
|
||||
@@ -48,10 +52,7 @@ export function RequestExtensionForm({ ticket }: RequestExtensionFormProps) {
|
||||
(!currentDueDate || requestedDueDate.getTime() > currentDueDate.getTime()),
|
||||
);
|
||||
const canSubmit =
|
||||
Boolean(assignmentId) &&
|
||||
isRequestedDateValid &&
|
||||
Boolean(reason.trim()) &&
|
||||
!isPending;
|
||||
isRequestedDateValid && Boolean(reason.trim()) && !isPending;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!requestedDueDate || !canSubmit) return;
|
||||
|
||||
@@ -77,16 +77,17 @@ function Requester({ request }: { request: TicketExtensionRequest }) {
|
||||
|
||||
function ExtensionReviewForm({
|
||||
ticket,
|
||||
assignmentId,
|
||||
request,
|
||||
}: {
|
||||
ticket: TicketDetail;
|
||||
assignmentId: number;
|
||||
request: TicketExtensionRequest;
|
||||
}) {
|
||||
const [note, setNote] = useState('');
|
||||
const [selectedAction, setSelectedAction] = useState<
|
||||
'approve' | 'reject' | null
|
||||
>(null);
|
||||
const assignmentId = ticket.worker?.assignment_id as number;
|
||||
const reviewMutation = useReviewTicketExtensionMutation(
|
||||
ticket.id,
|
||||
assignmentId,
|
||||
@@ -197,16 +198,17 @@ function ExtensionReviewForm({
|
||||
|
||||
export function ReviewExtensionRequestAction({
|
||||
ticket,
|
||||
assignmentId,
|
||||
}: {
|
||||
ticket: TicketDetail;
|
||||
assignmentId: number;
|
||||
}) {
|
||||
const assignmentId = ticket.worker?.assignment_id;
|
||||
const extensionRequestQuery = useTicketExtensionRequestQuery(
|
||||
ticket.id,
|
||||
assignmentId,
|
||||
);
|
||||
|
||||
if (!assignmentId || extensionRequestQuery.isError) {
|
||||
if (extensionRequestQuery.isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -227,5 +229,11 @@ export function ReviewExtensionRequestAction({
|
||||
|
||||
if (!pendingRequest) return null;
|
||||
|
||||
return <ExtensionReviewForm ticket={ticket} request={pendingRequest} />;
|
||||
return (
|
||||
<ExtensionReviewForm
|
||||
ticket={ticket}
|
||||
assignmentId={assignmentId}
|
||||
request={pendingRequest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,23 +187,18 @@ function ExtensionRequestDetails({
|
||||
|
||||
export function TicketExtensionRequestPanel({
|
||||
ticket,
|
||||
assignmentId,
|
||||
dueAt,
|
||||
}: {
|
||||
ticket: TicketDetail;
|
||||
assignmentId: number;
|
||||
dueAt: string | null;
|
||||
}) {
|
||||
const assignmentId = ticket.worker?.assignment_id;
|
||||
const extensionRequestQuery = useTicketExtensionRequestQuery(
|
||||
ticket.id,
|
||||
assignmentId,
|
||||
);
|
||||
|
||||
if (!assignmentId) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
|
||||
No active assignment is available for this ticket.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (extensionRequestQuery.isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-40 items-center justify-center text-muted-foreground">
|
||||
@@ -237,7 +232,13 @@ export function TicketExtensionRequestPanel({
|
||||
: null;
|
||||
|
||||
if (!latestRequest) {
|
||||
return <RequestExtensionForm ticket={ticket} />;
|
||||
return (
|
||||
<RequestExtensionForm
|
||||
ticket={ticket}
|
||||
assignmentId={assignmentId}
|
||||
currentDueAt={dueAt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ExtensionRequestDetails request={latestRequest} />;
|
||||
|
||||
@@ -23,6 +23,7 @@ import { TicketStatusActions } from './components/TicketStatusActions';
|
||||
import { TicketDefectClassTabs } from './components/TicketDefectClassTabs';
|
||||
import { useTicketDetailEvents } from '../hooks/useTicketDetailEvents';
|
||||
import {
|
||||
useTicketAssignmentsQuery,
|
||||
useTicketClassDetailQuery,
|
||||
useTicketOverviewQuery,
|
||||
} from '../hooks/useTicketQueries';
|
||||
@@ -35,11 +36,18 @@ export default function TicketDetailPage() {
|
||||
const [selectedAssignmentId, setSelectedAssignmentId] = useState<number>();
|
||||
|
||||
const overviewQuery = useTicketOverviewQuery(ticketId);
|
||||
const classDetailQuery = useTicketClassDetailQuery(ticketId, defectClass);
|
||||
const overview = overviewQuery.data;
|
||||
const assignmentsQuery = useTicketAssignmentsQuery(
|
||||
ticketId,
|
||||
overview?.assignments,
|
||||
);
|
||||
const classDetailQuery = useTicketClassDetailQuery(ticketId, defectClass);
|
||||
const classDetail = classDetailQuery.data;
|
||||
const activeAssignmentId =
|
||||
selectedAssignmentId ?? overview?.assignments?.[0]?.id;
|
||||
const assignments = assignmentsQuery.data?.items ?? [];
|
||||
const activeAssignmentId = selectedAssignmentId ?? assignments[0]?.id;
|
||||
const activeAssignment = assignments.find(
|
||||
(assignment) => assignment.id === activeAssignmentId,
|
||||
);
|
||||
const isClassDetailLoading = Boolean(
|
||||
defectClass && classDetailQuery.isLoading,
|
||||
);
|
||||
@@ -66,7 +74,7 @@ export default function TicketDetailPage() {
|
||||
<main className="relative z-10 min-w-0 max-w-full space-y-5 xl:flex xl:h-[calc(100vh-6.5rem)] xl:min-h-0 xl:flex-col xl:gap-5 xl:space-y-0 xl:overflow-hidden">
|
||||
<div className="xl:shrink-0">
|
||||
{overview ? (
|
||||
<TicketDetailHeader ticket={overview} />
|
||||
<TicketDetailHeader ticket={overview} classDetail={classDetail} />
|
||||
) : (
|
||||
<TicketOverviewHeaderSkeleton />
|
||||
)}
|
||||
@@ -101,7 +109,10 @@ export default function TicketDetailPage() {
|
||||
<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">
|
||||
{isClassDetailLoading ? <TicketClassActionsSkeleton /> : null}
|
||||
{!isClassDetailLoading && classDetail ? (
|
||||
<TicketStatusActions ticket={classDetail} />
|
||||
<TicketStatusActions
|
||||
ticket={classDetail}
|
||||
assignment={activeAssignment}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -405,6 +405,9 @@ export function useRequestTicketExtensionMutation(
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ticketKeys.classDetail(ticketId, defectClass),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ticketKeys.assignments(ticketId),
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
},
|
||||
onError: () => toast.error('Failed to request an extension'),
|
||||
@@ -432,6 +435,9 @@ export function useReviewTicketExtensionMutation(
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ticketKeys.classDetail(ticketId, defectClass),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ticketKeys.assignments(ticketId),
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
},
|
||||
onError: () => toast.error('Failed to review extension request'),
|
||||
|
||||
Reference in New Issue
Block a user