richtext added for dealer claim and in-step ,odal ui enhanced

This commit is contained in:
laxmanhalaki 2025-12-30 20:45:18 +05:30
parent 22cb42e06e
commit c6bd5a19ef
9 changed files with 754 additions and 269 deletions

View File

@ -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>

View File

@ -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

View File

@ -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';
@ -156,7 +157,7 @@ export function DealerClaimWorkflowTab({
user, user,
isInitiator, isInitiator,
onSkipApprover: _onSkipApprover, onSkipApprover: _onSkipApprover,
onRefresh onRefresh
}: DealerClaimWorkflowTabProps) { }: DealerClaimWorkflowTabProps) {
const [showProposalModal, setShowProposalModal] = useState(false); const [showProposalModal, setShowProposalModal] = useState(false);
const [showApprovalModal, setShowApprovalModal] = useState(false); const [showApprovalModal, setShowApprovalModal] = useState(false);
@ -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}
/>
)}
</> </>
); );
} }

View File

@ -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>
)} )}

View File

@ -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>
);
}

View File

@ -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 && (

View File

@ -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 */}

View File

@ -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';

View File

@ -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)}