feat(ticket): add shared extension request history for workers and managers
This commit is contained in:
@@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user