feat(ticket): add audit timeline and safeguard extension rejection

This commit is contained in:
2026-07-31 11:51:32 +05:30
parent 58098af0fe
commit 44d3be5d27
16 changed files with 394 additions and 210 deletions

View File

@@ -10,6 +10,16 @@ import {
X,
} from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
@@ -85,9 +95,12 @@ function ExtensionReviewForm({
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(
ticket.id,
assignmentId,
@@ -96,14 +109,35 @@ function ExtensionReviewForm({
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, note: note.trim() });
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">
@@ -142,25 +176,41 @@ function ExtensionReviewForm({
</section>
<div className="space-y-2">
<Label htmlFor="extension-manager-note">
Add Manager Comments{' '}
<span className="text-muted-foreground">(Optional)</span>
</Label>
<div className="space-y-0.5">
<Label htmlFor="extension-manager-note">Add Manager Comments</Label>
<p className="text-xs text-muted-foreground">
Required for rejection and optional for approval.
</p>
</div>
<div className="relative">
<Textarea
id="extension-manager-note"
value={note}
onChange={(event) =>
setNote(event.target.value.slice(0, MAX_NOTE_LENGTH))
}
onChange={(event) => {
const nextNote = event.target.value.slice(0, MAX_NOTE_LENGTH);
setNote(nextNote);
if (nextNote.trim()) setNoteError(null);
}}
placeholder="Add a note explaining your decision..."
disabled={reviewMutation.isPending}
aria-invalid={Boolean(noteError)}
aria-describedby={
noteError ? 'extension-manager-note-error' : undefined
}
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}
</span>
</div>
{noteError ? (
<p
id="extension-manager-note-error"
className="text-xs text-destructive"
>
{noteError}
</p>
) : null}
</div>
<div className="grid grid-cols-2 gap-3">
@@ -169,7 +219,7 @@ function ExtensionReviewForm({
variant="outline"
className="border-destructive/40 text-destructive hover:bg-destructive/5 hover:text-destructive"
disabled={reviewMutation.isPending}
onClick={() => submitDecision('reject')}
onClick={handleRejectRequest}
>
{selectedAction === 'reject' ? (
<Loader2 className="animate-spin" />
@@ -191,6 +241,33 @@ function ExtensionReviewForm({
Approve Extension
</Button>
</div>
<AlertDialog
open={isRejectConfirmationOpen}
onOpenChange={setIsRejectConfirmationOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reject extension request?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to reject this extension request? This
decision cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={reviewMutation.isPending}>
No
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={reviewMutation.isPending}
onClick={() => submitDecision('reject')}
>
Yes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TicketActionCard>
);