diff --git a/src/app/(modules)/extension-requests/components/ExtensionRequestColumns.tsx b/src/app/(modules)/extension-requests/components/ExtensionRequestColumns.tsx new file mode 100644 index 0000000..10da421 --- /dev/null +++ b/src/app/(modules)/extension-requests/components/ExtensionRequestColumns.tsx @@ -0,0 +1,155 @@ +'use client'; + +import { useMemo } from 'react'; +import type { ColumnDef } from '@tanstack/react-table'; + +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; +import type { + TicketExtensionRequest, + TicketExtensionRequestStatus, +} from '@/types'; +import { formatDate } from '@/utils/date'; + +const statusConfig: Record< + TicketExtensionRequestStatus, + { label: string; className: string } +> = { + pending: { + label: 'Pending', + className: + 'border-amber-200 bg-amber-100 text-amber-800 dark:border-amber-900 dark:bg-amber-950/50 dark:text-amber-300', + }, + approved: { + label: 'Approved', + className: + 'border-emerald-200 bg-emerald-100 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/50 dark:text-emerald-300', + }, + rejected: { + label: 'Rejected', + className: + 'border-red-200 bg-red-100 text-red-800 dark:border-red-900 dark:bg-red-950/50 dark:text-red-300', + }, + cancelled: { + label: 'Cancelled', + className: 'border-border bg-muted text-muted-foreground', + }, +}; + +function StatusBadge({ status }: { status: TicketExtensionRequestStatus }) { + const config = statusConfig[status]; + + return ( + + {config.label} + + ); +} + +function PersonCell({ + name, + role, +}: { + name: string | null | undefined; + role: string | null | undefined; +}) { + return ( +
+

{name || '-'}

+ {role ? ( +

{role}

+ ) : null} +
+ ); +} + +export function useExtensionRequestColumns(): ColumnDef[] { + return useMemo( + () => [ + { + accessorKey: 'ticket_name', + header: 'Ticket / Lot', + size: 170, + cell: ({ row }) => ( +
+

{row.original.ticket_name}

+

+ Lot #{row.original.assignment_id} +

+
+ ), + }, + { + id: 'requested_by', + header: 'Requested By', + size: 170, + meta: { disableTruncate: true }, + cell: ({ row }) => ( + + ), + }, + { + accessorKey: 'status', + header: 'Status', + size: 125, + meta: { disableTruncate: true }, + cell: ({ row }) => , + }, + { + accessorKey: 'current_due_at', + header: 'Current Due Date', + size: 175, + cell: ({ row }) => formatDate(row.original.current_due_at), + }, + { + accessorKey: 'requested_due_at', + header: 'Requested Due Date', + size: 175, + cell: ({ row }) => formatDate(row.original.requested_due_at), + }, + { + accessorKey: 'reason', + header: 'Request Reason', + size: 260, + }, + { + id: 'resolved_by', + header: 'Resolved By', + size: 170, + meta: { disableTruncate: true }, + cell: ({ row }) => ( + + ), + }, + { + accessorKey: 'decision_note', + header: 'Decision Note', + size: 220, + cell: ({ row }) => row.original.decision_note || '-', + }, + { + id: 'previous_requests', + header: 'Previous Requests', + size: 150, + cell: ({ row }) => ( + + {row.original.history_meta.previous_request_count} + + ), + }, + { + accessorKey: 'resolved_at', + header: 'Resolved On', + size: 175, + cell: ({ row }) => formatDate(row.original.resolved_at), + }, + ], + [], + ); +} diff --git a/src/app/(modules)/extension-requests/components/ExtensionRequestDecisionDialog.tsx b/src/app/(modules)/extension-requests/components/ExtensionRequestDecisionDialog.tsx new file mode 100644 index 0000000..a8bcae1 --- /dev/null +++ b/src/app/(modules)/extension-requests/components/ExtensionRequestDecisionDialog.tsx @@ -0,0 +1,229 @@ +'use client'; + +import { useCallback, useEffect } from 'react'; +import { ArrowRight, Check, Loader2, 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 { + Dialog, + DialogBody, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import type { TicketExtensionRequest } from '@/types'; +import { formatDate } from '@/utils/date'; + +import { + MAX_EXTENSION_DECISION_NOTE_LENGTH, + type ExtensionRequestDecisionAction, + useExtensionRequestDecision, +} from '../hooks/useExtensionRequestDecision'; + +interface ExtensionRequestDecisionDialogProps { + request: TicketExtensionRequest | null; + action: ExtensionRequestDecisionAction | null; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function ExtensionRequestDecisionDialog({ + request, + action, + open, + onOpenChange, +}: ExtensionRequestDecisionDialogProps) { + const handleSuccess = useCallback(() => onOpenChange(false), [onOpenChange]); + const { + note, + setNote, + noteError, + selectedAction, + isPending, + isRejectConfirmationOpen, + setIsRejectConfirmationOpen, + submitDecision, + requestRejection, + resetDecision, + } = useExtensionRequestDecision({ + ticketId: request?.ticket_id ?? '', + assignmentId: request?.assignment_id ?? 0, + onSuccess: handleSuccess, + }); + + useEffect(() => { + resetDecision(); + }, [action, open, request?.id, resetDecision]); + + const handleOpenChange = (nextOpen: boolean) => { + if (isPending) return; + onOpenChange(nextOpen); + }; + + if (!request || !action) return null; + + const isApproval = action === 'approve'; + const noteErrorId = `extension-manager-note-error-${request.id}`; + + return ( + <> + + + + + {isApproval + ? 'Approve extension request' + : 'Reject extension request'} + + + Review {request.ticket_name}, Lot #{request.assignment_id} before + confirming this decision. + + + + +
+
+

+ Current Due Date +

+

+ {formatDate(request.current_due_at)} +

+
+ +
+

+ Requested Due Date +

+

+ {formatDate(request.requested_due_at)} +

+
+
+ +
+

Requested By

+

+ {request.requested_by?.name ?? 'Assigned worker'} +

+

Reason

+

{request.reason}

+
+ +
+
+ +

+ {isApproval + ? 'Optional for approval.' + : 'Required for rejection.'} +

+
+
+