delaer onboarding end to end fleo checked and also made some new changes in the progress trck now after the LOI approvl finance and securaty details kept separate for approval also LOI issue is differnt approval
This commit is contained in:
parent
3c54b8499e
commit
c37ca50d4c
@ -30,6 +30,7 @@ import { FnFDetails } from './components/applications/FnFDetails';
|
||||
import { FinanceOnboardingPage } from './components/applications/FinanceOnboardingPage';
|
||||
import { FinanceFnFPage } from './components/applications/FinanceFnFPage';
|
||||
import { FinancePaymentDetailsPage } from './components/applications/FinancePaymentDetailsPage';
|
||||
import { FinanceFddDetailPage } from './components/applications/FinanceFddDetailPage';
|
||||
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
|
||||
import { MasterPage } from './components/applications/MasterPage';
|
||||
import { UserManagementPage } from './components/admin/UserManagementPage';
|
||||
@ -205,7 +206,7 @@ export default function App() {
|
||||
{/* Dashboards */}
|
||||
<Route path="/dashboard" element={
|
||||
currentUser?.role === 'Finance Admin' || currentUser?.role === 'Finance' ?
|
||||
<FinanceDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> :
|
||||
<FinanceDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> :
|
||||
currentUser?.role === 'Dealer' ?
|
||||
<DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> :
|
||||
<Dashboard onNavigate={(path) => navigate(`/${path}`)} />
|
||||
@ -257,8 +258,9 @@ export default function App() {
|
||||
<Route path="/fnf" element={<FnFPage currentUser={currentUser} onViewDetails={(id) => navigate(`/fnf/${id}`)} />} />
|
||||
<Route path="/fnf/:id" element={<FnFDetails fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/fnf')} currentUser={currentUser} />} />
|
||||
|
||||
<Route path="/finance-onboarding" element={<FinanceOnboardingPage onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} />} />
|
||||
<Route path="/finance-onboarding" element={<FinanceOnboardingPage onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} />} />
|
||||
<Route path="/finance-onboarding/:id" element={<FinancePaymentDetailsPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />} />
|
||||
<Route path="/finance-audit/:id" element={<FinanceFddDetailPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />} />
|
||||
|
||||
<Route path="/finance-fnf" element={<FinanceFnFPage onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} />} />
|
||||
<Route path="/finance-fnf/:id" element={<FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />} />
|
||||
|
||||
@ -12,7 +12,6 @@ import {
|
||||
} from '../ui/select';
|
||||
import {
|
||||
Search,
|
||||
Filter,
|
||||
Download,
|
||||
Grid3x3,
|
||||
List,
|
||||
@ -35,6 +34,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
|
||||
import { Label } from '../ui/label';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { toast } from 'sonner';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
|
||||
interface AllApplicationsPageProps {
|
||||
onViewDetails: (id: string) => void;
|
||||
@ -130,14 +130,17 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
|
||||
'Questionnaire Pending': 'bg-yellow-100 text-yellow-800',
|
||||
'Questionnaire Completed': 'bg-cyan-100 text-cyan-800',
|
||||
'Shortlisted': 'bg-purple-100 text-purple-800',
|
||||
'Level 1 Pending': 'bg-orange-100 text-orange-800',
|
||||
'Level 1 Interview Pending': 'bg-orange-100 text-orange-800',
|
||||
'Level 1 Approved': 'bg-green-100 text-green-800',
|
||||
'Level 2 Pending': 'bg-orange-100 text-orange-800',
|
||||
'Level 2 Interview Pending': 'bg-orange-100 text-orange-800',
|
||||
'Level 2 Approved': 'bg-green-100 text-green-800',
|
||||
'Level 2 Recommended': 'bg-teal-100 text-teal-800',
|
||||
'Level 3 Pending': 'bg-orange-100 text-orange-800',
|
||||
'Level 3 Interview Pending': 'bg-orange-100 text-orange-800',
|
||||
'In Review': 'bg-slate-100 text-slate-800',
|
||||
'Level 3 Approved': 'bg-green-100 text-green-800',
|
||||
'FDD Verification': 'bg-indigo-100 text-indigo-800',
|
||||
'Payment Pending': 'bg-amber-100 text-amber-800',
|
||||
'LOI In Progress': 'bg-sky-100 text-sky-800',
|
||||
'LOI Issued': 'bg-sky-100 text-sky-800',
|
||||
'Dealer Code Generation': 'bg-purple-100 text-purple-800',
|
||||
'Architecture Team Assigned': 'bg-blue-100 text-blue-800',
|
||||
@ -149,15 +152,19 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
|
||||
'Statutory Check': 'bg-emerald-100 text-emerald-800',
|
||||
'Statutory Partnership': 'bg-emerald-100 text-emerald-800',
|
||||
'Statutory Firm Reg': 'bg-emerald-100 text-emerald-800',
|
||||
'Statutory Rental': 'bg-emerald-100 text-emerald-800',
|
||||
'Statutory Virtual Code': 'bg-emerald-100 text-emerald-800',
|
||||
'Statutory Domain': 'bg-emerald-100 text-emerald-800',
|
||||
'Statutory MSD': 'bg-emerald-100 text-emerald-800',
|
||||
'Statutory LOI Ack': 'bg-emerald-100 text-emerald-800',
|
||||
'EOR In Progress': 'bg-violet-100 text-violet-800',
|
||||
'EOR Complete': 'bg-violet-100 text-violet-800',
|
||||
'LOA Pending': 'bg-pink-100 text-pink-800',
|
||||
'Inauguration': 'bg-amber-100 text-amber-800',
|
||||
'Approved': 'bg-green-100 text-green-800',
|
||||
'Rejected': 'bg-red-100 text-red-800',
|
||||
'Disqualified': 'bg-gray-100 text-gray-800'
|
||||
'Disqualified': 'bg-gray-100 text-gray-800',
|
||||
'Onboarded': 'bg-emerald-100 text-emerald-800',
|
||||
};
|
||||
return colors[status] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
@ -375,7 +382,7 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-slate-600">{app.submissionDate}</span>
|
||||
<span className="text-slate-600">{formatDateTime(app.submissionDate)}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@ -3,6 +3,7 @@ import { Button } from '../ui/button';
|
||||
import { Progress } from '../ui/progress';
|
||||
import { Application } from '../../lib/mock-data';
|
||||
import { MapPin, Phone, Mail, Award, Calendar, Building } from 'lucide-react';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
|
||||
interface ApplicationCardProps {
|
||||
application: Application;
|
||||
@ -106,7 +107,7 @@ export function ApplicationCard({ application, onViewDetails }: ApplicationCardP
|
||||
|
||||
<div className="flex items-center gap-2 text-slate-600">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>Submitted: {new Date(application.submissionDate).toLocaleDateString()}</span>
|
||||
<span>Submitted: {formatDateTime(application.submissionDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -123,7 +124,7 @@ export function ApplicationCard({ application, onViewDetails }: ApplicationCardP
|
||||
{application.deadline && application.status === 'Questionnaire Pending' && (
|
||||
<div className="mb-4 p-3 bg-orange-50 border border-orange-200 rounded-md">
|
||||
<p className="text-orange-800">
|
||||
Deadline: {new Date(application.deadline).toLocaleDateString()}
|
||||
Deadline: {formatDateTime(application.deadline)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { Application, ApplicationStatus } from '../../lib/mock-data';
|
||||
import { onboardingService } from '../../services/onboarding.service';
|
||||
@ -8,7 +8,7 @@ import { eorService } from '../../services/eor.service';
|
||||
import QuestionnaireResponseView from './QuestionnaireResponseView';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../store';
|
||||
import { cn } from '@/components/ui/utils';
|
||||
import { cn, formatDateTime } from '@/components/ui/utils';
|
||||
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
@ -263,8 +263,8 @@ export function ApplicationDetails() {
|
||||
// Helper to find stage date
|
||||
const getStageDate = (stageName: string) => {
|
||||
const stage = data.progressTracking?.find((p: any) => p.stageName === stageName);
|
||||
return stage?.stageCompletedAt ? new Date(stage.stageCompletedAt).toISOString().split('T')[0] :
|
||||
stage?.stageStartedAt ? new Date(stage.stageStartedAt).toISOString().split('T')[0] : undefined;
|
||||
return stage?.stageCompletedAt ? new Date(stage.stageCompletedAt).toISOString() :
|
||||
stage?.stageStartedAt ? new Date(stage.stageStartedAt).toISOString() : undefined;
|
||||
};
|
||||
|
||||
// Map backend data to frontend Application interface
|
||||
@ -301,7 +301,7 @@ export function ApplicationDetails() {
|
||||
ownRoyalEnfield: data.ownRoyalEnfield,
|
||||
address: data.address,
|
||||
// Map timeline dates from progressTracking
|
||||
submissionDate: data.createdAt ? new Date(data.createdAt).toISOString().split('T')[0] : '',
|
||||
submissionDate: data.createdAt ? new Date(data.createdAt).toISOString() : '',
|
||||
questionnaireDate: getStageDate('Questionnaire'),
|
||||
shortlistDate: getStageDate('Shortlist'),
|
||||
level1InterviewDate: getStageDate('1st Level Interview'),
|
||||
@ -318,7 +318,7 @@ export function ApplicationDetails() {
|
||||
loaDate: getStageDate('LOA'),
|
||||
eorCompleteDate: getStageDate('EOR Complete'),
|
||||
inaugurationDate: getStageDate('Inauguration'),
|
||||
onboardedDate: data.overallStatus === 'Onboarded' ? (data.updatedAt ? new Date(data.updatedAt).toISOString().split('T')[0] : new Date().toISOString().split('T')[0]) : undefined,
|
||||
onboardedDate: data.overallStatus === 'Onboarded' ? (data.updatedAt ? new Date(data.updatedAt).toISOString() : new Date().toISOString()) : undefined,
|
||||
progressTracking: data.progressTracking || [],
|
||||
participants: data.participants || [],
|
||||
dealerCode: data.dealerCode,
|
||||
@ -327,6 +327,7 @@ export function ApplicationDetails() {
|
||||
areaId: data.areaId,
|
||||
districtId: data.districtId,
|
||||
stageApprovals: data.stageApprovals || [],
|
||||
fddAssignments: data.fddAssignments || [],
|
||||
};
|
||||
setApplication(mappedApp);
|
||||
} catch (error) {
|
||||
@ -387,7 +388,8 @@ export function ApplicationDetails() {
|
||||
}
|
||||
}, [applicationId]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState('questionnaire');
|
||||
const routerLocation = useLocation();
|
||||
const [activeTab, setActiveTab] = useState(routerLocation.state?.activeTab || 'questionnaire');
|
||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||
const [showOnboardModal, setShowOnboardModal] = useState(false);
|
||||
const [isOnboarding, setIsOnboarding] = useState(false);
|
||||
@ -433,6 +435,9 @@ export function ApplicationDetails() {
|
||||
const [architectureStatus, setArchitectureStatus] = useState<string>('COMPLETED');
|
||||
const [architectureRemarks, setArchitectureRemarks] = useState<string>('');
|
||||
const [isUpdatingArchitecture, setIsUpdatingArchitecture] = useState(false);
|
||||
const [isAssigningParticipant, setIsAssigningParticipant] = useState(false);
|
||||
const [isApproving, setIsApproving] = useState(false);
|
||||
const [isRejecting, setIsRejecting] = useState(false);
|
||||
|
||||
// KT Matrix State
|
||||
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
|
||||
@ -1229,6 +1234,8 @@ export function ApplicationDetails() {
|
||||
};
|
||||
|
||||
const handleApprove = async () => {
|
||||
try {
|
||||
setIsApproving(true);
|
||||
// Check if user has an active interview to approve
|
||||
const activeInterview = interviews.find(i =>
|
||||
i.status !== 'Completed' && i.status !== 'Cancelled' &&
|
||||
@ -1295,7 +1302,6 @@ export function ApplicationDetails() {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Application level approval - Robust State Machine
|
||||
let newStatus = application.status;
|
||||
|
||||
@ -1398,10 +1404,14 @@ export function ApplicationDetails() {
|
||||
} catch (error) {
|
||||
console.error('Approval error:', error);
|
||||
toast.error('Failed to process approval');
|
||||
} finally {
|
||||
setIsApproving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
try {
|
||||
setIsRejecting(true);
|
||||
// Check if user has an active interview to reject
|
||||
const activeInterview = interviews.find(i =>
|
||||
i.status !== 'Completed' && i.status !== 'Cancelled' &&
|
||||
@ -1432,7 +1442,6 @@ export function ApplicationDetails() {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const policyManagedStages: { [key: string]: string } = {
|
||||
'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1',
|
||||
'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2',
|
||||
@ -1466,6 +1475,8 @@ export function ApplicationDetails() {
|
||||
} catch (error) {
|
||||
console.error('Rejection error:', error);
|
||||
toast.error('Failed to process rejection');
|
||||
} finally {
|
||||
setIsRejecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1518,11 +1529,12 @@ export function ApplicationDetails() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsAssigningParticipant(true);
|
||||
await onboardingService.addParticipant({
|
||||
requestId: applicationId,
|
||||
requestType: 'application',
|
||||
userId: selectedUser,
|
||||
participantType: 'contributor'
|
||||
participantType: participantType || 'contributor'
|
||||
});
|
||||
toast.success('User assigned successfully!');
|
||||
// Refresh application data
|
||||
@ -1531,6 +1543,8 @@ export function ApplicationDetails() {
|
||||
setShowAssignModal(false);
|
||||
} catch (error) {
|
||||
toast.error('Failed to assign user');
|
||||
} finally {
|
||||
setIsAssigningParticipant(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1624,18 +1638,175 @@ export function ApplicationDetails() {
|
||||
'LOA Pending', 'EOR Complete', 'Inauguration'
|
||||
].includes(application.status);
|
||||
|
||||
const finalDepositVerified = getDeposit('FINAL')?.status === 'Verified';
|
||||
const isLoaLocked = application.status === 'LOA Pending' && !finalDepositVerified;
|
||||
|
||||
// Show Approve/Reject if:
|
||||
// 1. It's an interview and feedback is submitted AND no decision made yet
|
||||
// 2. OR it's an administrative stage and user is Admin AND hasn't made a decision yet
|
||||
const shouldShowApproveReject =
|
||||
!isLoaLocked && (
|
||||
(!hasMadeDecisionForUser && hasSubmittedFeedbackForActive) ||
|
||||
(isAdmin && isAdministrativeStage && !hasMadeStageDecision);
|
||||
(isAdmin && isAdministrativeStage && !hasMadeStageDecision)
|
||||
);
|
||||
|
||||
const shouldShowDecisionMessage = hasMadeDecisionForUser && (!isAdministrativeStage || hasMadeStageDecision);
|
||||
|
||||
|
||||
|
||||
|
||||
const renderFddAuditContent = () => {
|
||||
const assignments = application?.fddAssignments || [];
|
||||
|
||||
if (assignments.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-12 bg-slate-50 rounded-xl border-2 border-dashed border-slate-200">
|
||||
<ShieldCheck className="w-12 h-12 text-slate-300 mb-4" />
|
||||
<h3 className="text-slate-900 font-semibold">No FDD Assignment</h3>
|
||||
<p className="text-slate-500 text-sm text-center max-w-xs mt-2">
|
||||
The Financial Due Diligence process has not been initiated for this application yet.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Financial Due Diligence Reports</h3>
|
||||
<Badge variant="outline" className="bg-amber-50 text-amber-600 border-amber-200">
|
||||
{assignments.length} Assignment(s)
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{assignments.map((assignment: any) => (
|
||||
<Card key={assignment.id} className="overflow-hidden border-slate-200 shadow-sm">
|
||||
<CardHeader className="bg-slate-50/50 py-4 border-b border-slate-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center border border-slate-200 shadow-sm">
|
||||
<ShieldCheck className="w-6 h-6 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-slate-900 font-bold truncate max-w-[200px]">FDD Agency Audit</h4>
|
||||
<p className="text-slate-500 text-[10px] uppercase tracking-wider font-bold">
|
||||
Agency ID: {assignment.assignedToAgency || 'Assigned'} • Status: {assignment.status}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={cn(
|
||||
assignment.status === 'completed' ? "bg-green-100 text-green-700 hover:bg-green-100" : "bg-amber-100 text-amber-700 hover:bg-amber-100"
|
||||
)}>
|
||||
{assignment.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{(!assignment.reports || assignment.reports.length === 0) ? (
|
||||
<div className="p-12 text-center">
|
||||
<Clock className="w-8 h-8 text-slate-300 mx-auto mb-3" />
|
||||
<p className="text-slate-500 italic text-sm">Waiting for internal or external agency to submit the final audit report...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-slate-100">
|
||||
{assignment.reports.map((report: any) => (
|
||||
<div key={report.id} className="p-6 space-y-6 bg-white">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Auditor Recommendation</Label>
|
||||
<div className={cn(
|
||||
"inline-flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-black shadow-sm",
|
||||
report.recommendation === 'Green' ? "bg-green-50 border-green-200 text-green-700" :
|
||||
report.recommendation === 'Amber' ? "bg-amber-50 border-amber-200 text-amber-700" :
|
||||
"bg-red-50 border-red-200 text-red-700"
|
||||
)}>
|
||||
<div className={cn("w-2 h-2 rounded-full",
|
||||
report.recommendation === 'Green' ? "bg-green-500" :
|
||||
report.recommendation === 'Amber' ? "bg-amber-500" : "bg-red-500"
|
||||
)} />
|
||||
{report.recommendation?.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Findings Summary</Label>
|
||||
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 text-slate-700 text-sm leading-relaxed shadow-inner italic">
|
||||
"{report.findings || 'No detail findings provided by the auditor.'}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Report Document</Label>
|
||||
{report.reportDocument ? (
|
||||
<div className="group bg-white border-2 border-slate-100 rounded-xl p-4 flex items-center justify-between hover:border-amber-400 transition-all hover:shadow-md cursor-pointer">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center group-hover:scale-110 transition-all shadow-sm">
|
||||
<FileText className="w-6 h-6 text-red-500" />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<p className="text-slate-900 font-bold text-sm truncate max-w-[180px]">{report.reportDocument.fileName}</p>
|
||||
<p className="text-slate-500 text-[10px] font-medium">SUBMITTED {new Date(report.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-slate-400 hover:text-amber-600 hover:bg-amber-50"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(`http://localhost:5000/${report.reportDocument.filePath}`, '_blank');
|
||||
}}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-slate-400 hover:text-amber-600 hover:bg-amber-50"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setPreviewDoc(report.reportDocument);
|
||||
setShowPreviewModal(true);
|
||||
}}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 bg-slate-50 rounded-xl border border-dashed border-slate-200 text-center text-slate-400 text-xs font-medium">
|
||||
No audit report file attached
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 flex items-center gap-3">
|
||||
{report.verifiedAt ? (
|
||||
<div className="flex items-center gap-2 bg-green-50 border border-green-100 px-3 py-1.5 rounded-full">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span className="text-[10px] font-black text-green-700 uppercase">Audit Verified by Finance</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 bg-amber-50 border border-amber-100 px-3 py-1.5 rounded-full">
|
||||
<Clock className="w-4 h-4 text-amber-600" />
|
||||
<span className="text-[10px] font-black text-amber-700 uppercase">Pending Finance Review</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@ -1828,6 +1999,7 @@ export function ApplicationDetails() {
|
||||
<TabsTrigger value="progress" className="min-w-[80px]">Progress</TabsTrigger>
|
||||
<TabsTrigger value="documents" className="min-w-[100px]">Documents</TabsTrigger>
|
||||
<TabsTrigger value="interviews" className="min-w-[100px]">Interviews</TabsTrigger>
|
||||
<TabsTrigger value="fdd" className="min-w-[120px]">FDD Audit</TabsTrigger>
|
||||
<TabsTrigger value="eor" className="min-w-[120px]">EOR Checklist</TabsTrigger>
|
||||
<TabsTrigger value="payments" className="min-w-[100px]">Payments</TabsTrigger>
|
||||
<TabsTrigger value="audit" className="min-w-[100px]">Audit Trail</TabsTrigger>
|
||||
@ -2045,7 +2217,7 @@ export function ApplicationDetails() {
|
||||
})()}
|
||||
|
||||
<p className="text-slate-500 mt-1 text-xs">
|
||||
{stage.status === 'completed' && stage.date && `Completed: ${new Date(stage.date).toLocaleDateString()}`}
|
||||
{stage.status === 'completed' && stage.date && `Completed: ${formatDateTime(stage.date)}`}
|
||||
{stage.status === 'active' && 'In Progress'}
|
||||
{stage.status === 'pending' && 'Pending'}
|
||||
</p>
|
||||
@ -2139,7 +2311,7 @@ export function ApplicationDetails() {
|
||||
);
|
||||
})()}
|
||||
<p className="text-slate-400 text-[10px] mt-1">
|
||||
{branchStage.status === 'completed' && branchStage.date && `Done: ${new Date(branchStage.date).toLocaleDateString()}`}
|
||||
{branchStage.status === 'completed' && branchStage.date && `Done: ${formatDateTime(branchStage.date)}`}
|
||||
{branchStage.status === 'active' && 'Evaluating'}
|
||||
{branchStage.status === 'pending' && 'Pending'}
|
||||
</p>
|
||||
@ -2381,6 +2553,10 @@ export function ApplicationDetails() {
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="fdd" className="space-y-6">
|
||||
{renderFddAuditContent()}
|
||||
</TabsContent>
|
||||
|
||||
{/* EOR Checklist Tab */}
|
||||
<TabsContent value="eor" className="space-y-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@ -2795,6 +2971,16 @@ export function ApplicationDetails() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* Show Approve/Reject block */}
|
||||
{isLoaLocked && (
|
||||
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800">
|
||||
<Lock className="w-4 h-4 text-amber-600" />
|
||||
<AlertTitle className="text-amber-900 font-semibold">Stage Locked</AlertTitle>
|
||||
<AlertDescription className="text-amber-800">
|
||||
Final Security Deposit (₹15L) must be verified by Finance before LOA Approval can proceed.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{shouldShowApproveReject && (
|
||||
<>
|
||||
<Button
|
||||
@ -3019,8 +3205,19 @@ export function ApplicationDetails() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button className="w-full bg-amber-600 hover:bg-amber-700" onClick={handleAddParticipant}>
|
||||
Assign User
|
||||
<Button
|
||||
className="w-full bg-amber-600 hover:bg-amber-700 font-bold h-11"
|
||||
onClick={handleAddParticipant}
|
||||
disabled={isAssigningParticipant}
|
||||
>
|
||||
{isAssigningParticipant ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Assigning...
|
||||
</>
|
||||
) : (
|
||||
'Assign User'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@ -3065,14 +3262,23 @@ export function ApplicationDetails() {
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => setShowApproveModal(false)}
|
||||
disabled={isApproving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 bg-green-600 hover:bg-green-700"
|
||||
onClick={handleApprove}
|
||||
disabled={isApproving}
|
||||
>
|
||||
Submit Approval
|
||||
{isApproving ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Approving...
|
||||
</>
|
||||
) : (
|
||||
'Submit Approval'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -3171,6 +3377,7 @@ export function ApplicationDetails() {
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => setShowRejectModal(false)}
|
||||
disabled={isRejecting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@ -3178,8 +3385,16 @@ export function ApplicationDetails() {
|
||||
variant="destructive"
|
||||
className="flex-1"
|
||||
onClick={handleReject}
|
||||
disabled={isRejecting}
|
||||
>
|
||||
Confirm Rejection
|
||||
{isRejecting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Rejecting...
|
||||
</>
|
||||
) : (
|
||||
'Confirm Rejection'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { mockApplications, locations, ApplicationStatus, Application } from '../../lib/mock-data';
|
||||
import { locations, ApplicationStatus, Application } from '../../lib/mock-data';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
import { onboardingService } from '../../services/onboarding.service';
|
||||
import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
@ -12,10 +13,8 @@ import {
|
||||
} from '../ui/select';
|
||||
import {
|
||||
Search,
|
||||
Filter,
|
||||
Download,
|
||||
Mail,
|
||||
Plus
|
||||
Mail
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Checkbox } from '../ui/checkbox';
|
||||
@ -51,12 +50,10 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
|
||||
|
||||
// Real Data Integration
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchApplications = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await onboardingService.getApplications();
|
||||
// Check if response is array or wrapped in data property
|
||||
const applicationsData = response.data || (Array.isArray(response) ? response : []);
|
||||
@ -100,7 +97,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch applications', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchApplications();
|
||||
@ -346,7 +343,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(app.submissionDate).toLocaleDateString()}
|
||||
{formatDateTime(app.submissionDate)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
|
||||
@ -8,11 +8,23 @@ import {
|
||||
FileText,
|
||||
Upload,
|
||||
Loader2,
|
||||
Eye
|
||||
Eye,
|
||||
CheckCircle2,
|
||||
Clock
|
||||
} from 'lucide-react';
|
||||
import { WorkNotesPage } from './WorkNotesPage';
|
||||
import { toast } from 'sonner';
|
||||
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter
|
||||
} from '../ui/dialog';
|
||||
import { Button } from '../ui/button';
|
||||
import { AlertTriangle, Info, ShieldCheck } from 'lucide-react';
|
||||
|
||||
export function FDDApplicationDetails() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@ -24,6 +36,8 @@ export function FDDApplicationDetails() {
|
||||
const [activeTab, setActiveTab] = useState<'details' | 'worknotes'>('details');
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const [selectedPreviewDoc, setSelectedPreviewDoc] = useState<any>(null);
|
||||
const [showFinalizeModal, setShowFinalizeModal] = useState(false);
|
||||
const [showFlagModal, setShowFlagModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) fetchApplication();
|
||||
@ -104,6 +118,14 @@ export function FDDApplicationDetails() {
|
||||
|
||||
if (!application) return null;
|
||||
|
||||
const isFDDStageActive = application.currentStage === 'FDD_VERIFICATION' || application.currentStage === 'FDD';
|
||||
|
||||
// Check if the application has already passed the FDD stage
|
||||
const isCompleted = !isFDDStageActive && (application.overallStatus !== 'Active' || application.currentProgress >= 75);
|
||||
|
||||
// Check if the application has not yet arrived at the FDD stage
|
||||
const isNotReachedYet = !isFDDStageActive && application.currentProgress < 70 && !isCompleted;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 max-w-7xl mx-auto mb-10">
|
||||
{/* Action Bar */}
|
||||
@ -118,6 +140,13 @@ export function FDDApplicationDetails() {
|
||||
Back to Dashboard
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
{isNotReachedYet ? (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-slate-100 border border-slate-200 text-slate-500 font-bold text-[10px] uppercase tracking-[0.1em] rounded-lg">
|
||||
<Clock className="w-4 h-4" />
|
||||
Awaiting Previous Stages
|
||||
</div>
|
||||
) : !isCompleted ? (
|
||||
<>
|
||||
<button
|
||||
disabled={uploading}
|
||||
onClick={() => {
|
||||
@ -151,48 +180,25 @@ export function FDDApplicationDetails() {
|
||||
</button>
|
||||
<button
|
||||
disabled={uploading}
|
||||
onClick={async () => {
|
||||
if (!window.confirm('Are you sure you want to flag this application as non-responsive?')) return;
|
||||
try {
|
||||
setUploading(true);
|
||||
await API.submitStageDecision({
|
||||
applicationId: application.id,
|
||||
stageCode: 'FDD_VERIFICATION',
|
||||
decision: 'Rejected',
|
||||
remarks: 'Applicant is non-responsive to FDD queries.'
|
||||
});
|
||||
toast.error('Application flagged and returned to admin.');
|
||||
navigate('/fdd-dashboard');
|
||||
} catch (e) { toast.error('Action failed'); } finally { setUploading(false); }
|
||||
}}
|
||||
onClick={() => setShowFlagModal(true)}
|
||||
className="px-4 py-2 text-red-600 font-bold text-xs uppercase tracking-wider hover:bg-red-50 rounded-lg transition-all"
|
||||
>
|
||||
Flag Non-Responsive
|
||||
</button>
|
||||
<button
|
||||
disabled={uploading}
|
||||
onClick={async () => {
|
||||
if (!window.confirm('Finalizing the report will submit your findings and lock this case. Proceed?')) return;
|
||||
try {
|
||||
setUploading(true);
|
||||
const res: any = await API.submitStageDecision({
|
||||
applicationId: application.id,
|
||||
stageCode: 'FDD_VERIFICATION',
|
||||
decision: 'Approved',
|
||||
remarks: 'FDD Verification completed and report uploaded.',
|
||||
nextStatus: 'Security Details',
|
||||
nextProgress: 75
|
||||
});
|
||||
if (res.data?.success) {
|
||||
toast.success('FDD Report submitted successfully.');
|
||||
navigate('/fdd-dashboard');
|
||||
}
|
||||
} catch (e) { toast.error('Failed to submit report'); } finally { setUploading(false); }
|
||||
}}
|
||||
onClick={() => setShowFinalizeModal(true)}
|
||||
className="px-6 py-2 bg-blue-600 text-white font-bold text-xs uppercase tracking-wider rounded-lg shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all hover:scale-[1.02] disabled:opacity-50"
|
||||
>
|
||||
{uploading ? 'Processing...' : 'Finalize & Submit Report'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-green-50 border border-green-200 text-green-700 font-bold text-[10px] uppercase tracking-[0.1em] rounded-lg shadow-inner">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
Final Audit Report Submitted
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -261,10 +267,34 @@ export function FDDApplicationDetails() {
|
||||
<CardHeader className="border-b border-slate-100 px-6 py-4">
|
||||
<CardTitle className="text-base font-bold flex items-center gap-2 text-slate-800">
|
||||
<Upload className="w-4 h-4 text-slate-500" />
|
||||
Financial Report Submission
|
||||
{isCompleted ? 'Finalized Financial Reports' : isNotReachedYet ? 'Audit Workspace' : 'Financial Report Submission'}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
{isNotReachedYet && (
|
||||
<div className="mb-6 p-8 bg-slate-50 border border-dashed border-slate-200 rounded-xl flex flex-col items-center justify-center text-center">
|
||||
<div className="w-16 h-16 bg-white rounded-full flex items-center justify-center text-slate-300 mb-4 shadow-sm">
|
||||
<Clock className="w-8 h-8" />
|
||||
</div>
|
||||
<h4 className="text-lg font-bold text-slate-900 mb-2">Stage Not Yet Active</h4>
|
||||
<p className="text-sm text-slate-500 max-w-sm mb-6">This application is still being processed in previous documentation or interview stages. The FDD workspace will activate once the previous stages are approved.</p>
|
||||
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-widest px-4 py-1.5 bg-white rounded-full border border-slate-200">
|
||||
Status: {application.status || 'Pending Review'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isCompleted && (
|
||||
<div className="mb-6 p-4 bg-green-50/50 border border-green-100 rounded-xl flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center text-green-600 shrink-0">
|
||||
<CheckCircle2 className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-green-800">Verification Stage Completed</p>
|
||||
<p className="text-[11px] text-green-600 font-medium">The FDD report has been submitted and the case is now locked for further audits.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isCompleted && !isNotReachedYet && (
|
||||
<div className="p-10 border-2 border-dashed border-slate-200 rounded-lg flex flex-col items-center justify-center text-center">
|
||||
<div className="w-12 h-12 bg-slate-50 text-slate-400 rounded-full flex items-center justify-center mb-4">
|
||||
<FileText className="w-6 h-6" />
|
||||
@ -310,6 +340,7 @@ export function FDDApplicationDetails() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* List of Uploaded Documents */}
|
||||
<div className="mt-8 border-t border-slate-100 pt-8">
|
||||
@ -473,6 +504,133 @@ export function FDDApplicationDetails() {
|
||||
onClose={() => setIsPreviewOpen(false)}
|
||||
document={selectedPreviewDoc}
|
||||
/>
|
||||
|
||||
{/* Finalize Confirmation Modal */}
|
||||
<Dialog open={showFinalizeModal} onOpenChange={setShowFinalizeModal}>
|
||||
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl">
|
||||
<div className="bg-slate-950 p-6 flex items-center justify-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-amber-600/20 to-transparent" />
|
||||
<div className="w-16 h-16 bg-amber-600/20 rounded-full flex items-center justify-center animate-pulse relative z-10">
|
||||
<ShieldCheck className="w-8 h-8 text-amber-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-8 space-y-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold text-slate-900 text-center">Finalize Audit Report</DialogTitle>
|
||||
<DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-base">
|
||||
You are about to submit your final findings. This action will <span className="font-bold text-slate-800 underline decoration-amber-500 decoration-2">lock the report</span> and move the application to the next stage.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="bg-amber-50 p-4 rounded-xl flex gap-3 border border-amber-100 italic">
|
||||
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-amber-800 leading-normal">
|
||||
Ensure all required financial documents are uploaded and verified before proceeding.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex flex-col sm:flex-row gap-3 pt-4 sm:pt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full sm:flex-1 h-12 rounded-xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200"
|
||||
onClick={() => setShowFinalizeModal(false)}
|
||||
disabled={uploading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full sm:flex-1 h-12 rounded-xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-2 border-amber-600"
|
||||
onClick={async () => {
|
||||
try {
|
||||
setUploading(true);
|
||||
const res: any = await API.submitStageDecision({
|
||||
applicationId: application.id,
|
||||
stageCode: 'FDD_VERIFICATION',
|
||||
decision: 'Approved',
|
||||
remarks: 'FDD Verification completed and report uploaded.',
|
||||
nextStatus: 'LOI In Progress',
|
||||
nextProgress: 65
|
||||
});
|
||||
if (res.data?.success) {
|
||||
toast.success('FDD Report submitted successfully.');
|
||||
setShowFinalizeModal(false);
|
||||
navigate('/fdd-dashboard');
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('Failed to submit report');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm & Submit'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Flag Non-Responsive Confirmation Modal */}
|
||||
<Dialog open={showFlagModal} onOpenChange={setShowFlagModal}>
|
||||
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl">
|
||||
<div className="bg-slate-950 p-6 flex items-center justify-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-red-600/20 to-transparent" />
|
||||
<div className="w-16 h-16 bg-red-600/20 rounded-full flex items-center justify-center relative z-10">
|
||||
<AlertTriangle className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-8 space-y-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold text-slate-900 text-center">Flag Applicant</DialogTitle>
|
||||
<DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-base">
|
||||
Are you sure you want to flag this applicant as <span className="font-bold text-red-600">Non-Responsive</span>?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="bg-red-50 p-4 rounded-xl flex gap-3 border border-red-100 italic">
|
||||
<p className="text-xs text-red-800 leading-normal text-center w-full">
|
||||
"Applicant is non-responsive to FDD queries."
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex flex-col sm:flex-row gap-3 pt-4 sm:pt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full sm:flex-1 h-12 rounded-xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200"
|
||||
onClick={() => setShowFlagModal(false)}
|
||||
disabled={uploading}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full sm:flex-1 h-12 rounded-xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-2 border-red-600"
|
||||
onClick={async () => {
|
||||
try {
|
||||
setUploading(true);
|
||||
await API.submitStageDecision({
|
||||
applicationId: application.id,
|
||||
stageCode: 'FDD_VERIFICATION',
|
||||
decision: 'Rejected',
|
||||
remarks: 'Applicant is non-responsive to FDD queries.'
|
||||
});
|
||||
toast.error('Application flagged and returned to admin.');
|
||||
setShowFlagModal(false);
|
||||
navigate('/fdd-dashboard');
|
||||
} catch (e) {
|
||||
toast.error('Action failed');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Flag Applicant'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
636
src/components/applications/FinanceFddDetailPage.tsx
Normal file
636
src/components/applications/FinanceFddDetailPage.tsx
Normal file
@ -0,0 +1,636 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
import { Label } from '../ui/label';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../store';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger
|
||||
} from '../ui/tabs';
|
||||
import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ShieldCheck,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
FileText,
|
||||
User,
|
||||
Clock,
|
||||
Download,
|
||||
Eye,
|
||||
AlertCircle,
|
||||
MessageSquare,
|
||||
FileCheck,
|
||||
RotateCcw,
|
||||
History,
|
||||
Send
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { onboardingService } from '../../services/onboarding.service';
|
||||
import { worknoteService } from '../../services/worknote.service';
|
||||
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||
|
||||
// Simple helper for class merging
|
||||
const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
|
||||
|
||||
interface FinanceFddDetailPageProps {
|
||||
applicationId: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetailPageProps) {
|
||||
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
||||
const [application, setApplication] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [approvalRemark, setApprovalRemark] = useState('');
|
||||
const [newNote, setNewNote] = useState('');
|
||||
const [isNoteSubmitting, setIsNoteSubmitting] = useState(false);
|
||||
const [showPreviewModal, setShowPreviewModal] = useState(false);
|
||||
const [previewDoc, setPreviewDoc] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [applicationId]);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const appData = await onboardingService.getApplicationById(applicationId);
|
||||
setApplication(appData);
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
toast.error('Failed to load application data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDecision = async (decision: 'Approved' | 'Rejected') => {
|
||||
if (!approvalRemark.trim()) {
|
||||
toast.warning('Please enter a remark or justification');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Map current status to next status for LOI stage
|
||||
let nextStatus = 'LOI Issued'; // Default
|
||||
if (application.status === 'LOI In Progress') {
|
||||
nextStatus = 'LOI Issued';
|
||||
}
|
||||
|
||||
const response = await onboardingService.submitStageDecision({
|
||||
applicationId: application.id,
|
||||
stageCode: 'LOI_APPROVAL',
|
||||
decision,
|
||||
remarks: approvalRemark,
|
||||
nextStatus
|
||||
});
|
||||
|
||||
if (response.data?.statusUpdated) {
|
||||
toast.success(response.message || `Application ${decision.toLowerCase()} successfully`);
|
||||
} else {
|
||||
toast.info(response.message || 'Decision recorded. Waiting for other mandatory approvers.');
|
||||
}
|
||||
|
||||
setApprovalRemark('');
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Decision error:', error);
|
||||
toast.error('Failed to process decision');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePostNote = async () => {
|
||||
if (!newNote.trim()) return;
|
||||
|
||||
try {
|
||||
setIsNoteSubmitting(true);
|
||||
await worknoteService.addWorknote({
|
||||
requestId: application.id,
|
||||
requestType: 'application',
|
||||
noteText: newNote,
|
||||
noteType: 'fdd_query'
|
||||
});
|
||||
|
||||
setNewNote('');
|
||||
toast.success('Work note posted successfully');
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Add note error:', error);
|
||||
toast.error('Failed to post work note');
|
||||
} finally {
|
||||
setIsNoteSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-20">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!application) {
|
||||
return <div className="p-20 text-center">Application not found</div>;
|
||||
}
|
||||
|
||||
const isFinance = currentUser?.role === 'Finance' || currentUser?.role === 'Finance Admin';
|
||||
const isReadOnly = !isFinance;
|
||||
|
||||
const assignments = application.fddAssignments || [];
|
||||
const workNotes = application.workNotes || [];
|
||||
const hasMadeDecision = application.stageApprovals?.some(
|
||||
(a: any) => a.stageCode === 'LOI_APPROVAL' && String(a.actorUserId) === String(currentUser?.id)
|
||||
);
|
||||
|
||||
const MANDATORY_FINANCIAL_DOCS = [
|
||||
{ type: 'Bank Statement', label: 'Bank Statements' },
|
||||
{ type: 'Income Tax Returns (ITR)', label: 'ITR (Last 3 Years)' },
|
||||
{ type: 'Credit Reports', label: 'CIBIL / Credit Reports' },
|
||||
{ type: 'Property Documents', label: 'Property Documents' },
|
||||
{ type: 'Business Valuation Report', label: 'Valuation Reports' }
|
||||
];
|
||||
|
||||
const getDocByTypeName = (typeName: string) => {
|
||||
return application.uploadedDocuments?.find((d: any) => d.documentType === typeName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" onClick={onBack} className="shrink-0 rounded-xl">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-slate-900 tracking-tight">FDD Audit Detail</h1>
|
||||
<p className="text-slate-500 text-sm font-medium">Review findings and provide finance sign-off for LOI stage</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-slate-50 text-slate-600 border-slate-200 py-1.5 px-3 rounded-full text-[10px] font-black uppercase tracking-widest">
|
||||
APP ID: {application.applicationId || application.id}
|
||||
</Badge>
|
||||
<Badge className="bg-amber-100 text-amber-700 hover:bg-amber-100 py-1.5 px-3 rounded-full text-[10px] font-black uppercase tracking-widest border border-amber-200">
|
||||
{application.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="audit" className="w-full">
|
||||
<TabsList className="bg-slate-100 p-1 rounded-xl mb-6">
|
||||
<TabsTrigger value="audit" className="rounded-lg px-6 font-bold data-[state=active]:bg-white data-[state=active]:shadow-sm">
|
||||
Audit Review
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="worknotes" className="rounded-lg px-6 font-bold data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4" /> Work Notes
|
||||
{workNotes.length > 0 && <Badge className="ml-1 h-5 w-5 p-0 flex items-center justify-center bg-blue-600 rounded-full text-[10px] text-white font-black">{workNotes.length}</Badge>}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="history" className="rounded-lg px-6 font-bold data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2">
|
||||
<History className="w-4 h-4" /> Audit Trail
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="audit">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Content Area */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
|
||||
{/* Applicant Summary Card */}
|
||||
<Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl">
|
||||
<CardHeader className="bg-slate-50/50 border-b border-slate-100 py-4">
|
||||
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2">
|
||||
<User className="w-4 h-4" /> Applicant Summary
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-400 uppercase font-black tracking-tighter">Business Name</Label>
|
||||
<p className="text-slate-900 font-bold">{application.name || application.applicantName}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-400 uppercase font-black tracking-tighter">Location</Label>
|
||||
<p className="text-slate-900 font-bold">{application.city || application.preferredLocation}, {application.state}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-400 uppercase font-black tracking-tighter">Contact Details</Label>
|
||||
<div className="flex flex-col text-sm font-medium text-slate-600">
|
||||
<span>{application.email}</span>
|
||||
<span>{application.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-400 uppercase font-black tracking-tighter">Constitution</Label>
|
||||
<p className="text-slate-800 font-bold">{application.constitutionType || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Financial Document Checklist Card */}
|
||||
<Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl">
|
||||
<CardHeader className="bg-slate-50/50 border-b border-slate-100 py-4">
|
||||
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2"><FileCheck className="w-4 h-4" /> Financial Artefacts Checklist</div>
|
||||
<Badge variant="outline" className="text-[10px] bg-white">Mandatory for FDD Sign-off</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y divide-slate-100">
|
||||
{MANDATORY_FINANCIAL_DOCS.map((docType) => {
|
||||
const doc = getDocByTypeName(docType.type);
|
||||
return (
|
||||
<div key={docType.type} className="flex items-center justify-between p-4 px-6 hover:bg-slate-50/50 transition-colors">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={cn(
|
||||
"w-8 h-8 rounded-lg flex items-center justify-center",
|
||||
doc ? "bg-emerald-50 text-emerald-600" : "bg-slate-50 text-slate-300"
|
||||
)}>
|
||||
{doc ? <CheckCircle className="w-5 h-5" /> : <AlertCircle className="w-5 h-5" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-slate-800">{docType.label}</p>
|
||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">
|
||||
{doc ? `Uploaded: ${new Date(doc.createdAt).toLocaleDateString()}` : 'Missing in Documentation'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{doc && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" className="h-8 text-blue-600 font-black text-[10px] uppercase tracking-widest"
|
||||
onClick={() => {
|
||||
setPreviewDoc(doc);
|
||||
setShowPreviewModal(true);
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Audit Reports Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<h3 className="text-lg font-black text-slate-900 flex items-center gap-2">
|
||||
<ShieldCheck className="w-5 h-5 text-amber-600" /> Audit Findings & Reports
|
||||
</h3>
|
||||
<Badge variant="outline" className="bg-white text-slate-500 font-bold border-slate-200">
|
||||
{assignments.length} Reports Found
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{assignments.length === 0 ? (
|
||||
<div className="bg-slate-50 rounded-2xl border-2 border-dashed border-slate-200 p-12 text-center">
|
||||
<AlertCircle className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<h4 className="text-slate-900 font-bold">No Audit Reports Available</h4>
|
||||
<p className="text-slate-500 text-sm max-w-sm mx-auto mt-2 italic">The FDD team has not yet uploaded the audit reports for this application.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{assignments.map((assignment: any) => (
|
||||
<Card key={assignment.id} className="border-slate-200 shadow-sm overflow-hidden rounded-2xl group hover:border-amber-400 transition-all duration-300">
|
||||
<div className="bg-white p-6 border-b border-slate-50">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-amber-50 rounded-2xl flex items-center justify-center border border-amber-100 shadow-inner group-hover:scale-105 transition-transform">
|
||||
<ShieldCheck className="w-6 h-6 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-slate-900 font-black text-lg leading-none">FDD Audit Assignment</h4>
|
||||
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-widest mt-1">Status: {assignment.status}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={cn(
|
||||
"py-1 px-3 rounded-full text-[10px] font-black uppercase tracking-widest",
|
||||
assignment.status === 'completed' ? "bg-green-100 text-green-700" : "bg-amber-100 text-amber-700"
|
||||
)}>
|
||||
{assignment.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{!assignment.reports || assignment.reports.length === 0 ? (
|
||||
<div className="py-8 text-center bg-slate-50 rounded-xl border border-slate-100">
|
||||
<Clock className="w-6 h-6 text-slate-300 mx-auto mb-2" />
|
||||
<p className="text-slate-500 text-xs italic">Waiting for agency report submission...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
{assignment.reports.map((report: any) => (
|
||||
<div key={report.id} className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-2">
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Auditor Recommendation</Label>
|
||||
<div className={cn(
|
||||
"inline-flex items-center gap-2 px-4 py-2 rounded-xl border text-xs font-black shadow-sm",
|
||||
report.recommendation === 'Green' ? "bg-green-50 border-green-200 text-green-700" :
|
||||
report.recommendation === 'Amber' ? "bg-amber-50 border-amber-200 text-amber-700" :
|
||||
"bg-red-50 border-red-200 text-red-700"
|
||||
)}>
|
||||
<div className={cn("w-2.5 h-2.5 rounded-full animate-pulse",
|
||||
report.recommendation === 'Green' ? "bg-green-500" :
|
||||
report.recommendation === 'Amber' ? "bg-amber-500" : "bg-red-500"
|
||||
)} />
|
||||
{report.recommendation?.toUpperCase()} SIGNAL
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50/50 p-5 rounded-2xl border border-slate-100 relative">
|
||||
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-3 block">Findings Summary</Label>
|
||||
<p className="text-slate-700 text-sm leading-relaxed italic font-medium">
|
||||
"{report.findings || 'No detail findings provided.'}"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Available Documents</Label>
|
||||
{report.reportDocument ? (
|
||||
<div className="bg-white border-2 border-slate-100 rounded-2xl p-4 flex items-center justify-between hover:border-amber-400 transition-all hover:shadow-lg cursor-default">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center shadow-sm">
|
||||
<FileText className="w-6 h-6 text-red-500" />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<p className="text-slate-900 font-black text-sm truncate max-w-[140px] uppercase">{report.reportDocument.fileName}</p>
|
||||
<p className="text-slate-500 text-[10px] font-bold">SUBMITTED {new Date(report.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-xl"
|
||||
onClick={() => window.open(`http://localhost:5000/${report.reportDocument.filePath}`, '_blank')}
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-xl"
|
||||
onClick={() => {
|
||||
setPreviewDoc(report.reportDocument);
|
||||
setShowPreviewModal(true);
|
||||
}}
|
||||
>
|
||||
<Eye className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 bg-slate-50 rounded-2xl border border-dashed border-slate-200 text-center text-slate-400 text-xs font-bold uppercase tracking-tighter">
|
||||
No report file attached
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 flex items-center gap-3">
|
||||
{report.verifiedAt ? (
|
||||
<div className="flex items-center gap-2 bg-green-50 border border-green-100 px-4 py-2 rounded-full shadow-sm">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span className="text-[10px] font-black text-green-700 uppercase tracking-widest">Audited & Verified</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 bg-amber-50 border border-amber-100 px-4 py-2 rounded-full shadow-sm">
|
||||
<Clock className="w-4 h-4 text-amber-600" />
|
||||
<span className="text-[10px] font-black text-amber-700 uppercase tracking-widest">Pending Verification</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Sidebar */}
|
||||
<div className="space-y-6">
|
||||
<Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl sticky top-6">
|
||||
<CardHeader className="bg-slate-900 border-b border-slate-800 py-5">
|
||||
<CardTitle className="text-white text-sm font-black uppercase tracking-widest flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-amber-400" /> Finance Action
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="remarks" className="text-[10px] text-slate-400 font-black uppercase tracking-widest mb-2 block">Approval Remarks / Notes</Label>
|
||||
<Textarea
|
||||
id="remarks"
|
||||
placeholder="Enter your assessment or audit sign-off remarks here..."
|
||||
className="min-h-[150px] bg-slate-50 border-slate-200 rounded-xl focus:ring-amber-500 focus:border-amber-500 text-sm font-medium"
|
||||
value={approvalRemark}
|
||||
onChange={(e) => setApprovalRemark(e.target.value)}
|
||||
disabled={hasMadeDecision || isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!hasMadeDecision ? (
|
||||
!isReadOnly ? (
|
||||
<div className="space-y-3 pt-2">
|
||||
<Button
|
||||
className="w-full h-14 bg-amber-600 hover:bg-amber-700 text-white font-black uppercase tracking-widest rounded-xl shadow-lg shadow-amber-200/50 transition-all active:scale-95"
|
||||
onClick={() => handleDecision('Approved')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 animate-spin" /> Processing...
|
||||
</span>
|
||||
) : (
|
||||
<><CheckCircle className="w-5 h-5 mr-2" /> Approve Audit</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-12 text-amber-600 border-amber-200 hover:bg-amber-50 font-black uppercase tracking-widest rounded-xl"
|
||||
onClick={() => toast.info('Clarification request functionality coming soon')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<RotateCcw className="w-5 h-5 mr-2" /> Request Revision
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full h-12 text-red-600 hover:text-red-700 hover:bg-red-50 font-black uppercase tracking-widest rounded-xl"
|
||||
onClick={() => handleDecision('Rejected')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<XCircle className="w-5 h-5 mr-2" /> Disqualify Candidate
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 bg-slate-50 border border-slate-200 rounded-2xl text-center space-y-2">
|
||||
<Eye className="w-8 h-8 text-slate-400 mx-auto" />
|
||||
<h4 className="text-slate-800 font-black uppercase tracking-widest text-xs">Read-only View</h4>
|
||||
<p className="text-slate-500 text-[10px] font-bold">Only Finance team members can take actions on this stage.</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="p-6 bg-green-50 border border-green-100 rounded-2xl text-center space-y-2">
|
||||
<CheckCircle className="w-8 h-8 text-green-600 mx-auto" />
|
||||
<h4 className="text-green-800 font-black uppercase tracking-widest text-xs">Action Recorded</h4>
|
||||
<p className="text-green-700 text-[10px] font-bold">You have already submitted your decision for this stage.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 border-t border-slate-100">
|
||||
<h5 className="text-[10px] text-slate-400 font-black uppercase tracking-widest mb-4">Verification Policy</h5>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-3">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-1.5 shrink-0" />
|
||||
<p className="text-[10px] text-slate-500 font-bold leading-tight">Must review PDF audit report for financial discrepancies before approval.</p>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-1.5 shrink-0" />
|
||||
<p className="text-[10px] text-slate-500 font-bold leading-tight">Approval triggers the progression to Security Deposit payment state.</p>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-1.5 shrink-0" />
|
||||
<p className="text-[10px] text-slate-500 font-bold leading-tight">Remarks are mandatory for audit trail and compliance tracking.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-50 border-slate-200 rounded-2xl">
|
||||
<CardContent className="p-6 flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center shrink-0">
|
||||
<Clock className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-[10px] font-black uppercase tracking-widest text-slate-400">Current Progress</h5>
|
||||
<p className="text-slate-900 font-bold">75% Complete</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="worknotes">
|
||||
<Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl min-h-[600px] flex flex-col">
|
||||
<CardHeader className="bg-slate-50/50 border-b border-slate-100 py-4 px-6">
|
||||
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2"><MessageSquare className="w-4 h-4" /> Communication History</div>
|
||||
<Badge variant="outline" className="bg-white">Queries & Escalations</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 flex-1 overflow-y-auto max-h-[600px]">
|
||||
{workNotes.length === 0 ? (
|
||||
<div className="p-20 text-center text-slate-400 italic">No communication logged yet.</div>
|
||||
) : (
|
||||
<div className="divide-y divide-slate-100">
|
||||
{workNotes.map((note: any) => (
|
||||
<div key={note.id} className="p-6 hover:bg-slate-50 transition-colors">
|
||||
<div className="flex items-start gap-4">
|
||||
<Avatar className="h-10 w-10 border border-slate-200">
|
||||
<AvatarFallback className="bg-indigo-100 text-indigo-700 font-black text-xs">
|
||||
{note.author?.fullName?.substring(0, 2).toUpperCase() || 'SYS'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-black text-slate-900">{note.author?.fullName || 'System'}</h4>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase">{new Date(note.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">{note.author?.roleCode || 'RE Stakeholder'}</p>
|
||||
<div className="mt-4 p-4 bg-white rounded-xl border border-slate-100 text-sm text-slate-700 leading-relaxed shadow-sm">
|
||||
{note.noteText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<div className="p-6 bg-slate-50 border-t border-slate-100">
|
||||
<Label className="text-[10px] text-slate-400 font-black uppercase tracking-widest mb-2 block">Direct Query to FDD Agency</Label>
|
||||
<div className="flex gap-2">
|
||||
<Textarea
|
||||
placeholder="Need clarification from the FDD agency? Post a note here..."
|
||||
className="min-h-[60px] bg-white border-slate-200 rounded-xl text-sm"
|
||||
value={newNote}
|
||||
onChange={(e) => setNewNote(e.target.value)}
|
||||
disabled={isNoteSubmitting}
|
||||
/>
|
||||
<Button
|
||||
className="shrink-0 bg-blue-600 hover:bg-blue-700 h-auto self-stretch rounded-xl px-6"
|
||||
onClick={handlePostNote}
|
||||
disabled={isNoteSubmitting || !newNote.trim()}
|
||||
>
|
||||
{isNoteSubmitting ? <Clock className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="history">
|
||||
<Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl">
|
||||
<CardHeader className="bg-slate-50/50 border-b border-slate-100 py-4 px-6">
|
||||
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2">
|
||||
<History className="w-4 h-4" /> Complete Application Journey
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
<div className="space-y-6">
|
||||
{(application.progressTracking || []).map((step: any, idx: number) => (
|
||||
<div key={idx} className="flex gap-4 relative">
|
||||
{idx !== application.progressTracking.length - 1 && (
|
||||
<div className="absolute left-3 top-6 bottom-[-24px] w-0.5 bg-slate-100" />
|
||||
)}
|
||||
<div className={cn(
|
||||
"w-6 h-6 rounded-full flex items-center justify-center shrink-0 z-10",
|
||||
step.stageCompletedAt ? "bg-emerald-500 text-white" : "bg-slate-200 text-slate-400"
|
||||
)}>
|
||||
{step.stageCompletedAt ? <CheckCircle className="w-4 h-4" /> : <div className="w-1.5 h-1.5 rounded-full bg-current" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-slate-900">{step.stageName}</p>
|
||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">
|
||||
{step.stageCompletedAt ? `Completed ${new Date(step.stageCompletedAt).toLocaleDateString()}` : 'Pending'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Global Preview Modal */}
|
||||
<DocumentPreviewModal
|
||||
isOpen={showPreviewModal}
|
||||
onClose={() => setShowPreviewModal(false)}
|
||||
document={previewDoc}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Card, CardContent, CardDescription } from '../ui/card';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
@ -16,20 +16,21 @@ import {
|
||||
Clock,
|
||||
FileText,
|
||||
Calendar,
|
||||
User,
|
||||
MapPin,
|
||||
CreditCard
|
||||
CreditCard,
|
||||
ShieldCheck
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { onboardingService } from '../../services/onboarding.service';
|
||||
|
||||
interface FinanceOnboardingPageProps {
|
||||
onViewPaymentDetails?: (applicationId: string) => void;
|
||||
onViewAuditDetails?: (applicationId: string) => void;
|
||||
}
|
||||
|
||||
export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardingPageProps = {}) {
|
||||
export function FinanceOnboardingPage({ onViewPaymentDetails, onViewAuditDetails }: FinanceOnboardingPageProps = {}) {
|
||||
const [applications, setApplications] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'payments' | 'audits'>('payments');
|
||||
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'verified'>('pending');
|
||||
|
||||
useEffect(() => {
|
||||
@ -40,17 +41,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await onboardingService.getApplications();
|
||||
// Filter for applications that are in stages requiring finance verification
|
||||
const financeApps = data.filter((app: any) => {
|
||||
const s = app.overallStatus || app.status;
|
||||
const stage = app.currentStage;
|
||||
return [
|
||||
'LOI In Progress', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
|
||||
'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT',
|
||||
'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
|
||||
].includes(s) || stage === 'Finance';
|
||||
});
|
||||
setApplications(financeApps);
|
||||
setApplications(data);
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
toast.error('Failed to fetch applications');
|
||||
@ -62,34 +53,66 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
||||
const getRelevantPaymentStatus = (app: any) => {
|
||||
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment';
|
||||
const s = app.overallStatus || app.status;
|
||||
const relevantType = (s.includes('LOI') || s === 'PAYMENT_VERIFICATION') ? 'INITIAL' : 'FINAL';
|
||||
const relevantType = (s.includes('LOI') || s === 'PAYMENT_VERIFICATION' || s === 'Security Details') ? 'INITIAL' : 'FINAL';
|
||||
const deposit = app.securityDeposits.find((d: any) => d.depositType === relevantType);
|
||||
return deposit ? deposit.status : 'Awaiting Payment';
|
||||
};
|
||||
|
||||
const filteredApplications = applications.filter(app => {
|
||||
const getFddApprovalStatus = (app: any) => {
|
||||
// Check for Finance approval record specifically in LOI_APPROVAL stage
|
||||
const financeApproval = app.stageApprovals?.find((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'Finance');
|
||||
return financeApproval ? financeApproval.decision : 'Pending Review';
|
||||
};
|
||||
|
||||
// Filter for Payment Mode
|
||||
const paymentApps = applications.filter((app: any) => {
|
||||
const s = app.overallStatus || app.status;
|
||||
return [
|
||||
'LOI In Progress', 'Security Details', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
|
||||
'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT'
|
||||
].includes(s);
|
||||
});
|
||||
|
||||
// Filter for FDD Audit Mode
|
||||
const auditApps = applications.filter((app: any) => {
|
||||
const s = app.overallStatus || app.status;
|
||||
return s === 'LOI In Progress' || s === 'FDD Verification' || s === 'LOI_APPROVAL';
|
||||
});
|
||||
|
||||
const displayApps = activeTab === 'payments'
|
||||
? paymentApps.filter(app => {
|
||||
const status = getRelevantPaymentStatus(app);
|
||||
if (filterStatus === 'all') return true;
|
||||
if (filterStatus === 'pending') return status !== 'Verified';
|
||||
if (filterStatus === 'verified') return status === 'Verified';
|
||||
return true;
|
||||
})
|
||||
: auditApps.filter(app => {
|
||||
const status = getFddApprovalStatus(app);
|
||||
if (filterStatus === 'all') return true;
|
||||
if (filterStatus === 'pending') return status !== 'Approved';
|
||||
if (filterStatus === 'verified') return status === 'Approved';
|
||||
return true;
|
||||
});
|
||||
|
||||
const handleViewDetails = (appId: string) => {
|
||||
if (onViewPaymentDetails) {
|
||||
const handleAction = (appId: string) => {
|
||||
if (activeTab === 'audits') {
|
||||
if (onViewAuditDetails) {
|
||||
onViewAuditDetails(appId);
|
||||
}
|
||||
} else if (onViewPaymentDetails) {
|
||||
onViewPaymentDetails(appId);
|
||||
}
|
||||
};
|
||||
|
||||
const pendingCount = applications.filter(app => getRelevantPaymentStatus(app) !== 'Verified').length;
|
||||
const verifiedCount = applications.filter(app => getRelevantPaymentStatus(app) === 'Verified').length;
|
||||
const totalAmount = applications.length * 200000;
|
||||
const pendingPayments = paymentApps.filter(app => getRelevantPaymentStatus(app) !== 'Verified').length;
|
||||
const pendingAudits = auditApps.filter(app => getFddApprovalStatus(app) !== 'Approved').length;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-20 text-amber-600">
|
||||
<div className="flex items-center justify-center p-20 text-blue-600">
|
||||
<Clock className="w-8 h-8 animate-spin mr-3" />
|
||||
<span>Loading payment queue...</span>
|
||||
<span>Loading Finance Queue...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -97,165 +120,163 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between bg-white p-6 rounded-2xl border border-slate-100 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-1">Financial Compliance</h1>
|
||||
<p className="text-slate-600">Verify security deposits and advance payments for dealership onboarding</p>
|
||||
<h1 className="text-3xl font-bold text-slate-900 tracking-tight mb-1">Financial Operations</h1>
|
||||
<p className="text-slate-500">Manage payment verifications and FDD audit report reviews</p>
|
||||
</div>
|
||||
<Button onClick={fetchApplications} variant="outline" size="sm">
|
||||
Refresh List
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={fetchApplications} variant="outline" size="sm" className="bg-white hover:bg-slate-50">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
Sync Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mode Switcher */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="inline-flex p-1 bg-slate-100 rounded-xl">
|
||||
<Button
|
||||
variant={activeTab === 'payments' ? 'default' : 'ghost'}
|
||||
className={activeTab === 'payments' ? 'rounded-lg bg-white text-slate-900 shadow-sm hover:bg-white' : 'rounded-lg text-slate-600'}
|
||||
onClick={() => { setActiveTab('payments'); setFilterStatus('pending'); }}
|
||||
>
|
||||
<DollarSign className="w-4 h-4 mr-2" />
|
||||
Payments ({pendingPayments})
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'audits' ? 'default' : 'ghost'}
|
||||
className={activeTab === 'audits' ? 'rounded-lg bg-white text-slate-900 shadow-sm hover:bg-white' : 'rounded-lg text-slate-600'}
|
||||
onClick={() => { setActiveTab('audits'); setFilterStatus('pending'); }}
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
FDD Audit Reviews ({pendingAudits})
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="border-amber-200 bg-amber-50/30">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-amber-800">Pending Verification</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-3xl font-bold text-amber-700">{pendingCount}</div>
|
||||
<Clock className="w-8 h-8 text-amber-400" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-green-200 bg-green-50/30">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-green-800">Verified This Week</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-3xl font-bold text-green-700">{verifiedCount}</div>
|
||||
<CheckCircle className="w-8 h-8 text-green-400" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-blue-200 bg-blue-50/30">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-blue-800">Active Queue</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-3xl font-bold text-blue-700">{applications.length}</div>
|
||||
<FileText className="w-8 h-8 text-blue-400" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200 bg-slate-50/30">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-800">Estimated Total (Cr)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-3xl font-bold text-slate-700">₹{(totalAmount / 10000000).toFixed(2)}</div>
|
||||
<DollarSign className="w-8 h-8 text-slate-400" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex gap-2 bg-slate-100 p-1 rounded-lg w-fit">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={filterStatus === 'pending' ? 'default' : 'ghost'}
|
||||
variant={filterStatus === 'pending' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('pending')}
|
||||
className={filterStatus === 'pending' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600'}
|
||||
className={filterStatus === 'pending' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'}
|
||||
>
|
||||
Pending Verifications ({pendingCount})
|
||||
Pending
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'verified' ? 'default' : 'ghost'}
|
||||
variant={filterStatus === 'verified' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('verified')}
|
||||
className={filterStatus === 'verified' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600'}
|
||||
className={filterStatus === 'verified' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'}
|
||||
>
|
||||
Verified ({verifiedCount})
|
||||
Completed
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'all' ? 'default' : 'ghost'}
|
||||
variant={filterStatus === 'all' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('all')}
|
||||
className={filterStatus === 'all' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600'}
|
||||
className={filterStatus === 'all' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'}
|
||||
>
|
||||
View All
|
||||
All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Applications Table */}
|
||||
<Card className="border-none shadow-md">
|
||||
<Card className="border-none shadow-xl overflow-hidden rounded-2xl bg-white">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader className="bg-slate-50">
|
||||
<TableRow>
|
||||
<TableHead className="w-[180px]">Application ID</TableHead>
|
||||
<TableHead>Applicant Name</TableHead>
|
||||
<TableHeader className="bg-slate-50/50">
|
||||
<TableRow className="border-b border-slate-100 uppercase text-[10px] tracking-wider font-bold text-slate-400">
|
||||
<TableHead className="py-4 pl-6">Application Details</TableHead>
|
||||
<TableHead>Location</TableHead>
|
||||
<TableHead>Payment Stage</TableHead>
|
||||
<TableHead>{activeTab === 'payments' ? 'Payment Stage' : 'Audit Stage'}</TableHead>
|
||||
<TableHead>Current Status</TableHead>
|
||||
<TableHead className="text-right">Action</TableHead>
|
||||
<TableHead className="text-right pr-6">Workflow Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredApplications.length > 0 ? (
|
||||
filteredApplications.map((app) => (
|
||||
<TableRow key={app.id} className="hover:bg-slate-50/50 transition-colors">
|
||||
<TableCell className="font-mono text-sm font-bold">{app.applicationId || app.id}</TableCell>
|
||||
<TableCell>
|
||||
{displayApps.length > 0 ? (
|
||||
displayApps.map((app) => {
|
||||
const statusLabel = activeTab === 'payments' ? getRelevantPaymentStatus(app) : getFddApprovalStatus(app);
|
||||
return (
|
||||
<TableRow key={app.id} className="hover:bg-blue-50/20 group transition-all">
|
||||
<TableCell className="py-4 pl-6">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-mono text-xs font-bold text-blue-600 mb-1">{app.applicationId || app.id}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-slate-400" />
|
||||
<span className="font-medium text-slate-900">{app.applicantName}</span>
|
||||
<span className="font-semibold text-slate-900">{app.applicantName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2 text-slate-600">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span>{app.city || app.preferredLocation}, {app.state}</span>
|
||||
<div className="flex flex-col text-sm">
|
||||
<span className="text-slate-600 leading-none">{app.city}</span>
|
||||
<span className="text-slate-400 text-xs mt-1">{app.state}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{activeTab === 'payments' ? (
|
||||
<>
|
||||
<CreditCard className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm">
|
||||
{app.status === 'LOI_APPROVAL' || app.status === 'PAYMENT_VERIFICATION' ? 'Initial Deposit (₹2L)' : 'Final Deposit (₹15L)'}
|
||||
<span className="text-sm font-medium">
|
||||
{app.overallStatus === 'LOI Issued' || app.overallStatus === 'Security Details' ? 'Initial (₹2L)' : 'Final (₹15L)'}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShieldCheck className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm font-medium">FDD Report Approval</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
className={
|
||||
getRelevantPaymentStatus(app) === 'Verified' ? 'bg-green-100 text-green-700 border-green-200' :
|
||||
getRelevantPaymentStatus(app) === 'Rejected' ? 'bg-red-100 text-red-700 border-red-200' :
|
||||
'bg-amber-100 text-amber-700 border-amber-200'
|
||||
statusLabel === 'Verified' || statusLabel === 'Approved' ? 'bg-emerald-50 text-emerald-700 border-emerald-100 px-3 py-1 rounded-full' :
|
||||
statusLabel === 'Rejected' ? 'bg-rose-50 text-rose-700 border-rose-100 px-3 py-1 rounded-full' :
|
||||
'bg-amber-50 text-amber-700 border-amber-100 px-3 py-1 rounded-full'
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
{getRelevantPaymentStatus(app)}
|
||||
{statusLabel}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<TableCell className="text-right pr-6">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={getRelevantPaymentStatus(app) === 'Verified' ? 'outline' : 'default'}
|
||||
className={getRelevantPaymentStatus(app) !== 'Verified' ? 'bg-amber-600 hover:bg-amber-700' : ''}
|
||||
onClick={() => handleViewDetails(app.applicationId || app.id)}
|
||||
variant={statusLabel === 'Verified' || statusLabel === 'Approved' ? 'outline' : 'default'}
|
||||
className={statusLabel !== 'Verified' && statusLabel !== 'Approved'
|
||||
? (activeTab === 'payments' ? 'bg-blue-600 hover:bg-blue-700' : 'bg-indigo-600 hover:bg-indigo-700 shadow-md')
|
||||
: 'bg-white text-slate-600 border-slate-200'}
|
||||
onClick={() => handleAction(app.applicationId || app.id)}
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
{app.paymentStatus === 'Verified' ? 'View Details' : 'Verify Now'}
|
||||
{activeTab === 'payments' ? (
|
||||
<>
|
||||
<DollarSign className="w-4 h-4 mr-2" />
|
||||
{statusLabel === 'Verified' ? 'View Receipt' : 'Record Payment'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShieldCheck className="w-4 h-4 mr-2" />
|
||||
{statusLabel === 'Approved' ? 'View Decision' : 'Review Audit'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-40 text-center text-slate-500">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<CheckCircle className="w-10 h-10 text-slate-200" />
|
||||
<p>No applications found matching this filter.</p>
|
||||
<TableCell colSpan={6} className="h-48 text-center text-slate-400">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-6 h-6 text-slate-200" />
|
||||
</div>
|
||||
<p className="text-sm">No applications pending in the {activeTab} queue</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
import { toast } from 'sonner';
|
||||
import { onboardingService } from '../../services/onboarding.service';
|
||||
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
|
||||
// Simple helper for class merging if 'cn' is not available
|
||||
const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
|
||||
@ -213,7 +214,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
|
||||
</p>
|
||||
<p className="text-sm text-slate-600">
|
||||
{activeDeposit?.status === 'Verified'
|
||||
? `Verified on ${new Date(activeDeposit.verifiedAt).toLocaleDateString()}`
|
||||
? `Verified on ${formatDateTime(activeDeposit.verifiedAt)}`
|
||||
: activeDeposit?.status === 'Rejected'
|
||||
? 'Payment Rejected'
|
||||
: 'Awaiting Verification'}
|
||||
@ -301,7 +302,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500">Verified By</Label>
|
||||
<p className="text-slate-900">{activeDeposit.verifiedBy || 'N/A'}</p>
|
||||
<p className="text-slate-900">{activeDeposit.verifier?.fullName || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { API } from '../../api/API';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@ -149,7 +150,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
||||
<div>
|
||||
<p className="text-sm text-slate-500 mb-1">Applied Date</p>
|
||||
<p className="font-medium text-slate-900">
|
||||
{details.createdAt ? new Date(details.createdAt).toLocaleDateString() : '-'}
|
||||
{details.createdAt ? formatDateTime(details.createdAt) : '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { DollarSign, FileText, CheckCircle, Plus, Trash2, Save, Calculator, Clock, TrendingUp, TrendingDown, Wallet, AlertCircle, Receipt } from 'lucide-react';
|
||||
import { DollarSign, FileText, CheckCircle, Plus, Trash2, Save, Calculator, Clock, TrendingUp, TrendingDown, ShieldCheck } from 'lucide-react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
|
||||
import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
@ -18,6 +18,7 @@ interface FinanceDashboardProps {
|
||||
currentUser: User | null;
|
||||
onNavigate?: (view: string) => void;
|
||||
onViewPaymentDetails?: (applicationId: string) => void;
|
||||
onViewAuditDetails?: (applicationId: string) => void;
|
||||
onViewFnFDetails?: (fnfId: string) => void;
|
||||
}
|
||||
|
||||
@ -59,7 +60,7 @@ interface FinanceLineItem {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails, onViewFnFDetails }: FinanceDashboardProps) {
|
||||
export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAuditDetails, onViewFnFDetails }: FinanceDashboardProps) {
|
||||
const [applications, setApplications] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@ -89,14 +90,6 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
|
||||
}
|
||||
};
|
||||
|
||||
const [paymentDialog, setPaymentDialog] = useState(false);
|
||||
const [selectedOnboarding, setSelectedOnboarding] = useState<any>(null);
|
||||
const [paymentDetails, setPaymentDetails] = useState({
|
||||
transactionId: '',
|
||||
receivedDate: '',
|
||||
remarks: ''
|
||||
});
|
||||
|
||||
const [fnfDialog, setFnfDialog] = useState(false);
|
||||
const [fnfDetailsDialog, setFnfDetailsDialog] = useState(false);
|
||||
const [selectedFnF, setSelectedFnF] = useState<any>(null);
|
||||
@ -109,16 +102,6 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
|
||||
});
|
||||
const [finalRemarks, setFinalRemarks] = useState('');
|
||||
|
||||
const handleVerifyPayment = () => {
|
||||
if (!paymentDetails.transactionId || !paymentDetails.receivedDate) {
|
||||
toast.error('Please fill in all required fields');
|
||||
return;
|
||||
}
|
||||
toast.success('Payment verified successfully');
|
||||
setPaymentDialog(false);
|
||||
setPaymentDetails({ transactionId: '', receivedDate: '', remarks: '' });
|
||||
setSelectedOnboarding(null);
|
||||
};
|
||||
|
||||
const handleAddLineItem = () => {
|
||||
if (!newLineItem.department || !newLineItem.description || !newLineItem.amount) {
|
||||
@ -175,31 +158,6 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
|
||||
setFinalRemarks('');
|
||||
};
|
||||
|
||||
const calculateSettlement = (data: any) => {
|
||||
const receivables =
|
||||
(data.outstandingInvoices || 0) +
|
||||
(data.serviceDues || 0) +
|
||||
(data.partsDues || 0) +
|
||||
(data.advancesGiven || 0) +
|
||||
(data.penalties || 0) +
|
||||
(data.otherCharges || 0);
|
||||
|
||||
const payables =
|
||||
(data.securityDeposit || 0) +
|
||||
(data.inventoryValue || 0) +
|
||||
(data.equipmentValue || 0);
|
||||
|
||||
const deductions = data.warrantyPending || 0;
|
||||
const netAmount = payables - receivables - deductions;
|
||||
|
||||
return {
|
||||
receivables,
|
||||
payables,
|
||||
deductions,
|
||||
settlementAmount: Math.abs(netAmount),
|
||||
settlementType: netAmount >= 0 ? 'Payable to Dealer' : 'Receivable from Dealer'
|
||||
};
|
||||
};
|
||||
|
||||
const getRelevantPaymentStatus = (app: any) => {
|
||||
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment';
|
||||
@ -213,6 +171,7 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
|
||||
|
||||
const pendingOnboarding = applications.filter(app => getRelevantPaymentStatus(app) !== 'Verified');
|
||||
const verifiedOnboarding = applications.filter(app => getRelevantPaymentStatus(app) === 'Verified');
|
||||
const pendingAudits = applications.filter(app => app.status === 'LOI_APPROVAL' || app.overallStatus === 'LOI In Progress');
|
||||
const pendingFnF = mockFnFCases.filter(f => !f.hasFinanceSummary);
|
||||
const completedFnF = mockFnFCases.filter(f => f.hasFinanceSummary);
|
||||
|
||||
@ -238,7 +197,29 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-3">
|
||||
<Card
|
||||
className="cursor-pointer hover:shadow-lg transition-shadow border-amber-200 bg-amber-50/20"
|
||||
onClick={() => {
|
||||
if (pendingAudits.length > 0 && onViewAuditDetails) {
|
||||
onViewAuditDetails(pendingAudits[0].applicationId || pendingAudits[0].id);
|
||||
} else {
|
||||
onNavigate?.('finance-onboarding');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription className="text-amber-600 font-bold">Pending Audits</CardDescription>
|
||||
<CardTitle className="text-3xl text-amber-600">{pendingAudits.length}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-slate-600 text-xs font-medium">FDD Sign-offs</p>
|
||||
<Button variant="link" className="p-0 h-auto text-amber-600 mt-2 text-xs">
|
||||
Review Now →
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="cursor-pointer hover:shadow-lg transition-shadow border-yellow-200"
|
||||
onClick={() => onNavigate?.('finance-onboarding')}
|
||||
@ -384,6 +365,20 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
View Details
|
||||
</Button>
|
||||
{app.status === 'LOI_APPROVAL' || app.overallStatus === 'LOI In Progress' ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-amber-600 hover:bg-amber-700 font-bold"
|
||||
onClick={() => {
|
||||
if (onViewAuditDetails) {
|
||||
onViewAuditDetails(app.applicationId || app.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ShieldCheck className="w-4 h-4 mr-2" />
|
||||
Review Audit
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700 font-bold"
|
||||
@ -396,6 +391,7 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Verify Payment
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@ -123,6 +123,7 @@ export interface Application {
|
||||
regionId?: string;
|
||||
areaId?: string;
|
||||
districtId?: string;
|
||||
fddAssignments?: any[];
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user