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" 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}`)} />} />

View File

@ -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),

View File

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

View File

@ -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',

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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">

View File

@ -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>

View File

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

View File

@ -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" />

View File

@ -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 (

View File

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

View File

@ -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">

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(), 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 = [

View File

@ -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

View File

@ -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 (

View File

@ -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"

View File

@ -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}`}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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