typo chnge errors and Loi Document request mail templatd added

This commit is contained in:
Laxman 2026-05-26 12:34:32 +05:30
parent dc49fa9065
commit 05211fe90a
25 changed files with 606 additions and 214 deletions

View File

@ -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/: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/: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 */}
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/dealer-resignation/${id}`)} />} />

View File

@ -54,6 +54,10 @@ export const API = {
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 }),
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),
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),

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@ -11,6 +11,7 @@ import { setCredentials } from '@/store/slices/authSlice';
export function ProspectiveLoginPage() {
const navigate = useNavigate();
const routerLocation = useLocation();
const dispatch = useDispatch();
const [step, setStep] = useState<'PHONE' | 'OTP'>('PHONE');
const [phone, setPhone] = useState('');
@ -18,6 +19,20 @@ export function ProspectiveLoginPage() {
const [isLoading, setIsLoading] = useState(false);
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) => {
e.preventDefault();
if (!phone || phone.length < 10) {
@ -76,7 +91,7 @@ export function ProspectiveLoginPage() {
localStorage.setItem('token', token);
toast.success('Logged in successfully!');
navigate('/prospective-dashboard');
navigate(resolveRedirectTarget());
} else {
const errorMessage = response.data?.message || 'Invalid OTP';
setError(errorMessage);

View File

@ -141,10 +141,6 @@ const normalizeConstitutionType = (value: string) => {
};
export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: ConstitutionalChangeDetailsProps) {
const { get: getSla } = useSlaBatchStatus(
requestId ? [{ entityType: 'constitutional', entityId: requestId }] : [],
Boolean(requestId)
);
const navigate = useNavigate();
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
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 [activeDocumentTab, setActiveDocumentTab] = useState('required');
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 [isLoading, setIsLoading] = useState(true);
const [isActionLoading, setIsActionLoading] = useState(false);
@ -620,7 +624,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<Badge className={requestStatusBadgeClass}>
{request.status}
</Badge>
<SlaBadge status={getSla('constitutional', requestId)} />
<SlaBadge status={getSla('constitutional', slaEntityId)} />
</div>
{/* Request Overview */}
@ -856,7 +860,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
</Badge>
{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
</Badge>
)}
@ -1341,7 +1345,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<div className="border-t border-slate-200 pt-3 mt-3">
<Button
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}`, {
state: {
requestType: 'constitutional',

View File

@ -15,7 +15,7 @@ import { dealerService } from '@/services/dealer.service';
import { formatDateTime } from '@/components/ui/utils';
import { API } from '@/api/API';
import { normalizeDealerProfileConstitution } from '@/lib/constitutional-change';
import { getRequestStatusBadgeClass } from '@/lib/statusProgressTheme';
import { getRequestStatusBadgeClass, getStatusProgressBarClass } from '@/lib/statusProgressTheme';
interface DealerConstitutionalChangePageProps {
currentUser?: UserType | null;
@ -119,7 +119,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
title: 'Total Requests',
value: requests.length,
icon: RefreshCcw,
color: 'bg-blue-500',
color: 'bg-re-red',
},
{
title: 'Pending',
@ -140,7 +140,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
{/* Loading Overlay */}
{loading && (
<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>
)}
@ -157,7 +157,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<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" />
New Constitutional Change
</Button>
@ -262,9 +262,9 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
)}
{/* Document Requirements */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-blue-900 mb-2">Documents Required (to be uploaded later)</h4>
<ul className="text-blue-800 text-sm space-y-1">
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<h4 className="text-re-red-hover mb-2">Documents Required (to be uploaded later)</h4>
<ul className="text-re-red-hover text-sm space-y-1">
<li> GST Registration Certificate</li>
<li> Firm PAN Copy</li>
<li> Self-attested KYC documents</li>
@ -285,9 +285,9 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
>
Cancel
</Button>
<Button
<Button
type="submit"
className="bg-blue-600 hover:bg-blue-700"
className="bg-re-red hover:bg-re-red-hover"
disabled={submitting}
>
{submitting ? (
@ -363,7 +363,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
<Badge variant="outline">{request.currentConstitution}</Badge>
</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}
</Badge>
</TableCell>
@ -379,7 +379,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
<div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
<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}%` }}
/>
</div>

View File

@ -535,7 +535,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</div>
<div>
<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>
<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
size="sm"
className="bg-blue-600 hover:bg-blue-700"
className="bg-re-red hover:bg-re-red-hover"
onClick={() => {
setSelectedFnF(fnf);
setLineItems([]);
@ -845,10 +845,10 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</Card>
<div className="grid grid-cols-2 gap-4">
<Card className="border-blue-100">
<CardHeader className="bg-blue-50/50 pb-2">
<Card className="border-red-100">
<CardHeader className="bg-red-50/50 pb-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
</CardTitle>
</CardHeader>
@ -861,7 +861,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
<span className="text-slate-500">Other Payable Credits</span>
<span className="font-bold text-slate-900">0</span>
</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>{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
</div>
@ -909,7 +909,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</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
</Button>
</div>

View File

@ -1050,7 +1050,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</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" />
<div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
@ -1065,7 +1065,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</TabsContent>
<TabsContent value="financial" className="space-y-4">
<Card className="border-blue-200 bg-blue-50">
<Card className="border-red-200 bg-red-50">
<CardHeader>
<CardTitle className="text-base">Department Claim vs Finance Validation</CardTitle>
<CardDescription>
@ -1580,7 +1580,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card>
{/* Final Settlement Summary */}
<Card className="border-2 border-blue-300 bg-blue-50">
<Card className="border-2 border-red-300 bg-red-50">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-re-red" />
@ -1603,7 +1603,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</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 ${
settlement.settlementType === 'Payable to Dealer'
@ -1794,7 +1794,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card>
{/* Important Notes */}
<Card className="bg-blue-50 border-red-200">
<Card className="bg-red-50 border-red-200">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<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">
{bankDetails.length > 0 ? (
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 && (
<div className="absolute top-0 right-0 p-1 bg-re-red text-white text-[10px] uppercase font-bold px-2 rounded-bl">
Primary
@ -2217,7 +2217,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<Button
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}
disabled={submitting}
>

View File

@ -196,7 +196,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<CardContent>
<div className="flex items-center justify-between">
<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>
</CardContent>
</Card>
@ -513,8 +513,8 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
<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" />
<div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<p className="text-sm text-slate-600">

View File

@ -282,12 +282,12 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
</div>
<div className={cn(
"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>
<p className={cn(
"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'}
</p>

View File

@ -570,7 +570,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const getStatusColor = (status: string) => {
switch (status) {
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":
return "bg-yellow-100 text-yellow-700 border-yellow-300";
case "Under Review":
@ -702,7 +702,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
{/* {canSendToStakeholders && fnfCase.status === "New" && (
<Button
className="bg-re-red hover:bg-blue-700"
className="bg-re-red hover:bg-re-red-hover"
onClick={() => setSendStakeholdersDialog(true)}
>
<Send className="w-4 h-4 mr-2" />
@ -921,7 +921,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
fnfCase.status,
)
? "bg-green-50 border-green-200"
: "bg-blue-50 border-red-200"
: "bg-red-50 border-red-200"
}
>
<CardContent className="p-4">
@ -1042,7 +1042,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
className={
fnfCase.status === "Completed"
? "bg-green-50 border-green-200"
: "bg-blue-50 border-red-200"
: "bg-red-50 border-red-200"
}
>
<CardContent className="p-4">
@ -1068,7 +1068,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</p>
</div>
<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
</p>
<p
@ -1262,7 +1262,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
Case closed. All obligations fulfilled.
</p>
{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">
<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">
@ -1333,9 +1333,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</CardContent>
</Card>
<Card className="border-red-200 bg-blue-50/30">
<Card className="border-red-200 bg-red-50/30">
<CardHeader>
<CardTitle className="text-blue-900">
<CardTitle className="text-re-red-hover">
F&F Settlement Information
</CardTitle>
</CardHeader>
@ -1504,7 +1504,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Button
variant="ghost"
size="sm"
className="text-re-red hover:text-blue-700"
className="text-re-red hover:text-re-red-hover"
onClick={() => {
setSelectedDept(dept);
setClearanceForm({
@ -1531,7 +1531,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Card>
{/* 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>
<CardTitle>Department Claim vs Finance Validation</CardTitle>
<CardDescription>
@ -1607,8 +1607,8 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
Warranty holdbacks / Policy penalties
</p>
</div>
<div className="p-6 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p>
<div className="p-6 bg-red-50 rounded-lg border border-red-200">
<p className="text-sm text-re-red-hover mb-2">Net Settlement Amount</p>
<p
className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
? "text-red-600"
@ -1617,7 +1617,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
>
{Math.abs(fnfCase.netAmount || 0).toLocaleString()}
</p>
<p className="text-xs text-blue-600 mt-1">
<p className="text-xs text-re-red mt-1">
{(fnfCase.netAmount || 0) < 0
? "Receivable from 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">
{bankDetails.length > 0 ? (
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 && (
<div className="absolute top-0 right-0 p-1 bg-re-red text-white text-[10px] uppercase font-bold px-2 rounded-bl">
Primary
@ -1962,11 +1962,11 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</DialogHeader>
<div className="space-y-4">
<div className="p-4 bg-blue-50 rounded-lg border border-red-200">
<p className="text-sm text-blue-900 mb-2">
<div className="p-4 bg-red-50 rounded-lg border border-red-200">
<p className="text-sm text-re-red-hover mb-2">
Notifications will be sent to:
</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> Case Number: {fnfCase.caseNumber}</li>
<li> Dealer: {fnfCase.dealerName}</li>
@ -1984,7 +1984,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Button>
<Button
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 Notifications
@ -2058,7 +2058,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
Cancel
</Button>
<Button
className="bg-re-red hover:bg-blue-700"
className="bg-re-red hover:bg-re-red-hover"
onClick={handleUpdateClearance}
disabled={isUpdatingClearance}
>

View File

@ -17,7 +17,7 @@ interface FnFPageProps {
const getStatusColor = (status: string) => {
switch (status) {
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 'Legal Clearance':
return 'bg-yellow-100 text-yellow-700 border-yellow-300';
@ -65,7 +65,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
if (loading) {
return (
<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>
);
}
@ -102,7 +102,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Card>
<CardHeader className="pb-3">
<CardDescription>Initiated</CardDescription>
<CardTitle className="text-3xl text-blue-600">
<CardTitle className="text-3xl text-re-red">
{initiatedCases.length}
</CardTitle>
</CardHeader>
@ -186,8 +186,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<div className="p-3 bg-blue-100 rounded-lg">
<FileCheck className="w-6 h-6 text-blue-600" />
<div className="p-3 bg-red-100 rounded-lg">
<FileCheck className="w-6 h-6 text-re-red" />
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
@ -239,7 +239,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Button
size="sm"
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)}
>
<Send className="w-4 h-4 mr-2" />
@ -276,12 +276,12 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<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 === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-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 === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
'text-green-600'
@ -322,7 +322,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Button
size="sm"
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)}
>
<Send className="w-4 h-4 mr-2" />

View File

@ -11,42 +11,20 @@ interface 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 statusColors: Record<string, string> = {
'Submitted': 'bg-slate-500',
'Questionnaire Pending': 'bg-orange-500',
'Questionnaire Completed': 'bg-blue-500',
'Shortlisted': 'bg-cyan-500',
'Level 1 Pending': 'bg-red-500',
'Level 1 Approved': 'bg-green-500',
'Level 2 Pending': 'bg-purple-500',
'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';
const s = String(status || '');
if (s === 'Rejected' || s === 'Disqualified' || s.includes('Rejected')) {
return 'bg-red-50 text-re-red-hover border border-red-200';
}
if (s === 'Approved' || s === 'Onboarded' || s === 'Completed') {
return 'bg-slate-900 text-white border border-transparent';
}
return 'bg-slate-200 text-slate-800 border border-slate-300';
};
return (

View File

@ -1,3 +1,4 @@
import { useState } from 'react';
import {
AlertCircle,
Calendar,
@ -5,6 +6,7 @@ import {
ChevronDown,
ClipboardCheck,
Clock,
FileText,
GitBranch,
Info,
Lock,
@ -15,6 +17,7 @@ import {
XCircle,
Zap,
} from 'lucide-react';
import { RequestDocumentsModal } from './RequestDocumentsModal';
import { cn, formatDateTime } from '@/components/ui/utils';
import {
getRequestStatusBadgeSolidClass,
@ -75,8 +78,41 @@ interface ApplicationDetailsSidebarProps {
setParticipantType: (value: string) => void;
handleAddParticipant: () => void;
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) {
const {
application,
@ -117,8 +153,18 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
setParticipantType,
handleAddParticipant,
isAssigningParticipant,
documents = [],
documentConfigs = [],
} = 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 (
<div className="space-y-6">
<Card data-testid="onboarding-details-summary-card">
@ -284,11 +330,23 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
</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) &&
['Dealer Code Generation', 'LOA Pending', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion'].includes(application.status) && (
<>
{!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" />
Generate Dealer Codes
</Button>
@ -297,7 +355,7 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
{application.dealerCode && !application.architectureAssignedTo && (
<Button
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}
data-testid="onboarding-details-assign-architecture"
>
@ -478,6 +536,15 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
</CardContent>
</Card>
)}
<RequestDocumentsModal
open={showRequestDocsModal}
onClose={() => setShowRequestDocsModal(false)}
applicationId={application?.id || ''}
applicantName={application?.name || application?.applicantName || 'the prospect'}
documentConfigs={documentConfigs}
uploadedDocuments={documents}
/>
</div>
);
}

View File

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { toast } from 'sonner';
import {
AlertCircle,
@ -65,6 +66,11 @@ interface ApplicationDetailsTabsProps {
eorChecklist: any[];
setUploadDocType: (value: string) => void;
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;
fetchEorData: () => void;
deposits: any[];
@ -100,6 +106,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
eorChecklist,
setUploadDocType,
isAdmin,
canViewFinanceTabs,
fetchApplication,
fetchEorData,
deposits,
@ -112,6 +119,15 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
auditLogActionBadgeClass,
} = 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 =>
String(value || '')
.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="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="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="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>
</TabsList>
</div>
@ -326,7 +346,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
5: 2,
6: 2,
8: 2,
12: 2
13: 2
};
const stageId = Number(stage.id);
const expectedCount = expectedMap[stageId];
@ -337,7 +357,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
5: 2,
6: 3,
8: 'LOI_APPROVAL',
12: 'LOA_APPROVAL',
13: 'LOA_APPROVAL',
};
const mappedStageCode = stageCodeById[stageId];
const actualCount = mappedStageCode ? getApproverStatus(mappedStageCode).length : (stage.evaluators?.length || 0);
@ -770,9 +790,11 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
)}
</TabsContent>
<TabsContent value="fdd" className="space-y-6" data-testid="onboarding-tab-content-fdd">
{renderFddAuditContent()}
</TabsContent>
{canViewFinanceTabs && (
<TabsContent value="fdd" className="space-y-6" data-testid="onboarding-tab-content-fdd">
{renderFddAuditContent()}
</TabsContent>
)}
<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">
@ -927,6 +949,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
)}
</TabsContent>
{canViewFinanceTabs && (
<TabsContent value="payments" className="space-y-6" data-testid="onboarding-tab-content-payments">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-slate-900">Security Deposits</h3>
@ -1101,6 +1124,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
})()}
</div>
</TabsContent>
)}
<TabsContent value="audit" data-testid="onboarding-tab-content-audit">
<ScrollArea className="h-[30rem] rounded-md border border-slate-100 bg-slate-50/50">

View File

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

View File

@ -124,64 +124,80 @@ export function useApplicationDetailsStageData({
id: 9, name: 'Security Deposit', status: getSecurityDepositStageStatus(),
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'),
date: application.loiIssueDate, description: 'Letter of Intent issued', isParallel: true,
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: 'LOI Issue', status: getStageStatus('LOI Issue'),
date: application.loiIssueDate, description: 'Letter of Intent issued'
},
{
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,
branches: [
{
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: '11a-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-1', name: 'Architecture Assignment', status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', description: 'Assigned to architecture team' },
{ id: '12a-2', name: 'Site Plan Blueprint', status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', description: 'Blueprints and site plans' },
{ 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: [
{ id: '11b-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: '11b-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: '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: '11b-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: '11b-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: '11b-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-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
{ id: '12b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
{ id: '12b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
{ id: '12b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
{ 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: '12b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
{ 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: '12b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
{ id: '12b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
{ id: '12b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
{ 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',
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))),
description: 'Letter of Authorization'
description: 'Letter of Agreement'
},
{ id: 13, name: 'EOR Complete', status: getStageStatus('EOR Complete'), description: 'Essential Operating Requirements' },
{ id: 14, name: 'Inauguration', status: getStageStatus('Inauguration'), description: 'Dealership inauguration' },
{ id: 15, name: 'Dealership Active', status: getStageStatus('Onboarded'), description: 'Dealer profile active' },
{ id: 14, name: 'EOR Complete', status: getStageStatus('EOR Complete'), description: 'Essential Operating Requirements' },
{ id: 15, name: 'Inauguration', status: getStageStatus('Inauguration'), description: 'Dealership inauguration' },
{ id: 16, name: 'Dealership Active', status: getStageStatus('Onboarded'), description: 'Dealer profile active' },
];
const eorChecklist = [

View File

@ -178,6 +178,18 @@ export const ApplicationDetails = () => {
currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' ||
currentUser?.role === 'NBH' || currentUser?.role === '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(() => {
const fetchConfigs = async () => {
@ -471,6 +483,7 @@ export const ApplicationDetails = () => {
eorChecklist={eorChecklist}
setUploadDocType={setUploadDocType}
isAdmin={isAdmin}
canViewFinanceTabs={canViewFinanceTabs}
fetchApplication={fetchApplication}
fetchEorData={fetchEorData}
deposits={deposits}
@ -537,6 +550,8 @@ export const ApplicationDetails = () => {
setParticipantType={setParticipantType}
handleAddParticipant={handleAddParticipant}
isAssigningParticipant={isAssigningParticipant}
documents={documents}
documentConfigs={documentConfigs}
/>
<ApplicationDetailsActionModals

View File

@ -197,42 +197,23 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
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 statusColors: Record<string, string> = {
'Submitted': 'bg-slate-500',
'Questionnaire Pending': 'bg-orange-500',
'Questionnaire Completed': 'bg-blue-500',
'Shortlisted': 'bg-cyan-500',
'Level 1 Pending': 'bg-red-500',
'Level 1 Approved': 'bg-green-500',
'Level 2 Pending': 'bg-purple-500',
'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';
const s = String(status || '');
if (s === 'Rejected' || s === 'Disqualified' || s.includes('Rejected')) {
return 'bg-red-50 text-re-red-hover border border-red-200';
}
if (s === 'Approved' || s === 'Onboarded' || s === 'Completed') {
return 'bg-slate-900 text-white border border-transparent';
}
return 'bg-slate-200 text-slate-800 border border-slate-300';
};
return (

View File

@ -197,7 +197,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
</TabsTrigger>
<TabsTrigger value="worknotes" className="rounded-lg px-6 font-bold data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2" data-testid="onboarding-finance-fdd-tab-worknotes">
<MessageSquare className="w-4 h-4" /> Work Notes
{workNotes.length > 0 && <Badge className="ml-1 h-5 w-5 p-0 flex items-center justify-center bg-blue-600 rounded-full text-[10px] text-white font-black" data-testid="onboarding-finance-fdd-worknotes-count">{workNotes.length}</Badge>}
{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 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
@ -271,7 +271,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
</div>
{doc && (
<div className="flex gap-2">
<Button variant="ghost" size="sm" className="h-8 text-blue-600 font-black text-[10px] uppercase tracking-widest"
<Button variant="ghost" size="sm" className="h-8 text-re-red font-black text-[10px] uppercase tracking-widest"
onClick={() => {
setPreviewDoc(doc);
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">
<CardContent className="p-6 flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-blue-600" />
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-re-red" />
</div>
<div>
<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}
data-testid="onboarding-finance-fdd-new-note-input"
/>
<Button
className="shrink-0 bg-blue-600 hover:bg-blue-700 h-auto self-stretch rounded-xl px-6"
<Button
className="shrink-0 bg-re-red hover:bg-re-red-hover h-auto self-stretch rounded-xl px-6"
onClick={handlePostNote}
disabled={isNoteSubmitting || !newNote.trim()}
data-testid="onboarding-finance-fdd-send-note-btn"

View File

@ -121,7 +121,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
if (loading) {
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" />
<span>Loading Finance Queue...</span>
</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="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">
<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})
</div>
</div>
@ -203,10 +203,10 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
const statusLabel = row.paymentStatus || 'Awaiting Payment';
const app = row.application || {};
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">
<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">
<span className="font-semibold text-slate-900" data-testid={`onboarding-finance-queue-name-${idx}`}>{app.applicantName}</span>
</div>
@ -244,7 +244,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
size="sm"
variant={isVerifiedLikeStatus(statusLabel) ? 'outline' : 'default'}
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'}
onClick={() => handleAction(row.applicationId || app.id)}
data-testid={`onboarding-finance-queue-action-btn-${idx}`}

View File

@ -14,7 +14,7 @@ import { toast } from 'sonner';
import { dealerService } from '@/services/dealer.service';
import { masterService } from '@/services/master.service';
import { formatDateTime } from '@/components/ui/utils';
import { getRequestStatusBadgeClass } from '@/lib/statusProgressTheme';
import { getRequestStatusBadgeClass, getStatusProgressBarClass } from '@/lib/statusProgressTheme';
interface DealerRelocationPageProps {
currentUser: UserType | null;
@ -495,7 +495,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
<div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
<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}%` }}
/>
</div>

View File

@ -206,12 +206,15 @@ const getApiErrorMessage = (error: any, fallback: string) => {
};
export function RelocationRequestDetails({ requestId, onBack, currentUser }: RelocationRequestDetailsProps) {
const { get: getSla } = useSlaBatchStatus(
requestId ? [{ entityType: 'relocation', entityId: requestId }] : [],
Boolean(requestId)
);
const navigate = useNavigate();
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 [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
@ -621,7 +624,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{request.outlet?.name} ({request.outlet?.code})
</p>
<div className="mt-1">
<SlaBadge status={getSla('relocation', requestId)} />
<SlaBadge status={getSla('relocation', slaEntityId)} />
</div>
</div>
</div>

View File

@ -67,10 +67,6 @@ const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
};
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 allDocs = [
...(resignationData?.documents || []),
@ -104,6 +100,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const [forceTriggerFnF, setForceTriggerFnF] = useState(false);
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
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 [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
@ -595,7 +598,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
? 'Completed'
: formatOffboardingStatusLabel(resignationData?.status || 'Pending')}
</Badge>
<SlaBadge status={getSla('resignation', resignationId)} />
<SlaBadge status={getSla('resignation', slaEntityId)} />
</div>
</div>

View File

@ -39,10 +39,6 @@ interface TerminationDetailsProps {
export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) {
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 [remarks, setRemarks] = 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 [isLoading, setIsLoading] = useState(true);
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 [showSCNDialog, setShowSCNDialog] = useState(false);
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')}
</Badge>
<SlaBadge status={getSla('termination', terminationId)} />
<SlaBadge status={getSla('termination', slaEntityId)} />
</div>
</div>

View File

@ -100,6 +100,14 @@ export const onboardingService = {
if (!response.ok) throw new Error(response.data?.message || 'Failed to generate dealer codes');
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) => {
const response: any = await API.updateApplicationStatus(id, data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to update application status');