feat(extension-requests): add manager queue with quick decisions
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user