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 { Input } from '@/components/ui/input';
|
||||
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 { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
@ -548,16 +549,19 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
{/* Request Detail */}
|
||||
<div>
|
||||
<Label htmlFor="requestDescription" className="text-base font-semibold">Request in Detail - Brief Requirement *</Label>
|
||||
<Textarea
|
||||
id="requestDescription"
|
||||
placeholder="Provide a detailed description of your claim requirement..."
|
||||
value={formData.requestDescription}
|
||||
onChange={(e) => updateFormData('requestDescription', e.target.value)}
|
||||
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 className="text-sm text-gray-600 mb-3">
|
||||
Explain what you need approval for, why it's needed, and any relevant background information.
|
||||
<span className="block mt-1 text-xs text-blue-600">
|
||||
💡 Tip: You can paste formatted content (lists, tables) and the formatting will be preserved.
|
||||
</span>
|
||||
</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>
|
||||
|
||||
{/* Period (Optional) */}
|
||||
@ -853,7 +857,10 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
<div>
|
||||
<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">
|
||||
<p className="text-gray-900 whitespace-pre-wrap">{formData.requestDescription}</p>
|
||||
<FormattedDescription
|
||||
content={formData.requestDescription || ''}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@ -64,7 +64,11 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
// Load existing IO block details from apiRequest
|
||||
useEffect(() => {
|
||||
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)
|
||||
// When amount is blocked, organizedBy stores the user who blocked it
|
||||
const blockedByName = organizer?.displayName ||
|
||||
@ -84,6 +88,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
const backendRemaining = Number(existingRemainingBalance) || 0;
|
||||
|
||||
// Calculate expected remaining balance for validation/debugging
|
||||
// Formula: remaining = availableBeforeBlock - blockedAmount
|
||||
const expectedRemaining = availableBeforeBlock - blockedAmt;
|
||||
|
||||
// Log for debugging backend calculation
|
||||
|
||||
@ -10,9 +10,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { formatHoursMinutes } from '@/utils/slaTracker';
|
||||
import { AdditionalApproverReviewModal } from './modals';
|
||||
import { DealerProposalSubmissionModal } from './modals';
|
||||
import { InitiatorProposalApprovalModal } from './modals';
|
||||
import { DeptLeadIOApprovalModal } from './modals';
|
||||
@ -156,7 +157,7 @@ export function DealerClaimWorkflowTab({
|
||||
user,
|
||||
isInitiator,
|
||||
onSkipApprover: _onSkipApprover,
|
||||
onRefresh
|
||||
onRefresh
|
||||
}: DealerClaimWorkflowTabProps) {
|
||||
const [showProposalModal, setShowProposalModal] = useState(false);
|
||||
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
||||
@ -166,6 +167,8 @@ export function DealerClaimWorkflowTab({
|
||||
const [showCreditNoteModal, setShowCreditNoteModal] = useState(false);
|
||||
const [showEmailTemplateModal, setShowEmailTemplateModal] = useState(false);
|
||||
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
|
||||
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
||||
@ -1501,6 +1504,127 @@ export function DealerClaimWorkflowTab({
|
||||
View & Send Credit Note
|
||||
</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>
|
||||
)}
|
||||
|
||||
@ -1667,6 +1791,68 @@ export function DealerClaimWorkflowTab({
|
||||
requestNumber={request?.requestNumber || request?.id || request?.request_number}
|
||||
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 { format } from 'date-fns';
|
||||
import { formatDateTime } from '@/utils/dateFormatter';
|
||||
import { FormattedDescription } from '@/components/common/FormattedDescription';
|
||||
|
||||
interface ActivityInformationCardProps {
|
||||
activityInfo: ClaimActivityInfo;
|
||||
@ -173,9 +174,12 @@ export function ActivityInformationCard({
|
||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||
Description
|
||||
</label>
|
||||
<p className="text-sm text-gray-700 mt-2 bg-gray-50 p-3 rounded-lg whitespace-pre-line">
|
||||
{activityInfo.description}
|
||||
</p>
|
||||
<div className="mt-2 bg-gray-50 p-3 rounded-lg border border-gray-200">
|
||||
<FormattedDescription
|
||||
content={activityInfo.description || ''}
|
||||
className="text-sm"
|
||||
/>
|
||||
</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 { Textarea } from '@/components/ui/textarea';
|
||||
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 '@/components/common/FilePreview/FilePreview.css';
|
||||
import './DealerProposalModal.css';
|
||||
@ -272,9 +272,11 @@ export function DealerProposalSubmissionModal({
|
||||
</DialogHeader>
|
||||
|
||||
<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 */}
|
||||
<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">
|
||||
<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>
|
||||
@ -354,147 +356,8 @@ export function DealerProposalSubmissionModal({
|
||||
</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 */}
|
||||
<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">
|
||||
<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>
|
||||
@ -603,6 +466,150 @@ export function DealerProposalSubmissionModal({
|
||||
)}
|
||||
</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 */}
|
||||
<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..."
|
||||
value={dealerComments}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Full Width Sections */}
|
||||
|
||||
{/* Warning Message */}
|
||||
{!isFormValid && (
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
FileText,
|
||||
DollarSign,
|
||||
IndianRupee,
|
||||
Calendar,
|
||||
MessageSquare,
|
||||
Download,
|
||||
@ -290,9 +290,11 @@ export function InitiatorProposalApprovalModal({
|
||||
</DialogHeader>
|
||||
|
||||
<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 */}
|
||||
<div className="space-y-2 lg:space-y-2">
|
||||
<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" />
|
||||
@ -300,19 +302,21 @@ export function InitiatorProposalApprovalModal({
|
||||
</h3>
|
||||
</div>
|
||||
{proposalData?.proposalDocument ? (
|
||||
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 lg:gap-3">
|
||||
<FileText className="w-5 h-5 lg:w-6 lg:h-6 text-blue-600" />
|
||||
<div>
|
||||
<p className="font-medium text-xs lg:text-sm text-gray-900">{proposalData.proposalDocument.name}</p>
|
||||
<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 min-w-0 flex-1">
|
||||
<FileText className="w-5 h-5 lg:w-6 lg:h-6 text-blue-600 flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={proposalData.proposalDocument.name}>
|
||||
{proposalData.proposalDocument.name}
|
||||
</p>
|
||||
{proposalData?.submittedAt && (
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-gray-500 truncate">
|
||||
Submitted on {formatDate(proposalData.submittedAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{proposalData.proposalDocument.id && (
|
||||
<>
|
||||
{canPreviewDocument(proposalData.proposalDocument) && (
|
||||
@ -356,80 +360,9 @@ export function InitiatorProposalApprovalModal({
|
||||
)}
|
||||
</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 */}
|
||||
{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">
|
||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-gray-600" />
|
||||
@ -443,14 +376,16 @@ export function InitiatorProposalApprovalModal({
|
||||
{proposalData.otherDocuments.map((doc, index) => (
|
||||
<div
|
||||
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">
|
||||
<FileText className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
|
||||
<p className="text-xs lg:text-sm font-medium text-gray-900">{doc.name}</p>
|
||||
<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 flex-shrink-0" />
|
||||
<p className="text-xs lg:text-sm font-medium text-gray-900 truncate" title={doc.name}>
|
||||
{doc.name}
|
||||
</p>
|
||||
</div>
|
||||
{doc.id && (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{canPreviewDocument(doc) && (
|
||||
<button
|
||||
type="button"
|
||||
@ -490,32 +425,112 @@ export function InitiatorProposalApprovalModal({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dealer Comments */}
|
||||
<div className="space-y-2 lg:space-y-2">
|
||||
{/* Right Column - Planning & Details */}
|
||||
<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">
|
||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4 text-blue-600" />
|
||||
Dealer Comments
|
||||
<IndianRupee className="w-4 h-4 text-green-600" />
|
||||
Cost Breakup
|
||||
</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'}
|
||||
{(() => {
|
||||
// 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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decision Section */}
|
||||
<div className="space-y-2 lg:space-y-2 border-t pt-3 lg:pt-3 lg:col-span-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-[80px] lg:min-h-[90px] text-xs lg:text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">{comments.length} characters</p>
|
||||
{/* Comments Section - Side by Side */}
|
||||
<div className="space-y-2 border-t pt-3 lg:pt-3 lg:col-span-2 mt-2">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
|
||||
{/* Dealer Comments */}
|
||||
<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" />
|
||||
Dealer Comments
|
||||
</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>
|
||||
|
||||
{/* Warning for missing comments */}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
* Located in: src/dealer-claim/components/request-detail/modals/
|
||||
*/
|
||||
|
||||
export { AdditionalApproverReviewModal } from './AdditionalApproverReviewModal';
|
||||
export { CreditNoteSAPModal } from './CreditNoteSAPModal';
|
||||
export { DealerCompletionDocumentsModal } from './DealerCompletionDocumentsModal';
|
||||
export { DealerProposalSubmissionModal } from './DealerProposalSubmissionModal';
|
||||
|
||||
@ -39,6 +39,7 @@ import { useModalManager } from '@/hooks/useModalManager';
|
||||
import { useConclusionRemark } from '@/hooks/useConclusionRemark';
|
||||
import { downloadDocument } from '@/services/workflowApi';
|
||||
import { getSocket, joinUserRoom } from '@/utils/socket';
|
||||
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||
|
||||
// Dealer Claim Components (import from index to get properly aliased exports)
|
||||
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
|
||||
@ -178,6 +179,12 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
||||
setDocumentError,
|
||||
} = 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 {
|
||||
showApproveModal,
|
||||
setShowApproveModal,
|
||||
@ -195,12 +202,23 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
||||
setSkipApproverData,
|
||||
actionStatus,
|
||||
setActionStatus,
|
||||
handleApproveConfirm,
|
||||
handleRejectConfirm,
|
||||
handleApproveConfirm: originalHandleApproveConfirm,
|
||||
handleRejectConfirm: originalHandleRejectConfirm,
|
||||
handleAddApprover,
|
||||
handleSkipApprover,
|
||||
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
|
||||
// Check both lowercase and uppercase status values
|
||||
@ -631,7 +649,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
||||
request={request}
|
||||
isInitiator={isInitiator}
|
||||
isSpectator={isSpectator}
|
||||
currentApprovalLevel={currentApprovalLevel}
|
||||
currentApprovalLevel={isClaimManagementRequest(apiRequest) ? null : currentApprovalLevel}
|
||||
onAddApprover={() => setShowAddApproverModal(true)}
|
||||
onAddSpectator={() => setShowAddSpectatorModal(true)}
|
||||
onApprove={() => setShowApproveModal(true)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user