feat(ticket): add shared extension request history for workers and managers

This commit is contained in:
2026-08-03 16:04:15 +05:30
parent 0a4d9512cb
commit 6169bbdc28
5 changed files with 284 additions and 15 deletions

View File

@@ -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<TicketExtensionRequest | null>(
(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)}
/>
<DetailRow
label="Requested By"
value={request.requested_by?.name ?? 'Assigned worker'}
/>
<DetailRow
label="Requested Due Date"
value={formatDate(request.requested_due_at)}
@@ -160,6 +170,12 @@ function ExtensionRequestDetails({
value={formatDate(request.requested_due_at)}
/>
) : null}
{request.decision_note ? (
<DetailRow
label="Manager Comments"
value={request.decision_note}
/>
) : null}
</div>
</section>
) : null}
@@ -176,10 +192,15 @@ function ExtensionRequestDetails({
</div>
</section>
<Button type="button" variant="outline" className="w-full">
<History />
View Approval History
</Button>
<div className="space-y-2">
<ExtensionRequestHistoryDialog requests={requests} />
{request.status !== 'pending' && onRequestAnother ? (
<Button type="button" className="w-full" onClick={onRequestAnother}>
<Plus />
Request Another Extension
</Button>
) : null}
</div>
</div>
);
}
@@ -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 (
<p className="text-sm text-muted-foreground">
No extension request history is available for this ticket lot.
</p>
);
}
return (
<RequestExtensionForm
ticketId={ticketId}
@@ -240,5 +272,33 @@ export function TicketExtensionRequestPanel({
);
}
return <ExtensionRequestDetails request={latestRequest} />;
if (isRequestFormOpen && latestRequest.status !== 'pending') {
return (
<div className="space-y-4">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setIsRequestFormOpen(false)}
>
Back to latest request
</Button>
<RequestExtensionForm
ticketId={ticketId}
assignmentId={assignmentId}
currentDueAt={dueAt}
/>
</div>
);
}
return (
<ExtensionRequestDetails
request={latestRequest}
requests={extensionRequestQuery.data?.items ?? []}
onRequestAnother={
canRequest ? () => setIsRequestFormOpen(true) : undefined
}
/>
);
}