tab filters in differnt module changes and amount update from F&F fixed checking for the dues update from two placeds finance & normal departmental response need to finalisre one

This commit is contained in:
laxman h 2026-04-16 17:45:19 +05:30
parent bc2b7faf08
commit 873a097185
10 changed files with 470 additions and 176 deletions

View File

@ -72,6 +72,11 @@ export default function App() {
const [showAdminLogin, setShowAdminLogin] = useState(false); const [showAdminLogin, setShowAdminLogin] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const currentRole = currentUser?.role || '';
const resignationRoles = ['DD Admin', 'ASM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Super Admin'];
const terminationRoles = ['ASM', 'DD Lead', 'DD Admin', 'Super Admin'];
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin'];
const financeRoles = ['Finance', 'Finance Admin'];
useEffect(() => { useEffect(() => {
dispatch(initializeAuth()); dispatch(initializeAuth());
@ -205,9 +210,9 @@ export default function App() {
{/* Dashboards */} {/* Dashboards */}
<Route path="/dashboard" element={ <Route path="/dashboard" element={
currentUser?.role === 'Finance Admin' || currentUser?.role === 'Finance' ? financeRoles.includes(currentRole) ?
<FinanceDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> : <FinanceDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> :
currentUser?.role === 'Dealer' ? currentRole === 'Dealer' ?
<DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> : <DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> :
<Dashboard onNavigate={(path) => navigate(`/${path}`)} /> <Dashboard onNavigate={(path) => navigate(`/${path}`)} />
} /> } />
@ -249,21 +254,61 @@ export default function App() {
<Route path="/questionnaires" element={<QuestionnaireList />} /> <Route path="/questionnaires" element={<QuestionnaireList />} />
{/* HR/Finance Modules (Simplified for brevity, following pattern) */} {/* HR/Finance Modules (Simplified for brevity, following pattern) */}
<Route path="/resignation" element={<ResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />} /> <Route path="/resignation" element={
<Route path="/resignation/:id" element={<ResignationDetails resignationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/resignation')} currentUser={currentUser} />} /> resignationRoles.includes(currentRole)
? <ResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />
: <Navigate to="/dashboard" />
} />
<Route path="/resignation/:id" element={
resignationRoles.includes(currentRole)
? <ResignationDetails resignationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/resignation')} currentUser={currentUser} />
: <Navigate to="/dashboard" />
} />
<Route path="/termination" element={<TerminationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/termination/${id}`)} />} /> <Route path="/termination" element={
<Route path="/termination/:id" element={<TerminationDetails terminationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/termination')} currentUser={currentUser} />} /> terminationRoles.includes(currentRole)
? <TerminationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/termination/${id}`)} />
: <Navigate to="/dashboard" />
} />
<Route path="/termination/:id" element={
terminationRoles.includes(currentRole)
? <TerminationDetails terminationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/termination')} currentUser={currentUser} />
: <Navigate to="/dashboard" />
} />
<Route path="/fnf" element={<FnFPage currentUser={currentUser} onViewDetails={(id) => navigate(`/fnf/${id}`)} />} /> <Route path="/fnf" element={
<Route path="/fnf/:id" element={<FnFDetails fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/fnf')} currentUser={currentUser} />} /> fnfRoles.includes(currentRole)
? <FnFPage currentUser={currentUser} onViewDetails={(id) => navigate(`/fnf/${id}`)} />
: <Navigate to="/dashboard" />
} />
<Route path="/fnf/:id" element={
fnfRoles.includes(currentRole)
? <FnFDetails fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/fnf')} currentUser={currentUser} />
: <Navigate to="/dashboard" />
} />
<Route path="/finance-onboarding" element={<FinanceOnboardingPage onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} />} /> <Route path="/finance-onboarding" element={
<Route path="/finance-onboarding/:id" element={<FinancePaymentDetailsPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />} /> financeRoles.includes(currentRole)
? <FinanceOnboardingPage onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} />
: <Navigate to="/dashboard" />
} />
<Route path="/finance-onboarding/:id" element={
financeRoles.includes(currentRole)
? <FinancePaymentDetailsPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />
: <Navigate to="/dashboard" />
} />
<Route path="/finance-audit/:id" element={<ApplicationDetails />} /> <Route path="/finance-audit/:id" element={<ApplicationDetails />} />
<Route path="/finance-fnf" element={<FinanceFnFPage onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} />} /> <Route path="/finance-fnf" element={
<Route path="/finance-fnf/:id" element={<FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />} /> financeRoles.includes(currentRole)
? <FinanceFnFPage onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} />
: <Navigate to="/dashboard" />
} />
<Route path="/finance-fnf/:id" element={
financeRoles.includes(currentRole)
? <FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />
: <Navigate to="/dashboard" />
} />
<Route path="/constitutional-change" element={<ConstitutionalChangePage currentUser={currentUser} onViewDetails={(id) => navigate(`/constitutional-change/${id}`)} />} /> <Route path="/constitutional-change" element={<ConstitutionalChangePage currentUser={currentUser} onViewDetails={(id) => 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('/constitutional-change')} currentUser={currentUser} />} />

View File

@ -181,10 +181,13 @@ export function UserManagementPage() {
}); });
setShowUserModal(false); setShowUserModal(false);
fetchData(); fetchData();
} else {
toast.error(res.message || 'Failed to create user');
} }
} }
} catch (error) { } catch (error) {
toast.error('Operation failed'); const message = (error as any)?.response?.data?.message || (error as any)?.message || 'Operation failed';
toast.error(message);
} }
}; };

View File

@ -87,6 +87,18 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [dialogDataLoading, setDialogDataLoading] = useState(false); const [dialogDataLoading, setDialogDataLoading] = useState(false);
const isCompletedRequest = (request: any) =>
request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed';
const isRejectedRequest = (request: any) =>
request.status === 'Rejected' || request.status === 'Revoked' || request.currentStage === 'Rejected' || request.currentStage === 'Revoked';
const isPendingReviewRequest = (request: any) =>
!isCompletedRequest(request) && !isRejectedRequest(request) && request.status !== 'Submitted';
const isSubmittedRequest = (request: any) =>
request.status === 'Submitted' || request.currentStage === 'Submitted';
useEffect(() => { useEffect(() => {
fetchRequests(); fetchRequests();
}, []); }, []);
@ -248,22 +260,22 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
color: 'bg-blue-500', color: 'bg-blue-500',
}, },
{ {
title: 'In Progress', title: 'Submitted / Review',
value: requests.filter(r => r.status !== 'Completed' && !r.status.includes('Rejected')).length, value: requests.filter(r => isSubmittedRequest(r) || isPendingReviewRequest(r)).length,
icon: Calendar, icon: Calendar,
color: 'bg-yellow-500', color: 'bg-yellow-500',
}, },
{ {
title: 'Completed', title: 'Completed',
value: requests.filter(r => r.status === 'Completed').length, value: requests.filter(r => isCompletedRequest(r)).length,
icon: Shield, icon: Shield,
color: 'bg-green-500', color: 'bg-green-500',
}, },
{ {
title: 'Pending Action', title: 'Rejected / Revoked',
value: requests.filter(r => r.status.includes('Review') || r.status.includes('Pending')).length, value: requests.filter(r => isRejectedRequest(r)).length,
icon: Building, icon: Building,
color: 'bg-amber-500', color: 'bg-red-500',
}, },
]; ];
@ -507,8 +519,8 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
<Tabs defaultValue="all" className="w-full"> <Tabs defaultValue="all" className="w-full">
<TabsList className="grid w-full grid-cols-4"> <TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="all">All Requests</TabsTrigger> <TabsTrigger value="all">All Requests</TabsTrigger>
<TabsTrigger value="pending">Pending</TabsTrigger> <TabsTrigger value="pending">Submitted / Review</TabsTrigger>
<TabsTrigger value="in-progress">In Progress</TabsTrigger> <TabsTrigger value="in-progress">Rejected / Revoked</TabsTrigger>
<TabsTrigger value="completed">Completed</TabsTrigger> <TabsTrigger value="completed">Completed</TabsTrigger>
</TabsList> </TabsList>
@ -612,7 +624,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{requests {requests
.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending')) .filter((r: any) => isSubmittedRequest(r) || isPendingReviewRequest(r))
.map((request: any) => ( .map((request: any) => (
<TableRow key={request.requestId}> <TableRow key={request.requestId}>
<TableCell> <TableCell>
@ -659,7 +671,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{requests.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending')).length === 0 && ( {requests.filter((r: any) => isSubmittedRequest(r) || isPendingReviewRequest(r)).length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={6} className="text-center py-8 text-slate-500"> <TableCell colSpan={6} className="text-center py-8 text-slate-500">
No pending requests found No pending requests found
@ -686,7 +698,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{requests {requests
.filter((r: any) => r.status !== 'Completed' && !r.status.includes('Rejected')) .filter((r: any) => isRejectedRequest(r))
.map((request: any) => ( .map((request: any) => (
<TableRow key={request.requestId}> <TableRow key={request.requestId}>
<TableCell> <TableCell>
@ -740,7 +752,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{requests.filter((r: any) => r.status !== 'Completed' && !r.status.includes('Rejected')).length === 0 && ( {requests.filter((r: any) => isRejectedRequest(r)).length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={6} className="text-center py-8 text-slate-500"> <TableCell colSpan={6} className="text-center py-8 text-slate-500">
No in-progress requests found No in-progress requests found
@ -767,7 +779,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{requests {requests
.filter((r: any) => r.status === 'Completed' || r.status === 'Closed') .filter((r: any) => isCompletedRequest(r))
.map((request: any) => ( .map((request: any) => (
<TableRow key={request.requestId}> <TableRow key={request.requestId}>
<TableCell> <TableCell>
@ -812,7 +824,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{requests.filter((r: any) => r.status === 'Completed' || r.status === 'Closed').length === 0 && ( {requests.filter((r: any) => isCompletedRequest(r)).length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={6} className="text-center py-8 text-slate-500"> <TableCell colSpan={6} className="text-center py-8 text-slate-500">
No completed requests found No completed requests found

View File

@ -16,13 +16,16 @@ interface FnFPageProps {
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case 'New': case 'Initiated':
return 'bg-blue-100 text-blue-700 border-blue-300'; return 'bg-blue-100 text-blue-700 border-blue-300';
case 'In Progress': case 'DD Clearance':
case 'Legal Clearance':
return 'bg-yellow-100 text-yellow-700 border-yellow-300'; return 'bg-yellow-100 text-yellow-700 border-yellow-300';
case 'Under Review': case 'Finance Approval':
case 'Calculated':
return 'bg-orange-100 text-orange-700 border-orange-300'; return 'bg-orange-100 text-orange-700 border-orange-300';
case 'Completed': case 'Completed':
case 'Settled':
return 'bg-green-100 text-green-700 border-green-300'; return 'bg-green-100 text-green-700 border-green-300';
default: default:
return 'bg-slate-100 text-slate-700 border-slate-300'; return 'bg-slate-100 text-slate-700 border-slate-300';
@ -95,6 +98,10 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
}); });
const displaySettlements: any[] = settlements.map(getMappedData); const displaySettlements: any[] = settlements.map(getMappedData);
const initiatedCases = displaySettlements.filter(c => c.status === 'Initiated');
const clearanceCases = displaySettlements.filter(c => c.status === 'DD Clearance' || c.status === 'Legal Clearance');
const financeApprovalCases = displaySettlements.filter(c => c.status === 'Finance Approval' || c.status === 'Calculated');
const completedCases = displaySettlements.filter(c => c.status === 'Completed' || c.status === 'Settled');
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@ -102,37 +109,37 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<div className="grid grid-cols-1 md:grid-cols-5 gap-4"> <div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>New Cases</CardDescription> <CardDescription>Initiated</CardDescription>
<CardTitle className="text-3xl text-blue-600"> <CardTitle className="text-3xl text-blue-600">
{displaySettlements.filter(c => c.status === 'Initiated' || c.status === 'New').length} {initiatedCases.length}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-slate-600">Just Arrived</p> <p className="text-slate-600">Newly created</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>In Progress</CardDescription> <CardDescription>Clearance</CardDescription>
<CardTitle className="text-3xl text-yellow-600"> <CardTitle className="text-3xl text-yellow-600">
{displaySettlements.filter(c => c.status === 'In Progress').length} {clearanceCases.length}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-slate-600">Awaiting Response</p> <p className="text-slate-600">Department / legal stage</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Under Review</CardDescription> <CardDescription>Finance Approval</CardDescription>
<CardTitle className="text-3xl text-orange-600"> <CardTitle className="text-3xl text-orange-600">
{displaySettlements.filter(c => c.status === 'Under Review' || c.status === 'Calculated').length} {financeApprovalCases.length}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-slate-600">Discussion Ongoing</p> <p className="text-slate-600">Ready for finance review</p>
</CardContent> </CardContent>
</Card> </Card>
@ -171,18 +178,17 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardContent> <CardContent>
<Tabs defaultValue="all" className="w-full"> <Tabs defaultValue="all" className="w-full">
<TabsList> <TabsList>
<TabsTrigger value="new">New Cases</TabsTrigger>
<TabsTrigger value="all">All Cases</TabsTrigger> <TabsTrigger value="all">All Cases</TabsTrigger>
<TabsTrigger value="progress">In Progress</TabsTrigger> <TabsTrigger value="initiated">Initiated</TabsTrigger>
<TabsTrigger value="review">Under Review</TabsTrigger> <TabsTrigger value="clearance">Clearance</TabsTrigger>
<TabsTrigger value="finance">Finance Approval</TabsTrigger>
<TabsTrigger value="completed">Completed</TabsTrigger> <TabsTrigger value="completed">Completed</TabsTrigger>
</TabsList> </TabsList>
{/* New Cases Tab */} {/* Initiated Tab */}
<TabsContent value="new" className="mt-6"> <TabsContent value="initiated" className="mt-6">
<div className="space-y-4"> <div className="space-y-4">
{displaySettlements {initiatedCases
.filter(c => c.status === 'New' || c.status === 'Initiated')
.map((fnfCase) => ( .map((fnfCase) => (
<Card key={fnfCase.id} className="border-slate-200"> <Card key={fnfCase.id} className="border-slate-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
@ -261,10 +267,10 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</CardContent> </CardContent>
</Card> </Card>
))} ))}
{displaySettlements.filter((c: any) => c.status === 'New' || c.status === 'Initiated').length === 0 && ( {initiatedCases.length === 0 && (
<div className="text-center py-12 text-slate-500"> <div className="text-center py-12 text-slate-500">
<FileCheck className="w-12 h-12 mx-auto mb-4 text-slate-400" /> <FileCheck className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p>No new cases to display</p> <p>No initiated cases to display</p>
</div> </div>
)} )}
</div> </div>
@ -279,15 +285,15 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<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 ${ <div className={`p-3 rounded-lg ${
fnfCase.status === 'New' ? 'bg-blue-100' : fnfCase.status === 'Initiated' ? 'bg-blue-100' :
fnfCase.status === 'In Progress' ? 'bg-yellow-100' : (fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
fnfCase.status === 'Under Review' ? '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 ${ <IndianRupee className={`w-6 h-6 ${
fnfCase.status === 'New' ? 'text-blue-600' : fnfCase.status === 'Initiated' ? 'text-blue-600' :
fnfCase.status === 'In Progress' ? 'text-yellow-600' : (fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
fnfCase.status === 'Under Review' ? 'text-orange-600' : (fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
'text-green-600' 'text-green-600'
}`} /> }`} />
</div> </div>
@ -322,7 +328,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div> </div>
</div> </div>
<div className="flex items-center gap-2 ml-4"> <div className="flex items-center gap-2 ml-4">
{canSendToStakeholders && fnfCase.status === 'New' && ( {canSendToStakeholders && fnfCase.status === 'Initiated' && (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@ -349,11 +355,10 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div> </div>
</TabsContent> </TabsContent>
{/* In Progress Tab */} {/* Clearance Tab */}
<TabsContent value="progress" className="mt-6"> <TabsContent value="clearance" className="mt-6">
<div className="space-y-4"> <div className="space-y-4">
{displaySettlements {clearanceCases
.filter(c => c.status === 'In Progress')
.map((fnfCase) => ( .map((fnfCase) => (
<Card key={fnfCase.id} className="border-slate-200"> <Card key={fnfCase.id} className="border-slate-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
@ -403,20 +408,19 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</CardContent> </CardContent>
</Card> </Card>
))} ))}
{displaySettlements.filter((c: any) => c.status === 'In Progress').length === 0 && ( {clearanceCases.length === 0 && (
<div className="text-center py-12 text-slate-500"> <div className="text-center py-12 text-slate-500">
<IndianRupee className="w-12 h-12 mx-auto mb-4 text-slate-400" /> <IndianRupee className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p>No cases in progress</p> <p>No clearance-stage cases</p>
</div> </div>
)} )}
</div> </div>
</TabsContent> </TabsContent>
{/* Under Review Tab */} {/* Finance Approval Tab */}
<TabsContent value="review" className="mt-6"> <TabsContent value="finance" className="mt-6">
<div className="space-y-4"> <div className="space-y-4">
{displaySettlements {financeApprovalCases
.filter(c => c.status === 'Under Review' || c.status === 'Calculated')
.map((fnfCase) => ( .map((fnfCase) => (
<Card key={fnfCase.id} className="border-slate-200"> <Card key={fnfCase.id} className="border-slate-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
@ -468,10 +472,10 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</CardContent> </CardContent>
</Card> </Card>
))} ))}
{displaySettlements.filter(c => c.status === 'Under Review' || c.status === 'Calculated').length === 0 && ( {financeApprovalCases.length === 0 && (
<div className="text-center py-12 text-slate-500"> <div className="text-center py-12 text-slate-500">
<IndianRupee className="w-12 h-12 mx-auto mb-4 text-slate-400" /> <IndianRupee className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p>No cases under review</p> <p>No finance-approval cases</p>
</div> </div>
)} )}
</div> </div>
@ -480,8 +484,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
{/* Completed Tab */} {/* Completed Tab */}
<TabsContent value="completed" className="mt-6"> <TabsContent value="completed" className="mt-6">
<div className="space-y-4"> <div className="space-y-4">
{displaySettlements {completedCases
.filter(c => c.status === 'Completed' || c.status === 'Settled')
.map((fnfCase) => ( .map((fnfCase) => (
<Card key={fnfCase.id} className="border-slate-200"> <Card key={fnfCase.id} className="border-slate-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
@ -529,7 +532,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</CardContent> </CardContent>
</Card> </Card>
))} ))}
{displaySettlements.filter(c => c.status === 'Completed' || c.status === 'Settled').length === 0 && ( {completedCases.length === 0 && (
<div className="text-center py-12 text-slate-500"> <div className="text-center py-12 text-slate-500">
<FileCheck className="w-12 h-12 mx-auto mb-4 text-slate-400" /> <FileCheck className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p>No completed cases</p> <p>No completed cases</p>

View File

@ -15,7 +15,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { formatDateTime } from '../ui/utils'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
interface Props { interface Props {
id: string; id: string;
@ -23,6 +23,27 @@ interface Props {
} }
export function ProspectiveApplicationDetails({ id, onBack }: Props) { export function ProspectiveApplicationDetails({ id, onBack }: Props) {
const requiredDocumentTypes = [
'PAN Card',
'GST Certificate',
'Aadhaar Card',
'Security Deposit Receipt',
'First Fill Receipt',
'Partnership Deed',
'LLP Agreement',
'Certificate of Incorporation',
'MOA',
'AOA',
'Firm Registration',
'Rental Agreement',
'Property Documents',
'Nodal Agreement',
'Cancelled Check',
'LOI Acknowledgement',
'Architecture Blueprint',
'Site Plan',
'Other'
];
const [details, setDetails] = useState<any>(null); const [details, setDetails] = useState<any>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [documents, setDocuments] = useState<any[]>([]); const [documents, setDocuments] = useState<any[]>([]);
@ -31,8 +52,36 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const normalizeStatus = (value?: string) =>
String(value || '')
.toLowerCase()
.trim();
const getTimelineColorClass = (progressStatus?: string) => {
if (progressStatus === 'completed') {
return {
border: 'border-green-500',
dot: 'bg-green-500',
text: 'text-green-700'
};
}
if (progressStatus === 'active') {
return {
border: 'border-sky-500',
dot: 'bg-sky-500',
text: 'text-sky-700'
};
}
// Default and explicit pending
return {
border: 'border-amber-500',
dot: 'bg-amber-500',
text: 'text-amber-700'
};
};
// Statutory & Bank State // Statutory & Bank State
const [form, setForm] = useState({ const emptyForm = {
panNumber: '', panNumber: '',
gstNumber: '', gstNumber: '',
registeredAddress: '', registeredAddress: '',
@ -41,7 +90,16 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
ifscCode: '', ifscCode: '',
branchName: '', branchName: '',
accountHolderName: '' accountHolderName: ''
}); };
const [form, setForm] = useState(emptyForm);
const [savedForm, setSavedForm] = useState(emptyForm);
const isFormDirty = JSON.stringify(form) !== JSON.stringify(savedForm);
const uploadedDocumentTypes = new Set(
documents.map((doc) => String(doc.documentType || '').trim().toLowerCase())
);
const selectedDocAlreadyUploaded = selectedDocType
? uploadedDocumentTypes.has(selectedDocType.toLowerCase())
: false;
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@ -57,8 +115,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
if (detailsRes.data?.success) { if (detailsRes.data?.success) {
const data = detailsRes.data.data; const data = detailsRes.data.data;
setDetails(data); const nextForm = {
setForm({
panNumber: data.panNumber || '', panNumber: data.panNumber || '',
gstNumber: data.gstNumber || '', gstNumber: data.gstNumber || '',
registeredAddress: data.registeredAddress || data.address || '', registeredAddress: data.registeredAddress || data.address || '',
@ -67,7 +124,10 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
ifscCode: data.ifscCode || '', ifscCode: data.ifscCode || '',
branchName: data.branchName || '', branchName: data.branchName || '',
accountHolderName: data.accountHolderName || data.applicantName || '' accountHolderName: data.accountHolderName || data.applicantName || ''
}); };
setDetails(data);
setForm(nextForm);
setSavedForm(nextForm);
} }
if (docsRes.data?.success || docsRes.ok) { if (docsRes.data?.success || docsRes.ok) {
setDocuments(docsRes.data.data || []); setDocuments(docsRes.data.data || []);
@ -277,7 +337,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<button <button
onClick={handleSaveDetails} onClick={handleSaveDetails}
disabled={isSaving} disabled={isSaving}
className="text-xs bg-amber-600 hover:bg-amber-700 text-white px-3 py-1 rounded font-bold transition-all flex items-center gap-1 disabled:opacity-50" className={`text-xs text-white px-3 py-1 rounded font-bold transition-all flex items-center gap-1 disabled:opacity-50 ${isFormDirty ? 'bg-emerald-600 hover:bg-emerald-700 ring-2 ring-emerald-300 animate-pulse' : 'bg-amber-600 hover:bg-amber-700'}`}
> >
{isSaving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <CheckCircle2 className="w-3 h-3" />} {isSaving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <CheckCircle2 className="w-3 h-3" />}
Save Business Info Save Business Info
@ -389,34 +449,39 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg border border-slate-100"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg border border-slate-100">
<div className="space-y-1"> <div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label> <div className="flex items-center justify-between gap-2">
<select <label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label>
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-amber-500" {selectedDocType && (
value={selectedDocType} <span className={`rounded-full px-2 py-0.5 text-[10px] font-bold ${selectedDocAlreadyUploaded ? 'bg-blue-100 text-blue-700' : 'bg-amber-100 text-amber-700'}`}>
onChange={(e) => setSelectedDocType(e.target.value)} {selectedDocAlreadyUploaded ? 'Already uploaded' : 'Pending upload'}
disabled={isUploading} </span>
> )}
<option value="">Select type...</option> </div>
<option value="PAN Card">PAN Card</option> <div className="relative">
<option value="GST Certificate">GST Certificate</option> <CheckCircle2 className={`pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 ${selectedDocAlreadyUploaded ? 'text-green-600' : 'text-slate-300'}`} />
<option value="Aadhaar Card">Aadhaar Card</option> <Select value={selectedDocType} onValueChange={setSelectedDocType} disabled={isUploading}>
<option value="Security Deposit Receipt">Security Deposit Receipt</option> <SelectTrigger className="h-12 rounded-xl border-slate-200 bg-gradient-to-r from-white to-slate-50 pl-10 pr-3 text-sm font-medium text-slate-700 shadow-sm focus:border-amber-300 focus:ring-2 focus:ring-amber-500">
<option value="First Fill Receipt">First Fill Receipt</option> <SelectValue placeholder="Choose document type" />
<option value="Partnership Deed">Partnership Deed</option> </SelectTrigger>
<option value="LLP Agreement">LLP Agreement</option> <SelectContent className="rounded-xl border-slate-200 shadow-lg">
<option value="Certificate of Incorporation">Certificate of Incorporation</option> {requiredDocumentTypes.map((docType) => {
<option value="MOA">MOA (Memorandum of Association)</option> const isUploaded = uploadedDocumentTypes.has(docType.toLowerCase());
<option value="AOA">AOA (Articles of Association)</option> return (
<option value="Firm Registration">Firm Registration</option> <SelectItem
<option value="Rental Agreement">Rental Agreement</option> key={docType}
<option value="Property Documents">Property Documents</option> value={docType}
<option value="Nodal Agreement">Nodal Agreement</option> className="rounded-lg px-3 py-2 text-sm text-slate-700 focus:bg-amber-50 focus:text-slate-900"
<option value="Cancelled Check">Cancelled Check</option> >
<option value="LOI Acknowledgement">LOI Acknowledgement</option> <span className="flex items-center gap-2">
<option value="Architecture Blueprint">Architecture Blueprint</option> <CheckCircle2 className={`h-4 w-4 ${isUploaded ? 'text-green-600' : 'text-slate-300'}`} />
<option value="Site Plan">Site Plan</option> {docType}
<option value="Other">Other</option> </span>
</select> </SelectItem>
);
})}
</SelectContent>
</Select>
</div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Select File</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Select File</label>
@ -481,23 +546,35 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
</h3> </h3>
</div> </div>
<div className="p-6"> <div className="p-6">
{details.statusHistory?.length > 0 ? ( {(details.progressTracking || []).length > 0 ? (
<div className="relative space-y-6"> <div className="relative space-y-6">
<div className="absolute left-[11px] top-2 bottom-4 w-0.5 bg-slate-100"></div> <div className="absolute left-[11px] top-2 bottom-4 w-0.5 bg-slate-100"></div>
{[...details.statusHistory].reverse().map((item: any, idx: number) => ( {[...(details.progressTracking || [])]
<div key={item.id} className="relative pl-8 animate-in slide-in-from-left duration-300" style={{ animationDelay: `${idx * 100}ms` }}> .sort((a: any, b: any) => Number(a.stageOrder || 0) - Number(b.stageOrder || 0))
<div className="absolute left-0 top-1 w-[24px] h-[24px] rounded-full border-2 bg-white flex items-center justify-center border-amber-500 shadow-sm"> .map((item: any, idx: number) => {
<div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div> const progressStatus = normalizeStatus(item.status);
const colorClass = getTimelineColorClass(progressStatus);
return (
<div key={item.id} className="relative pl-8 animate-in slide-in-from-left duration-300" style={{ animationDelay: `${idx * 100}ms` }}>
<div className={`absolute left-0 top-1 w-[24px] h-[24px] rounded-full border-2 bg-white flex items-center justify-center shadow-sm ${colorClass.border}`}>
<div className={`w-1.5 h-1.5 rounded-full ${colorClass.dot}`}></div>
</div>
<div>
<p className={`text-xs font-bold uppercase tracking-tight ${colorClass.text}`}>{item.stageName}</p>
<p className="text-[10px] text-slate-400 font-medium">
{item.stageCompletedAt
? new Date(item.stageCompletedAt).toLocaleString('en-IN')
: item.stageStartedAt
? new Date(item.stageStartedAt).toLocaleString('en-IN')
: new Date(item.createdAt).toLocaleString('en-IN')}
</p>
<p className="text-[10px] text-slate-500 mt-1 italic leading-tight">
Status: {item.status || 'pending'}
</p>
</div>
</div> </div>
<div> );
<p className="text-xs font-bold text-slate-900 uppercase tracking-tight">{item.newStatus}</p> })}
<p className="text-[10px] text-slate-400 font-medium">{new Date(item.createdAt).toLocaleString('en-IN')}</p>
{item.changeReason && (
<p className="text-[10px] text-slate-500 mt-1 italic leading-tight">"{item.changeReason}"</p>
)}
</div>
</div>
))}
</div> </div>
) : ( ) : (
<div className="text-center py-6"> <div className="text-center py-6">

View File

@ -27,6 +27,15 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
const [requests, setRequests] = useState<any[]>([]); const [requests, setRequests] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const isCompletedRequest = (request: any) =>
request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed';
const isRejectedRequest = (request: any) =>
request.status === 'Rejected' || request.status === 'Revoked' || request.currentStage === 'Rejected';
const isPendingReviewRequest = (request: any) =>
!isCompletedRequest(request) && !isRejectedRequest(request) && String(request.status || '').startsWith('Pending');
useEffect(() => { useEffect(() => {
fetchRequests(); fetchRequests();
}, []); }, []);
@ -57,22 +66,22 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
color: 'bg-blue-500', color: 'bg-blue-500',
}, },
{ {
title: 'In Progress', title: 'Pending Review',
value: requests.filter((r: any) => r.status !== 'Completed' && r.status !== 'Closed' && !r.status.includes('Rejected') && !r.status.includes('Revoked')).length, value: requests.filter((r: any) => isPendingReviewRequest(r)).length,
icon: Calendar, icon: Calendar,
color: 'bg-yellow-500', color: 'bg-yellow-500',
}, },
{ {
title: 'Completed', title: 'Completed',
value: requests.filter((r: any) => r.status === 'Completed' || r.status === 'Closed').length, value: requests.filter((r: any) => isCompletedRequest(r)).length,
icon: MapPin, icon: MapPin,
color: 'bg-green-500', color: 'bg-green-500',
}, },
{ {
title: 'Pending Action', title: 'Rejected / Revoked',
value: requests.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending')).length, value: requests.filter((r: any) => isRejectedRequest(r)).length,
icon: Building, icon: Building,
color: 'bg-amber-500', color: 'bg-red-500',
}, },
]; ];
@ -125,8 +134,8 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
<Tabs defaultValue="all" className="w-full"> <Tabs defaultValue="all" className="w-full">
<TabsList className="grid w-full grid-cols-4"> <TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="all">All Requests</TabsTrigger> <TabsTrigger value="all">All Requests</TabsTrigger>
<TabsTrigger value="pending">Pending</TabsTrigger> <TabsTrigger value="pending">Pending Review</TabsTrigger>
<TabsTrigger value="in-progress">In Progress</TabsTrigger> <TabsTrigger value="in-progress">Rejected / Revoked</TabsTrigger>
<TabsTrigger value="completed">Completed</TabsTrigger> <TabsTrigger value="completed">Completed</TabsTrigger>
</TabsList> </TabsList>
@ -248,7 +257,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</TableRow> </TableRow>
) : ( ) : (
requests requests
.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending')) .filter((r: any) => isPendingReviewRequest(r))
.map((request: any) => ( .map((request: any) => (
<TableRow key={request.id}> <TableRow key={request.id}>
<TableCell> <TableCell>
@ -309,7 +318,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</TableRow> </TableRow>
) : ( ) : (
requests requests
.filter((r: any) => r.status !== 'Completed' && r.status !== 'Closed' && !r.status.includes('Rejected')) .filter((r: any) => isRejectedRequest(r))
.map((request: any) => ( .map((request: any) => (
<TableRow key={request.id}> <TableRow key={request.id}>
<TableCell> <TableCell>
@ -329,7 +338,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden"> <div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-amber-600 transition-all duration-300" className="h-full bg-red-500 transition-all duration-300"
style={{ width: `${request.progressPercentage || 0}%` }} style={{ width: `${request.progressPercentage || 0}%` }}
/> />
</div> </div>
@ -380,7 +389,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</TableRow> </TableRow>
) : ( ) : (
requests requests
.filter((r: any) => r.status === 'Completed' || r.status === 'Closed') .filter((r: any) => isCompletedRequest(r))
.map((request: any) => ( .map((request: any) => (
<TableRow key={request.id}> <TableRow key={request.id}>
<TableCell> <TableCell>

View File

@ -74,6 +74,8 @@ interface ResignationDetailsProps {
currentUser: UserType | null; currentUser: UserType | null;
} }
export default ResignationDetails;
const STAGE_TO_ROLE_MAP: Record<string, string> = { const STAGE_TO_ROLE_MAP: Record<string, string> = {
'ASM': 'ASM', 'ASM': 'ASM',
'RBM': 'RBM', 'RBM': 'RBM',
@ -109,8 +111,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
...(stageKey ? (RESIGNATION_STAGE_ALIASES[stageKey] || []) : []), ...(stageKey ? (RESIGNATION_STAGE_ALIASES[stageKey] || []) : []),
...(RESIGNATION_STAGE_ALIASES[stageName] || []) ...(RESIGNATION_STAGE_ALIASES[stageName] || [])
] ]
.filter(Boolean) .filter((value): value is string => Boolean(value))
.map((value: string) => value.trim().toLowerCase()); .map((value) => value.trim().toLowerCase());
return allDocs.filter((doc: any) => { return allDocs.filter((doc: any) => {
if (!doc?.stage) return false; if (!doc?.stage) return false;
@ -136,7 +138,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const [previewDocument, setPreviewDocument] = useState<any>(null); const [previewDocument, setPreviewDocument] = useState<any>(null);
const [showUploadDialog, setShowUploadDialog] = useState(false); const [showUploadDialog, setShowUploadDialog] = useState(false);
const [uploadFile, setUploadFile] = useState<File | null>(null); const [uploadFile, setUploadFile] = useState<File | null>(null);
const [uploadDocType, setUploadDocType] = useState(RESIGNATION_DOCUMENT_TYPES[0]); const [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]);
const [uploadStage, setUploadStage] = useState(''); const [uploadStage, setUploadStage] = useState('');
const fetchResignation = async () => { const fetchResignation = async () => {
try { try {
@ -181,6 +183,31 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed']; const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed'];
const legalStageApproved = (() => {
if (!resignationData) return false;
const inOrBeyondFnF = ['F&F Initiated', 'Completed', 'Settled', 'FNF_INITIATED'].includes(
String(resignationData.status || resignationData.currentStage || '')
);
if (inOrBeyondFnF) return true;
const timeline = Array.isArray(resignationData.timeline) ? resignationData.timeline : [];
return timeline.some((entry: any) => {
const stage = String(entry?.stage || '').trim().toLowerCase();
const targetStage = String(entry?.targetStage || '').trim().toLowerCase();
const action = String(entry?.action || '').trim().toLowerCase();
const atLegal = stage === 'legal' || stage === 'legal - resignation letter';
const legalApprovedTransition =
targetStage === 'legal' ||
targetStage === 'f&f initiated' ||
targetStage === 'fnf_initiated' ||
action.includes('approved');
return atLegal && legalApprovedTransition;
});
})();
const getResignationPermissions = () => { const getResignationPermissions = () => {
if (!resignationData || !currentUser) { if (!resignationData || !currentUser) {
return { canApprove: false, canWithdraw: false, canSendBack: false, canPushToFnF: false, canAssign: false }; return { canApprove: false, canWithdraw: false, canSendBack: false, canPushToFnF: false, canAssign: false };
@ -202,8 +229,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === STAGE_TO_ROLE_MAP[currentStage]; const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === STAGE_TO_ROLE_MAP[currentStage];
const canApprove = isCurrentlyAssigned &&
!isFinalState &&
!isSettlementPhase &&
!(currentStage === 'Legal' && legalStageApproved);
return { return {
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase, canApprove,
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0, canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState, canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState,
canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) && canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) &&
@ -226,6 +258,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
if (stageIndex <= currentIndex) return 'completed'; if (stageIndex <= currentIndex) return 'completed';
} }
if (stageKey === 'Legal' && legalStageApproved) return 'completed';
if (currentIndex === -1) return 'pending'; if (currentIndex === -1) return 'pending';
if (stageIndex < currentIndex) return 'completed'; if (stageIndex < currentIndex) return 'completed';
if (stageIndex === currentIndex) return 'active'; if (stageIndex === currentIndex) return 'active';
@ -277,11 +311,24 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
setAssignToUser(''); setAssignToUser('');
setSelectedSpecificUser(''); setSelectedSpecificUser('');
setAvailableUsers([]); setAvailableUsers([]);
setForceTriggerFnF(false);
fetchResignation(); fetchResignation();
} else {
const message = response.data?.message || 'Failed to submit action';
toast.error(message);
// When Legal approval bumps into LWD gate for F&F initiation, guide user explicitly.
if (response.data?.canForce) {
toast.info('LWD restriction hit. Use "Push to F&F" and enable "Force Initiate F&F Settlement Immediately" if urgent.');
}
} }
} catch (error: any) { } catch (error: any) {
console.error('Error submitting action:', error); console.error('Error submitting action:', error);
toast.error(error.response?.data?.message || 'Failed to submit action'); toast.error(error.response?.data?.message || 'Failed to submit action');
if (error?.response?.data?.canForce) {
toast.info('LWD restriction hit. Use "Push to F&F" with force option if business-approved.');
}
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }

View File

@ -44,6 +44,9 @@ const getStatusColor = (status: string) => {
export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) { export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dealers, setDealers] = useState<any[]>([]);
const [selectedDealerId, setSelectedDealerId] = useState('');
const [dialogDataLoading, setDialogDataLoading] = useState(false);
const [dealerCode, setDealerCode] = useState(''); const [dealerCode, setDealerCode] = useState('');
const [autoFilledData, setAutoFilledData] = useState<any>(null); const [autoFilledData, setAutoFilledData] = useState<any>(null);
const [terminations, setTerminations] = useState<any[]>([]); const [terminations, setTerminations] = useState<any[]>([]);
@ -76,40 +79,97 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
fetchTerminations(); fetchTerminations();
}, []); }, []);
const handleDealerCodeChange = async (code: string) => { useEffect(() => {
setDealerCode(code); if (!isDialogOpen || !isDDLead) return;
if (code.length >= 5) { let cancelled = false;
(async () => {
try { try {
const response = await API.getOutletByCode(code); setDialogDataLoading(true);
const response = await API.getDealers({ onboarded: 'true' });
const data = response.data as any; const data = response.data as any;
if (data?.success) { if (!cancelled && data?.success) {
setAutoFilledData(data.outlet); setDealers(Array.isArray(data.data) ? data.data : []);
toast.success('Dealer details loaded');
} else {
setAutoFilledData(null);
} }
} catch (error) { } catch (error) {
setAutoFilledData(null); if (!cancelled) {
console.error('Error fetching dealers:', error);
toast.error('Failed to load dealer list');
}
} finally {
if (!cancelled) {
setDialogDataLoading(false);
}
} }
} else { })();
return () => {
cancelled = true;
};
}, [isDialogOpen]);
const mapDealerToFormData = (dealer: any) => ({
id: dealer.id,
dealerId: dealer.id,
dealerCode: dealer.dealerCode?.dealerCode || '',
legalName: dealer.legalName || 'N/A',
businessName: dealer.businessName || 'N/A',
gstNumber: dealer.gstNumber || 'N/A',
address: dealer.registeredAddress || dealer.application?.preferredLocation || 'N/A',
city: dealer.application?.city || 'N/A',
state: dealer.application?.state || 'N/A',
email: dealer.user?.email || 'N/A',
phoneNumber: dealer.user?.mobileNumber || 'N/A'
});
const handleDealerSelect = (dealerId: string) => {
setSelectedDealerId(dealerId);
const dealer = dealers.find((row: any) => String(row.id) === String(dealerId));
if (!dealer) {
setDealerCode('');
setAutoFilledData(null); setAutoFilledData(null);
return;
} }
const mappedDealer = mapDealerToFormData(dealer);
setDealerCode(mappedDealer.dealerCode);
setAutoFilledData(mappedDealer);
};
const handleDealerCodeChange = (code: string) => {
setDealerCode(code);
const normalizedCode = code.trim().toLowerCase();
if (!normalizedCode) {
setSelectedDealerId('');
setAutoFilledData(null);
return;
}
const matchedDealer = dealers.find((dealer: any) =>
String(dealer.dealerCode?.dealerCode || '').toLowerCase() === normalizedCode
);
if (!matchedDealer) {
setSelectedDealerId('');
setAutoFilledData(null);
return;
}
setSelectedDealerId(String(matchedDealer.id));
setAutoFilledData(mapDealerToFormData(matchedDealer));
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!autoFilledData) { if (!autoFilledData) {
toast.error('Please enter a valid dealer code'); toast.error('Please select a dealer');
return; return;
} }
try { try {
// Backend expects: { dealerId, category, reason, proposedLwd, comments }
// Note: dealerId in TerminationRequest refers to 'Dealer' model ID.
// Outlet model has associate dealer? Let's check.
// In my outlet.controller.ts, I included 'dealer'.
const payload = { const payload = {
dealerId: autoFilledData.Dealer?.id || autoFilledData.id, // outlet.id might be used if dealerId is missing, but backend expects dealerId (Dealer model) dealerId: autoFilledData.dealerId || autoFilledData.id,
category: formData.terminationCategory, category: formData.terminationCategory,
reason: formData.reason, reason: formData.reason,
proposedLwd: formData.proposedLwd, proposedLwd: formData.proposedLwd,
@ -117,7 +177,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
}; };
if (!payload.dealerId) { if (!payload.dealerId) {
toast.error('Dealer record not found for this code'); toast.error('Dealer record not found for the selected dealer');
return; return;
} }
@ -128,7 +188,9 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
setIsDialogOpen(false); setIsDialogOpen(false);
fetchTerminations(); fetchTerminations();
// Reset form // Reset form
setSelectedDealerId('');
setDealerCode(''); setDealerCode('');
setDealers([]);
setAutoFilledData(null); setAutoFilledData(null);
setFormData({ setFormData({
terminationCategory: '', terminationCategory: '',
@ -259,14 +321,31 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Dealer Code - Auto-fetch trigger */} {/* Dealer selection */}
<div className="space-y-2">
<Label>Select Dealer *</Label>
<Select value={selectedDealerId} onValueChange={handleDealerSelect} disabled={dialogDataLoading}>
<SelectTrigger>
<SelectValue placeholder={dialogDataLoading ? 'Loading dealers...' : 'Select dealer'} />
</SelectTrigger>
<SelectContent>
{dealers.map((dealer: any) => (
<SelectItem key={dealer.id} value={String(dealer.id)}>
{dealer.legalName || dealer.businessName || 'Unnamed Dealer'} - {dealer.dealerCode?.dealerCode || 'No Code'}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Optional dealer code lookup */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="dealerCode">Dealer Code *</Label> <Label htmlFor="dealerCode">Dealer Code *</Label>
<Input <Input
id="dealerCode" id="dealerCode"
value={dealerCode} value={dealerCode}
onChange={(e) => handleDealerCodeChange(e.target.value)} onChange={(e) => handleDealerCodeChange(e.target.value)}
placeholder="e.g., DL-MH-025" placeholder="Type dealer code to auto-select"
required required
/> />
</div> </div>
@ -276,15 +355,15 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg"> <div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg">
<div> <div>
<Label className="text-slate-600">Dealer Name (Legal)</Label> <Label className="text-slate-600">Dealer Name (Legal)</Label>
<p>{autoFilledData.Dealer?.legalName || 'N/A'}</p> <p>{autoFilledData.legalName || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Business Name</Label> <Label className="text-slate-600">Business Name</Label>
<p>{autoFilledData.Dealer?.businessName || 'N/A'}</p> <p>{autoFilledData.businessName || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">GST</Label> <Label className="text-slate-600">GST</Label>
<p>{autoFilledData.Dealer?.gstNumber || 'N/A'}</p> <p>{autoFilledData.gstNumber || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Address</Label> <Label className="text-slate-600">Address</Label>
@ -295,8 +374,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<p>{autoFilledData.city}, {autoFilledData.state}</p> <p>{autoFilledData.city}, {autoFilledData.state}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Outlet Name</Label> <Label className="text-slate-600">Dealer Code</Label>
<p>{autoFilledData.name}</p> <p>{autoFilledData.dealerCode || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Contact</Label> <Label className="text-slate-600">Contact</Label>

View File

@ -36,45 +36,54 @@ export function Sidebar({ onLogout }: SidebarProps) {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [offboardingExpanded, setOffboardingExpanded] = useState(false); const [offboardingExpanded, setOffboardingExpanded] = useState(false);
const [allRequestsExpanded, setAllRequestsExpanded] = useState(false); const [allRequestsExpanded, setAllRequestsExpanded] = useState(false);
const currentRole = currentUser?.role || '';
const resignationRoles = ['DD Admin', 'ASM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Super Admin'];
const terminationRoles = ['ASM', 'DD Lead', 'DD Admin', 'Super Admin'];
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin'];
const canSeeResignation = resignationRoles.includes(currentRole);
const canSeeTermination = terminationRoles.includes(currentRole);
const canSeeFnF = fnfRoles.includes(currentRole);
const offboardingSubmenu = [
canSeeResignation ? { id: 'resignation', label: 'Resignation' } : null,
canSeeTermination ? { id: 'termination', label: 'Termination' } : null,
canSeeFnF ? { id: 'fnf', label: 'F&F' } : null
].filter(Boolean) as { id: string; label: string }[];
// Finance role has only specific menu items // Finance role has only specific menu items
const menuItems = currentUser?.role === 'Finance' ? [ const menuItems = currentRole === 'Finance' || currentRole === 'Finance Admin' ? [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'finance-onboarding', label: 'Onboarding', icon: FileText }, { id: 'finance-onboarding', label: 'Onboarding', icon: FileText },
{ id: 'finance-fnf', label: 'F&F', icon: UserMinus }, { id: 'finance-fnf', label: 'F&F', icon: UserMinus },
] : currentUser?.role === 'Dealer' ? [ ] : currentRole === 'Dealer' ? [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus }, { id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus },
{ id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw }, { id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw },
{ id: 'dealer-relocation', label: 'Relocation Requests', icon: MapPin }, { id: 'dealer-relocation', label: 'Relocation Requests', icon: MapPin },
] : currentUser?.role === 'FDD' ? [ ] : currentRole === 'FDD' ? [
{ id: 'fdd-dashboard', label: 'FDD Dashboard', icon: LayoutDashboard }, { id: 'fdd-dashboard', label: 'FDD Dashboard', icon: LayoutDashboard },
] : [ ] : [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'applications', label: 'Dealership Requests', icon: FileText }, { id: 'applications', label: 'Dealership Requests', icon: FileText },
{ ...(offboardingSubmenu.length > 0 ? [{
id: 'offboarding', id: 'offboarding',
label: 'Offboarding', label: 'Offboarding',
icon: UserMinus, icon: UserMinus,
hasSubmenu: true, hasSubmenu: true,
submenuKey: 'offboarding', submenuKey: 'offboarding',
submenu: [ submenu: offboardingSubmenu
{ id: 'resignation', label: 'Resignation' }, }] : []),
{ id: 'termination', label: 'Termination' },
{ id: 'fnf', label: 'F&F' }
]
},
{ id: 'constitutional-change', label: 'Constitutional Change', icon: RefreshCcw }, { id: 'constitutional-change', label: 'Constitutional Change', icon: RefreshCcw },
{ id: 'relocation-requests', label: 'Relocation Requests', icon: MapPin }, { id: 'relocation-requests', label: 'Relocation Requests', icon: MapPin },
]; ];
// Add All Applications for DD role (before Dealership Requests) // Add All Applications for DD role (before Dealership Requests)
if (currentUser?.role === 'DD') { if (currentRole === 'DD') {
menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox }); menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox });
} }
// Add All Requests for DD Lead role (before Dealership Requests) // Add All Requests for DD Lead role (before Dealership Requests)
if (currentUser?.role === 'DD Lead' || currentUser?.role === 'Super Admin') { if (currentRole === 'DD Lead' || currentRole === 'Super Admin') {
menuItems.splice(1, 0, { menuItems.splice(1, 0, {
id: 'all-requests', id: 'all-requests',
label: 'All Requests', label: 'All Requests',
@ -89,11 +98,11 @@ export function Sidebar({ onLogout }: SidebarProps) {
} }
// Add Master for Super Admin, DD Admin, and DD Lead // Add Master for Super Admin, DD Admin, and DD Lead
if (currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' || currentUser?.role === 'DD Lead') { if (currentRole === 'Super Admin' || currentRole === 'DD Admin' || currentRole === 'DD Lead') {
menuItems.push({ id: 'master', label: 'Master', icon: Settings }); menuItems.push({ id: 'master', label: 'Master', icon: Settings });
} }
if (currentUser?.role === 'Super Admin') { if (currentRole === 'Super Admin') {
menuItems.push({ id: 'users', label: 'User Management', icon: Users }); menuItems.push({ id: 'users', label: 'User Management', icon: Users });
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList }); menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
} }

View File

@ -2,6 +2,15 @@ import API from '../api/API';
import { toast } from 'sonner'; import { toast } from 'sonner';
export const adminService = { export const adminService = {
extractErrorMessage(error: any, fallback: string) {
return (
error?.response?.data?.message ||
error?.data?.message ||
error?.message ||
fallback
);
},
async getAllUsers() { async getAllUsers() {
try { try {
const response = await API.getUsers() as any; const response = await API.getUsers() as any;
@ -22,8 +31,9 @@ export const adminService = {
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
console.error('Error creating user:', error); console.error('Error creating user:', error);
toast.error(error.response?.data?.message || 'Failed to create user'); const message = this.extractErrorMessage(error, 'Failed to create user');
return { success: false }; toast.error(message);
return { success: false, message };
} }
}, },