From 6169bbdc28f7aa703ed1cf8339553fda80afff98 Mon Sep 17 00:00:00 2001 From: "santasri.pachhal" Date: Mon, 3 Aug 2026 16:04:15 +0530 Subject: [PATCH] feat(ticket): add shared extension request history for workers and managers --- .../components/TicketStatusActions.tsx | 18 +- .../actions/ExtensionRequestHistoryDialog.tsx | 162 ++++++++++++++++++ .../actions/RequestExtensionAction.tsx | 5 +- .../actions/ReviewExtensionRequestAction.tsx | 32 +++- .../actions/TicketExtensionRequestPanel.tsx | 82 +++++++-- 5 files changed, 284 insertions(+), 15 deletions(-) create mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/ExtensionRequestHistoryDialog.tsx diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx index 8676c4b..5175198 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx @@ -43,19 +43,32 @@ function getTicketAction( ticketId: string, assignment?: TicketAssignmentSummary, ) { - if (assignment && !assignment.is_complete) { + if (assignment) { return ( } + fallback={ + } + > + + + } > } @@ -63,6 +76,7 @@ function getTicketAction( ); diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/ExtensionRequestHistoryDialog.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/ExtensionRequestHistoryDialog.tsx new file mode 100644 index 0000000..55279b3 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/actions/ExtensionRequestHistoryDialog.tsx @@ -0,0 +1,162 @@ +'use client'; + +import { ArrowRight, History } from 'lucide-react'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogBody, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { cn } from '@/lib/utils'; +import type { + TicketExtensionRequest, + TicketExtensionRequestStatus, +} from '@/types'; +import { formatDate } from '@/utils/date'; + +const statusConfig: Record< + TicketExtensionRequestStatus, + { label: string; className: string } +> = { + pending: { + label: 'Pending', + className: + 'border-amber-200 bg-amber-100 text-amber-800 dark:border-amber-900 dark:bg-amber-950/50 dark:text-amber-300', + }, + approved: { + label: 'Approved', + className: + 'border-emerald-200 bg-emerald-100 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/50 dark:text-emerald-300', + }, + rejected: { + label: 'Rejected', + className: + 'border-red-200 bg-red-100 text-red-800 dark:border-red-900 dark:bg-red-950/50 dark:text-red-300', + }, + cancelled: { + label: 'Cancelled', + className: 'border-border bg-muted text-muted-foreground', + }, +}; + +function HistoryItem({ request }: { request: TicketExtensionRequest }) { + const status = statusConfig[request.status]; + + return ( +
+
+ + {status.label} + + + Requested {formatDate(request.requested_at)} + +
+ +
+

Requested By

+

+ {request.requested_by?.name ?? 'Assigned worker'} +

+
+ +
+
+

Previous Due Date

+

+ {formatDate(request.current_due_at)} +

+
+ +
+

Requested Due Date

+

+ {formatDate(request.requested_due_at)} +

+
+
+ +
+

Reason

+

{request.reason}

+
+ + {request.resolved_at || request.resolved_by || request.decision_note ? ( +
+

Manager Response

+
+ {request.resolved_by ? ( +
+

Resolved By

+

+ {request.resolved_by.name} ยท {request.resolved_by.role.name} +

+
+ ) : null} + {request.resolved_at ? ( +
+

Resolved On

+

+ {formatDate(request.resolved_at)} +

+
+ ) : null} +
+ {request.decision_note ? ( +
+

Manager Comments

+

+ {request.decision_note} +

+
+ ) : null} +
+ ) : null} +
+ ); +} + +export function ExtensionRequestHistoryDialog({ + requests, +}: { + requests: TicketExtensionRequest[]; +}) { + const sortedRequests = [...requests].sort( + (left, right) => + new Date(right.requested_at).getTime() - + new Date(left.requested_at).getTime(), + ); + + return ( + + + + + + + Extension Request History + + Review every deadline extension request and manager decision for + this ticket lot. + + + + {sortedRequests.map((request) => ( + + ))} + + + + + ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/RequestExtensionAction.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/RequestExtensionAction.tsx index 5bcdbe3..de0cd2d 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/actions/RequestExtensionAction.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/actions/RequestExtensionAction.tsx @@ -7,21 +7,24 @@ export function RequestExtensionAction({ ticketId, assignmentId, dueAt, + canRequest, }: { ticketId: string; assignmentId: number; dueAt: string | null; + canRequest: boolean; }) { return ( ); diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/ReviewExtensionRequestAction.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/ReviewExtensionRequestAction.tsx index 31fc9f1..1a864d2 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/actions/ReviewExtensionRequestAction.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/actions/ReviewExtensionRequestAction.tsx @@ -33,6 +33,11 @@ import { useExtensionRequestDecision, } from '../../../../extension-requests/hooks/useExtensionRequestDecision'; import { useTicketExtensionRequestQuery } from '../../../hooks/useTicketQueries'; +import { ExtensionRequestHistoryDialog } from './ExtensionRequestHistoryDialog'; +import { + ExtensionRequestDetails, + getLatestRequest, +} from './TicketExtensionRequestPanel'; import { TicketActionCard } from './TicketActionCard'; function getPendingRequest(response: TicketExtensionRequestsResponse) { @@ -86,10 +91,12 @@ function ExtensionReviewForm({ ticketId, assignmentId, request, + requests, }: { ticketId: string; assignmentId: number; request: TicketExtensionRequest; + requests: TicketExtensionRequest[]; }) { const { note, @@ -210,6 +217,8 @@ function ExtensionReviewForm({ + + + + + ); + } return ( ); } diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/TicketExtensionRequestPanel.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/TicketExtensionRequestPanel.tsx index c623a1f..4846777 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/actions/TicketExtensionRequestPanel.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/actions/TicketExtensionRequestPanel.tsx @@ -1,12 +1,13 @@ 'use client'; +import { useState } from 'react'; import { AlertCircle, CalendarDays, CheckCircle2, Clock3, - History, Loader2, + Plus, RotateCcw, XCircle, } from 'lucide-react'; @@ -20,6 +21,7 @@ import type { import { formatDate } from '@/utils/date'; import { useTicketExtensionRequestQuery } from '../../../hooks/useTicketQueries'; +import { ExtensionRequestHistoryDialog } from './ExtensionRequestHistoryDialog'; import { RequestExtensionForm } from './RequestExtensionForm'; interface DetailRowProps { @@ -44,21 +46,21 @@ function DetailRow({ label, value, children, className }: DetailRowProps) { const statusConfig = { pending: { title: 'Request Pending', - description: 'Your extension request is waiting for manager approval.', + description: 'This extension request is waiting for manager approval.', icon: Clock3, className: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-300', }, approved: { title: 'Request Approved', - description: 'Your extension request has been approved by the manager.', + description: 'This extension request was approved by the manager.', icon: CheckCircle2, className: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/30 dark:text-emerald-300', }, rejected: { title: 'Request Rejected', - description: 'Your extension request has been rejected by the manager.', + description: 'This extension request was rejected by the manager.', icon: AlertCircle, className: 'border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/30 dark:text-red-300', @@ -79,7 +81,7 @@ const statusConfig = { } >; -function getLatestRequest(response: TicketExtensionRequestsResponse) { +export function getLatestRequest(response: TicketExtensionRequestsResponse) { return response.items.reduce( (latest, item) => { if (!latest) return item; @@ -98,10 +100,14 @@ function getExtensionDays(request: TicketExtensionRequest) { return Math.max(0, Math.ceil(difference / (24 * 60 * 60 * 1000))); } -function ExtensionRequestDetails({ +export function ExtensionRequestDetails({ request, + requests, + onRequestAnother, }: { request: TicketExtensionRequest; + requests: TicketExtensionRequest[]; + onRequestAnother?: () => void; }) { const config = statusConfig[request.status]; const StatusIcon = config.icon; @@ -124,6 +130,10 @@ function ExtensionRequestDetails({ label="Requested On" value={formatDate(request.requested_at)} /> + ) : null} + {request.decision_note ? ( + + ) : null} ) : null} @@ -176,10 +192,15 @@ function ExtensionRequestDetails({ - +
+ + {request.status !== 'pending' && onRequestAnother ? ( + + ) : null} +
); } @@ -188,11 +209,14 @@ export function TicketExtensionRequestPanel({ ticketId, assignmentId, dueAt, + canRequest, }: { ticketId: string; assignmentId: number; dueAt: string | null; + canRequest: boolean; }) { + const [isRequestFormOpen, setIsRequestFormOpen] = useState(false); const extensionRequestQuery = useTicketExtensionRequestQuery( ticketId, assignmentId, @@ -231,6 +255,14 @@ export function TicketExtensionRequestPanel({ : null; if (!latestRequest) { + if (!canRequest) { + return ( +

+ No extension request history is available for this ticket lot. +

+ ); + } + return ( ; + if (isRequestFormOpen && latestRequest.status !== 'pending') { + return ( +
+ + +
+ ); + } + + return ( + setIsRequestFormOpen(true) : undefined + } + /> + ); }