typo chnge errors and Loi Document request mail templatd added
This commit is contained in:
parent
dc49fa9065
commit
05211fe90a
@ -506,10 +506,10 @@ export default function App() {
|
|||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/constitutional-change" element={<ConstitutionalChangePage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/constitutional-change/${id}`)} />} />
|
<Route path="/constitutional-change" element={<ConstitutionalChangePage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/constitutional-change/${id}`)} />} />
|
||||||
<Route path="/constitutional-change/:id" element={<ConstitutionalChangeDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/constitutional-change')} currentUser={currentUser} />} />
|
<Route path="/constitutional-change/:id" element={<ConstitutionalChangeDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate(hasRole(['Dealer']) ? '/dealer-constitutional' : '/constitutional-change')} currentUser={currentUser} />} />
|
||||||
|
|
||||||
<Route path="/relocation-requests" element={<RelocationRequestPage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/relocation-requests/${id}`)} />} />
|
<Route path="/relocation-requests" element={<RelocationRequestPage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/relocation-requests/${id}`)} />} />
|
||||||
<Route path="/relocation-requests/:id" element={<RelocationRequestDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/relocation-requests')} currentUser={currentUser} />} />
|
<Route path="/relocation-requests/:id" element={<RelocationRequestDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate(hasRole(['Dealer']) ? '/dealer-relocation' : '/relocation-requests')} currentUser={currentUser} />} />
|
||||||
|
|
||||||
{/* Dealer Routes */}
|
{/* Dealer Routes */}
|
||||||
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/dealer-resignation/${id}`)} />} />
|
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/dealer-resignation/${id}`)} />} />
|
||||||
|
|||||||
@ -54,6 +54,10 @@ export const API = {
|
|||||||
assignArchitectureTeam: (applicationId: string, assignedTo: string) => client.post(`/onboarding/applications/${applicationId}/assign-architecture`, { assignedTo }),
|
assignArchitectureTeam: (applicationId: string, assignedTo: string) => client.post(`/onboarding/applications/${applicationId}/assign-architecture`, { assignedTo }),
|
||||||
updateArchitectureStatus: (applicationId: string, status: string, remarks?: string) => client.post(`/onboarding/applications/${applicationId}/architecture-status`, { status, remarks }),
|
updateArchitectureStatus: (applicationId: string, status: string, remarks?: string) => client.post(`/onboarding/applications/${applicationId}/architecture-status`, { status, remarks }),
|
||||||
generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`),
|
generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`),
|
||||||
|
requestProspectDocuments: (
|
||||||
|
applicationId: string,
|
||||||
|
data: { documentTypes: string[]; dueDays?: number; customMessage?: string }
|
||||||
|
) => client.post(`/onboarding/applications/${applicationId}/request-documents`, data),
|
||||||
updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data),
|
updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data),
|
||||||
convertToOpportunity: (id: string, data?: any) => client.post(`/onboarding/applications/${id}/convert-to-opportunity`, data),
|
convertToOpportunity: (id: string, data?: any) => client.post(`/onboarding/applications/${id}/convert-to-opportunity`, data),
|
||||||
bulkConvertToOpportunity: (data: any) => client.post('/onboarding/applications/bulk-convert-to-opportunity', data),
|
bulkConvertToOpportunity: (data: any) => client.post('/onboarding/applications/bulk-convert-to-opportunity', data),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@ -11,6 +11,7 @@ import { setCredentials } from '@/store/slices/authSlice';
|
|||||||
|
|
||||||
export function ProspectiveLoginPage() {
|
export function ProspectiveLoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const routerLocation = useLocation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [step, setStep] = useState<'PHONE' | 'OTP'>('PHONE');
|
const [step, setStep] = useState<'PHONE' | 'OTP'>('PHONE');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
@ -18,6 +19,20 @@ export function ProspectiveLoginPage() {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
// Honour an optional `?next=...` deep-link sent by applicant emails. We allow only
|
||||||
|
// internal paths under `/prospective-dashboard/...` to prevent open-redirect abuse.
|
||||||
|
const resolveRedirectTarget = (): string => {
|
||||||
|
const raw = new URLSearchParams(routerLocation.search).get('next');
|
||||||
|
if (!raw) return '/prospective-dashboard';
|
||||||
|
try {
|
||||||
|
const decoded = decodeURIComponent(raw);
|
||||||
|
if (decoded.startsWith('/prospective-dashboard')) return decoded;
|
||||||
|
} catch {
|
||||||
|
// fall through to default
|
||||||
|
}
|
||||||
|
return '/prospective-dashboard';
|
||||||
|
};
|
||||||
|
|
||||||
const handleSendOtp = async (e: React.FormEvent) => {
|
const handleSendOtp = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!phone || phone.length < 10) {
|
if (!phone || phone.length < 10) {
|
||||||
@ -76,7 +91,7 @@ export function ProspectiveLoginPage() {
|
|||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
|
|
||||||
toast.success('Logged in successfully!');
|
toast.success('Logged in successfully!');
|
||||||
navigate('/prospective-dashboard');
|
navigate(resolveRedirectTarget());
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = response.data?.message || 'Invalid OTP';
|
const errorMessage = response.data?.message || 'Invalid OTP';
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
|
|||||||
@ -141,10 +141,6 @@ const normalizeConstitutionType = (value: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: ConstitutionalChangeDetailsProps) {
|
export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: ConstitutionalChangeDetailsProps) {
|
||||||
const { get: getSla } = useSlaBatchStatus(
|
|
||||||
requestId ? [{ entityType: 'constitutional', entityId: requestId }] : [],
|
|
||||||
Boolean(requestId)
|
|
||||||
);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
|
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
|
||||||
const [actionType, setActionType] = useState<'approve' | 'reject' | 'sendBack' | 'revoke'>('approve');
|
const [actionType, setActionType] = useState<'approve' | 'reject' | 'sendBack' | 'revoke'>('approve');
|
||||||
@ -158,6 +154,14 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
const [activeMainTab, setActiveMainTab] = useState('workflow');
|
const [activeMainTab, setActiveMainTab] = useState('workflow');
|
||||||
const [activeDocumentTab, setActiveDocumentTab] = useState('required');
|
const [activeDocumentTab, setActiveDocumentTab] = useState('required');
|
||||||
const [request, setRequest] = useState<any>(null);
|
const [request, setRequest] = useState<any>(null);
|
||||||
|
// The URL slug (`requestId` prop) is the human-readable code such as
|
||||||
|
// `CC-2026-MAY-00002`, but `sla_tracking.entityId` is a UUID column.
|
||||||
|
// Wait until the request has loaded and feed its UUID `id` to the SLA hook.
|
||||||
|
const slaEntityId: string = request?.id || '';
|
||||||
|
const { get: getSla } = useSlaBatchStatus(
|
||||||
|
slaEntityId ? [{ entityType: 'constitutional', entityId: slaEntityId }] : [],
|
||||||
|
Boolean(slaEntityId)
|
||||||
|
);
|
||||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||||
@ -620,7 +624,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<Badge className={requestStatusBadgeClass}>
|
<Badge className={requestStatusBadgeClass}>
|
||||||
{request.status}
|
{request.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
<SlaBadge status={getSla('constitutional', requestId)} />
|
<SlaBadge status={getSla('constitutional', slaEntityId)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Request Overview */}
|
{/* Request Overview */}
|
||||||
@ -856,7 +860,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
|
DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
|
||||||
</Badge>
|
</Badge>
|
||||||
{currentRoleApproval?.approvedByUserId && (
|
{currentRoleApproval?.approvedByUserId && (
|
||||||
<Badge className="bg-blue-100 text-blue-700 border-blue-300">
|
<Badge className="bg-red-50 text-re-red-hover border-red-200">
|
||||||
Approved by you
|
Approved by you
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@ -1341,7 +1345,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<div className="border-t border-slate-200 pt-3 mt-3">
|
<div className="border-t border-slate-200 pt-3 mt-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-blue-700 text-blue-800 hover:bg-blue-50"
|
className="w-full border-re-red text-re-red hover:bg-red-50"
|
||||||
onClick={() => navigate(`/worknotes/constitutional/${request?.id || requestId}`, {
|
onClick={() => navigate(`/worknotes/constitutional/${request?.id || requestId}`, {
|
||||||
state: {
|
state: {
|
||||||
requestType: 'constitutional',
|
requestType: 'constitutional',
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { dealerService } from '@/services/dealer.service';
|
|||||||
import { formatDateTime } from '@/components/ui/utils';
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
import { API } from '@/api/API';
|
import { API } from '@/api/API';
|
||||||
import { normalizeDealerProfileConstitution } from '@/lib/constitutional-change';
|
import { normalizeDealerProfileConstitution } from '@/lib/constitutional-change';
|
||||||
import { getRequestStatusBadgeClass } from '@/lib/statusProgressTheme';
|
import { getRequestStatusBadgeClass, getStatusProgressBarClass } from '@/lib/statusProgressTheme';
|
||||||
|
|
||||||
interface DealerConstitutionalChangePageProps {
|
interface DealerConstitutionalChangePageProps {
|
||||||
currentUser?: UserType | null;
|
currentUser?: UserType | null;
|
||||||
@ -119,7 +119,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
|
|||||||
title: 'Total Requests',
|
title: 'Total Requests',
|
||||||
value: requests.length,
|
value: requests.length,
|
||||||
icon: RefreshCcw,
|
icon: RefreshCcw,
|
||||||
color: 'bg-blue-500',
|
color: 'bg-re-red',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Pending',
|
title: 'Pending',
|
||||||
@ -140,7 +140,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
|
|||||||
{/* Loading Overlay */}
|
{/* Loading Overlay */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="min-h-[400px] flex items-center justify-center">
|
<div className="min-h-[400px] flex items-center justify-center">
|
||||||
<Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
|
<Loader2 className="w-8 h-8 text-re-red animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
|
|||||||
|
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
<Button className="bg-re-red hover:bg-re-red-hover">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
New Constitutional Change
|
New Constitutional Change
|
||||||
</Button>
|
</Button>
|
||||||
@ -262,9 +262,9 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Document Requirements */}
|
{/* Document Requirements */}
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
<h4 className="text-blue-900 mb-2">Documents Required (to be uploaded later)</h4>
|
<h4 className="text-re-red-hover mb-2">Documents Required (to be uploaded later)</h4>
|
||||||
<ul className="text-blue-800 text-sm space-y-1">
|
<ul className="text-re-red-hover text-sm space-y-1">
|
||||||
<li>• GST Registration Certificate</li>
|
<li>• GST Registration Certificate</li>
|
||||||
<li>• Firm PAN Copy</li>
|
<li>• Firm PAN Copy</li>
|
||||||
<li>• Self-attested KYC documents</li>
|
<li>• Self-attested KYC documents</li>
|
||||||
@ -285,9 +285,9 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-re-red hover:bg-re-red-hover"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? (
|
{submitting ? (
|
||||||
@ -363,7 +363,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
|
|||||||
<Badge variant="outline">{request.currentConstitution}</Badge>
|
<Badge variant="outline">{request.currentConstitution}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge className="bg-blue-100 text-blue-700 border-blue-300">
|
<Badge className="bg-red-50 text-re-red-hover border-red-200">
|
||||||
{request.changeType}
|
{request.changeType}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -379,7 +379,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
|
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
|
||||||
<div
|
<div
|
||||||
className="bg-blue-600 h-2 rounded-full"
|
className={`h-2 rounded-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
|
||||||
style={{ width: `${request.progressPercentage || 0}%` }}
|
style={{ width: `${request.progressPercentage || 0}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -535,7 +535,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Code</p>
|
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Code</p>
|
||||||
<p className="font-mono text-xs font-bold text-blue-600">{fnf.outlet?.code || 'N/A'}</p>
|
<p className="font-mono text-xs font-bold text-re-red">{fnf.outlet?.code || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Location</p>
|
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Location</p>
|
||||||
@ -564,7 +564,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-re-red hover:bg-re-red-hover"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedFnF(fnf);
|
setSelectedFnF(fnf);
|
||||||
setLineItems([]);
|
setLineItems([]);
|
||||||
@ -845,10 +845,10 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Card className="border-blue-100">
|
<Card className="border-red-100">
|
||||||
<CardHeader className="bg-blue-50/50 pb-2">
|
<CardHeader className="bg-red-50/50 pb-2">
|
||||||
<CardTitle className="text-sm font-bold flex items-center gap-2">
|
<CardTitle className="text-sm font-bold flex items-center gap-2">
|
||||||
<TrendingUp className="w-4 h-4 text-blue-600" />
|
<TrendingUp className="w-4 h-4 text-re-red" />
|
||||||
Receivables Check
|
Receivables Check
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -861,7 +861,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
<span className="text-slate-500">Other Payable Credits</span>
|
<span className="text-slate-500">Other Payable Credits</span>
|
||||||
<span className="font-bold text-slate-900">₹0</span>
|
<span className="font-bold text-slate-900">₹0</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t pt-2 flex justify-between font-bold text-blue-700">
|
<div className="border-t pt-2 flex justify-between font-bold text-re-red-hover">
|
||||||
<span>Total Payables</span>
|
<span>Total Payables</span>
|
||||||
<span>₹{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
|
<span>₹{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -909,7 +909,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="bg-blue-600 hover:bg-blue-500 px-6 font-bold shadow-md transition-all active:scale-95">
|
<Button className="bg-re-red hover:bg-re-red-hover px-6 font-bold shadow-md transition-all active:scale-95">
|
||||||
Generate PDF Summary
|
Generate PDF Summary
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1050,7 +1050,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-red-200 rounded-lg">
|
<div className="flex items-start gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
<AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
|
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
|
||||||
@ -1065,7 +1065,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="financial" className="space-y-4">
|
<TabsContent value="financial" className="space-y-4">
|
||||||
<Card className="border-blue-200 bg-blue-50">
|
<Card className="border-red-200 bg-red-50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Department Claim vs Finance Validation</CardTitle>
|
<CardTitle className="text-base">Department Claim vs Finance Validation</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@ -1580,7 +1580,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Final Settlement Summary */}
|
{/* Final Settlement Summary */}
|
||||||
<Card className="border-2 border-blue-300 bg-blue-50">
|
<Card className="border-2 border-red-300 bg-red-50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CheckCircle className="w-5 h-5 text-re-red" />
|
<CheckCircle className="w-5 h-5 text-re-red" />
|
||||||
@ -1603,7 +1603,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-px bg-blue-300"></div>
|
<div className="h-px bg-red-300"></div>
|
||||||
|
|
||||||
<div className={`p-4 rounded-lg border-2 ${
|
<div className={`p-4 rounded-lg border-2 ${
|
||||||
settlement.settlementType === 'Payable to Dealer'
|
settlement.settlementType === 'Payable to Dealer'
|
||||||
@ -1794,7 +1794,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Important Notes */}
|
{/* Important Notes */}
|
||||||
<Card className="bg-blue-50 border-red-200">
|
<Card className="bg-red-50 border-red-200">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
|
||||||
@ -1944,7 +1944,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{bankDetails.length > 0 ? (
|
{bankDetails.length > 0 ? (
|
||||||
bankDetails.map((bank: any) => (
|
bankDetails.map((bank: any) => (
|
||||||
<Card key={bank.id} className={`relative ${bank.isPrimary ? 'border-re-red bg-blue-50/30' : ''}`}>
|
<Card key={bank.id} className={`relative ${bank.isPrimary ? 'border-re-red bg-red-50/30' : ''}`}>
|
||||||
{bank.isPrimary && (
|
{bank.isPrimary && (
|
||||||
<div className="absolute top-0 right-0 p-1 bg-re-red text-white text-[10px] uppercase font-bold px-2 rounded-bl">
|
<div className="absolute top-0 right-0 p-1 bg-re-red text-white text-[10px] uppercase font-bold px-2 rounded-bl">
|
||||||
Primary
|
Primary
|
||||||
@ -2217,7 +2217,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-blue-300 text-re-red hover:bg-blue-50"
|
className="w-full border-red-300 text-re-red hover:bg-red-50"
|
||||||
onClick={handleRequestClarification}
|
onClick={handleRequestClarification}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -196,7 +196,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-slate-900 text-2xl">{displaySettlements.length}</div>
|
<div className="text-slate-900 text-2xl">{displaySettlements.length}</div>
|
||||||
<FileText className="w-8 h-8 text-blue-600" />
|
<FileText className="w-8 h-8 text-re-red" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -513,8 +513,8 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
<div className="flex items-start gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
|
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
|
||||||
<p className="text-sm text-slate-600">
|
<p className="text-sm text-slate-600">
|
||||||
|
|||||||
@ -282,12 +282,12 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
|
|||||||
</div>
|
</div>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"p-4 rounded-lg border",
|
"p-4 rounded-lg border",
|
||||||
activeDeposit?.status === 'Verified' ? "bg-green-50 border-green-200" : "bg-blue-50 border-blue-200"
|
activeDeposit?.status === 'Verified' ? "bg-green-50 border-green-200" : "bg-red-50 border-red-200"
|
||||||
)}>
|
)}>
|
||||||
<Label className="text-slate-500 block mb-1">Receipt Status</Label>
|
<Label className="text-slate-500 block mb-1">Receipt Status</Label>
|
||||||
<p className={cn(
|
<p className={cn(
|
||||||
"text-2xl font-bold",
|
"text-2xl font-bold",
|
||||||
activeDeposit?.status === 'Verified' ? "text-green-700" : "text-blue-700"
|
activeDeposit?.status === 'Verified' ? "text-green-700" : "text-re-red-hover"
|
||||||
)}>
|
)}>
|
||||||
{activeDeposit?.status || 'Not Started'}
|
{activeDeposit?.status || 'Not Started'}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -570,7 +570,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "New":
|
case "New":
|
||||||
return "bg-red-50 text-blue-700 border-blue-300";
|
return "bg-red-50 text-re-red-hover border-red-300";
|
||||||
case "In Progress":
|
case "In Progress":
|
||||||
return "bg-yellow-100 text-yellow-700 border-yellow-300";
|
return "bg-yellow-100 text-yellow-700 border-yellow-300";
|
||||||
case "Under Review":
|
case "Under Review":
|
||||||
@ -702,7 +702,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
|
|
||||||
{/* {canSendToStakeholders && fnfCase.status === "New" && (
|
{/* {canSendToStakeholders && fnfCase.status === "New" && (
|
||||||
<Button
|
<Button
|
||||||
className="bg-re-red hover:bg-blue-700"
|
className="bg-re-red hover:bg-re-red-hover"
|
||||||
onClick={() => setSendStakeholdersDialog(true)}
|
onClick={() => setSendStakeholdersDialog(true)}
|
||||||
>
|
>
|
||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
@ -921,7 +921,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
fnfCase.status,
|
fnfCase.status,
|
||||||
)
|
)
|
||||||
? "bg-green-50 border-green-200"
|
? "bg-green-50 border-green-200"
|
||||||
: "bg-blue-50 border-red-200"
|
: "bg-red-50 border-red-200"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
@ -1042,7 +1042,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
className={
|
className={
|
||||||
fnfCase.status === "Completed"
|
fnfCase.status === "Completed"
|
||||||
? "bg-green-50 border-green-200"
|
? "bg-green-50 border-green-200"
|
||||||
: "bg-blue-50 border-red-200"
|
: "bg-red-50 border-red-200"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
@ -1068,7 +1068,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-3 bg-red-50 rounded-lg">
|
<div className="text-center p-3 bg-red-50 rounded-lg">
|
||||||
<p className="text-xs text-blue-700 mb-1">
|
<p className="text-xs text-re-red-hover mb-1">
|
||||||
Net Amount
|
Net Amount
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
@ -1262,7 +1262,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
Case closed. All obligations fulfilled.
|
Case closed. All obligations fulfilled.
|
||||||
</p>
|
</p>
|
||||||
{fnfCase.status === "Completed" && (
|
{fnfCase.status === "Completed" && (
|
||||||
<Card className="bg-gradient-to-r from-green-50 to-blue-50 border-green-300">
|
<Card className="bg-gradient-to-r from-green-50 to-red-50 border-green-300">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="size-12 shrink-0 aspect-square rounded-full bg-green-600 flex items-center justify-center">
|
<div className="size-12 shrink-0 aspect-square rounded-full bg-green-600 flex items-center justify-center">
|
||||||
@ -1333,9 +1333,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="border-red-200 bg-blue-50/30">
|
<Card className="border-red-200 bg-red-50/30">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-blue-900">
|
<CardTitle className="text-re-red-hover">
|
||||||
F&F Settlement Information
|
F&F Settlement Information
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -1504,7 +1504,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-re-red hover:text-blue-700"
|
className="text-re-red hover:text-re-red-hover"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDept(dept);
|
setSelectedDept(dept);
|
||||||
setClearanceForm({
|
setClearanceForm({
|
||||||
@ -1531,7 +1531,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Department Claim vs Finance Validation */}
|
{/* Department Claim vs Finance Validation */}
|
||||||
<Card className="border-blue-200 bg-blue-50 mt-6">
|
<Card className="border-red-200 bg-red-50 mt-6">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Department Claim vs Finance Validation</CardTitle>
|
<CardTitle>Department Claim vs Finance Validation</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@ -1607,8 +1607,8 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
Warranty holdbacks / Policy penalties
|
Warranty holdbacks / Policy penalties
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 bg-blue-50 rounded-lg border border-blue-200">
|
<div className="p-6 bg-red-50 rounded-lg border border-red-200">
|
||||||
<p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p>
|
<p className="text-sm text-re-red-hover mb-2">Net Settlement Amount</p>
|
||||||
<p
|
<p
|
||||||
className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
|
className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
|
||||||
? "text-red-600"
|
? "text-red-600"
|
||||||
@ -1617,7 +1617,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
>
|
>
|
||||||
₹{Math.abs(fnfCase.netAmount || 0).toLocaleString()}
|
₹{Math.abs(fnfCase.netAmount || 0).toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-blue-600 mt-1">
|
<p className="text-xs text-re-red mt-1">
|
||||||
{(fnfCase.netAmount || 0) < 0
|
{(fnfCase.netAmount || 0) < 0
|
||||||
? "Receivable from dealer"
|
? "Receivable from dealer"
|
||||||
: "Payment to dealer"}
|
: "Payment to dealer"}
|
||||||
@ -1794,7 +1794,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{bankDetails.length > 0 ? (
|
{bankDetails.length > 0 ? (
|
||||||
bankDetails.map((bank: any) => (
|
bankDetails.map((bank: any) => (
|
||||||
<Card key={bank.id} className={`relative overflow-hidden ${bank.isPrimary ? 'border-re-red bg-blue-50/30' : ''}`}>
|
<Card key={bank.id} className={`relative overflow-hidden ${bank.isPrimary ? 'border-re-red bg-red-50/30' : ''}`}>
|
||||||
{bank.isPrimary && (
|
{bank.isPrimary && (
|
||||||
<div className="absolute top-0 right-0 p-1 bg-re-red text-white text-[10px] uppercase font-bold px-2 rounded-bl">
|
<div className="absolute top-0 right-0 p-1 bg-re-red text-white text-[10px] uppercase font-bold px-2 rounded-bl">
|
||||||
Primary
|
Primary
|
||||||
@ -1962,11 +1962,11 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="p-4 bg-blue-50 rounded-lg border border-red-200">
|
<div className="p-4 bg-red-50 rounded-lg border border-red-200">
|
||||||
<p className="text-sm text-blue-900 mb-2">
|
<p className="text-sm text-re-red-hover mb-2">
|
||||||
Notifications will be sent to:
|
Notifications will be sent to:
|
||||||
</p>
|
</p>
|
||||||
<ul className="text-sm text-blue-800 space-y-1 ml-4">
|
<ul className="text-sm text-re-red-hover space-y-1 ml-4">
|
||||||
<li>• All 16 departments</li>
|
<li>• All 16 departments</li>
|
||||||
<li>• Case Number: {fnfCase.caseNumber}</li>
|
<li>• Case Number: {fnfCase.caseNumber}</li>
|
||||||
<li>• Dealer: {fnfCase.dealerName}</li>
|
<li>• Dealer: {fnfCase.dealerName}</li>
|
||||||
@ -1984,7 +1984,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSendToStakeholders}
|
onClick={handleSendToStakeholders}
|
||||||
className="bg-re-red hover:bg-blue-700"
|
className="bg-re-red hover:bg-re-red-hover"
|
||||||
>
|
>
|
||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
Send Notifications
|
Send Notifications
|
||||||
@ -2058,7 +2058,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="bg-re-red hover:bg-blue-700"
|
className="bg-re-red hover:bg-re-red-hover"
|
||||||
onClick={handleUpdateClearance}
|
onClick={handleUpdateClearance}
|
||||||
disabled={isUpdatingClearance}
|
disabled={isUpdatingClearance}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -17,7 +17,7 @@ interface FnFPageProps {
|
|||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'Initiated':
|
case 'Initiated':
|
||||||
return 'bg-blue-100 text-blue-700 border-blue-300';
|
return 'bg-red-100 text-re-red-hover border-red-300';
|
||||||
case 'DD Clearance':
|
case 'DD Clearance':
|
||||||
case 'Legal Clearance':
|
case 'Legal Clearance':
|
||||||
return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
||||||
@ -65,7 +65,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center p-12">
|
<div className="flex items-center justify-center p-12">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
<Loader2 className="w-8 h-8 animate-spin text-re-red" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardDescription>Initiated</CardDescription>
|
<CardDescription>Initiated</CardDescription>
|
||||||
<CardTitle className="text-3xl text-blue-600">
|
<CardTitle className="text-3xl text-re-red">
|
||||||
{initiatedCases.length}
|
{initiatedCases.length}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -186,8 +186,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-start gap-4 flex-1">
|
<div className="flex items-start gap-4 flex-1">
|
||||||
<div className="p-3 bg-blue-100 rounded-lg">
|
<div className="p-3 bg-red-100 rounded-lg">
|
||||||
<FileCheck className="w-6 h-6 text-blue-600" />
|
<FileCheck className="w-6 h-6 text-re-red" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
@ -239,7 +239,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-blue-600 border-blue-300 hover:bg-blue-50"
|
className="text-re-red border-red-300 hover:bg-red-50"
|
||||||
onClick={() => handleSendToStakeholders(fnfCase.id)}
|
onClick={() => handleSendToStakeholders(fnfCase.id)}
|
||||||
>
|
>
|
||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
@ -276,12 +276,12 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-start gap-4 flex-1">
|
<div className="flex items-start gap-4 flex-1">
|
||||||
<div className={`p-3 rounded-lg ${fnfCase.status === 'Initiated' ? 'bg-blue-100' :
|
<div className={`p-3 rounded-lg ${fnfCase.status === 'Initiated' ? 'bg-red-100' :
|
||||||
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
|
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
|
||||||
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' :
|
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' :
|
||||||
'bg-green-100'
|
'bg-green-100'
|
||||||
}`}>
|
}`}>
|
||||||
<IndianRupee className={`w-6 h-6 ${fnfCase.status === 'Initiated' ? 'text-blue-600' :
|
<IndianRupee className={`w-6 h-6 ${fnfCase.status === 'Initiated' ? 'text-re-red' :
|
||||||
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
|
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
|
||||||
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
|
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
|
||||||
'text-green-600'
|
'text-green-600'
|
||||||
@ -322,7 +322,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-blue-600 border-blue-300 hover:bg-blue-50"
|
className="text-re-red border-red-300 hover:bg-red-50"
|
||||||
onClick={() => handleSendToStakeholders(fnfCase.id)}
|
onClick={() => handleSendToStakeholders(fnfCase.id)}
|
||||||
>
|
>
|
||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
|
|||||||
@ -11,42 +11,20 @@ interface ApplicationCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ApplicationCard({ application, onViewDetails }: ApplicationCardProps) {
|
export function ApplicationCard({ application, onViewDetails }: ApplicationCardProps) {
|
||||||
|
/**
|
||||||
|
* Status badge classes — see ApplicationsPage for the rationale.
|
||||||
|
* Three buckets only: positive (black), negative (muted brand red),
|
||||||
|
* default in-progress (light slate). Keeps the listing brand-aligned.
|
||||||
|
*/
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
const statusColors: Record<string, string> = {
|
const s = String(status || '');
|
||||||
'Submitted': 'bg-slate-500',
|
if (s === 'Rejected' || s === 'Disqualified' || s.includes('Rejected')) {
|
||||||
'Questionnaire Pending': 'bg-orange-500',
|
return 'bg-red-50 text-re-red-hover border border-red-200';
|
||||||
'Questionnaire Completed': 'bg-blue-500',
|
}
|
||||||
'Shortlisted': 'bg-cyan-500',
|
if (s === 'Approved' || s === 'Onboarded' || s === 'Completed') {
|
||||||
'Level 1 Pending': 'bg-red-500',
|
return 'bg-slate-900 text-white border border-transparent';
|
||||||
'Level 1 Approved': 'bg-green-500',
|
}
|
||||||
'Level 2 Pending': 'bg-purple-500',
|
return 'bg-slate-200 text-slate-800 border border-slate-300';
|
||||||
'Level 2 Approved': 'bg-green-600',
|
|
||||||
'Level 2 Recommended': 'bg-teal-500',
|
|
||||||
'Level 3 Pending': 'bg-indigo-500',
|
|
||||||
'FDD Verification': 'bg-violet-500',
|
|
||||||
'Payment Pending': 'bg-yellow-500',
|
|
||||||
'LOI Issued': 'bg-lime-500',
|
|
||||||
'Dealer Code Generation': 'bg-fuchsia-500',
|
|
||||||
'Architecture Team Assigned': 'bg-blue-500',
|
|
||||||
'Architecture Document Upload': 'bg-blue-500',
|
|
||||||
'Architecture Team Completion': 'bg-blue-500',
|
|
||||||
'Statutory GST': 'bg-emerald-500',
|
|
||||||
'Statutory PAN': 'bg-emerald-500',
|
|
||||||
'Statutory Nodal': 'bg-emerald-500',
|
|
||||||
'Statutory Check': 'bg-emerald-500',
|
|
||||||
'Statutory Partnership': 'bg-emerald-500',
|
|
||||||
'Statutory Firm Reg': 'bg-emerald-500',
|
|
||||||
'Statutory Virtual Code': 'bg-emerald-500',
|
|
||||||
'Statutory Domain': 'bg-emerald-500',
|
|
||||||
'Statutory MSD': 'bg-emerald-500',
|
|
||||||
'Statutory LOI Ack': 'bg-emerald-500',
|
|
||||||
'EOR In Progress': 'bg-sky-500',
|
|
||||||
'LOA Pending': 'bg-emerald-500',
|
|
||||||
'Approved': 'bg-green-700',
|
|
||||||
'Rejected': 'bg-red-500',
|
|
||||||
'Disqualified': 'bg-red-700'
|
|
||||||
};
|
|
||||||
return statusColors[status] || 'bg-slate-500';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Calendar,
|
Calendar,
|
||||||
@ -5,6 +6,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
Clock,
|
Clock,
|
||||||
|
FileText,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Info,
|
Info,
|
||||||
Lock,
|
Lock,
|
||||||
@ -15,6 +17,7 @@ import {
|
|||||||
XCircle,
|
XCircle,
|
||||||
Zap,
|
Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { RequestDocumentsModal } from './RequestDocumentsModal';
|
||||||
import { cn, formatDateTime } from '@/components/ui/utils';
|
import { cn, formatDateTime } from '@/components/ui/utils';
|
||||||
import {
|
import {
|
||||||
getRequestStatusBadgeSolidClass,
|
getRequestStatusBadgeSolidClass,
|
||||||
@ -75,8 +78,41 @@ interface ApplicationDetailsSidebarProps {
|
|||||||
setParticipantType: (value: string) => void;
|
setParticipantType: (value: string) => void;
|
||||||
handleAddParticipant: () => void;
|
handleAddParticipant: () => void;
|
||||||
isAssigningParticipant: boolean;
|
isAssigningParticipant: boolean;
|
||||||
|
documents?: any[];
|
||||||
|
documentConfigs?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Statuses where the admin can request supporting documents from the prospect (post-LOI approval).
|
||||||
|
const REQUEST_DOCUMENTS_ALLOWED_STATUSES = new Set<string>([
|
||||||
|
'Security Deposit',
|
||||||
|
'Security Details',
|
||||||
|
'Payment Pending',
|
||||||
|
'LOI Issuance Pending',
|
||||||
|
'LOI Issued',
|
||||||
|
'Dealer Code Generation',
|
||||||
|
'Architecture Team Assigned',
|
||||||
|
'Architecture Document Upload',
|
||||||
|
'Architecture Team Completion',
|
||||||
|
'Statutory GST',
|
||||||
|
'Statutory PAN',
|
||||||
|
'Statutory Nodal',
|
||||||
|
'Statutory Check',
|
||||||
|
'Statutory Partnership',
|
||||||
|
'Statutory Firm Reg',
|
||||||
|
'Statutory Virtual Code',
|
||||||
|
'Statutory Domain',
|
||||||
|
'Statutory MSD',
|
||||||
|
'Statutory LOI Ack',
|
||||||
|
'LOA Pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const REQUEST_DOCUMENTS_ALLOWED_ROLES = new Set<string>([
|
||||||
|
'DD Admin',
|
||||||
|
'Super Admin',
|
||||||
|
'DD Lead',
|
||||||
|
'DD Head',
|
||||||
|
]);
|
||||||
|
|
||||||
export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps) {
|
export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps) {
|
||||||
const {
|
const {
|
||||||
application,
|
application,
|
||||||
@ -117,8 +153,18 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
|
|||||||
setParticipantType,
|
setParticipantType,
|
||||||
handleAddParticipant,
|
handleAddParticipant,
|
||||||
isAssigningParticipant,
|
isAssigningParticipant,
|
||||||
|
documents = [],
|
||||||
|
documentConfigs = [],
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const [showRequestDocsModal, setShowRequestDocsModal] = useState(false);
|
||||||
|
|
||||||
|
const userRoleResolved =
|
||||||
|
currentUser?.roleCode || currentUser?.role || '';
|
||||||
|
const canRequestDocuments =
|
||||||
|
REQUEST_DOCUMENTS_ALLOWED_ROLES.has(userRoleResolved) &&
|
||||||
|
REQUEST_DOCUMENTS_ALLOWED_STATUSES.has(application?.status || '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card data-testid="onboarding-details-summary-card">
|
<Card data-testid="onboarding-details-summary-card">
|
||||||
@ -284,11 +330,23 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{canRequestDocuments && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-amber-300 hover:bg-amber-50 text-amber-700"
|
||||||
|
onClick={() => setShowRequestDocsModal(true)}
|
||||||
|
data-testid="onboarding-details-request-documents"
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
|
Request Documents
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) &&
|
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) &&
|
||||||
['Dealer Code Generation', 'LOA Pending', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion'].includes(application.status) && (
|
['Dealer Code Generation', 'LOA Pending', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion'].includes(application.status) && (
|
||||||
<>
|
<>
|
||||||
{!application.dealerCode && (
|
{!application.dealerCode && (
|
||||||
<Button className="w-full bg-blue-600 hover:bg-blue-700" onClick={handleGenerateDealerCodes} data-testid="onboarding-details-generate-dealer-codes">
|
<Button className="w-full bg-re-red hover:bg-re-red-hover" onClick={handleGenerateDealerCodes} data-testid="onboarding-details-generate-dealer-codes">
|
||||||
<Zap className="w-4 h-4 mr-2" />
|
<Zap className="w-4 h-4 mr-2" />
|
||||||
Generate Dealer Codes
|
Generate Dealer Codes
|
||||||
</Button>
|
</Button>
|
||||||
@ -297,7 +355,7 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
|
|||||||
{application.dealerCode && !application.architectureAssignedTo && (
|
{application.dealerCode && !application.architectureAssignedTo && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-blue-200 hover:bg-blue-50 text-blue-700"
|
className="w-full border-red-200 hover:bg-red-50 text-re-red"
|
||||||
onClick={onOpenAssignArchitectureModal}
|
onClick={onOpenAssignArchitectureModal}
|
||||||
data-testid="onboarding-details-assign-architecture"
|
data-testid="onboarding-details-assign-architecture"
|
||||||
>
|
>
|
||||||
@ -478,6 +536,15 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<RequestDocumentsModal
|
||||||
|
open={showRequestDocsModal}
|
||||||
|
onClose={() => setShowRequestDocsModal(false)}
|
||||||
|
applicationId={application?.id || ''}
|
||||||
|
applicantName={application?.name || application?.applicantName || 'the prospect'}
|
||||||
|
documentConfigs={documentConfigs}
|
||||||
|
uploadedDocuments={documents}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
@ -65,6 +66,11 @@ interface ApplicationDetailsTabsProps {
|
|||||||
eorChecklist: any[];
|
eorChecklist: any[];
|
||||||
setUploadDocType: (value: string) => void;
|
setUploadDocType: (value: string) => void;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
/**
|
||||||
|
* Whether the viewer may see finance/process-sensitive tabs (FDD Audit
|
||||||
|
* and Payments). Restricted to DD-Admin / Super Admin by policy.
|
||||||
|
*/
|
||||||
|
canViewFinanceTabs: boolean;
|
||||||
fetchApplication: () => void;
|
fetchApplication: () => void;
|
||||||
fetchEorData: () => void;
|
fetchEorData: () => void;
|
||||||
deposits: any[];
|
deposits: any[];
|
||||||
@ -100,6 +106,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
eorChecklist,
|
eorChecklist,
|
||||||
setUploadDocType,
|
setUploadDocType,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
canViewFinanceTabs,
|
||||||
fetchApplication,
|
fetchApplication,
|
||||||
fetchEorData,
|
fetchEorData,
|
||||||
deposits,
|
deposits,
|
||||||
@ -112,6 +119,15 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
auditLogActionBadgeClass,
|
auditLogActionBadgeClass,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
// If the viewer loses (or never had) access to FDD/Payments tabs but the
|
||||||
|
// active tab is one of them — e.g. via direct deep-link or stale state —
|
||||||
|
// bounce them back to the Progress tab so they don't see an empty pane.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canViewFinanceTabs && (activeTab === 'fdd' || activeTab === 'payments')) {
|
||||||
|
setActiveTab('progress');
|
||||||
|
}
|
||||||
|
}, [canViewFinanceTabs, activeTab, setActiveTab]);
|
||||||
|
|
||||||
const normalizeRole = (value: unknown): string =>
|
const normalizeRole = (value: unknown): string =>
|
||||||
String(value || '')
|
String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@ -138,9 +154,13 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
<TabsTrigger value="progress" className="min-w-[80px]" data-testid="onboarding-tab-trigger-progress">Progress</TabsTrigger>
|
<TabsTrigger value="progress" className="min-w-[80px]" data-testid="onboarding-tab-trigger-progress">Progress</TabsTrigger>
|
||||||
<TabsTrigger value="documents" className="min-w-[100px]" data-testid="onboarding-tab-trigger-documents">Documents</TabsTrigger>
|
<TabsTrigger value="documents" className="min-w-[100px]" data-testid="onboarding-tab-trigger-documents">Documents</TabsTrigger>
|
||||||
<TabsTrigger value="interviews" className="min-w-[100px]" data-testid="onboarding-tab-trigger-interviews">Interviews</TabsTrigger>
|
<TabsTrigger value="interviews" className="min-w-[100px]" data-testid="onboarding-tab-trigger-interviews">Interviews</TabsTrigger>
|
||||||
<TabsTrigger value="fdd" className="min-w-[120px]" data-testid="onboarding-tab-trigger-fdd">FDD Audit</TabsTrigger>
|
{canViewFinanceTabs && (
|
||||||
|
<TabsTrigger value="fdd" className="min-w-[120px]" data-testid="onboarding-tab-trigger-fdd">FDD Audit</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="eor" className="min-w-[120px]" data-testid="onboarding-tab-trigger-eor">EOR Checklist</TabsTrigger>
|
<TabsTrigger value="eor" className="min-w-[120px]" data-testid="onboarding-tab-trigger-eor">EOR Checklist</TabsTrigger>
|
||||||
<TabsTrigger value="payments" className="min-w-[100px]" data-testid="onboarding-tab-trigger-payments">Payments</TabsTrigger>
|
{canViewFinanceTabs && (
|
||||||
|
<TabsTrigger value="payments" className="min-w-[100px]" data-testid="onboarding-tab-trigger-payments">Payments</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="audit" className="min-w-[100px]" data-testid="onboarding-tab-trigger-audit">Audit Trail</TabsTrigger>
|
<TabsTrigger value="audit" className="min-w-[100px]" data-testid="onboarding-tab-trigger-audit">Audit Trail</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@ -326,7 +346,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
5: 2,
|
5: 2,
|
||||||
6: 2,
|
6: 2,
|
||||||
8: 2,
|
8: 2,
|
||||||
12: 2
|
13: 2
|
||||||
};
|
};
|
||||||
const stageId = Number(stage.id);
|
const stageId = Number(stage.id);
|
||||||
const expectedCount = expectedMap[stageId];
|
const expectedCount = expectedMap[stageId];
|
||||||
@ -337,7 +357,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
5: 2,
|
5: 2,
|
||||||
6: 3,
|
6: 3,
|
||||||
8: 'LOI_APPROVAL',
|
8: 'LOI_APPROVAL',
|
||||||
12: 'LOA_APPROVAL',
|
13: 'LOA_APPROVAL',
|
||||||
};
|
};
|
||||||
const mappedStageCode = stageCodeById[stageId];
|
const mappedStageCode = stageCodeById[stageId];
|
||||||
const actualCount = mappedStageCode ? getApproverStatus(mappedStageCode).length : (stage.evaluators?.length || 0);
|
const actualCount = mappedStageCode ? getApproverStatus(mappedStageCode).length : (stage.evaluators?.length || 0);
|
||||||
@ -770,9 +790,11 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="fdd" className="space-y-6" data-testid="onboarding-tab-content-fdd">
|
{canViewFinanceTabs && (
|
||||||
{renderFddAuditContent()}
|
<TabsContent value="fdd" className="space-y-6" data-testid="onboarding-tab-content-fdd">
|
||||||
</TabsContent>
|
{renderFddAuditContent()}
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="eor" className="space-y-4 status-progress-ui" data-testid="onboarding-tab-content-eor">
|
<TabsContent value="eor" className="space-y-4 status-progress-ui" data-testid="onboarding-tab-content-eor">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
@ -927,6 +949,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{canViewFinanceTabs && (
|
||||||
<TabsContent value="payments" className="space-y-6" data-testid="onboarding-tab-content-payments">
|
<TabsContent value="payments" className="space-y-6" data-testid="onboarding-tab-content-payments">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h3 className="text-lg font-semibold text-slate-900">Security Deposits</h3>
|
<h3 className="text-lg font-semibold text-slate-900">Security Deposits</h3>
|
||||||
@ -1101,6 +1124,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="audit" data-testid="onboarding-tab-content-audit">
|
<TabsContent value="audit" data-testid="onboarding-tab-content-audit">
|
||||||
<ScrollArea className="h-[30rem] rounded-md border border-slate-100 bg-slate-50/50">
|
<ScrollArea className="h-[30rem] rounded-md border border-slate-100 bg-slate-50/50">
|
||||||
|
|||||||
@ -0,0 +1,271 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { CheckCircle2, FileQuestion, Loader2, Mail } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { onboardingService } from '@/services/onboarding.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal where an admin (DD Admin / Super Admin / DD Lead / DD Head) ticks the documents
|
||||||
|
* still missing for a prospect and dispatches one email per category. Uploaded docs
|
||||||
|
* are surfaced as disabled rows for transparency.
|
||||||
|
*/
|
||||||
|
interface RequestDocumentsModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
applicationId: string;
|
||||||
|
applicantName: string;
|
||||||
|
documentConfigs: any[];
|
||||||
|
uploadedDocuments: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryKey = 'LOI' | 'Statutory' | 'Architecture' | 'FDD' | 'Other';
|
||||||
|
|
||||||
|
const CATEGORY_LABEL: Record<CategoryKey, string> = {
|
||||||
|
LOI: 'LOI Documents',
|
||||||
|
Statutory: 'Statutory & Compliance',
|
||||||
|
Architecture: 'Architecture Inputs',
|
||||||
|
FDD: 'FDD / Financial',
|
||||||
|
Other: 'Other',
|
||||||
|
};
|
||||||
|
|
||||||
|
function categorize(stageCode?: string | null): CategoryKey {
|
||||||
|
const s = String(stageCode || '').toLowerCase();
|
||||||
|
if (s.startsWith('loi')) return 'LOI';
|
||||||
|
if (s.startsWith('statutory')) return 'Statutory';
|
||||||
|
if (s.startsWith('architecture')) return 'Architecture';
|
||||||
|
if (s.startsWith('fdd')) return 'FDD';
|
||||||
|
return 'Other';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestDocumentsModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
applicationId,
|
||||||
|
applicantName,
|
||||||
|
documentConfigs,
|
||||||
|
uploadedDocuments,
|
||||||
|
}: RequestDocumentsModalProps) {
|
||||||
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
|
const [dueDays, setDueDays] = useState<number>(14);
|
||||||
|
const [customMessage, setCustomMessage] = useState<string>('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const uploadedSet = useMemo(
|
||||||
|
() => new Set((uploadedDocuments || []).map((d: any) => d.documentType)),
|
||||||
|
[uploadedDocuments]
|
||||||
|
);
|
||||||
|
|
||||||
|
const grouped = useMemo(() => {
|
||||||
|
const buckets: Record<CategoryKey, any[]> = {
|
||||||
|
LOI: [], Statutory: [], Architecture: [], FDD: [], Other: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// De-duplicate by documentType — the master may return multiple rows per type if the
|
||||||
|
// doc is reused across stages. We keep the first occurrence (typically the canonical one).
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const cfg of documentConfigs || []) {
|
||||||
|
if (!cfg?.documentType || seen.has(cfg.documentType)) continue;
|
||||||
|
seen.add(cfg.documentType);
|
||||||
|
buckets[categorize(cfg.stageCode)].push(cfg);
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}, [documentConfigs]);
|
||||||
|
|
||||||
|
const toggle = (docType: string) => {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(docType)) next.delete(docType);
|
||||||
|
else next.add(docType);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const documentTypes = Array.from(selected);
|
||||||
|
if (documentTypes.length === 0) {
|
||||||
|
toast.warning('Pick at least one document to request');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const result: any = await onboardingService.requestProspectDocuments(applicationId, {
|
||||||
|
documentTypes,
|
||||||
|
dueDays,
|
||||||
|
customMessage: customMessage.trim() || undefined,
|
||||||
|
});
|
||||||
|
const emailsSent = (result?.emailsSent || []).filter((e: any) => e.status === 'sent');
|
||||||
|
const failed = (result?.emailsSent || []).filter((e: any) => e.status === 'failed');
|
||||||
|
const skipped = result?.skippedAlreadyUploaded || [];
|
||||||
|
|
||||||
|
if (emailsSent.length > 0) {
|
||||||
|
toast.success(
|
||||||
|
`Sent ${emailsSent.length} email${emailsSent.length === 1 ? '' : 's'} to ${applicantName}` +
|
||||||
|
(skipped.length ? ` — ${skipped.length} already uploaded, skipped` : '')
|
||||||
|
);
|
||||||
|
} else if (skipped.length) {
|
||||||
|
toast.info(`No email sent — all selected documents were already uploaded.`);
|
||||||
|
}
|
||||||
|
if (failed.length) {
|
||||||
|
toast.error(`Some email categories failed: ${failed.map((f: any) => f.category).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelected(new Set());
|
||||||
|
setCustomMessage('');
|
||||||
|
onClose();
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err?.message || 'Failed to send document request');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalSelectable = (Object.values(grouped) as any[][]).reduce((sum, list) => sum + list.length, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Mail className="w-5 h-5 text-re-red" />
|
||||||
|
Request Documents from Prospect
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Pick the documents you want {applicantName || 'the prospect'} to upload. We'll send one email per
|
||||||
|
category. Anything already uploaded is shown for context and skipped automatically.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{totalSelectable === 0 ? (
|
||||||
|
<div className="py-10 text-center text-slate-500 flex flex-col items-center gap-2">
|
||||||
|
<FileQuestion className="w-8 h-8" />
|
||||||
|
<p>No document configurations available for this application.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="max-h-[55vh] pr-3">
|
||||||
|
<div className="space-y-5">
|
||||||
|
{(Object.keys(CATEGORY_LABEL) as CategoryKey[]).map((cat) => {
|
||||||
|
const items = grouped[cat];
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div key={cat} className="border rounded-lg p-3 bg-slate-50">
|
||||||
|
<h4 className="font-medium text-slate-700 mb-2 text-sm uppercase tracking-wide">
|
||||||
|
{CATEGORY_LABEL[cat]}
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{items.map((cfg: any) => {
|
||||||
|
const isUploaded = uploadedSet.has(cfg.documentType);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${cat}-${cfg.documentType}`}
|
||||||
|
className={`flex items-start gap-3 p-2 rounded ${
|
||||||
|
isUploaded ? 'bg-green-50 border border-green-100' : 'bg-white border border-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isUploaded ? (
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<Checkbox
|
||||||
|
id={`req-doc-${cfg.documentType}`}
|
||||||
|
checked={selected.has(cfg.documentType)}
|
||||||
|
onCheckedChange={() => toggle(cfg.documentType)}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Label
|
||||||
|
htmlFor={`req-doc-${cfg.documentType}`}
|
||||||
|
className={`flex-1 cursor-pointer text-sm leading-snug ${
|
||||||
|
isUploaded ? 'text-slate-500 line-through' : 'text-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="font-medium">{cfg.documentType}</span>
|
||||||
|
{cfg.isMandatory && !isUploaded && (
|
||||||
|
<Badge className="ml-2 bg-red-100 text-red-700 text-[10px]">Mandatory</Badge>
|
||||||
|
)}
|
||||||
|
{isUploaded && (
|
||||||
|
<Badge className="ml-2 bg-green-100 text-green-700 text-[10px]">Uploaded</Badge>
|
||||||
|
)}
|
||||||
|
{cfg.description && (
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{cfg.description}</div>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-2 border-t">
|
||||||
|
<div className="sm:col-span-1">
|
||||||
|
<Label htmlFor="req-doc-due-days" className="text-xs text-slate-600">
|
||||||
|
Due in (days)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="req-doc-due-days"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={60}
|
||||||
|
value={dueDays}
|
||||||
|
onChange={(e) => setDueDays(Math.max(1, Number(e.target.value) || 14))}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Label htmlFor="req-doc-message" className="text-xs text-slate-600">
|
||||||
|
Custom message (optional)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="req-doc-message"
|
||||||
|
placeholder="Add a short note for the prospect…"
|
||||||
|
value={customMessage}
|
||||||
|
onChange={(e) => setCustomMessage(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose} disabled={submitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={submitting || selected.size === 0}
|
||||||
|
className="bg-re-red hover:bg-re-red/90 text-white"
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Sending…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Mail className="w-4 h-4 mr-2" />
|
||||||
|
Send Request ({selected.size})
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -124,64 +124,80 @@ export function useApplicationDetailsStageData({
|
|||||||
id: 9, name: 'Security Deposit', status: getSecurityDepositStageStatus(),
|
id: 9, name: 'Security Deposit', status: getSecurityDepositStageStatus(),
|
||||||
date: application.securityDetailsDate, description: 'Security Deposit verification', documentsUploaded: 3
|
date: application.securityDetailsDate, description: 'Security Deposit verification', documentsUploaded: 3
|
||||||
},
|
},
|
||||||
|
(() => {
|
||||||
|
const loiDocConfigs = documentConfigs.filter((c: any) => c.stageCode === 'LOI Issue' || c.stageCode === 'LOI Documents');
|
||||||
|
const loiDocList = loiDocConfigs.length
|
||||||
|
? loiDocConfigs.map((c: any) => c.documentType)
|
||||||
|
: ['Letter of Intent', 'Signed LOI'];
|
||||||
|
const allUploaded = loiDocList.every((dt: string) =>
|
||||||
|
isDocumentUploaded(dt) || (dt === 'Letter of Intent' && isDocumentUploaded('LOI')) || (dt === 'Signed LOI' && isDocumentUploaded('LOI Signed Copy'))
|
||||||
|
);
|
||||||
|
const loiApprovalDone = getStageStatus('LOI Approval') === 'completed';
|
||||||
|
const status: ProcessStage['status'] = allUploaded ? 'completed' : loiApprovalDone ? 'active' : 'pending';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 10, name: 'LOI Documents', status,
|
||||||
|
description: 'Upload Letter of Intent documents before issuance', isParallel: true,
|
||||||
|
branches: [
|
||||||
|
{
|
||||||
|
name: 'Documents Required', color: 'green', stages:
|
||||||
|
loiDocConfigs.length
|
||||||
|
? loiDocConfigs.map((c: any, i: number) => ({
|
||||||
|
id: `10a-${i}`,
|
||||||
|
name: c.documentType,
|
||||||
|
status: isDocumentUploaded(c.documentType) ? 'completed' : 'active',
|
||||||
|
description: c.isMandatory ? `Upload ${c.documentType} (Mandatory)` : `Upload ${c.documentType}`
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{ id: '10a-1', name: 'Letter of Intent', status: isDocumentUploaded('Letter of Intent') || isDocumentUploaded('LOI') ? 'completed' : 'active', description: 'Letter of Intent document' },
|
||||||
|
{ id: '10a-2', name: 'Signed LOI', status: isDocumentUploaded('Signed LOI') || isDocumentUploaded('LOI Signed Copy') ? 'completed' : 'active', description: 'Signed Letter of Intent' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} satisfies ProcessStage;
|
||||||
|
})(),
|
||||||
{
|
{
|
||||||
id: 10, name: 'LOI Issue', status: getStageStatus('LOI Issue'),
|
id: 11, name: 'LOI Issue', status: getStageStatus('LOI Issue'),
|
||||||
date: application.loiIssueDate, description: 'Letter of Intent issued', isParallel: true,
|
date: application.loiIssueDate, description: 'Letter of Intent issued'
|
||||||
branches: [
|
|
||||||
{
|
|
||||||
name: 'LOI Documents', color: 'green', stages:
|
|
||||||
documentConfigs.some((c: any) => c.stageCode === 'LOI Issue')
|
|
||||||
? documentConfigs.filter((c: any) => c.stageCode === 'LOI Issue').map((c: any, i: number) => ({
|
|
||||||
id: `10a-${i}`,
|
|
||||||
name: c.documentType,
|
|
||||||
status: isDocumentUploaded(c.documentType) ? 'completed' : 'active',
|
|
||||||
description: c.isMandatory ? `Upload ${c.documentType} (Mandatory)` : `Upload ${c.documentType}`
|
|
||||||
}))
|
|
||||||
: [
|
|
||||||
{ id: '10a-1', name: 'Letter of Intent', status: isDocumentUploaded('Letter of Intent') || isDocumentUploaded('LOI') ? 'completed' : 'active', description: 'Letter of Intent document' },
|
|
||||||
{ id: '10a-2', name: 'Signed LOI', status: isDocumentUploaded('Signed LOI') || isDocumentUploaded('LOI Signed Copy') ? 'completed' : 'active', description: 'Signed Letter of Intent' },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11, name: 'Dealer Code Generation', status: getStageStatus('Dealer Code Generation'),
|
id: 12, name: 'Dealer Code Generation', status: getStageStatus('Dealer Code Generation'),
|
||||||
date: application.dealerCodeDate, description: 'Dealer code generated and assigned', isParallel: true,
|
date: application.dealerCodeDate, description: 'Dealer code generated and assigned', isParallel: true,
|
||||||
branches: [
|
branches: [
|
||||||
{
|
{
|
||||||
name: 'Architectural Work', color: 'green', stages: [
|
name: 'Architectural Work', color: 'green', stages: [
|
||||||
{ id: '11a-1', name: 'Architecture Assignment', status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', description: 'Assigned to architecture team' },
|
{ id: '12a-1', name: 'Architecture Assignment', status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', description: 'Assigned to architecture team' },
|
||||||
{ id: '11a-2', name: 'Site Plan Blueprint', status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', description: 'Blueprints and site plans' },
|
{ id: '12a-2', name: 'Site Plan Blueprint', status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', description: 'Blueprints and site plans' },
|
||||||
{ id: '11a-3', name: 'Architecture Work', status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending', description: 'Final architecture approval' },
|
{ id: '12a-3', name: 'Architecture Work', status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending', description: 'Final architecture approval' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Statutory Documents', color: 'green', stages: [
|
name: 'Statutory Documents', color: 'green', stages: [
|
||||||
{ id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
|
{ id: '12b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
|
||||||
{ id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
|
{ id: '12b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
|
||||||
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
|
{ id: '12b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
|
||||||
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
|
{ id: '12b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
|
||||||
{ id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' },
|
{ id: '12b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' },
|
||||||
{ id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
|
{ id: '12b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
|
||||||
{ id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' },
|
{ id: '12b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' },
|
||||||
{ id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
|
{ id: '12b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
|
||||||
{ id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
|
{ id: '12b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
|
||||||
{ id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
|
{ id: '12b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
|
||||||
{ id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' },
|
{ id: '12b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12, name: 'LOA', status: getStageStatus('LOA'),
|
id: 13, name: 'LOA', status: getStageStatus('LOA'),
|
||||||
isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified',
|
isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified',
|
||||||
lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.',
|
lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.',
|
||||||
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map(participantLabel))),
|
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map(participantLabel))),
|
||||||
description: 'Letter of Authorization'
|
description: 'Letter of Agreement'
|
||||||
},
|
},
|
||||||
{ id: 13, name: 'EOR Complete', status: getStageStatus('EOR Complete'), description: 'Essential Operating Requirements' },
|
{ id: 14, name: 'EOR Complete', status: getStageStatus('EOR Complete'), description: 'Essential Operating Requirements' },
|
||||||
{ id: 14, name: 'Inauguration', status: getStageStatus('Inauguration'), description: 'Dealership inauguration' },
|
{ id: 15, name: 'Inauguration', status: getStageStatus('Inauguration'), description: 'Dealership inauguration' },
|
||||||
{ id: 15, name: 'Dealership Active', status: getStageStatus('Onboarded'), description: 'Dealer profile active' },
|
{ id: 16, name: 'Dealership Active', status: getStageStatus('Onboarded'), description: 'Dealer profile active' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const eorChecklist = [
|
const eorChecklist = [
|
||||||
|
|||||||
@ -178,6 +178,18 @@ export const ApplicationDetails = () => {
|
|||||||
currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' ||
|
currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' ||
|
||||||
currentUser?.role === 'NBH' || currentUser?.role === 'DD Head' ||
|
currentUser?.role === 'NBH' || currentUser?.role === 'DD Head' ||
|
||||||
currentUser?.roleCode === 'NBH' || currentUser?.roleCode === 'DD_HEAD';
|
currentUser?.roleCode === 'NBH' || currentUser?.roleCode === 'DD_HEAD';
|
||||||
|
// FDD Audit and Payments tabs are finance-/process-sensitive. Per ops policy,
|
||||||
|
// only DD-Admin, Super Admin and Finance roles should see them — the broader
|
||||||
|
// `isAdmin` bucket (which also includes NBH and DD-Head) is too wide here.
|
||||||
|
const canViewFinanceTabs =
|
||||||
|
currentUser?.roleCode === 'Super Admin' ||
|
||||||
|
currentUser?.roleCode === 'DD Admin' ||
|
||||||
|
currentUser?.roleCode === 'Finance' ||
|
||||||
|
currentUser?.roleCode === 'Finance Admin' ||
|
||||||
|
currentUser?.role === 'Super Admin' ||
|
||||||
|
currentUser?.role === 'DD Admin' ||
|
||||||
|
currentUser?.role === 'Finance' ||
|
||||||
|
currentUser?.role === 'Finance Admin';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchConfigs = async () => {
|
const fetchConfigs = async () => {
|
||||||
@ -471,6 +483,7 @@ export const ApplicationDetails = () => {
|
|||||||
eorChecklist={eorChecklist}
|
eorChecklist={eorChecklist}
|
||||||
setUploadDocType={setUploadDocType}
|
setUploadDocType={setUploadDocType}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
|
canViewFinanceTabs={canViewFinanceTabs}
|
||||||
fetchApplication={fetchApplication}
|
fetchApplication={fetchApplication}
|
||||||
fetchEorData={fetchEorData}
|
fetchEorData={fetchEorData}
|
||||||
deposits={deposits}
|
deposits={deposits}
|
||||||
@ -537,6 +550,8 @@ export const ApplicationDetails = () => {
|
|||||||
setParticipantType={setParticipantType}
|
setParticipantType={setParticipantType}
|
||||||
handleAddParticipant={handleAddParticipant}
|
handleAddParticipant={handleAddParticipant}
|
||||||
isAssigningParticipant={isAssigningParticipant}
|
isAssigningParticipant={isAssigningParticipant}
|
||||||
|
documents={documents}
|
||||||
|
documentConfigs={documentConfigs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ApplicationDetailsActionModals
|
<ApplicationDetailsActionModals
|
||||||
|
|||||||
@ -197,42 +197,23 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
|
|||||||
alert('Exporting applications to CSV...');
|
alert('Exporting applications to CSV...');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status badge classes — keep the palette restrained to the brand
|
||||||
|
* (re-red + black + slate). A rainbow palette across status pills
|
||||||
|
* fights the rest of the UI, so we collapse to three buckets:
|
||||||
|
* - positive end-states (Approved / Onboarded / Completed) → black
|
||||||
|
* - terminal negative (Rejected / Disqualified) → muted brand red
|
||||||
|
* - everything else (in-progress / pending / default) → light slate
|
||||||
|
*/
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
const statusColors: Record<string, string> = {
|
const s = String(status || '');
|
||||||
'Submitted': 'bg-slate-500',
|
if (s === 'Rejected' || s === 'Disqualified' || s.includes('Rejected')) {
|
||||||
'Questionnaire Pending': 'bg-orange-500',
|
return 'bg-red-50 text-re-red-hover border border-red-200';
|
||||||
'Questionnaire Completed': 'bg-blue-500',
|
}
|
||||||
'Shortlisted': 'bg-cyan-500',
|
if (s === 'Approved' || s === 'Onboarded' || s === 'Completed') {
|
||||||
'Level 1 Pending': 'bg-red-500',
|
return 'bg-slate-900 text-white border border-transparent';
|
||||||
'Level 1 Approved': 'bg-green-500',
|
}
|
||||||
'Level 2 Pending': 'bg-purple-500',
|
return 'bg-slate-200 text-slate-800 border border-slate-300';
|
||||||
'Level 2 Approved': 'bg-green-600',
|
|
||||||
'Level 2 Recommended': 'bg-teal-500',
|
|
||||||
'Level 3 Pending': 'bg-indigo-500',
|
|
||||||
'FDD Verification': 'bg-violet-500',
|
|
||||||
'Payment Pending': 'bg-yellow-500',
|
|
||||||
'LOI Issued': 'bg-lime-500',
|
|
||||||
'Dealer Code Generation': 'bg-fuchsia-500',
|
|
||||||
'Architecture Team Assigned': 'bg-blue-500',
|
|
||||||
'Architecture Document Upload': 'bg-blue-500',
|
|
||||||
'Architecture Team Completion': 'bg-blue-500',
|
|
||||||
'Statutory GST': 'bg-emerald-500',
|
|
||||||
'Statutory PAN': 'bg-emerald-500',
|
|
||||||
'Statutory Nodal': 'bg-emerald-500',
|
|
||||||
'Statutory Check': 'bg-emerald-500',
|
|
||||||
'Statutory Partnership': 'bg-emerald-500',
|
|
||||||
'Statutory Firm Reg': 'bg-emerald-500',
|
|
||||||
'Statutory Virtual Code': 'bg-emerald-500',
|
|
||||||
'Statutory Domain': 'bg-emerald-500',
|
|
||||||
'Statutory MSD': 'bg-emerald-500',
|
|
||||||
'Statutory LOI Ack': 'bg-emerald-500',
|
|
||||||
'EOR In Progress': 'bg-sky-500',
|
|
||||||
'LOA Pending': 'bg-emerald-500',
|
|
||||||
'Approved': 'bg-green-700',
|
|
||||||
'Rejected': 'bg-red-500',
|
|
||||||
'Disqualified': 'bg-red-700'
|
|
||||||
};
|
|
||||||
return statusColors[status] || 'bg-slate-500';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -197,7 +197,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
|
|||||||
</TabsTrigger>
|
</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">
|
<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
|
<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>}
|
{workNotes.length > 0 && <Badge className="ml-1 h-5 w-5 p-0 flex items-center justify-center bg-re-red rounded-full text-[10px] text-white font-black" data-testid="onboarding-finance-fdd-worknotes-count">{workNotes.length}</Badge>}
|
||||||
</TabsTrigger>
|
</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">
|
<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
|
<History className="w-4 h-4" /> Audit Trail
|
||||||
@ -271,7 +271,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
|
|||||||
</div>
|
</div>
|
||||||
{doc && (
|
{doc && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="ghost" size="sm" className="h-8 text-blue-600 font-black text-[10px] uppercase tracking-widest"
|
<Button variant="ghost" size="sm" className="h-8 text-re-red font-black text-[10px] uppercase tracking-widest"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPreviewDoc(doc);
|
setPreviewDoc(doc);
|
||||||
setShowPreviewModal(true);
|
setShowPreviewModal(true);
|
||||||
@ -527,8 +527,8 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
|
|||||||
|
|
||||||
<Card className="bg-slate-50 border-slate-200 rounded-2xl" data-testid="onboarding-finance-fdd-progress-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">
|
<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">
|
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center shrink-0">
|
||||||
<Clock className="w-5 h-5 text-blue-600" />
|
<Clock className="w-5 h-5 text-re-red" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-[10px] font-black uppercase tracking-widest text-slate-400">Current Progress</h5>
|
<h5 className="text-[10px] font-black uppercase tracking-widest text-slate-400">Current Progress</h5>
|
||||||
@ -588,8 +588,8 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
|
|||||||
disabled={isNoteSubmitting}
|
disabled={isNoteSubmitting}
|
||||||
data-testid="onboarding-finance-fdd-new-note-input"
|
data-testid="onboarding-finance-fdd-new-note-input"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="shrink-0 bg-blue-600 hover:bg-blue-700 h-auto self-stretch rounded-xl px-6"
|
className="shrink-0 bg-re-red hover:bg-re-red-hover h-auto self-stretch rounded-xl px-6"
|
||||||
onClick={handlePostNote}
|
onClick={handlePostNote}
|
||||||
disabled={isNoteSubmitting || !newNote.trim()}
|
disabled={isNoteSubmitting || !newNote.trim()}
|
||||||
data-testid="onboarding-finance-fdd-send-note-btn"
|
data-testid="onboarding-finance-fdd-send-note-btn"
|
||||||
|
|||||||
@ -121,7 +121,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center p-20 text-blue-600">
|
<div className="flex items-center justify-center p-20 text-re-red">
|
||||||
<Clock className="w-8 h-8 animate-spin mr-3" />
|
<Clock className="w-8 h-8 animate-spin mr-3" />
|
||||||
<span>Loading Finance Queue...</span>
|
<span>Loading Finance Queue...</span>
|
||||||
</div>
|
</div>
|
||||||
@ -148,7 +148,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
<div className="inline-flex p-1 bg-slate-100 rounded-xl">
|
<div className="inline-flex p-1 bg-slate-100 rounded-xl">
|
||||||
<div className="flex items-center px-4 py-2 bg-white rounded-lg text-slate-900 shadow-sm font-medium text-sm" data-testid="onboarding-finance-queue-pending-count">
|
<div className="flex items-center px-4 py-2 bg-white rounded-lg text-slate-900 shadow-sm font-medium text-sm" data-testid="onboarding-finance-queue-pending-count">
|
||||||
<IndianRupee className="w-4 h-4 mr-2 text-blue-600" />
|
<IndianRupee className="w-4 h-4 mr-2 text-re-red" />
|
||||||
Pending Payments ({paymentRows.filter((row: any) => !isVerifiedLikeStatus(row.paymentStatus)).length})
|
Pending Payments ({paymentRows.filter((row: any) => !isVerifiedLikeStatus(row.paymentStatus)).length})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -203,10 +203,10 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
const statusLabel = row.paymentStatus || 'Awaiting Payment';
|
const statusLabel = row.paymentStatus || 'Awaiting Payment';
|
||||||
const app = row.application || {};
|
const app = row.application || {};
|
||||||
return (
|
return (
|
||||||
<TableRow key={row.id} className="hover:bg-blue-50/20 group transition-all" data-testid={`onboarding-finance-queue-row-${idx}`}>
|
<TableRow key={row.id} className="hover:bg-red-50/20 group transition-all" data-testid={`onboarding-finance-queue-row-${idx}`}>
|
||||||
<TableCell className="py-4 pl-6">
|
<TableCell className="py-4 pl-6">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-mono text-xs font-bold text-blue-600 mb-1" data-testid={`onboarding-finance-queue-app-id-${idx}`}>{app.applicationId || app.id}</span>
|
<span className="font-mono text-xs font-bold text-re-red mb-1" data-testid={`onboarding-finance-queue-app-id-${idx}`}>{app.applicationId || app.id}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-semibold text-slate-900" data-testid={`onboarding-finance-queue-name-${idx}`}>{app.applicantName}</span>
|
<span className="font-semibold text-slate-900" data-testid={`onboarding-finance-queue-name-${idx}`}>{app.applicantName}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -244,7 +244,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant={isVerifiedLikeStatus(statusLabel) ? 'outline' : 'default'}
|
variant={isVerifiedLikeStatus(statusLabel) ? 'outline' : 'default'}
|
||||||
className={!isVerifiedLikeStatus(statusLabel)
|
className={!isVerifiedLikeStatus(statusLabel)
|
||||||
? 'bg-blue-600 hover:bg-blue-700 shadow-md'
|
? 'bg-re-red hover:bg-re-red-hover shadow-md'
|
||||||
: 'bg-white text-slate-600 border-slate-200'}
|
: 'bg-white text-slate-600 border-slate-200'}
|
||||||
onClick={() => handleAction(row.applicationId || app.id)}
|
onClick={() => handleAction(row.applicationId || app.id)}
|
||||||
data-testid={`onboarding-finance-queue-action-btn-${idx}`}
|
data-testid={`onboarding-finance-queue-action-btn-${idx}`}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { toast } from 'sonner';
|
|||||||
import { dealerService } from '@/services/dealer.service';
|
import { dealerService } from '@/services/dealer.service';
|
||||||
import { masterService } from '@/services/master.service';
|
import { masterService } from '@/services/master.service';
|
||||||
import { formatDateTime } from '@/components/ui/utils';
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
import { getRequestStatusBadgeClass } from '@/lib/statusProgressTheme';
|
import { getRequestStatusBadgeClass, getStatusProgressBarClass } from '@/lib/statusProgressTheme';
|
||||||
|
|
||||||
interface DealerRelocationPageProps {
|
interface DealerRelocationPageProps {
|
||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
@ -495,7 +495,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
|
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
|
||||||
<div
|
<div
|
||||||
className="bg-re-red h-2 rounded-full"
|
className={`h-2 rounded-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
|
||||||
style={{ width: `${request.progressPercentage || 0}%` }}
|
style={{ width: `${request.progressPercentage || 0}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -206,12 +206,15 @@ const getApiErrorMessage = (error: any, fallback: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function RelocationRequestDetails({ requestId, onBack, currentUser }: RelocationRequestDetailsProps) {
|
export function RelocationRequestDetails({ requestId, onBack, currentUser }: RelocationRequestDetailsProps) {
|
||||||
const { get: getSla } = useSlaBatchStatus(
|
|
||||||
requestId ? [{ entityType: 'relocation', entityId: requestId }] : [],
|
|
||||||
Boolean(requestId)
|
|
||||||
);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [request, setRequest] = useState<any>(null);
|
const [request, setRequest] = useState<any>(null);
|
||||||
|
// URL slug may be the human-readable code (e.g. `REL-...`); SLA expects UUID.
|
||||||
|
// Feed the SLA hook the resolved UUID once the request has loaded.
|
||||||
|
const slaEntityId: string = request?.id || '';
|
||||||
|
const { get: getSla } = useSlaBatchStatus(
|
||||||
|
slaEntityId ? [{ entityType: 'relocation', entityId: slaEntityId }] : [],
|
||||||
|
Boolean(slaEntityId)
|
||||||
|
);
|
||||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@ -621,7 +624,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
|
|||||||
{request.outlet?.name} ({request.outlet?.code})
|
{request.outlet?.name} ({request.outlet?.code})
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<SlaBadge status={getSla('relocation', requestId)} />
|
<SlaBadge status={getSla('relocation', slaEntityId)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -67,10 +67,6 @@ const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) {
|
export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) {
|
||||||
const { get: getSla } = useSlaBatchStatus(
|
|
||||||
resignationId ? [{ entityType: 'resignation', entityId: resignationId }] : [],
|
|
||||||
Boolean(resignationId)
|
|
||||||
);
|
|
||||||
const getDocumentsForStage = (stageName: string, stageKey?: string) => {
|
const getDocumentsForStage = (stageName: string, stageKey?: string) => {
|
||||||
const allDocs = [
|
const allDocs = [
|
||||||
...(resignationData?.documents || []),
|
...(resignationData?.documents || []),
|
||||||
@ -104,6 +100,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
const [forceTriggerFnF, setForceTriggerFnF] = useState(false);
|
const [forceTriggerFnF, setForceTriggerFnF] = useState(false);
|
||||||
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
||||||
const [resignationData, setResignationData] = useState<any>(null); // Real data from API
|
const [resignationData, setResignationData] = useState<any>(null); // Real data from API
|
||||||
|
// URL slug may be the human-readable code (e.g. `RES-...`); SLA expects UUID.
|
||||||
|
// Feed the SLA hook the resolved UUID once the request has loaded.
|
||||||
|
const slaEntityId: string = resignationData?.id || '';
|
||||||
|
const { get: getSla } = useSlaBatchStatus(
|
||||||
|
slaEntityId ? [{ entityType: 'resignation', entityId: slaEntityId }] : [],
|
||||||
|
Boolean(slaEntityId)
|
||||||
|
);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@ -595,7 +598,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
? 'Completed'
|
? 'Completed'
|
||||||
: formatOffboardingStatusLabel(resignationData?.status || 'Pending')}
|
: formatOffboardingStatusLabel(resignationData?.status || 'Pending')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<SlaBadge status={getSla('resignation', resignationId)} />
|
<SlaBadge status={getSla('resignation', slaEntityId)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -39,10 +39,6 @@ interface TerminationDetailsProps {
|
|||||||
|
|
||||||
export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) {
|
export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { get: getSla } = useSlaBatchStatus(
|
|
||||||
terminationId ? [{ entityType: 'termination', entityId: terminationId }] : [],
|
|
||||||
Boolean(terminationId)
|
|
||||||
);
|
|
||||||
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'hold' | null }>({ open: false, type: null });
|
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'hold' | null }>({ open: false, type: null });
|
||||||
const [remarks, setRemarks] = useState('');
|
const [remarks, setRemarks] = useState('');
|
||||||
const [assignToUser, setAssignToUser] = useState('');
|
const [assignToUser, setAssignToUser] = useState('');
|
||||||
@ -50,6 +46,13 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [terminationData, setTerminationData] = useState<any>(null);
|
const [terminationData, setTerminationData] = useState<any>(null);
|
||||||
|
// URL slug may be the human-readable code (e.g. `TER-...`); SLA expects UUID.
|
||||||
|
// Feed the SLA hook the resolved UUID once the request has loaded.
|
||||||
|
const slaEntityId: string = terminationData?.id || '';
|
||||||
|
const { get: getSla } = useSlaBatchStatus(
|
||||||
|
slaEntityId ? [{ entityType: 'termination', entityId: slaEntityId }] : [],
|
||||||
|
Boolean(slaEntityId)
|
||||||
|
);
|
||||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||||
const [showSCNDialog, setShowSCNDialog] = useState(false);
|
const [showSCNDialog, setShowSCNDialog] = useState(false);
|
||||||
const [scnFile, setScnFile] = useState<File | null>(null);
|
const [scnFile, setScnFile] = useState<File | null>(null);
|
||||||
@ -644,7 +647,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
}>
|
}>
|
||||||
{request.status === 'Settled' ? 'Completed' : formatTerminationStatusLabel(request.status || 'Pending')}
|
{request.status === 'Settled' ? 'Completed' : formatTerminationStatusLabel(request.status || 'Pending')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<SlaBadge status={getSla('termination', terminationId)} />
|
<SlaBadge status={getSla('termination', slaEntityId)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -100,6 +100,14 @@ export const onboardingService = {
|
|||||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to generate dealer codes');
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to generate dealer codes');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
requestProspectDocuments: async (
|
||||||
|
applicationId: string,
|
||||||
|
data: { documentTypes: string[]; dueDays?: number; customMessage?: string }
|
||||||
|
) => {
|
||||||
|
const response: any = await API.requestProspectDocuments(applicationId, data);
|
||||||
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to send document request');
|
||||||
|
return response.data?.data || response.data;
|
||||||
|
},
|
||||||
updateApplicationStatus: async (id: string, data: any) => {
|
updateApplicationStatus: async (id: string, data: any) => {
|
||||||
const response: any = await API.updateApplicationStatus(id, data);
|
const response: any = await API.updateApplicationStatus(id, data);
|
||||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to update application status');
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to update application status');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user