feat(extension-requests): add manager queue with quick decisions

This commit is contained in:
2026-07-31 16:33:20 +05:30
parent 2caeae7c91
commit 87fa04e1b5
19 changed files with 1011 additions and 117 deletions

View File

@@ -1,6 +1,5 @@
'use client';
import { useState } from 'react';
import {
ArrowRight,
CalendarClock,
@@ -30,13 +29,12 @@ import type {
import { formatDate } from '@/utils/date';
import {
useReviewTicketExtensionMutation,
useTicketExtensionRequestQuery,
} from '../../../hooks/useTicketQueries';
MAX_EXTENSION_DECISION_NOTE_LENGTH,
useExtensionRequestDecision,
} from '../../../../extension-requests/hooks/useExtensionRequestDecision';
import { useTicketExtensionRequestQuery } from '../../../hooks/useTicketQueries';
import { TicketActionCard } from './TicketActionCard';
const MAX_NOTE_LENGTH = 300;
function getPendingRequest(response: TicketExtensionRequestsResponse) {
return response.items
.filter((request) => request.status === 'pending')
@@ -93,49 +91,19 @@ function ExtensionReviewForm({
assignmentId: number;
request: TicketExtensionRequest;
}) {
const [note, setNote] = useState('');
const [noteError, setNoteError] = useState<string | null>(null);
const [selectedAction, setSelectedAction] = useState<
'approve' | 'reject' | null
>(null);
const [isRejectConfirmationOpen, setIsRejectConfirmationOpen] =
useState(false);
const reviewMutation = useReviewTicketExtensionMutation(
ticketId,
assignmentId,
);
const {
note,
setNote,
noteError,
selectedAction,
isPending,
isRejectConfirmationOpen,
setIsRejectConfirmationOpen,
submitDecision,
requestRejection,
} = useExtensionRequestDecision({ ticketId, assignmentId });
const extensionDays = getExtensionDays(request);
const submitDecision = async (action: 'approve' | 'reject') => {
const trimmedNote = note.trim();
if (action === 'reject' && !trimmedNote) {
setNoteError('Manager comments are required to reject this request.');
setIsRejectConfirmationOpen(false);
return;
}
setSelectedAction(action);
try {
await reviewMutation.mutateAsync({
action,
...(trimmedNote ? { note: trimmedNote } : {}),
});
} finally {
setSelectedAction(null);
}
};
const handleRejectRequest = () => {
if (!note.trim()) {
setNoteError('Manager comments are required to reject this request.');
return;
}
setNoteError(null);
setIsRejectConfirmationOpen(true);
};
return (
<TicketActionCard icon={CalendarClock} title="Extension Request Details">
<div className="space-y-4">
@@ -185,12 +153,10 @@ function ExtensionReviewForm({
id="extension-manager-note"
value={note}
onChange={(event) => {
const nextNote = event.target.value.slice(0, MAX_NOTE_LENGTH);
setNote(nextNote);
if (nextNote.trim()) setNoteError(null);
setNote(event.target.value);
}}
placeholder="Add a note explaining your decision..."
disabled={reviewMutation.isPending}
disabled={isPending}
aria-invalid={Boolean(noteError)}
aria-describedby={
noteError ? 'extension-manager-note-error' : undefined
@@ -198,7 +164,7 @@ function ExtensionReviewForm({
className="min-h-24 resize-none pb-8"
/>
<span className="pointer-events-none absolute right-3 bottom-2 text-xs text-muted-foreground">
{note.length} / {MAX_NOTE_LENGTH}
{note.length} / {MAX_EXTENSION_DECISION_NOTE_LENGTH}
</span>
</div>
{noteError ? (
@@ -216,8 +182,8 @@ function ExtensionReviewForm({
type="button"
variant="outline"
className="border-destructive/40 text-destructive hover:bg-destructive/5 hover:text-destructive"
disabled={reviewMutation.isPending}
onClick={handleRejectRequest}
disabled={isPending}
onClick={requestRejection}
>
{selectedAction === 'reject' ? (
<Loader2 className="animate-spin" />
@@ -228,8 +194,8 @@ function ExtensionReviewForm({
</Button>
<Button
type="button"
disabled={reviewMutation.isPending}
onClick={() => submitDecision('approve')}
disabled={isPending}
onClick={() => void submitDecision('approve')}
>
{selectedAction === 'approve' ? (
<Loader2 className="animate-spin" />
@@ -253,13 +219,11 @@ function ExtensionReviewForm({
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={reviewMutation.isPending}>
No
</AlertDialogCancel>
<AlertDialogCancel disabled={isPending}>No</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={reviewMutation.isPending}
onClick={() => submitDecision('reject')}
disabled={isPending}
onClick={() => void submitDecision('reject')}
>
Yes
</AlertDialogAction>