richtext added for dealer claim and in-step ,odal ui enhanced
This commit is contained in:
parent
22cb42e06e
commit
c6bd5a19ef
@ -3,7 +3,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { RichTextEditor } from '@/components/ui/rich-text-editor';
|
||||||
|
import { FormattedDescription } from '@/components/common/FormattedDescription';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
@ -548,16 +549,19 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
{/* Request Detail */}
|
{/* Request Detail */}
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="requestDescription" className="text-base font-semibold">Request in Detail - Brief Requirement *</Label>
|
<Label htmlFor="requestDescription" className="text-base font-semibold">Request in Detail - Brief Requirement *</Label>
|
||||||
<Textarea
|
<p className="text-sm text-gray-600 mb-3">
|
||||||
id="requestDescription"
|
Explain what you need approval for, why it's needed, and any relevant background information.
|
||||||
placeholder="Provide a detailed description of your claim requirement..."
|
<span className="block mt-1 text-xs text-blue-600">
|
||||||
value={formData.requestDescription}
|
💡 Tip: You can paste formatted content (lists, tables) and the formatting will be preserved.
|
||||||
onChange={(e) => updateFormData('requestDescription', e.target.value)}
|
</span>
|
||||||
className="mt-2 min-h-[120px]"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Include key details about the claim, objectives, and expected outcomes
|
|
||||||
</p>
|
</p>
|
||||||
|
<RichTextEditor
|
||||||
|
value={formData.requestDescription || ''}
|
||||||
|
onChange={(html) => updateFormData('requestDescription', html)}
|
||||||
|
placeholder="Provide comprehensive details about your claim requirement including scope, objectives, expected outcomes, and any supporting context that will help approvers make an informed decision."
|
||||||
|
className="min-h-[120px] text-base border-2 border-gray-300 focus-within:border-blue-500 bg-white shadow-sm"
|
||||||
|
minHeight="120px"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Period (Optional) */}
|
{/* Period (Optional) */}
|
||||||
@ -853,7 +857,10 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
<div>
|
<div>
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Brief Requirement</Label>
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Brief Requirement</Label>
|
||||||
<div className="mt-2 p-4 bg-gray-50 rounded-lg border">
|
<div className="mt-2 p-4 bg-gray-50 rounded-lg border">
|
||||||
<p className="text-gray-900 whitespace-pre-wrap">{formData.requestDescription}</p>
|
<FormattedDescription
|
||||||
|
content={formData.requestDescription || ''}
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -64,7 +64,11 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
// Load existing IO block details from apiRequest
|
// Load existing IO block details from apiRequest
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (internalOrder && existingIONumber) {
|
if (internalOrder && existingIONumber) {
|
||||||
const availableBeforeBlock = Number(existingAvailableBalance) + Number(existingBlockedAmount) || Number(existingAvailableBalance);
|
// IMPORTANT: ioAvailableBalance is already the available balance BEFORE blocking
|
||||||
|
// We should NOT add blockedAmount to it - that would cause double deduction
|
||||||
|
// Backend stores: availableBalance (before block), blockedAmount, remainingBalance (after block)
|
||||||
|
const availableBeforeBlock = Number(existingAvailableBalance) || 0;
|
||||||
|
|
||||||
// Get blocked by user name from organizer association (who blocked the amount)
|
// Get blocked by user name from organizer association (who blocked the amount)
|
||||||
// When amount is blocked, organizedBy stores the user who blocked it
|
// When amount is blocked, organizedBy stores the user who blocked it
|
||||||
const blockedByName = organizer?.displayName ||
|
const blockedByName = organizer?.displayName ||
|
||||||
@ -84,6 +88,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
const backendRemaining = Number(existingRemainingBalance) || 0;
|
const backendRemaining = Number(existingRemainingBalance) || 0;
|
||||||
|
|
||||||
// Calculate expected remaining balance for validation/debugging
|
// Calculate expected remaining balance for validation/debugging
|
||||||
|
// Formula: remaining = availableBeforeBlock - blockedAmount
|
||||||
const expectedRemaining = availableBeforeBlock - blockedAmt;
|
const expectedRemaining = availableBeforeBlock - blockedAmt;
|
||||||
|
|
||||||
// Log for debugging backend calculation
|
// Log for debugging backend calculation
|
||||||
|
|||||||
@ -10,9 +10,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon } from 'lucide-react';
|
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle } from 'lucide-react';
|
||||||
import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
|
import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
|
||||||
import { formatHoursMinutes } from '@/utils/slaTracker';
|
import { formatHoursMinutes } from '@/utils/slaTracker';
|
||||||
|
import { AdditionalApproverReviewModal } from './modals';
|
||||||
import { DealerProposalSubmissionModal } from './modals';
|
import { DealerProposalSubmissionModal } from './modals';
|
||||||
import { InitiatorProposalApprovalModal } from './modals';
|
import { InitiatorProposalApprovalModal } from './modals';
|
||||||
import { DeptLeadIOApprovalModal } from './modals';
|
import { DeptLeadIOApprovalModal } from './modals';
|
||||||
@ -166,6 +167,8 @@ export function DealerClaimWorkflowTab({
|
|||||||
const [showCreditNoteModal, setShowCreditNoteModal] = useState(false);
|
const [showCreditNoteModal, setShowCreditNoteModal] = useState(false);
|
||||||
const [showEmailTemplateModal, setShowEmailTemplateModal] = useState(false);
|
const [showEmailTemplateModal, setShowEmailTemplateModal] = useState(false);
|
||||||
const [selectedStepForEmail, setSelectedStepForEmail] = useState<{ stepNumber: number; stepName: string } | null>(null);
|
const [selectedStepForEmail, setSelectedStepForEmail] = useState<{ stepNumber: number; stepName: string } | null>(null);
|
||||||
|
const [showAdditionalApproverReviewModal, setShowAdditionalApproverReviewModal] = useState(false);
|
||||||
|
const [selectedLevelForReview, setSelectedLevelForReview] = useState<{ levelId: string; levelName: string; approverName: string } | null>(null);
|
||||||
|
|
||||||
// Load approval flows from real API
|
// Load approval flows from real API
|
||||||
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
||||||
@ -1501,6 +1504,127 @@ export function DealerClaimWorkflowTab({
|
|||||||
View & Send Credit Note
|
View & Send Credit Note
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Additional Approvers: Show approve/reject buttons for steps that don't have specific workflow actions */}
|
||||||
|
{(() => {
|
||||||
|
// Check if this is an additional approver step (not one of the fixed workflow steps)
|
||||||
|
const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step);
|
||||||
|
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
||||||
|
const isAdditionalApprover = levelName.includes('additional approver');
|
||||||
|
|
||||||
|
// Check if this step doesn't have any of the specific workflow action buttons above
|
||||||
|
const hasSpecificWorkflowAction =
|
||||||
|
step.step === 1 ||
|
||||||
|
step.step === initiatorStepNumber ||
|
||||||
|
(() => {
|
||||||
|
const deptLeadStepLevel = approvalFlow.find((l: any) => {
|
||||||
|
const ln = (l.levelName || '').toLowerCase();
|
||||||
|
return ln.includes('department lead');
|
||||||
|
});
|
||||||
|
return deptLeadStepLevel &&
|
||||||
|
(step.step === (deptLeadStepLevel.step || deptLeadStepLevel.levelNumber || deptLeadStepLevel.level_number));
|
||||||
|
})() ||
|
||||||
|
(() => {
|
||||||
|
const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step);
|
||||||
|
const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase();
|
||||||
|
const isDealerForThisStep = isDealer && stepApproverEmail === dealerEmail;
|
||||||
|
const ln = (stepLevel?.levelName || step.title || '').toLowerCase();
|
||||||
|
const isDealerCompletionStep = ln.includes('dealer completion') || ln.includes('completion documents');
|
||||||
|
return isDealerForThisStep && isDealerCompletionStep;
|
||||||
|
})() ||
|
||||||
|
(() => {
|
||||||
|
const requestorClaimStepLevel = approvalFlow.find((l: any) => {
|
||||||
|
const ln = (l.levelName || '').toLowerCase();
|
||||||
|
return ln.includes('requestor claim') || ln.includes('requestor - claim');
|
||||||
|
});
|
||||||
|
return requestorClaimStepLevel &&
|
||||||
|
(step.step === (requestorClaimStepLevel.step || requestorClaimStepLevel.levelNumber || requestorClaimStepLevel.level_number));
|
||||||
|
})() ||
|
||||||
|
step.step === 8;
|
||||||
|
|
||||||
|
// Show "Review Request" button for additional approvers or steps without specific workflow actions
|
||||||
|
// Similar to the requestor approval step
|
||||||
|
if (isAdditionalApprover || !hasSpecificWorkflowAction) {
|
||||||
|
const levelId = stepLevel?.levelId || stepLevel?.level_id;
|
||||||
|
|
||||||
|
// Show review modal with both approve and reject options
|
||||||
|
if (levelId) {
|
||||||
|
const levelName = stepLevel?.levelName || stepLevel?.level_name || step.title || 'Approval Level';
|
||||||
|
const approverName = stepLevel?.approverName || stepLevel?.approver_name || step.approver || 'Approver';
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedLevelForReview({ levelId, levelName, approverName });
|
||||||
|
setShowAdditionalApproverReviewModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
Review Request
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Direct API call if levelId not available (shouldn't happen in normal flow)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (!request?.id && !request?.requestId) {
|
||||||
|
throw new Error('Request ID not found');
|
||||||
|
}
|
||||||
|
const requestId = request.id || request.requestId;
|
||||||
|
if (!levelId) {
|
||||||
|
toast.error('Approval level not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await approveLevel(requestId, levelId, '');
|
||||||
|
toast.success('Request approved successfully');
|
||||||
|
handleRefresh();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to approve:', error);
|
||||||
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to approve request. Please try again.';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (!request?.id && !request?.requestId) {
|
||||||
|
throw new Error('Request ID not found');
|
||||||
|
}
|
||||||
|
const requestId = request.id || request.requestId;
|
||||||
|
if (!levelId) {
|
||||||
|
toast.error('Approval level not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const comments = prompt('Please provide a reason for rejection:');
|
||||||
|
if (comments === null) return; // User cancelled
|
||||||
|
await rejectLevel(requestId, levelId, 'Request rejected', comments);
|
||||||
|
toast.success('Request rejected successfully');
|
||||||
|
handleRefresh();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to reject:', error);
|
||||||
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to reject request. Please try again.';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XCircle className="w-4 h-4 mr-2" />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -1667,6 +1791,68 @@ export function DealerClaimWorkflowTab({
|
|||||||
requestNumber={request?.requestNumber || request?.id || request?.request_number}
|
requestNumber={request?.requestNumber || request?.id || request?.request_number}
|
||||||
recipientEmail="system@royalenfield.com"
|
recipientEmail="system@royalenfield.com"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Additional Approver Review Modal */}
|
||||||
|
{selectedLevelForReview && (
|
||||||
|
<AdditionalApproverReviewModal
|
||||||
|
isOpen={showAdditionalApproverReviewModal}
|
||||||
|
onClose={() => {
|
||||||
|
setShowAdditionalApproverReviewModal(false);
|
||||||
|
setSelectedLevelForReview(null);
|
||||||
|
}}
|
||||||
|
onApprove={async (comments: string) => {
|
||||||
|
try {
|
||||||
|
if (!request?.id && !request?.requestId) {
|
||||||
|
throw new Error('Request ID not found');
|
||||||
|
}
|
||||||
|
const requestId = request.id || request.requestId;
|
||||||
|
const levelId = selectedLevelForReview.levelId;
|
||||||
|
if (!levelId) {
|
||||||
|
toast.error('Approval level not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await approveLevel(requestId, levelId, comments);
|
||||||
|
toast.success('Request approved successfully');
|
||||||
|
handleRefresh();
|
||||||
|
setShowAdditionalApproverReviewModal(false);
|
||||||
|
setSelectedLevelForReview(null);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to approve:', error);
|
||||||
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to approve request. Please try again.';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={async (comments: string) => {
|
||||||
|
try {
|
||||||
|
if (!request?.id && !request?.requestId) {
|
||||||
|
throw new Error('Request ID not found');
|
||||||
|
}
|
||||||
|
const requestId = request.id || request.requestId;
|
||||||
|
const levelId = selectedLevelForReview.levelId;
|
||||||
|
if (!levelId) {
|
||||||
|
toast.error('Approval level not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await rejectLevel(requestId, levelId, 'Request rejected', comments);
|
||||||
|
toast.success('Request rejected successfully');
|
||||||
|
handleRefresh();
|
||||||
|
setShowAdditionalApproverReviewModal(false);
|
||||||
|
setSelectedLevelForReview(null);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to reject:', error);
|
||||||
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to reject request. Please try again.';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
requestTitle={request?.title || 'Request'}
|
||||||
|
requestDescription={request?.description || ''}
|
||||||
|
requestId={request?.id || request?.requestId}
|
||||||
|
levelName={selectedLevelForReview.levelName}
|
||||||
|
approverName={selectedLevelForReview.approverName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { Calendar, MapPin, DollarSign, Receipt } from 'lucide-react';
|
|||||||
import { ClaimActivityInfo } from '@/pages/RequestDetail/types/claimManagement.types';
|
import { ClaimActivityInfo } from '@/pages/RequestDetail/types/claimManagement.types';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { formatDateTime } from '@/utils/dateFormatter';
|
import { formatDateTime } from '@/utils/dateFormatter';
|
||||||
|
import { FormattedDescription } from '@/components/common/FormattedDescription';
|
||||||
|
|
||||||
interface ActivityInformationCardProps {
|
interface ActivityInformationCardProps {
|
||||||
activityInfo: ClaimActivityInfo;
|
activityInfo: ClaimActivityInfo;
|
||||||
@ -173,9 +174,12 @@ export function ActivityInformationCard({
|
|||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||||
Description
|
Description
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-700 mt-2 bg-gray-50 p-3 rounded-lg whitespace-pre-line">
|
<div className="mt-2 bg-gray-50 p-3 rounded-lg border border-gray-200">
|
||||||
{activityInfo.description}
|
<FormattedDescription
|
||||||
</p>
|
content={activityInfo.description || ''}
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,239 @@
|
|||||||
|
/**
|
||||||
|
* AdditionalApproverReviewModal Component
|
||||||
|
* Modal for Additional Approvers to review request and approve/reject
|
||||||
|
* Similar to InitiatorProposalApprovalModal but simpler - shows request details
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
FileText,
|
||||||
|
User,
|
||||||
|
Calendar,
|
||||||
|
MessageSquare,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { FormattedDescription } from '@/components/common/FormattedDescription';
|
||||||
|
|
||||||
|
interface AdditionalApproverReviewModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onApprove: (comments: string) => Promise<void>;
|
||||||
|
onReject: (comments: string) => Promise<void>;
|
||||||
|
requestTitle?: string;
|
||||||
|
requestDescription?: string;
|
||||||
|
requestId?: string;
|
||||||
|
levelName?: string;
|
||||||
|
approverName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdditionalApproverReviewModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
requestTitle = 'Request',
|
||||||
|
requestDescription = '',
|
||||||
|
requestId,
|
||||||
|
levelName = 'Approval Level',
|
||||||
|
approverName = 'Approver',
|
||||||
|
}: AdditionalApproverReviewModalProps) {
|
||||||
|
const [comments, setComments] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [actionType, setActionType] = useState<'approve' | 'reject' | null>(null);
|
||||||
|
|
||||||
|
const handleApprove = async () => {
|
||||||
|
if (!comments.trim()) {
|
||||||
|
toast.error('Please provide approval comments');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
setActionType('approve');
|
||||||
|
await onApprove(comments);
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to approve request:', error);
|
||||||
|
toast.error('Failed to approve request. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
setActionType(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = async () => {
|
||||||
|
if (!comments.trim()) {
|
||||||
|
toast.error('Please provide rejection reason');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
setActionType('reject');
|
||||||
|
await onReject(comments);
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to reject request:', error);
|
||||||
|
toast.error('Failed to reject request. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
setActionType(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setComments('');
|
||||||
|
setActionType(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!submitting) {
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="dealer-proposal-modal overflow-hidden flex flex-col max-w-3xl">
|
||||||
|
<DialogHeader className="flex-shrink-0 pb-3 lg:pb-4 px-6 pt-4 lg:pt-6 border-b">
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-lg lg:text-xl">
|
||||||
|
<CheckCircle className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
|
||||||
|
Review Request
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-xs lg:text-sm">
|
||||||
|
{levelName}: Review request details and make a decision
|
||||||
|
</DialogDescription>
|
||||||
|
<div className="space-y-1 mt-2 text-xs text-gray-600">
|
||||||
|
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
||||||
|
<div>
|
||||||
|
<strong>Request ID:</strong> {requestId || 'N/A'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Approver:</strong> {approverName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto overflow-x-hidden py-3 lg:py-4 px-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Request Title */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4 text-blue-600" />
|
||||||
|
Request Title
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50">
|
||||||
|
<p className="text-sm lg:text-base font-medium text-gray-900">{requestTitle}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Request Description */}
|
||||||
|
{requestDescription && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
|
<MessageSquare className="w-4 h-4 text-blue-600" />
|
||||||
|
Request Description
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 max-h-[200px] overflow-y-auto">
|
||||||
|
<FormattedDescription
|
||||||
|
content={requestDescription}
|
||||||
|
className="text-xs lg:text-sm text-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Decision Section */}
|
||||||
|
<div className="space-y-2 border-t pt-3 lg:pt-3">
|
||||||
|
<h3 className="font-semibold text-sm lg:text-base">Your Decision & Comments</h3>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Provide your evaluation comments, approval conditions, or rejection reasons..."
|
||||||
|
value={comments}
|
||||||
|
onChange={(e) => setComments(e.target.value)}
|
||||||
|
className="min-h-[80px] lg:min-h-[90px] text-xs lg:text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">{comments.length} characters</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warning for missing comments */}
|
||||||
|
{!comments.trim() && (
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-2 flex items-start gap-2">
|
||||||
|
<XCircle className="w-3.5 h-3.5 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-xs text-amber-800">
|
||||||
|
Please provide comments before making a decision. Comments are required and will be visible to all participants.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end px-6 pb-6 pt-3 lg:pt-4 flex-shrink-0 border-t bg-gray-50">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={submitting}
|
||||||
|
className="border-2"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleReject}
|
||||||
|
disabled={!comments.trim() || submitting}
|
||||||
|
variant="destructive"
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
{submitting && actionType === 'reject' ? (
|
||||||
|
'Rejecting...'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<XCircle className="w-4 h-4 mr-2" />
|
||||||
|
Reject
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleApprove}
|
||||||
|
disabled={!comments.trim() || submitting}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white"
|
||||||
|
>
|
||||||
|
{submitting && actionType === 'approve' ? (
|
||||||
|
'Approving...'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
Approve
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Upload, Plus, X, Calendar, DollarSign, CircleAlert, CheckCircle2, FileText, Eye, Download } from 'lucide-react';
|
import { Upload, Plus, X, Calendar, IndianRupee, CircleAlert, CheckCircle2, FileText, Eye, Download } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import '@/components/common/FilePreview/FilePreview.css';
|
import '@/components/common/FilePreview/FilePreview.css';
|
||||||
import './DealerProposalModal.css';
|
import './DealerProposalModal.css';
|
||||||
@ -272,9 +272,11 @@ export function DealerProposalSubmissionModal({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden py-3 lg:py-4">
|
<div className="flex-1 overflow-y-auto overflow-x-hidden py-3 lg:py-4">
|
||||||
<div className="space-y-4 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-6">
|
<div className="space-y-4 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-6 lg:items-start lg:content-start">
|
||||||
|
{/* Left Column - Documents */}
|
||||||
|
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
||||||
{/* Proposal Document Section */}
|
{/* Proposal Document Section */}
|
||||||
<div className="space-y-3 lg:space-y-2">
|
<div className="space-y-2 lg:space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-base lg:text-lg">Proposal Document</h3>
|
<h3 className="font-semibold text-base lg:text-lg">Proposal Document</h3>
|
||||||
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
||||||
@ -354,147 +356,8 @@ export function DealerProposalSubmissionModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cost Breakup Section */}
|
|
||||||
<div className="space-y-3 lg:space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-base lg:text-lg">Cost Breakup</h3>
|
|
||||||
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleAddCostItem}
|
|
||||||
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<Plus className="w-3 h-3 lg:w-4 lg:h-4 mr-1" />
|
|
||||||
<span className="hidden sm:inline">Add Item</span>
|
|
||||||
<span className="sm:hidden">Add</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 lg:space-y-2 max-h-[200px] lg:max-h-[180px] overflow-y-auto">
|
|
||||||
{costItems.map((item) => (
|
|
||||||
<div key={item.id} className="flex gap-2 items-start">
|
|
||||||
<div className="flex-1">
|
|
||||||
<Input
|
|
||||||
placeholder="Item description (e.g., Banner printing, Event setup)"
|
|
||||||
value={item.description}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleCostItemChange(item.id, 'description', e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-40">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="Amount"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
value={item.amount || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleCostItemChange(item.id, 'amount', e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="mt-0.5 hover:bg-red-100 hover:text-red-700"
|
|
||||||
onClick={() => handleRemoveCostItem(item.id)}
|
|
||||||
disabled={costItems.length === 1}
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="border-2 border-gray-300 rounded-lg p-3 lg:p-4 bg-white">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<DollarSign className="w-4 h-4 lg:w-5 lg:h-5 text-gray-700" />
|
|
||||||
<span className="font-semibold text-sm lg:text-base text-gray-900">Estimated Budget</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xl lg:text-2xl font-bold text-gray-900">
|
|
||||||
₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline for Closure Section */}
|
|
||||||
<div className="space-y-3 lg:space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-base lg:text-lg">Timeline for Closure</h3>
|
|
||||||
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 lg:space-y-2">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setTimelineMode('date')}
|
|
||||||
className={
|
|
||||||
timelineMode === 'date'
|
|
||||||
? 'bg-[#2d4a3e] hover:bg-[#1f3329] text-white'
|
|
||||||
: 'border-2 hover:bg-gray-50'
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<Calendar className="w-4 h-4 mr-1" />
|
|
||||||
Specific Date
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setTimelineMode('days')}
|
|
||||||
className={
|
|
||||||
timelineMode === 'days'
|
|
||||||
? 'bg-[#2d4a3e] hover:bg-[#1f3329] text-white'
|
|
||||||
: 'border-2 hover:bg-gray-50'
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Number of Days
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{timelineMode === 'date' ? (
|
|
||||||
<div className="w-full">
|
|
||||||
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
|
|
||||||
Expected Completion Date
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
min={minDate}
|
|
||||||
value={expectedCompletionDate}
|
|
||||||
onChange={(e) => setExpectedCompletionDate(e.target.value)}
|
|
||||||
onClick={(e) => {
|
|
||||||
// Open calendar picker when clicking anywhere on the input
|
|
||||||
if (e.currentTarget.showPicker) {
|
|
||||||
e.currentTarget.showPicker();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="h-9 lg:h-10 w-full max-w-[280px] text-left pr-10 cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="w-full">
|
|
||||||
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
|
|
||||||
Number of Days
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="Enter number of days"
|
|
||||||
min="1"
|
|
||||||
value={numberOfDays}
|
|
||||||
onChange={(e) => setNumberOfDays(e.target.value)}
|
|
||||||
className="h-9 lg:h-10 w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Other Supporting Documents Section */}
|
{/* Other Supporting Documents Section */}
|
||||||
<div className="space-y-3 lg:space-y-2">
|
<div className="space-y-2 lg:space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-base lg:text-lg">Other Supporting Documents</h3>
|
<h3 className="font-semibold text-base lg:text-lg">Other Supporting Documents</h3>
|
||||||
<Badge variant="outline" className="text-xs border-gray-300 text-gray-600 bg-gray-50 font-medium">Optional</Badge>
|
<Badge variant="outline" className="text-xs border-gray-300 text-gray-600 bg-gray-50 font-medium">Optional</Badge>
|
||||||
@ -603,6 +466,150 @@ export function DealerProposalSubmissionModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column - Planning & Budget */}
|
||||||
|
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
||||||
|
{/* Cost Breakup Section */}
|
||||||
|
<div className="space-y-2 lg:space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-base lg:text-lg">Cost Breakup</h3>
|
||||||
|
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddCostItem}
|
||||||
|
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3 lg:w-4 lg:h-4 mr-1" />
|
||||||
|
<span className="hidden sm:inline">Add Item</span>
|
||||||
|
<span className="sm:hidden">Add</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 lg:space-y-2 max-h-[200px] lg:max-h-[180px] overflow-y-auto">
|
||||||
|
{costItems.map((item) => (
|
||||||
|
<div key={item.id} className="flex gap-2 items-start w-full">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<Input
|
||||||
|
placeholder="Item description (e.g., Banner printing, Event setup)"
|
||||||
|
value={item.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleCostItemChange(item.id, 'description', e.target.value)
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32 lg:w-36 flex-shrink-0">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Amount"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={item.amount || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleCostItemChange(item.id, 'amount', e.target.value)
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="mt-0.5 hover:bg-red-100 hover:text-red-700 flex-shrink-0"
|
||||||
|
onClick={() => handleRemoveCostItem(item.id)}
|
||||||
|
disabled={costItems.length === 1}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="border-2 border-gray-300 rounded-lg p-3 lg:p-4 bg-white">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IndianRupee className="w-4 h-4 lg:w-5 lg:h-5 text-gray-700" />
|
||||||
|
<span className="font-semibold text-sm lg:text-base text-gray-900">Estimated Budget</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xl lg:text-2xl font-bold text-gray-900">
|
||||||
|
₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline for Closure Section */}
|
||||||
|
<div className="space-y-2 lg:space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-base lg:text-lg">Timeline for Closure</h3>
|
||||||
|
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTimelineMode('date')}
|
||||||
|
className={
|
||||||
|
timelineMode === 'date'
|
||||||
|
? 'bg-[#2d4a3e] hover:bg-[#1f3329] text-white'
|
||||||
|
: 'border-2 hover:bg-gray-50'
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Calendar className="w-4 h-4 mr-1" />
|
||||||
|
Specific Date
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTimelineMode('days')}
|
||||||
|
className={
|
||||||
|
timelineMode === 'days'
|
||||||
|
? 'bg-[#2d4a3e] hover:bg-[#1f3329] text-white'
|
||||||
|
: 'border-2 hover:bg-gray-50'
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Number of Days
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{timelineMode === 'date' ? (
|
||||||
|
<div className="w-full">
|
||||||
|
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
|
||||||
|
Expected Completion Date
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
min={minDate}
|
||||||
|
value={expectedCompletionDate}
|
||||||
|
onChange={(e) => setExpectedCompletionDate(e.target.value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
// Open calendar picker when clicking anywhere on the input
|
||||||
|
if (e.currentTarget.showPicker) {
|
||||||
|
e.currentTarget.showPicker();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-9 lg:h-10 w-full text-left pr-10 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-full">
|
||||||
|
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
|
||||||
|
Number of Days
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter number of days"
|
||||||
|
min="1"
|
||||||
|
value={numberOfDays}
|
||||||
|
onChange={(e) => setNumberOfDays(e.target.value)}
|
||||||
|
className="h-9 lg:h-10 w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Dealer Comments Section */}
|
{/* Dealer Comments Section */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -614,10 +621,13 @@ export function DealerProposalSubmissionModal({
|
|||||||
placeholder="Provide detailed comments about this claim request, including any special considerations, execution details, or additional information..."
|
placeholder="Provide detailed comments about this claim request, including any special considerations, execution details, or additional information..."
|
||||||
value={dealerComments}
|
value={dealerComments}
|
||||||
onChange={(e) => setDealerComments(e.target.value)}
|
onChange={(e) => setDealerComments(e.target.value)}
|
||||||
className="min-h-[80px] lg:min-h-[100px] text-sm"
|
className="min-h-[80px] lg:min-h-[100px] text-sm w-full"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500">{dealerComments.length} characters</p>
|
<p className="text-xs text-gray-500">{dealerComments.length} characters</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Full Width Sections */}
|
||||||
|
|
||||||
{/* Warning Message */}
|
{/* Warning Message */}
|
||||||
{!isFormValid && (
|
{!isFormValid && (
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
FileText,
|
FileText,
|
||||||
DollarSign,
|
IndianRupee,
|
||||||
Calendar,
|
Calendar,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Download,
|
Download,
|
||||||
@ -290,9 +290,11 @@ export function InitiatorProposalApprovalModal({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden py-3 lg:py-4 px-6">
|
<div className="flex-1 overflow-y-auto overflow-x-hidden py-3 lg:py-4 px-6">
|
||||||
<div className="space-y-4 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-6">
|
<div className="space-y-4 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-6 lg:items-start lg:content-start">
|
||||||
|
{/* Left Column - Documents */}
|
||||||
|
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
||||||
{/* Proposal Document Section */}
|
{/* Proposal Document Section */}
|
||||||
<div className="space-y-2 lg:space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
<FileText className="w-4 h-4 text-blue-600" />
|
<FileText className="w-4 h-4 text-blue-600" />
|
||||||
@ -300,19 +302,21 @@ export function InitiatorProposalApprovalModal({
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{proposalData?.proposalDocument ? (
|
{proposalData?.proposalDocument ? (
|
||||||
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between">
|
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2 lg:gap-3">
|
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
||||||
<FileText className="w-5 h-5 lg:w-6 lg:h-6 text-blue-600" />
|
<FileText className="w-5 h-5 lg:w-6 lg:h-6 text-blue-600 flex-shrink-0" />
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<p className="font-medium text-xs lg:text-sm text-gray-900">{proposalData.proposalDocument.name}</p>
|
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={proposalData.proposalDocument.name}>
|
||||||
|
{proposalData.proposalDocument.name}
|
||||||
|
</p>
|
||||||
{proposalData?.submittedAt && (
|
{proposalData?.submittedAt && (
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500 truncate">
|
||||||
Submitted on {formatDate(proposalData.submittedAt)}
|
Submitted on {formatDate(proposalData.submittedAt)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{proposalData.proposalDocument.id && (
|
{proposalData.proposalDocument.id && (
|
||||||
<>
|
<>
|
||||||
{canPreviewDocument(proposalData.proposalDocument) && (
|
{canPreviewDocument(proposalData.proposalDocument) && (
|
||||||
@ -356,80 +360,9 @@ export function InitiatorProposalApprovalModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cost Breakup Section */}
|
|
||||||
<div className="space-y-2 lg:space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
|
||||||
<DollarSign className="w-4 h-4 text-green-600" />
|
|
||||||
Cost Breakup
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
{(() => {
|
|
||||||
// Ensure costBreakup is an array
|
|
||||||
const costBreakup = proposalData?.costBreakup
|
|
||||||
? (Array.isArray(proposalData.costBreakup)
|
|
||||||
? proposalData.costBreakup
|
|
||||||
: (typeof proposalData.costBreakup === 'string'
|
|
||||||
? JSON.parse(proposalData.costBreakup)
|
|
||||||
: []))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return costBreakup && Array.isArray(costBreakup) && costBreakup.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<div className="border rounded-lg overflow-hidden max-h-[200px] lg:max-h-[180px] overflow-y-auto">
|
|
||||||
<div className="bg-gray-50 px-3 lg:px-4 py-2 border-b sticky top-0">
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs lg:text-sm font-semibold text-gray-700">
|
|
||||||
<div>Item Description</div>
|
|
||||||
<div className="text-right">Amount</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y">
|
|
||||||
{costBreakup.map((item: any, index: number) => (
|
|
||||||
<div key={item?.id || item?.description || index} className="px-3 lg:px-4 py-2 lg:py-3 grid grid-cols-2 gap-4">
|
|
||||||
<div className="text-xs lg:text-sm text-gray-700">{item?.description || 'N/A'}</div>
|
|
||||||
<div className="text-xs lg:text-sm font-semibold text-gray-900 text-right">
|
|
||||||
₹{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="border-2 border-[--re-green] rounded-lg p-2.5 lg:p-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<DollarSign className="w-4 h-4 text-[--re-green]" />
|
|
||||||
<span className="font-semibold text-xs lg:text-sm text-gray-700">Total Estimated Budget</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-lg lg:text-xl font-bold text-[--re-green]">
|
|
||||||
₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-xs text-gray-500 italic">No cost breakdown available</p>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline Section */}
|
|
||||||
<div className="space-y-2 lg:space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
|
||||||
<Calendar className="w-4 h-4 text-purple-600" />
|
|
||||||
Expected Completion Date
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50">
|
|
||||||
<p className="text-sm lg:text-base font-semibold text-gray-900">
|
|
||||||
{proposalData?.expectedCompletionDate ? formatDate(proposalData.expectedCompletionDate) : 'Not specified'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Other Supporting Documents */}
|
{/* Other Supporting Documents */}
|
||||||
{proposalData?.otherDocuments && proposalData.otherDocuments.length > 0 && (
|
{proposalData?.otherDocuments && proposalData.otherDocuments.length > 0 && (
|
||||||
<div className="space-y-2 lg:space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
<FileText className="w-4 h-4 text-gray-600" />
|
<FileText className="w-4 h-4 text-gray-600" />
|
||||||
@ -443,14 +376,16 @@ export function InitiatorProposalApprovalModal({
|
|||||||
{proposalData.otherDocuments.map((doc, index) => (
|
{proposalData.otherDocuments.map((doc, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="border rounded-lg p-2 lg:p-3 bg-gray-50 flex items-center justify-between"
|
className="border rounded-lg p-2 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 lg:gap-3">
|
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
||||||
<FileText className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
|
<FileText className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600 flex-shrink-0" />
|
||||||
<p className="text-xs lg:text-sm font-medium text-gray-900">{doc.name}</p>
|
<p className="text-xs lg:text-sm font-medium text-gray-900 truncate" title={doc.name}>
|
||||||
|
{doc.name}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{doc.id && (
|
{doc.id && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
{canPreviewDocument(doc) && (
|
{canPreviewDocument(doc) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -490,32 +425,112 @@ export function InitiatorProposalApprovalModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Dealer Comments */}
|
{/* Right Column - Planning & Details */}
|
||||||
<div className="space-y-2 lg:space-y-2">
|
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
||||||
|
{/* Cost Breakup Section */}
|
||||||
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
<MessageSquare className="w-4 h-4 text-blue-600" />
|
<IndianRupee className="w-4 h-4 text-green-600" />
|
||||||
Dealer Comments
|
Cost Breakup
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 max-h-[150px] lg:max-h-[140px] overflow-y-auto">
|
{(() => {
|
||||||
<p className="text-xs text-gray-700 whitespace-pre-wrap">
|
// Ensure costBreakup is an array
|
||||||
{proposalData?.dealerComments || 'No comments provided'}
|
const costBreakup = proposalData?.costBreakup
|
||||||
|
? (Array.isArray(proposalData.costBreakup)
|
||||||
|
? proposalData.costBreakup
|
||||||
|
: (typeof proposalData.costBreakup === 'string'
|
||||||
|
? JSON.parse(proposalData.costBreakup)
|
||||||
|
: []))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return costBreakup && Array.isArray(costBreakup) && costBreakup.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="border rounded-lg overflow-hidden max-h-[200px] lg:max-h-[180px] overflow-y-auto">
|
||||||
|
<div className="bg-gray-50 px-3 lg:px-4 py-2 border-b sticky top-0">
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-xs lg:text-sm font-semibold text-gray-700">
|
||||||
|
<div>Item Description</div>
|
||||||
|
<div className="text-right">Amount</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y">
|
||||||
|
{costBreakup.map((item: any, index: number) => (
|
||||||
|
<div key={item?.id || item?.description || index} className="px-3 lg:px-4 py-2 lg:py-3 grid grid-cols-2 gap-4">
|
||||||
|
<div className="text-xs lg:text-sm text-gray-700">{item?.description || 'N/A'}</div>
|
||||||
|
<div className="text-xs lg:text-sm font-semibold text-gray-900 text-right">
|
||||||
|
₹{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-2 border-[--re-green] rounded-lg p-2.5 lg:p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IndianRupee className="w-4 h-4 text-[--re-green]" />
|
||||||
|
<span className="font-semibold text-xs lg:text-sm text-gray-700">Total Estimated Budget</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-lg lg:text-xl font-bold text-[--re-green]">
|
||||||
|
₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-gray-500 italic">No cost breakdown available</p>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline Section */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
|
<Calendar className="w-4 h-4 text-purple-600" />
|
||||||
|
Expected Completion Date
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50">
|
||||||
|
<p className="text-sm lg:text-base font-semibold text-gray-900">
|
||||||
|
{proposalData?.expectedCompletionDate ? formatDate(proposalData.expectedCompletionDate) : 'Not specified'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Decision Section */}
|
{/* Comments Section - Side by Side */}
|
||||||
<div className="space-y-2 lg:space-y-2 border-t pt-3 lg:pt-3 lg:col-span-2">
|
<div className="space-y-2 border-t pt-3 lg:pt-3 lg:col-span-2 mt-2">
|
||||||
<h3 className="font-semibold text-sm lg:text-base">Your Decision & Comments</h3>
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
|
||||||
<Textarea
|
{/* Dealer Comments */}
|
||||||
placeholder="Provide your evaluation comments, approval conditions, or rejection reasons..."
|
<div className="space-y-2">
|
||||||
value={comments}
|
<div className="flex items-center gap-2">
|
||||||
onChange={(e) => setComments(e.target.value)}
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
className="min-h-[80px] lg:min-h-[90px] text-xs lg:text-sm"
|
<MessageSquare className="w-4 h-4 text-blue-600" />
|
||||||
/>
|
Dealer Comments
|
||||||
<p className="text-xs text-gray-500">{comments.length} characters</p>
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 max-h-[150px] lg:max-h-[140px] overflow-y-auto">
|
||||||
|
<p className="text-xs text-gray-700 whitespace-pre-wrap">
|
||||||
|
{proposalData?.dealerComments || 'No comments provided'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Your Decision & Comments */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-semibold text-sm lg:text-base">Your Decision & Comments</h3>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Provide your evaluation comments, approval conditions, or rejection reasons..."
|
||||||
|
value={comments}
|
||||||
|
onChange={(e) => setComments(e.target.value)}
|
||||||
|
className="min-h-[150px] lg:min-h-[140px] text-xs lg:text-sm w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">{comments.length} characters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Warning for missing comments */}
|
{/* Warning for missing comments */}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
* Located in: src/dealer-claim/components/request-detail/modals/
|
* Located in: src/dealer-claim/components/request-detail/modals/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { AdditionalApproverReviewModal } from './AdditionalApproverReviewModal';
|
||||||
export { CreditNoteSAPModal } from './CreditNoteSAPModal';
|
export { CreditNoteSAPModal } from './CreditNoteSAPModal';
|
||||||
export { DealerCompletionDocumentsModal } from './DealerCompletionDocumentsModal';
|
export { DealerCompletionDocumentsModal } from './DealerCompletionDocumentsModal';
|
||||||
export { DealerProposalSubmissionModal } from './DealerProposalSubmissionModal';
|
export { DealerProposalSubmissionModal } from './DealerProposalSubmissionModal';
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import { useModalManager } from '@/hooks/useModalManager';
|
|||||||
import { useConclusionRemark } from '@/hooks/useConclusionRemark';
|
import { useConclusionRemark } from '@/hooks/useConclusionRemark';
|
||||||
import { downloadDocument } from '@/services/workflowApi';
|
import { downloadDocument } from '@/services/workflowApi';
|
||||||
import { getSocket, joinUserRoom } from '@/utils/socket';
|
import { getSocket, joinUserRoom } from '@/utils/socket';
|
||||||
|
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||||
|
|
||||||
// Dealer Claim Components (import from index to get properly aliased exports)
|
// Dealer Claim Components (import from index to get properly aliased exports)
|
||||||
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
|
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
|
||||||
@ -178,6 +179,12 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
setDocumentError,
|
setDocumentError,
|
||||||
} = useDocumentUpload(apiRequest, refreshDetails);
|
} = useDocumentUpload(apiRequest, refreshDetails);
|
||||||
|
|
||||||
|
// State to temporarily store approval level for modal (used for additional approvers)
|
||||||
|
const [temporaryApprovalLevel, setTemporaryApprovalLevel] = useState<any>(null);
|
||||||
|
|
||||||
|
// Use temporary level if set, otherwise use currentApprovalLevel
|
||||||
|
const effectiveApprovalLevel = temporaryApprovalLevel || currentApprovalLevel;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showApproveModal,
|
showApproveModal,
|
||||||
setShowApproveModal,
|
setShowApproveModal,
|
||||||
@ -195,12 +202,23 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
setSkipApproverData,
|
setSkipApproverData,
|
||||||
actionStatus,
|
actionStatus,
|
||||||
setActionStatus,
|
setActionStatus,
|
||||||
handleApproveConfirm,
|
handleApproveConfirm: originalHandleApproveConfirm,
|
||||||
handleRejectConfirm,
|
handleRejectConfirm: originalHandleRejectConfirm,
|
||||||
handleAddApprover,
|
handleAddApprover,
|
||||||
handleSkipApprover,
|
handleSkipApprover,
|
||||||
handleAddSpectator,
|
handleAddSpectator,
|
||||||
} = useModalManager(requestIdentifier, currentApprovalLevel, refreshDetails);
|
} = useModalManager(requestIdentifier, effectiveApprovalLevel, refreshDetails);
|
||||||
|
|
||||||
|
// Wrapper handlers that clear temporary level after action
|
||||||
|
const handleApproveConfirm = async (description: string) => {
|
||||||
|
await originalHandleApproveConfirm(description);
|
||||||
|
setTemporaryApprovalLevel(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRejectConfirm = async (description: string) => {
|
||||||
|
await originalHandleRejectConfirm(description);
|
||||||
|
setTemporaryApprovalLevel(null);
|
||||||
|
};
|
||||||
|
|
||||||
// Closure functionality - only for initiator when request is approved/rejected
|
// Closure functionality - only for initiator when request is approved/rejected
|
||||||
// Check both lowercase and uppercase status values
|
// Check both lowercase and uppercase status values
|
||||||
@ -631,7 +649,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
request={request}
|
request={request}
|
||||||
isInitiator={isInitiator}
|
isInitiator={isInitiator}
|
||||||
isSpectator={isSpectator}
|
isSpectator={isSpectator}
|
||||||
currentApprovalLevel={currentApprovalLevel}
|
currentApprovalLevel={isClaimManagementRequest(apiRequest) ? null : currentApprovalLevel}
|
||||||
onAddApprover={() => setShowAddApproverModal(true)}
|
onAddApprover={() => setShowAddApproverModal(true)}
|
||||||
onAddSpectator={() => setShowAddSpectatorModal(true)}
|
onAddSpectator={() => setShowAddSpectatorModal(true)}
|
||||||
onApprove={() => setShowApproveModal(true)}
|
onApprove={() => setShowApproveModal(true)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user