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:
laxman h 2026-04-06 19:12:27 +05:30
parent 3c54b8499e
commit c37ca50d4c
12 changed files with 1517 additions and 481 deletions

View File

@ -30,6 +30,7 @@ import { FnFDetails } from './components/applications/FnFDetails';
import { FinanceOnboardingPage } from './components/applications/FinanceOnboardingPage'; import { FinanceOnboardingPage } from './components/applications/FinanceOnboardingPage';
import { FinanceFnFPage } from './components/applications/FinanceFnFPage'; import { FinanceFnFPage } from './components/applications/FinanceFnFPage';
import { FinancePaymentDetailsPage } from './components/applications/FinancePaymentDetailsPage'; import { FinancePaymentDetailsPage } from './components/applications/FinancePaymentDetailsPage';
import { FinanceFddDetailPage } from './components/applications/FinanceFddDetailPage';
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage'; import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
import { MasterPage } from './components/applications/MasterPage'; import { MasterPage } from './components/applications/MasterPage';
import { UserManagementPage } from './components/admin/UserManagementPage'; import { UserManagementPage } from './components/admin/UserManagementPage';
@ -205,7 +206,7 @@ export default function App() {
{/* Dashboards */} {/* Dashboards */}
<Route path="/dashboard" element={ <Route path="/dashboard" element={
currentUser?.role === 'Finance Admin' || currentUser?.role === 'Finance' ? 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' ? currentUser?.role === 'Dealer' ?
<DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> : <DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> :
<Dashboard 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" 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="/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-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" 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')} />} /> <Route path="/finance-fnf/:id" element={<FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />} />

View File

@ -12,7 +12,6 @@ import {
} from '../ui/select'; } from '../ui/select';
import { import {
Search, Search,
Filter,
Download, Download,
Grid3x3, Grid3x3,
List, List,
@ -35,6 +34,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
import { Label } from '../ui/label'; import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { formatDateTime } from '../ui/utils';
interface AllApplicationsPageProps { interface AllApplicationsPageProps {
onViewDetails: (id: string) => void; onViewDetails: (id: string) => void;
@ -130,14 +130,17 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
'Questionnaire Pending': 'bg-yellow-100 text-yellow-800', 'Questionnaire Pending': 'bg-yellow-100 text-yellow-800',
'Questionnaire Completed': 'bg-cyan-100 text-cyan-800', 'Questionnaire Completed': 'bg-cyan-100 text-cyan-800',
'Shortlisted': 'bg-purple-100 text-purple-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 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 Approved': 'bg-green-100 text-green-800',
'Level 2 Recommended': 'bg-teal-100 text-teal-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', 'FDD Verification': 'bg-indigo-100 text-indigo-800',
'Payment Pending': 'bg-amber-100 text-amber-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', 'LOI Issued': 'bg-sky-100 text-sky-800',
'Dealer Code Generation': 'bg-purple-100 text-purple-800', 'Dealer Code Generation': 'bg-purple-100 text-purple-800',
'Architecture Team Assigned': 'bg-blue-100 text-blue-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 Check': 'bg-emerald-100 text-emerald-800',
'Statutory Partnership': 'bg-emerald-100 text-emerald-800', 'Statutory Partnership': 'bg-emerald-100 text-emerald-800',
'Statutory Firm Reg': '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 Virtual Code': 'bg-emerald-100 text-emerald-800',
'Statutory Domain': 'bg-emerald-100 text-emerald-800', 'Statutory Domain': 'bg-emerald-100 text-emerald-800',
'Statutory MSD': 'bg-emerald-100 text-emerald-800', 'Statutory MSD': 'bg-emerald-100 text-emerald-800',
'Statutory LOI Ack': '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 In Progress': 'bg-violet-100 text-violet-800',
'EOR Complete': 'bg-violet-100 text-violet-800',
'LOA Pending': 'bg-pink-100 text-pink-800', 'LOA Pending': 'bg-pink-100 text-pink-800',
'Inauguration': 'bg-amber-100 text-amber-800',
'Approved': 'bg-green-100 text-green-800', 'Approved': 'bg-green-100 text-green-800',
'Rejected': 'bg-red-100 text-red-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'; return colors[status] || 'bg-gray-100 text-gray-800';
}; };
@ -375,7 +382,7 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-slate-600">{app.submissionDate}</span> <span className="text-slate-600">{formatDateTime(app.submissionDate)}</span>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@ -3,6 +3,7 @@ import { Button } from '../ui/button';
import { Progress } from '../ui/progress'; import { Progress } from '../ui/progress';
import { Application } from '../../lib/mock-data'; import { Application } from '../../lib/mock-data';
import { MapPin, Phone, Mail, Award, Calendar, Building } from 'lucide-react'; import { MapPin, Phone, Mail, Award, Calendar, Building } from 'lucide-react';
import { formatDateTime } from '../ui/utils';
interface ApplicationCardProps { interface ApplicationCardProps {
application: Application; application: Application;
@ -106,7 +107,7 @@ export function ApplicationCard({ application, onViewDetails }: ApplicationCardP
<div className="flex items-center gap-2 text-slate-600"> <div className="flex items-center gap-2 text-slate-600">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
<span>Submitted: {new Date(application.submissionDate).toLocaleDateString()}</span> <span>Submitted: {formatDateTime(application.submissionDate)}</span>
</div> </div>
</div> </div>
@ -123,7 +124,7 @@ export function ApplicationCard({ application, onViewDetails }: ApplicationCardP
{application.deadline && application.status === 'Questionnaire Pending' && ( {application.deadline && application.status === 'Questionnaire Pending' && (
<div className="mb-4 p-3 bg-orange-50 border border-orange-200 rounded-md"> <div className="mb-4 p-3 bg-orange-50 border border-orange-200 rounded-md">
<p className="text-orange-800"> <p className="text-orange-800">
Deadline: {new Date(application.deadline).toLocaleDateString()} Deadline: {formatDateTime(application.deadline)}
</p> </p>
</div> </div>
)} )}

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; 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 { toast } from 'sonner';
import { Application, ApplicationStatus } from '../../lib/mock-data'; import { Application, ApplicationStatus } from '../../lib/mock-data';
import { onboardingService } from '../../services/onboarding.service'; import { onboardingService } from '../../services/onboarding.service';
@ -8,7 +8,7 @@ import { eorService } from '../../services/eor.service';
import QuestionnaireResponseView from './QuestionnaireResponseView'; import QuestionnaireResponseView from './QuestionnaireResponseView';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../../store'; import { RootState } from '../../store';
import { cn } from '@/components/ui/utils'; import { cn, formatDateTime } from '@/components/ui/utils';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
@ -263,8 +263,8 @@ export function ApplicationDetails() {
// Helper to find stage date // Helper to find stage date
const getStageDate = (stageName: string) => { const getStageDate = (stageName: string) => {
const stage = data.progressTracking?.find((p: any) => p.stageName === stageName); const stage = data.progressTracking?.find((p: any) => p.stageName === stageName);
return stage?.stageCompletedAt ? new Date(stage.stageCompletedAt).toISOString().split('T')[0] : return stage?.stageCompletedAt ? new Date(stage.stageCompletedAt).toISOString() :
stage?.stageStartedAt ? new Date(stage.stageStartedAt).toISOString().split('T')[0] : undefined; stage?.stageStartedAt ? new Date(stage.stageStartedAt).toISOString() : undefined;
}; };
// Map backend data to frontend Application interface // Map backend data to frontend Application interface
@ -301,7 +301,7 @@ export function ApplicationDetails() {
ownRoyalEnfield: data.ownRoyalEnfield, ownRoyalEnfield: data.ownRoyalEnfield,
address: data.address, address: data.address,
// Map timeline dates from progressTracking // 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'), questionnaireDate: getStageDate('Questionnaire'),
shortlistDate: getStageDate('Shortlist'), shortlistDate: getStageDate('Shortlist'),
level1InterviewDate: getStageDate('1st Level Interview'), level1InterviewDate: getStageDate('1st Level Interview'),
@ -318,7 +318,7 @@ export function ApplicationDetails() {
loaDate: getStageDate('LOA'), loaDate: getStageDate('LOA'),
eorCompleteDate: getStageDate('EOR Complete'), eorCompleteDate: getStageDate('EOR Complete'),
inaugurationDate: getStageDate('Inauguration'), 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 || [], progressTracking: data.progressTracking || [],
participants: data.participants || [], participants: data.participants || [],
dealerCode: data.dealerCode, dealerCode: data.dealerCode,
@ -327,6 +327,7 @@ export function ApplicationDetails() {
areaId: data.areaId, areaId: data.areaId,
districtId: data.districtId, districtId: data.districtId,
stageApprovals: data.stageApprovals || [], stageApprovals: data.stageApprovals || [],
fddAssignments: data.fddAssignments || [],
}; };
setApplication(mappedApp); setApplication(mappedApp);
} catch (error) { } catch (error) {
@ -387,7 +388,8 @@ export function ApplicationDetails() {
} }
}, [applicationId]); }, [applicationId]);
const [activeTab, setActiveTab] = useState('questionnaire'); const routerLocation = useLocation();
const [activeTab, setActiveTab] = useState(routerLocation.state?.activeTab || 'questionnaire');
const [showApproveModal, setShowApproveModal] = useState(false); const [showApproveModal, setShowApproveModal] = useState(false);
const [showOnboardModal, setShowOnboardModal] = useState(false); const [showOnboardModal, setShowOnboardModal] = useState(false);
const [isOnboarding, setIsOnboarding] = useState(false); const [isOnboarding, setIsOnboarding] = useState(false);
@ -433,6 +435,9 @@ export function ApplicationDetails() {
const [architectureStatus, setArchitectureStatus] = useState<string>('COMPLETED'); const [architectureStatus, setArchitectureStatus] = useState<string>('COMPLETED');
const [architectureRemarks, setArchitectureRemarks] = useState<string>(''); const [architectureRemarks, setArchitectureRemarks] = useState<string>('');
const [isUpdatingArchitecture, setIsUpdatingArchitecture] = useState(false); const [isUpdatingArchitecture, setIsUpdatingArchitecture] = useState(false);
const [isAssigningParticipant, setIsAssigningParticipant] = useState(false);
const [isApproving, setIsApproving] = useState(false);
const [isRejecting, setIsRejecting] = useState(false);
// KT Matrix State // KT Matrix State
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({}); const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
@ -1229,73 +1234,74 @@ export function ApplicationDetails() {
}; };
const handleApprove = async () => { const handleApprove = async () => {
// Check if user has an active interview to approve
const activeInterview = interviews.find(i =>
i.status !== 'Completed' && i.status !== 'Cancelled' &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
// Handle File Upload if exists
if (approvalFile && applicationId) {
try {
const formData = new FormData();
formData.append('file', approvalFile);
formData.append('documentType', 'Approval Attachment');
// Determine stage based on active interview
let stageName = null;
if (activeInterview) {
if (activeInterview.level === 1 || activeInterview.level === '1') stageName = '1st Level Interview';
else if (activeInterview.level === 2 || activeInterview.level === '2') stageName = '2nd Level Interview';
else if (activeInterview.level === 3 || activeInterview.level === '3') stageName = '3rd Level Interview';
}
// Fallback for document stage if it's a general approval
if (!stageName) {
if (application.status === 'Shortlisted' || application.status === 'Level 1 Interview Pending') stageName = '1st Level Interview';
else if (application.status === 'Level 1 Approved' || application.status === 'Level 2 Interview Pending') stageName = '2nd Level Interview';
else if (application.status === 'Level 2 Approved' || application.status === 'Level 3 Interview Pending') stageName = '3rd Level Interview';
}
if (stageName) {
formData.append('stage', stageName);
}
await onboardingService.uploadDocument(applicationId, formData);
toast.success('Document uploaded with approval');
} catch (error) {
console.error('Failed to upload approval document', error);
toast.error('Failed to upload document');
}
}
if (activeInterview) {
try {
await onboardingService.updateInterviewDecision({
interviewId: activeInterview.id,
decision: 'Approved',
remarks: approvalRemark
});
toast.success('Interview approved successfully');
setShowApproveModal(false);
setApprovalRemark('');
setApprovalFile(null); // Reset file
fetchInterviews();
// Refresh application to check if status updated
fetchApplication();
return;
} catch (error) {
toast.error('Failed to approve interview');
return;
}
}
if (!approvalRemark.trim()) {
toast.warning('Please enter a remark');
return;
}
try { try {
setIsApproving(true);
// Check if user has an active interview to approve
const activeInterview = interviews.find(i =>
i.status !== 'Completed' && i.status !== 'Cancelled' &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
// Handle File Upload if exists
if (approvalFile && applicationId) {
try {
const formData = new FormData();
formData.append('file', approvalFile);
formData.append('documentType', 'Approval Attachment');
// Determine stage based on active interview
let stageName = null;
if (activeInterview) {
if (activeInterview.level === 1 || activeInterview.level === '1') stageName = '1st Level Interview';
else if (activeInterview.level === 2 || activeInterview.level === '2') stageName = '2nd Level Interview';
else if (activeInterview.level === 3 || activeInterview.level === '3') stageName = '3rd Level Interview';
}
// Fallback for document stage if it's a general approval
if (!stageName) {
if (application.status === 'Shortlisted' || application.status === 'Level 1 Interview Pending') stageName = '1st Level Interview';
else if (application.status === 'Level 1 Approved' || application.status === 'Level 2 Interview Pending') stageName = '2nd Level Interview';
else if (application.status === 'Level 2 Approved' || application.status === 'Level 3 Interview Pending') stageName = '3rd Level Interview';
}
if (stageName) {
formData.append('stage', stageName);
}
await onboardingService.uploadDocument(applicationId, formData);
toast.success('Document uploaded with approval');
} catch (error) {
console.error('Failed to upload approval document', error);
toast.error('Failed to upload document');
}
}
if (activeInterview) {
try {
await onboardingService.updateInterviewDecision({
interviewId: activeInterview.id,
decision: 'Approved',
remarks: approvalRemark
});
toast.success('Interview approved successfully');
setShowApproveModal(false);
setApprovalRemark('');
setApprovalFile(null); // Reset file
fetchInterviews();
// Refresh application to check if status updated
fetchApplication();
return;
} catch (error) {
toast.error('Failed to approve interview');
return;
}
}
if (!approvalRemark.trim()) {
toast.warning('Please enter a remark');
return;
}
// Application level approval - Robust State Machine // Application level approval - Robust State Machine
let newStatus = application.status; let newStatus = application.status;
@ -1398,41 +1404,44 @@ export function ApplicationDetails() {
} catch (error) { } catch (error) {
console.error('Approval error:', error); console.error('Approval error:', error);
toast.error('Failed to process approval'); toast.error('Failed to process approval');
} finally {
setIsApproving(false);
} }
}; };
const handleReject = async () => { const handleReject = async () => {
// Check if user has an active interview to reject try {
const activeInterview = interviews.find(i => setIsRejecting(true);
i.status !== 'Completed' && i.status !== 'Cancelled' && // Check if user has an active interview to reject
i.participants?.some((p: any) => p.userId === currentUser?.id) const activeInterview = interviews.find(i =>
); i.status !== 'Completed' && i.status !== 'Cancelled' &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
if (activeInterview) { if (activeInterview) {
try { try {
await onboardingService.updateInterviewDecision({ await onboardingService.updateInterviewDecision({
interviewId: activeInterview.id, interviewId: activeInterview.id,
decision: 'Rejected', decision: 'Rejected',
remarks: rejectionReason remarks: rejectionReason
}); });
toast.success('Interview rejected'); toast.success('Interview rejected');
setShowRejectModal(false); setShowRejectModal(false);
setRejectionReason(''); setRejectionReason('');
fetchInterviews(); fetchInterviews();
fetchApplication(); fetchApplication();
return; return;
} catch (error) { } catch (error) {
toast.error('Failed to reject interview'); toast.error('Failed to reject interview');
return;
}
}
if (!rejectionReason.trim()) {
toast.warning('Please enter a reason for rejection');
return; return;
} }
}
if (!rejectionReason.trim()) {
toast.warning('Please enter a reason for rejection');
return;
}
try {
const policyManagedStages: { [key: string]: string } = { const policyManagedStages: { [key: string]: string } = {
'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1', 'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1',
'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2', 'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2',
@ -1466,6 +1475,8 @@ export function ApplicationDetails() {
} catch (error) { } catch (error) {
console.error('Rejection error:', error); console.error('Rejection error:', error);
toast.error('Failed to process rejection'); toast.error('Failed to process rejection');
} finally {
setIsRejecting(false);
} }
}; };
@ -1518,11 +1529,12 @@ export function ApplicationDetails() {
return; return;
} }
try { try {
setIsAssigningParticipant(true);
await onboardingService.addParticipant({ await onboardingService.addParticipant({
requestId: applicationId, requestId: applicationId,
requestType: 'application', requestType: 'application',
userId: selectedUser, userId: selectedUser,
participantType: 'contributor' participantType: participantType || 'contributor'
}); });
toast.success('User assigned successfully!'); toast.success('User assigned successfully!');
// Refresh application data // Refresh application data
@ -1531,6 +1543,8 @@ export function ApplicationDetails() {
setShowAssignModal(false); setShowAssignModal(false);
} catch (error) { } catch (error) {
toast.error('Failed to assign user'); toast.error('Failed to assign user');
} finally {
setIsAssigningParticipant(false);
} }
}; };
@ -1624,18 +1638,175 @@ export function ApplicationDetails() {
'LOA Pending', 'EOR Complete', 'Inauguration' 'LOA Pending', 'EOR Complete', 'Inauguration'
].includes(application.status); ].includes(application.status);
const finalDepositVerified = getDeposit('FINAL')?.status === 'Verified';
const isLoaLocked = application.status === 'LOA Pending' && !finalDepositVerified;
// Show Approve/Reject if: // Show Approve/Reject if:
// 1. It's an interview and feedback is submitted AND no decision made yet // 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 // 2. OR it's an administrative stage and user is Admin AND hasn't made a decision yet
const shouldShowApproveReject = const shouldShowApproveReject =
(!hasMadeDecisionForUser && hasSubmittedFeedbackForActive) || !isLoaLocked && (
(isAdmin && isAdministrativeStage && !hasMadeStageDecision); (!hasMadeDecisionForUser && hasSubmittedFeedbackForActive) ||
(isAdmin && isAdministrativeStage && !hasMadeStageDecision)
);
const shouldShowDecisionMessage = hasMadeDecisionForUser && (!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 ( return (
<div className="space-y-6"> <div className="space-y-6">
@ -1828,6 +1999,7 @@ export function ApplicationDetails() {
<TabsTrigger value="progress" className="min-w-[80px]">Progress</TabsTrigger> <TabsTrigger value="progress" className="min-w-[80px]">Progress</TabsTrigger>
<TabsTrigger value="documents" className="min-w-[100px]">Documents</TabsTrigger> <TabsTrigger value="documents" className="min-w-[100px]">Documents</TabsTrigger>
<TabsTrigger value="interviews" className="min-w-[100px]">Interviews</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="eor" className="min-w-[120px]">EOR Checklist</TabsTrigger>
<TabsTrigger value="payments" className="min-w-[100px]">Payments</TabsTrigger> <TabsTrigger value="payments" className="min-w-[100px]">Payments</TabsTrigger>
<TabsTrigger value="audit" className="min-w-[100px]">Audit Trail</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"> <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 === 'active' && 'In Progress'}
{stage.status === 'pending' && 'Pending'} {stage.status === 'pending' && 'Pending'}
</p> </p>
@ -2139,7 +2311,7 @@ export function ApplicationDetails() {
); );
})()} })()}
<p className="text-slate-400 text-[10px] mt-1"> <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 === 'active' && 'Evaluating'}
{branchStage.status === 'pending' && 'Pending'} {branchStage.status === 'pending' && 'Pending'}
</p> </p>
@ -2381,6 +2553,10 @@ export function ApplicationDetails() {
)} )}
</TabsContent> </TabsContent>
<TabsContent value="fdd" className="space-y-6">
{renderFddAuditContent()}
</TabsContent>
{/* EOR Checklist Tab */} {/* EOR Checklist Tab */}
<TabsContent value="eor" className="space-y-4"> <TabsContent value="eor" className="space-y-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
@ -2795,6 +2971,16 @@ export function ApplicationDetails() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{/* Show Approve/Reject block */} {/* 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 && ( {shouldShowApproveReject && (
<> <>
<Button <Button
@ -3019,8 +3205,19 @@ export function ApplicationDetails() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<Button className="w-full bg-amber-600 hover:bg-amber-700" onClick={handleAddParticipant}> <Button
Assign User 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> </Button>
</div> </div>
</DialogContent> </DialogContent>
@ -3065,14 +3262,23 @@ export function ApplicationDetails() {
variant="outline" variant="outline"
className="flex-1" className="flex-1"
onClick={() => setShowApproveModal(false)} onClick={() => setShowApproveModal(false)}
disabled={isApproving}
> >
Cancel Cancel
</Button> </Button>
<Button <Button
className="flex-1 bg-green-600 hover:bg-green-700" className="flex-1 bg-green-600 hover:bg-green-700"
onClick={handleApprove} onClick={handleApprove}
disabled={isApproving}
> >
Submit Approval {isApproving ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Approving...
</>
) : (
'Submit Approval'
)}
</Button> </Button>
</div> </div>
</div> </div>
@ -3171,6 +3377,7 @@ export function ApplicationDetails() {
variant="outline" variant="outline"
className="flex-1" className="flex-1"
onClick={() => setShowRejectModal(false)} onClick={() => setShowRejectModal(false)}
disabled={isRejecting}
> >
Cancel Cancel
</Button> </Button>
@ -3178,8 +3385,16 @@ export function ApplicationDetails() {
variant="destructive" variant="destructive"
className="flex-1" className="flex-1"
onClick={handleReject} onClick={handleReject}
disabled={isRejecting}
> >
Confirm Rejection {isRejecting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Rejecting...
</>
) : (
'Confirm Rejection'
)}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'; 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 { onboardingService } from '../../services/onboarding.service';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
@ -12,10 +13,8 @@ import {
} from '../ui/select'; } from '../ui/select';
import { import {
Search, Search,
Filter,
Download, Download,
Mail, Mail
Plus
} from 'lucide-react'; } from 'lucide-react';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { Checkbox } from '../ui/checkbox'; import { Checkbox } from '../ui/checkbox';
@ -51,12 +50,10 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
// Real Data Integration // Real Data Integration
const [applications, setApplications] = useState<Application[]>([]); const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const fetchApplications = async () => { const fetchApplications = async () => {
try { try {
setLoading(true);
const response = await onboardingService.getApplications(); const response = await onboardingService.getApplications();
// Check if response is array or wrapped in data property // Check if response is array or wrapped in data property
const applicationsData = response.data || (Array.isArray(response) ? response : []); const applicationsData = response.data || (Array.isArray(response) ? response : []);
@ -100,7 +97,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
} catch (error) { } catch (error) {
console.error('Failed to fetch applications', error); console.error('Failed to fetch applications', error);
} finally { } finally {
setLoading(false); // setLoading(false);
} }
}; };
fetchApplications(); fetchApplications();
@ -346,7 +343,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
{new Date(app.submissionDate).toLocaleDateString()} {formatDateTime(app.submissionDate)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button <Button

View File

@ -8,11 +8,23 @@ import {
FileText, FileText,
Upload, Upload,
Loader2, Loader2,
Eye Eye,
CheckCircle2,
Clock
} from 'lucide-react'; } from 'lucide-react';
import { WorkNotesPage } from './WorkNotesPage'; import { WorkNotesPage } from './WorkNotesPage';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; 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() { export function FDDApplicationDetails() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@ -24,6 +36,8 @@ export function FDDApplicationDetails() {
const [activeTab, setActiveTab] = useState<'details' | 'worknotes'>('details'); const [activeTab, setActiveTab] = useState<'details' | 'worknotes'>('details');
const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [selectedPreviewDoc, setSelectedPreviewDoc] = useState<any>(null); const [selectedPreviewDoc, setSelectedPreviewDoc] = useState<any>(null);
const [showFinalizeModal, setShowFinalizeModal] = useState(false);
const [showFlagModal, setShowFlagModal] = useState(false);
useEffect(() => { useEffect(() => {
if (id) fetchApplication(); if (id) fetchApplication();
@ -104,6 +118,14 @@ export function FDDApplicationDetails() {
if (!application) return null; 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 ( return (
<div className="flex flex-col gap-6 max-w-7xl mx-auto mb-10"> <div className="flex flex-col gap-6 max-w-7xl mx-auto mb-10">
{/* Action Bar */} {/* Action Bar */}
@ -118,81 +140,65 @@ export function FDDApplicationDetails() {
Back to Dashboard Back to Dashboard
</button> </button>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button {isNotReachedYet ? (
disabled={uploading} <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">
onClick={() => { <Clock className="w-4 h-4" />
const input = document.createElement('input'); Awaiting Previous Stages
input.type = 'file'; </div>
input.onchange = async (e: any) => { ) : !isCompleted ? (
const file = e.target.files[0]; <>
if (!file) return; <button
try { disabled={uploading}
setUploading(true); onClick={() => {
const formData = new FormData(); const input = document.createElement('input');
formData.append('file', file); input.type = 'file';
formData.append('documentType', 'FDD Audit Report'); input.onchange = async (e: any) => {
formData.append('applicationId', application.id); const file = e.target.files[0];
if (!file) return;
await API.uploadDocument(application.id, formData); try {
toast.success('FDD Audit Report uploaded successfully'); setUploading(true);
fetchApplication(); // Refresh to show the new doc const formData = new FormData();
} catch (err) { formData.append('file', file);
toast.error('Upload failed'); formData.append('documentType', 'FDD Audit Report');
} finally { formData.append('applicationId', application.id);
setUploading(false);
} await API.uploadDocument(application.id, formData);
}; toast.success('FDD Audit Report uploaded successfully');
input.click(); fetchApplication(); // Refresh to show the new doc
}} } catch (err) {
className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 text-slate-700 font-bold text-xs uppercase tracking-wider rounded-lg hover:bg-slate-50 transition-all" toast.error('Upload failed');
> } finally {
<Upload className="w-4 h-4" /> setUploading(false);
{uploading ? 'Uploading...' : 'Upload Report'} }
</button> };
<button input.click();
disabled={uploading} }}
onClick={async () => { className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 text-slate-700 font-bold text-xs uppercase tracking-wider rounded-lg hover:bg-slate-50 transition-all"
if (!window.confirm('Are you sure you want to flag this application as non-responsive?')) return; >
try { <Upload className="w-4 h-4" />
setUploading(true); {uploading ? 'Uploading...' : 'Upload Report'}
await API.submitStageDecision({ </button>
applicationId: application.id, <button
stageCode: 'FDD_VERIFICATION', disabled={uploading}
decision: 'Rejected', onClick={() => setShowFlagModal(true)}
remarks: 'Applicant is non-responsive to FDD queries.' className="px-4 py-2 text-red-600 font-bold text-xs uppercase tracking-wider hover:bg-red-50 rounded-lg transition-all"
}); >
toast.error('Application flagged and returned to admin.'); Flag Non-Responsive
navigate('/fdd-dashboard'); </button>
} catch (e) { toast.error('Action failed'); } finally { setUploading(false); } <button
}} disabled={uploading}
className="px-4 py-2 text-red-600 font-bold text-xs uppercase tracking-wider hover:bg-red-50 rounded-lg transition-all" 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"
Flag Non-Responsive >
</button> {uploading ? 'Processing...' : 'Finalize & Submit Report'}
<button </button>
disabled={uploading} </>
onClick={async () => { ) : (
if (!window.confirm('Finalizing the report will submit your findings and lock this case. Proceed?')) return; <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">
try { <CheckCircle2 className="w-4 h-4" />
setUploading(true); Final Audit Report Submitted
const res: any = await API.submitStageDecision({ </div>
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); }
}}
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> </div>
</div> </div>
@ -261,55 +267,80 @@ export function FDDApplicationDetails() {
<CardHeader className="border-b border-slate-100 px-6 py-4"> <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"> <CardTitle className="text-base font-bold flex items-center gap-2 text-slate-800">
<Upload className="w-4 h-4 text-slate-500" /> <Upload className="w-4 h-4 text-slate-500" />
Financial Report Submission {isCompleted ? 'Finalized Financial Reports' : isNotReachedYet ? 'Audit Workspace' : 'Financial Report Submission'}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="p-10 border-2 border-dashed border-slate-200 rounded-lg flex flex-col items-center justify-center text-center"> {isNotReachedYet && (
<div className="w-12 h-12 bg-slate-50 text-slate-400 rounded-full flex items-center justify-center mb-4"> <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">
<FileText className="w-6 h-6" /> <div className="w-16 h-16 bg-white rounded-full flex items-center justify-center text-slate-300 mb-4 shadow-sm">
</div> <Clock className="w-8 h-8" />
<p className="text-slate-600 font-medium mb-1">Select and upload the due diligence report</p> </div>
<p className="text-slate-400 text-xs mb-6">PDF or JPG formats accepted (Max 10MB)</p> <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="w-full max-w-sm space-y-4"> <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">
<select Status: {application.status || 'Pending Review'}
value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)}
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded text-sm font-medium text-slate-700 outline-none focus:ring-1 focus:ring-blue-500 transition-all"
>
<option value="">Select Document Category...</option>
<option value="Final FDD Audit Report">Final FDD Audit Report</option>
<option value="Bank Statement Analysis">Bank Statement Analysis</option>
<option value="Credit Compliance Report">Credit Compliance Report</option>
<option value="Business Valuation Report">Business Valuation Report</option>
<option value="Property Verification Report">Property Verification Report</option>
</select>
<div className="relative">
{uploading ? (
<div className="w-full py-2.5 bg-slate-100 rounded flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-slate-400" />
<span className="text-slate-500 text-xs font-bold uppercase tracking-wider">Uploading...</span>
</div>
) : (
<>
<input
type="file"
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={handleFileUpload}
disabled={!selectedDocType}
/>
<div className={`w-full py-2.5 text-center font-bold uppercase tracking-wider text-xs rounded transition-all ${
!selectedDocType ? 'bg-slate-100 text-slate-300' : 'bg-slate-900 text-white hover:bg-slate-800'
}`}>
Browse & Upload
</div>
</>
)}
</div> </div>
</div> </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" />
</div>
<p className="text-slate-600 font-medium mb-1">Select and upload the due diligence report</p>
<p className="text-slate-400 text-xs mb-6">PDF or JPG formats accepted (Max 10MB)</p>
<div className="w-full max-w-sm space-y-4">
<select
value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)}
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded text-sm font-medium text-slate-700 outline-none focus:ring-1 focus:ring-blue-500 transition-all"
>
<option value="">Select Document Category...</option>
<option value="Final FDD Audit Report">Final FDD Audit Report</option>
<option value="Bank Statement Analysis">Bank Statement Analysis</option>
<option value="Credit Compliance Report">Credit Compliance Report</option>
<option value="Business Valuation Report">Business Valuation Report</option>
<option value="Property Verification Report">Property Verification Report</option>
</select>
<div className="relative">
{uploading ? (
<div className="w-full py-2.5 bg-slate-100 rounded flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-slate-400" />
<span className="text-slate-500 text-xs font-bold uppercase tracking-wider">Uploading...</span>
</div>
) : (
<>
<input
type="file"
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={handleFileUpload}
disabled={!selectedDocType}
/>
<div className={`w-full py-2.5 text-center font-bold uppercase tracking-wider text-xs rounded transition-all ${
!selectedDocType ? 'bg-slate-100 text-slate-300' : 'bg-slate-900 text-white hover:bg-slate-800'
}`}>
Browse & Upload
</div>
</>
)}
</div>
</div>
</div>
)}
{/* List of Uploaded Documents */} {/* List of Uploaded Documents */}
<div className="mt-8 border-t border-slate-100 pt-8"> <div className="mt-8 border-t border-slate-100 pt-8">
@ -473,6 +504,133 @@ export function FDDApplicationDetails() {
onClose={() => setIsPreviewOpen(false)} onClose={() => setIsPreviewOpen(false)}
document={selectedPreviewDoc} 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> </div>
); );
} }

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

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; 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 { Badge } from '../ui/badge';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { import {
@ -16,20 +16,21 @@ import {
Clock, Clock,
FileText, FileText,
Calendar, Calendar,
User, CreditCard,
MapPin, ShieldCheck
CreditCard
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { onboardingService } from '../../services/onboarding.service'; import { onboardingService } from '../../services/onboarding.service';
interface FinanceOnboardingPageProps { interface FinanceOnboardingPageProps {
onViewPaymentDetails?: (applicationId: string) => void; 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 [applications, setApplications] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<'payments' | 'audits'>('payments');
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'verified'>('pending'); const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'verified'>('pending');
useEffect(() => { useEffect(() => {
@ -40,17 +41,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
try { try {
setLoading(true); setLoading(true);
const data = await onboardingService.getApplications(); const data = await onboardingService.getApplications();
// Filter for applications that are in stages requiring finance verification setApplications(data);
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);
} catch (error) { } catch (error) {
console.error('Fetch error:', error); console.error('Fetch error:', error);
toast.error('Failed to fetch applications'); toast.error('Failed to fetch applications');
@ -62,34 +53,66 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
const getRelevantPaymentStatus = (app: any) => { const getRelevantPaymentStatus = (app: any) => {
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment'; if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment';
const s = app.overallStatus || app.status; 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); const deposit = app.securityDeposits.find((d: any) => d.depositType === relevantType);
return deposit ? deposit.status : 'Awaiting Payment'; return deposit ? deposit.status : 'Awaiting Payment';
}; };
const filteredApplications = applications.filter(app => { const getFddApprovalStatus = (app: any) => {
const status = getRelevantPaymentStatus(app); // Check for Finance approval record specifically in LOI_APPROVAL stage
if (filterStatus === 'all') return true; const financeApproval = app.stageApprovals?.find((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'Finance');
if (filterStatus === 'pending') return status !== 'Verified'; return financeApproval ? financeApproval.decision : 'Pending Review';
if (filterStatus === 'verified') return status === 'Verified'; };
return true;
// 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);
}); });
const handleViewDetails = (appId: string) => { // Filter for FDD Audit Mode
if (onViewPaymentDetails) { 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 handleAction = (appId: string) => {
if (activeTab === 'audits') {
if (onViewAuditDetails) {
onViewAuditDetails(appId);
}
} else if (onViewPaymentDetails) {
onViewPaymentDetails(appId); onViewPaymentDetails(appId);
} }
}; };
const pendingCount = applications.filter(app => getRelevantPaymentStatus(app) !== 'Verified').length; const pendingPayments = paymentApps.filter(app => getRelevantPaymentStatus(app) !== 'Verified').length;
const verifiedCount = applications.filter(app => getRelevantPaymentStatus(app) === 'Verified').length; const pendingAudits = auditApps.filter(app => getFddApprovalStatus(app) !== 'Approved').length;
const totalAmount = applications.length * 200000;
if (loading) { if (loading) {
return ( 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" /> <Clock className="w-8 h-8 animate-spin mr-3" />
<span>Loading payment queue...</span> <span>Loading Finance Queue...</span>
</div> </div>
); );
} }
@ -97,165 +120,163 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{/* Header */} {/* 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> <div>
<h1 className="text-3xl font-bold text-slate-900 mb-1">Financial Compliance</h1> <h1 className="text-3xl font-bold text-slate-900 tracking-tight mb-1">Financial Operations</h1>
<p className="text-slate-600">Verify security deposits and advance payments for dealership onboarding</p> <p className="text-slate-500">Manage payment verifications and FDD audit report reviews</p>
</div>
<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>
<Button onClick={fetchApplications} variant="outline" size="sm">
Refresh List
</Button>
</div> </div>
{/* Stats Cards */} {/* Mode Switcher */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<Card className="border-amber-200 bg-amber-50/30"> <div className="inline-flex p-1 bg-slate-100 rounded-xl">
<CardHeader className="pb-2"> <Button
<CardTitle className="text-sm font-medium text-amber-800">Pending Verification</CardTitle> variant={activeTab === 'payments' ? 'default' : 'ghost'}
</CardHeader> className={activeTab === 'payments' ? 'rounded-lg bg-white text-slate-900 shadow-sm hover:bg-white' : 'rounded-lg text-slate-600'}
<CardContent> onClick={() => { setActiveTab('payments'); setFilterStatus('pending'); }}
<div className="flex items-center justify-between"> >
<div className="text-3xl font-bold text-amber-700">{pendingCount}</div> <DollarSign className="w-4 h-4 mr-2" />
<Clock className="w-8 h-8 text-amber-400" /> Payments ({pendingPayments})
</div> </Button>
</CardContent> <Button
</Card> 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>
<Card className="border-green-200 bg-green-50/30"> <div className="flex gap-2">
<CardHeader className="pb-2"> <Button
<CardTitle className="text-sm font-medium text-green-800">Verified This Week</CardTitle> variant={filterStatus === 'pending' ? 'secondary' : 'ghost'}
</CardHeader> size="sm"
<CardContent> onClick={() => setFilterStatus('pending')}
<div className="flex items-center justify-between"> className={filterStatus === 'pending' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'}
<div className="text-3xl font-bold text-green-700">{verifiedCount}</div> >
<CheckCircle className="w-8 h-8 text-green-400" /> Pending
</div> </Button>
</CardContent> <Button
</Card> variant={filterStatus === 'verified' ? 'secondary' : 'ghost'}
size="sm"
<Card className="border-blue-200 bg-blue-50/30"> onClick={() => setFilterStatus('verified')}
<CardHeader className="pb-2"> className={filterStatus === 'verified' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'}
<CardTitle className="text-sm font-medium text-blue-800">Active Queue</CardTitle> >
</CardHeader> Completed
<CardContent> </Button>
<div className="flex items-center justify-between"> <Button
<div className="text-3xl font-bold text-blue-700">{applications.length}</div> variant={filterStatus === 'all' ? 'secondary' : 'ghost'}
<FileText className="w-8 h-8 text-blue-400" /> size="sm"
</div> onClick={() => setFilterStatus('all')}
</CardContent> className={filterStatus === 'all' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'}
</Card> >
All
<Card className="border-slate-200 bg-slate-50/30"> </Button>
<CardHeader className="pb-2"> </div>
<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">
<Button
variant={filterStatus === 'pending' ? 'default' : 'ghost'}
size="sm"
onClick={() => setFilterStatus('pending')}
className={filterStatus === 'pending' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600'}
>
Pending Verifications ({pendingCount})
</Button>
<Button
variant={filterStatus === 'verified' ? 'default' : 'ghost'}
size="sm"
onClick={() => setFilterStatus('verified')}
className={filterStatus === 'verified' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600'}
>
Verified ({verifiedCount})
</Button>
<Button
variant={filterStatus === 'all' ? 'default' : 'ghost'}
size="sm"
onClick={() => setFilterStatus('all')}
className={filterStatus === 'all' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600'}
>
View All
</Button>
</div> </div>
{/* Applications Table */} {/* Applications Table */}
<Card className="border-none shadow-md"> <Card className="border-none shadow-xl overflow-hidden rounded-2xl bg-white">
<CardContent className="p-0"> <CardContent className="p-0">
<Table> <Table>
<TableHeader className="bg-slate-50"> <TableHeader className="bg-slate-50/50">
<TableRow> <TableRow className="border-b border-slate-100 uppercase text-[10px] tracking-wider font-bold text-slate-400">
<TableHead className="w-[180px]">Application ID</TableHead> <TableHead className="py-4 pl-6">Application Details</TableHead>
<TableHead>Applicant Name</TableHead>
<TableHead>Location</TableHead> <TableHead>Location</TableHead>
<TableHead>Payment Stage</TableHead> <TableHead>{activeTab === 'payments' ? 'Payment Stage' : 'Audit Stage'}</TableHead>
<TableHead>Current Status</TableHead> <TableHead>Current Status</TableHead>
<TableHead className="text-right">Action</TableHead> <TableHead className="text-right pr-6">Workflow Action</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredApplications.length > 0 ? ( {displayApps.length > 0 ? (
filteredApplications.map((app) => ( displayApps.map((app) => {
<TableRow key={app.id} className="hover:bg-slate-50/50 transition-colors"> const statusLabel = activeTab === 'payments' ? getRelevantPaymentStatus(app) : getFddApprovalStatus(app);
<TableCell className="font-mono text-sm font-bold">{app.applicationId || app.id}</TableCell> return (
<TableCell> <TableRow key={app.id} className="hover:bg-blue-50/20 group transition-all">
<div className="flex items-center gap-2"> <TableCell className="py-4 pl-6">
<User className="w-4 h-4 text-slate-400" /> <div className="flex flex-col">
<span className="font-medium text-slate-900">{app.applicantName}</span> <span className="font-mono text-xs font-bold text-blue-600 mb-1">{app.applicationId || app.id}</span>
</div> <div className="flex items-center gap-2">
</TableCell> <span className="font-semibold text-slate-900">{app.applicantName}</span>
<TableCell> </div>
<div className="flex items-center gap-2 text-slate-600"> </div>
<MapPin className="w-4 h-4" /> </TableCell>
<span>{app.city || app.preferredLocation}, {app.state}</span> <TableCell>
</div> <div className="flex flex-col text-sm">
</TableCell> <span className="text-slate-600 leading-none">{app.city}</span>
<TableCell> <span className="text-slate-400 text-xs mt-1">{app.state}</span>
<div className="flex items-center gap-2"> </div>
<CreditCard className="w-4 h-4 text-slate-400" /> </TableCell>
<span className="text-sm"> <TableCell>
{app.status === 'LOI_APPROVAL' || app.status === 'PAYMENT_VERIFICATION' ? 'Initial Deposit (₹2L)' : 'Final Deposit (₹15L)'} <div className="flex items-center gap-2">
</span> {activeTab === 'payments' ? (
</div> <>
</TableCell> <CreditCard className="w-4 h-4 text-slate-400" />
<TableCell> <span className="text-sm font-medium">
<Badge {app.overallStatus === 'LOI Issued' || app.overallStatus === 'Security Details' ? 'Initial (₹2L)' : 'Final (₹15L)'}
className={ </span>
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' <>
} <ShieldCheck className="w-4 h-4 text-slate-400" />
variant="outline" <span className="text-sm font-medium">FDD Report Approval</span>
> </>
{getRelevantPaymentStatus(app)} )}
</Badge> </div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell>
<Button <Badge
size="sm" className={
variant={getRelevantPaymentStatus(app) === 'Verified' ? 'outline' : 'default'} statusLabel === 'Verified' || statusLabel === 'Approved' ? 'bg-emerald-50 text-emerald-700 border-emerald-100 px-3 py-1 rounded-full' :
className={getRelevantPaymentStatus(app) !== 'Verified' ? 'bg-amber-600 hover:bg-amber-700' : ''} statusLabel === 'Rejected' ? 'bg-rose-50 text-rose-700 border-rose-100 px-3 py-1 rounded-full' :
onClick={() => handleViewDetails(app.applicationId || app.id)} 'bg-amber-50 text-amber-700 border-amber-100 px-3 py-1 rounded-full'
> }
<FileText className="w-4 h-4 mr-2" /> variant="outline"
{app.paymentStatus === 'Verified' ? 'View Details' : 'Verify Now'} >
</Button> {statusLabel}
</TableCell> </Badge>
</TableRow> </TableCell>
)) <TableCell className="text-right pr-6">
<Button
size="sm"
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)}
>
{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> <TableRow>
<TableCell colSpan={6} className="h-40 text-center text-slate-500"> <TableCell colSpan={6} className="h-48 text-center text-slate-400">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-3">
<CheckCircle className="w-10 h-10 text-slate-200" /> <div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center">
<p>No applications found matching this filter.</p> <CheckCircle className="w-6 h-6 text-slate-200" />
</div>
<p className="text-sm">No applications pending in the {activeTab} queue</p>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -20,6 +20,7 @@ import {
import { toast } from 'sonner'; import { toast } from 'sonner';
import { onboardingService } from '../../services/onboarding.service'; import { onboardingService } from '../../services/onboarding.service';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils';
// Simple helper for class merging if 'cn' is not available // Simple helper for class merging if 'cn' is not available
const cn = (...classes: any[]) => classes.filter(Boolean).join(' '); const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
@ -213,7 +214,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
</p> </p>
<p className="text-sm text-slate-600"> <p className="text-sm text-slate-600">
{activeDeposit?.status === 'Verified' {activeDeposit?.status === 'Verified'
? `Verified on ${new Date(activeDeposit.verifiedAt).toLocaleDateString()}` ? `Verified on ${formatDateTime(activeDeposit.verifiedAt)}`
: activeDeposit?.status === 'Rejected' : activeDeposit?.status === 'Rejected'
? 'Payment Rejected' ? 'Payment Rejected'
: 'Awaiting Verification'} : 'Awaiting Verification'}
@ -301,7 +302,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
</div> </div>
<div> <div>
<Label className="text-slate-500">Verified By</Label> <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>
</div> </div>
)} )}

View File

@ -8,6 +8,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { formatDateTime } from '../ui/utils';
interface Props { interface Props {
id: string; id: string;
@ -149,7 +150,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<div> <div>
<p className="text-sm text-slate-500 mb-1">Applied Date</p> <p className="text-sm text-slate-500 mb-1">Applied Date</p>
<p className="font-medium text-slate-900"> <p className="font-medium text-slate-900">
{details.createdAt ? new Date(details.createdAt).toLocaleDateString() : '-'} {details.createdAt ? formatDateTime(details.createdAt) : '-'}
</p> </p>
</div> </div>
</div> </div>

View File

@ -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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; 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 { Input } from '../ui/input';
import { Label } from '../ui/label'; import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';
@ -18,6 +18,7 @@ interface FinanceDashboardProps {
currentUser: User | null; currentUser: User | null;
onNavigate?: (view: string) => void; onNavigate?: (view: string) => void;
onViewPaymentDetails?: (applicationId: string) => void; onViewPaymentDetails?: (applicationId: string) => void;
onViewAuditDetails?: (applicationId: string) => void;
onViewFnFDetails?: (fnfId: string) => void; onViewFnFDetails?: (fnfId: string) => void;
} }
@ -59,7 +60,7 @@ interface FinanceLineItem {
amount: number; amount: number;
} }
export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails, onViewFnFDetails }: FinanceDashboardProps) { export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAuditDetails, onViewFnFDetails }: FinanceDashboardProps) {
const [applications, setApplications] = useState<any[]>([]); const [applications, setApplications] = useState<any[]>([]);
const [loading, setLoading] = useState(true); 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 [fnfDialog, setFnfDialog] = useState(false);
const [fnfDetailsDialog, setFnfDetailsDialog] = useState(false); const [fnfDetailsDialog, setFnfDetailsDialog] = useState(false);
const [selectedFnF, setSelectedFnF] = useState<any>(null); const [selectedFnF, setSelectedFnF] = useState<any>(null);
@ -109,16 +102,6 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
}); });
const [finalRemarks, setFinalRemarks] = useState(''); 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 = () => { const handleAddLineItem = () => {
if (!newLineItem.department || !newLineItem.description || !newLineItem.amount) { if (!newLineItem.department || !newLineItem.description || !newLineItem.amount) {
@ -175,31 +158,6 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
setFinalRemarks(''); 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) => { const getRelevantPaymentStatus = (app: any) => {
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment'; 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 pendingOnboarding = applications.filter(app => getRelevantPaymentStatus(app) !== 'Verified');
const verifiedOnboarding = 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 pendingFnF = mockFnFCases.filter(f => !f.hasFinanceSummary);
const completedFnF = mockFnFCases.filter(f => f.hasFinanceSummary); const completedFnF = mockFnFCases.filter(f => f.hasFinanceSummary);
@ -238,7 +197,29 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
</div> </div>
{/* Stats Cards */} {/* 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 <Card
className="cursor-pointer hover:shadow-lg transition-shadow border-yellow-200" className="cursor-pointer hover:shadow-lg transition-shadow border-yellow-200"
onClick={() => onNavigate?.('finance-onboarding')} onClick={() => onNavigate?.('finance-onboarding')}
@ -384,18 +365,33 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
<FileText className="w-4 h-4 mr-2" /> <FileText className="w-4 h-4 mr-2" />
View Details View Details
</Button> </Button>
<Button {app.status === 'LOI_APPROVAL' || app.overallStatus === 'LOI In Progress' ? (
size="sm" <Button
className="bg-green-600 hover:bg-green-700 font-bold" size="sm"
onClick={() => { className="bg-amber-600 hover:bg-amber-700 font-bold"
if (onViewPaymentDetails) { onClick={() => {
onViewPaymentDetails(app.applicationId || app.id); if (onViewAuditDetails) {
} onViewAuditDetails(app.applicationId || app.id);
}} }
> }}
<CheckCircle className="w-4 h-4 mr-2" /> >
Verify Payment <ShieldCheck className="w-4 h-4 mr-2" />
</Button> Review Audit
</Button>
) : (
<Button
size="sm"
className="bg-green-600 hover:bg-green-700 font-bold"
onClick={() => {
if (onViewPaymentDetails) {
onViewPaymentDetails(app.applicationId || app.id);
}
}}
>
<CheckCircle className="w-4 h-4 mr-2" />
Verify Payment
</Button>
)}
</div> </div>
</div> </div>
</CardContent> </CardContent>

View File

@ -123,6 +123,7 @@ export interface Application {
regionId?: string; regionId?: string;
areaId?: string; areaId?: string;
districtId?: string; districtId?: string;
fddAssignments?: any[];
} }
export interface Participant { export interface Participant {