Re_Figma_Code/src/dealer-claim/components/request-detail/modals/DeptLeadIOApprovalModal.tsx

369 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* DeptLeadIOApprovalModal Component
* Modal for Step 3: Dept Lead Approval and IO Organization
* Allows department lead to approve request and organize IO details
*/
import { useState, useMemo } from 'react';
import * as React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { CircleCheckBig, CircleX, Receipt, TriangleAlert } from 'lucide-react';
import { toast } from 'sonner';
interface DeptLeadIOApprovalModalProps {
isOpen: boolean;
onClose: () => void;
onApprove: (data: {
ioNumber: string;
ioRemark: string;
comments: string;
}) => Promise<void>;
onReject: (comments: string) => Promise<void>;
requestTitle?: string;
requestId?: string;
// Pre-filled IO data from IO table
preFilledIONumber?: string;
preFilledIORemark?: string;
preFilledBlockedAmount?: number;
preFilledRemainingBalance?: number;
}
export function DeptLeadIOApprovalModal({
isOpen,
onClose,
onApprove,
onReject,
requestTitle,
requestId: _requestId,
preFilledIONumber,
preFilledIORemark,
preFilledBlockedAmount,
preFilledRemainingBalance,
}: DeptLeadIOApprovalModalProps) {
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
const [ioRemark, setIoRemark] = useState('');
const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false);
// Get IO number from props (read-only, from IO table)
const ioNumber = preFilledIONumber || '';
// Reset form when modal opens/closes
React.useEffect(() => {
if (isOpen) {
// Prefill IO remark from props if available
setIoRemark(preFilledIORemark || '');
setComments('');
setActionType('approve');
}
}, [isOpen, preFilledIORemark]);
const ioRemarkChars = ioRemark.length;
const commentsChars = comments.length;
const maxIoRemarkChars = 300;
const maxCommentsChars = 500;
// Validate form
const isFormValid = useMemo(() => {
if (actionType === 'reject') {
return comments.trim().length > 0;
}
// For approve, need IO number (from table), IO remark, and comments
return (
ioNumber.trim().length > 0 && // IO number must exist from IO table
ioRemark.trim().length > 0 &&
comments.trim().length > 0
);
}, [actionType, ioNumber, ioRemark, comments]);
const handleSubmit = async () => {
if (!isFormValid) {
if (actionType === 'approve') {
if (!ioNumber.trim()) {
toast.error('IO number is required. Please block amount from IO tab first.');
return;
}
if (!ioRemark.trim()) {
toast.error('Please enter IO remark');
return;
}
}
if (!comments.trim()) {
toast.error('Please provide comments');
return;
}
return;
}
try {
setSubmitting(true);
if (actionType === 'approve') {
await onApprove({
ioNumber: ioNumber.trim(),
ioRemark: ioRemark.trim(),
comments: comments.trim(),
});
} else {
await onReject(comments.trim());
}
handleReset();
onClose();
} catch (error) {
console.error(`Failed to ${actionType} request:`, error);
toast.error(`Failed to ${actionType} request. Please try again.`);
} finally {
setSubmitting(false);
}
};
const handleReset = () => {
setActionType('approve');
setIoRemark('');
setComments('');
};
const handleClose = () => {
if (!submitting) {
handleReset();
onClose();
}
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-green-100">
<CircleCheckBig className="w-6 h-6 text-green-600" />
</div>
<div className="flex-1">
<DialogTitle className="font-semibold text-xl">
Approve and Organise IO
</DialogTitle>
<DialogDescription className="text-sm mt-1">
Review IO details and provide your approval comments
</DialogDescription>
</div>
</div>
{/* Request Info Card */}
<div className="space-y-3 p-4 bg-gray-50 rounded-lg border">
<div className="flex items-center justify-between">
<span className="font-medium text-gray-900">Workflow Step:</span>
<Badge variant="outline" className="font-mono">Step 3</Badge>
</div>
<div>
<span className="font-medium text-gray-900">Title:</span>
<p className="text-gray-700 mt-1">{requestTitle || '—'}</p>
</div>
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">Action:</span>
<Badge className="bg-green-100 text-green-800 border-green-200">
<CircleCheckBig className="w-3 h-3 mr-1" />
APPROVE
</Badge>
</div>
</div>
</DialogHeader>
<div className="space-y-3">
{/* Action Toggle Buttons */}
<div className="flex gap-2 p-1 bg-gray-100 rounded-lg">
<Button
type="button"
onClick={() => setActionType('approve')}
className={`flex-1 ${
actionType === 'approve'
? 'bg-green-600 text-white shadow-sm'
: 'text-gray-700 hover:bg-gray-200'
}`}
variant={actionType === 'approve' ? 'default' : 'ghost'}
>
<CircleCheckBig className="w-4 h-4 mr-1" />
Approve
</Button>
<Button
type="button"
onClick={() => setActionType('reject')}
className={`flex-1 ${
actionType === 'reject'
? 'bg-red-600 text-white shadow-sm'
: 'text-gray-700 hover:bg-gray-200'
}`}
variant={actionType === 'reject' ? 'destructive' : 'ghost'}
>
<CircleX className="w-4 h-4 mr-1" />
Reject
</Button>
</div>
{/* IO Organisation Details - Only shown when approving */}
{actionType === 'approve' && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg space-y-3">
<div className="flex items-center gap-2">
<Receipt className="w-4 h-4 text-blue-600" />
<h4 className="font-semibold text-blue-900">IO Organisation Details</h4>
</div>
{/* IO Number - Read-only from IO table */}
<div className="space-y-1">
<Label htmlFor="ioNumber" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
IO Number <span className="text-red-500">*</span>
</Label>
<Input
id="ioNumber"
value={ioNumber || '—'}
disabled
readOnly
className="bg-gray-100 h-8 cursor-not-allowed"
/>
{!ioNumber && (
<p className="text-xs text-red-600 mt-1">
IO number not found. Please block amount from IO tab first.
</p>
)}
{ioNumber && (
<p className="text-xs text-blue-600 mt-1">
Loaded from IO table
</p>
)}
</div>
{/* IO Balance Information - Read-only */}
<div className="grid grid-cols-2 gap-2">
{/* Blocked Amount Display */}
{preFilledBlockedAmount !== undefined && preFilledBlockedAmount > 0 && (
<div className="p-2 bg-green-50 border border-green-200 rounded">
<div className="flex flex-col">
<span className="text-xs font-semibold text-gray-700">Blocked Amount:</span>
<span className="text-sm font-bold text-green-700 mt-1">
{preFilledBlockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
</div>
)}
{/* Remaining Balance Display */}
{preFilledRemainingBalance !== undefined && preFilledRemainingBalance !== null && (
<div className="p-2 bg-blue-50 border border-blue-200 rounded">
<div className="flex flex-col">
<span className="text-xs font-semibold text-gray-700">Remaining Balance:</span>
<span className="text-sm font-bold text-blue-700 mt-1">
{preFilledRemainingBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
</div>
)}
</div>
{/* IO Remark - Editable field (prefilled from IO tab, but can be modified) */}
<div className="space-y-1">
<Label htmlFor="ioRemark" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
IO Remark <span className="text-red-500">*</span>
</Label>
<Textarea
id="ioRemark"
placeholder="Enter remarks about IO organization"
value={ioRemark}
onChange={(e) => {
const value = e.target.value;
if (value.length <= maxIoRemarkChars) {
setIoRemark(value);
}
}}
rows={3}
className="bg-white text-sm min-h-[80px] resize-none"
disabled={false}
readOnly={false}
/>
<div className="flex items-center justify-between text-xs">
{preFilledIORemark && (
<span className="text-blue-600">
Prefilled from IO tab (editable)
</span>
)}
<span className="text-gray-600">{ioRemarkChars}/{maxIoRemarkChars}</span>
</div>
</div>
</div>
)}
{/* Comments & Remarks */}
<div className="space-y-1.5">
<Label htmlFor="comment" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
Comments & Remarks <span className="text-red-500">*</span>
</Label>
<Textarea
id="comment"
placeholder={
actionType === 'approve'
? 'Enter your approval comments and any conditions or notes...'
: 'Enter detailed reasons for rejection...'
}
value={comments}
onChange={(e) => {
const value = e.target.value;
if (value.length <= maxCommentsChars) {
setComments(value);
}
}}
rows={4}
className="text-sm min-h-[80px] resize-none"
/>
<div className="flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center gap-1">
<TriangleAlert className="w-3 h-3" />
Required and visible to all
</div>
<span>{commentsChars}/{maxCommentsChars}</span>
</div>
</div>
</div>
<DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
<Button
variant="outline"
onClick={handleClose}
disabled={submitting}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={!isFormValid || submitting}
className={`${
actionType === 'approve'
? 'bg-green-600 hover:bg-green-700'
: 'bg-red-600 hover:bg-red-700'
} text-white`}
>
{submitting ? (
`${actionType === 'approve' ? 'Approving' : 'Rejecting'}...`
) : (
<>
<CircleCheckBig className="w-4 h-4 mr-2" />
{actionType === 'approve' ? 'Approve Request' : 'Reject Request'}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}