647 lines
40 KiB
TypeScript
647 lines
40 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { useSelector } from 'react-redux';
|
|
import { RootState } from '@/store';
|
|
import {
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger
|
|
} from '@/components/ui/tabs';
|
|
import { Avatar, AvatarFallback } from '@/components/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 '@/components/ui/DocumentPreviewModal';
|
|
import { formatDateTime } from '@/components/ui/utils';
|
|
|
|
// 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" data-testid="onboarding-finance-fdd-back-btn">
|
|
<ArrowLeft className="w-4 h-4" />
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-2xl font-black text-slate-900 tracking-tight" data-testid="onboarding-finance-fdd-title">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" data-testid="onboarding-finance-fdd-app-id">
|
|
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" data-testid="onboarding-finance-fdd-status">
|
|
{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" data-testid="onboarding-finance-fdd-tab-audit">
|
|
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" data-testid="onboarding-finance-fdd-tab-worknotes">
|
|
<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" data-testid="onboarding-finance-fdd-worknotes-count">{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" data-testid="onboarding-finance-fdd-tab-history">
|
|
<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" data-testid="onboarding-finance-fdd-summary-card">
|
|
<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" data-testid="onboarding-finance-fdd-summary-name">{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" data-testid="onboarding-finance-fdd-summary-location">{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" data-testid="onboarding-finance-fdd-summary-contact">
|
|
<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" data-testid="onboarding-finance-fdd-summary-constitution">{application.constitutionType || 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Financial Document Checklist Card */}
|
|
<Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl" data-testid="onboarding-finance-fdd-artefacts-card">
|
|
<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" data-testid={`onboarding-finance-fdd-artefact-row-${docType.type}`}>
|
|
<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"
|
|
)} data-testid={`onboarding-finance-fdd-artefact-status-${docType.type}`}>
|
|
{doc ? <CheckCircle className="w-5 h-5" /> : <AlertCircle className="w-5 h-5" />}
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-bold text-slate-800" data-testid={`onboarding-finance-fdd-artefact-label-${docType.type}`}>{docType.label}</p>
|
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter" data-testid={`onboarding-finance-fdd-artefact-date-${docType.type}`}>
|
|
{doc ? `Uploaded: ${formatDateTime(doc.createdAt)}` : '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);
|
|
}}
|
|
data-testid={`onboarding-finance-fdd-artefact-preview-${docType.type}`}
|
|
>
|
|
Preview
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Audit Reports Section */}
|
|
<div className="space-y-4" data-testid="onboarding-finance-fdd-reports-section">
|
|
<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" data-testid="onboarding-finance-fdd-reports-count-badge">
|
|
{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" data-testid="onboarding-finance-fdd-no-reports">
|
|
<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, idx: number) => (
|
|
<Card key={assignment.id} className="border-slate-200 shadow-sm overflow-hidden rounded-2xl group hover:border-amber-400 transition-all duration-300" data-testid={`onboarding-finance-fdd-assignment-card-${idx}`}>
|
|
<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"
|
|
)} data-testid={`onboarding-finance-fdd-assignment-status-${idx}`}>
|
|
{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" data-testid={`onboarding-finance-fdd-assignment-empty-${idx}`}>
|
|
<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" data-testid={`onboarding-finance-fdd-reports-list-${idx}`}>
|
|
{assignment.reports.map((report: any, reportIdx: number) => (
|
|
<div key={report.id} className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-2" data-testid={`onboarding-finance-fdd-report-row-${idx}-${reportIdx}`}>
|
|
<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"
|
|
)} data-testid={`onboarding-finance-fdd-report-signal-${idx}-${reportIdx}`}>
|
|
<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" data-testid={`onboarding-finance-fdd-report-findings-${idx}-${reportIdx}`}>
|
|
"{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" data-testid={`onboarding-finance-fdd-report-doc-${idx}-${reportIdx}`}>
|
|
<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" data-testid={`onboarding-finance-fdd-report-filename-${idx}-${reportIdx}`}>{report.reportDocument.fileName}</p>
|
|
<p className="text-slate-500 text-[10px] font-bold">SUBMITTED {formatDateTime(report.createdAt)}</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')}
|
|
data-testid={`onboarding-finance-fdd-report-download-${idx}-${reportIdx}`}
|
|
>
|
|
<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);
|
|
}}
|
|
data-testid={`onboarding-finance-fdd-report-preview-${idx}-${reportIdx}`}
|
|
>
|
|
<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" data-testid={`onboarding-finance-fdd-report-doc-empty-${idx}-${reportIdx}`}>
|
|
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" data-testid={`onboarding-finance-fdd-report-verified-${idx}-${reportIdx}`}>
|
|
<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" data-testid={`onboarding-finance-fdd-report-pending-${idx}-${reportIdx}`}>
|
|
<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" data-testid="onboarding-finance-fdd-action-sidebar">
|
|
<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}
|
|
data-testid="onboarding-finance-fdd-remarks-input"
|
|
/>
|
|
</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}
|
|
data-testid="onboarding-finance-fdd-approve-btn"
|
|
>
|
|
{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}
|
|
data-testid="onboarding-finance-fdd-revision-btn"
|
|
>
|
|
<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}
|
|
data-testid="onboarding-finance-fdd-reject-btn"
|
|
>
|
|
<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" data-testid="onboarding-finance-fdd-readonly-alert">
|
|
<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" data-testid="onboarding-finance-fdd-decision-badge">
|
|
<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" data-testid="onboarding-finance-fdd-policy-1">
|
|
<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" data-testid="onboarding-finance-fdd-policy-2">
|
|
<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" data-testid="onboarding-finance-fdd-policy-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" data-testid="onboarding-finance-fdd-progress-card">
|
|
<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" data-testid="onboarding-finance-fdd-worknotes-card">
|
|
<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" data-testid="onboarding-finance-fdd-worknotes-empty">No communication logged yet.</div>
|
|
) : (
|
|
<div className="divide-y divide-slate-100">
|
|
{workNotes.map((note: any, nIdx: number) => (
|
|
<div key={note.id} className="p-6 hover:bg-slate-50 transition-colors" data-testid={`onboarding-finance-fdd-worknote-row-${nIdx}`}>
|
|
<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" data-testid={`onboarding-finance-fdd-worknote-avatar-${nIdx}`}>
|
|
{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" data-testid={`onboarding-finance-fdd-worknote-author-${nIdx}`}>{note.author?.fullName || 'System'}</h4>
|
|
<span className="text-[10px] font-bold text-slate-400 uppercase" data-testid={`onboarding-finance-fdd-worknote-date-${nIdx}`}>{formatDateTime(note.createdAt)}</span>
|
|
</div>
|
|
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest" data-testid={`onboarding-finance-fdd-worknote-role-${nIdx}`}>{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" data-testid={`onboarding-finance-fdd-worknote-text-${nIdx}`}>
|
|
{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}
|
|
data-testid="onboarding-finance-fdd-new-note-input"
|
|
/>
|
|
<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()}
|
|
data-testid="onboarding-finance-fdd-send-note-btn"
|
|
>
|
|
{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" data-testid="onboarding-finance-fdd-history-card">
|
|
<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" data-testid={`onboarding-finance-fdd-journey-step-${idx}`}>
|
|
{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"
|
|
)} data-testid={`onboarding-finance-fdd-journey-icon-${idx}`}>
|
|
{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" data-testid={`onboarding-finance-fdd-journey-name-${idx}`}>{step.stageName}</p>
|
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest" data-testid={`onboarding-finance-fdd-journey-date-${idx}`}>
|
|
{step.stageCompletedAt ? `Completed ${formatDateTime(step.stageCompletedAt)}` : 'Pending'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* Global Preview Modal */}
|
|
<DocumentPreviewModal
|
|
isOpen={showPreviewModal}
|
|
onClose={() => setShowPreviewModal(false)}
|
|
document={previewDoc}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|