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:
parent
bc2b7faf08
commit
873a097185
69
src/App.tsx
69
src/App.tsx
@ -72,6 +72,11 @@ export default function App() {
|
||||
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
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(() => {
|
||||
dispatch(initializeAuth());
|
||||
@ -205,9 +210,9 @@ export default function App() {
|
||||
|
||||
{/* Dashboards */}
|
||||
<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}`)} /> :
|
||||
currentUser?.role === 'Dealer' ?
|
||||
currentRole === 'Dealer' ?
|
||||
<DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> :
|
||||
<Dashboard onNavigate={(path) => navigate(`/${path}`)} />
|
||||
} />
|
||||
@ -249,21 +254,61 @@ export default function App() {
|
||||
<Route path="/questionnaires" element={<QuestionnaireList />} />
|
||||
|
||||
{/* HR/Finance Modules (Simplified for brevity, following pattern) */}
|
||||
<Route path="/resignation" element={<ResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />} />
|
||||
<Route path="/resignation/:id" element={<ResignationDetails resignationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/resignation')} currentUser={currentUser} />} />
|
||||
<Route path="/resignation" element={
|
||||
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/:id" element={<TerminationDetails terminationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/termination')} currentUser={currentUser} />} />
|
||||
<Route path="/termination" element={
|
||||
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/:id" element={<FnFDetails fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/fnf')} currentUser={currentUser} />} />
|
||||
<Route path="/fnf" element={
|
||||
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/:id" element={<FinancePaymentDetailsPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />} />
|
||||
<Route path="/finance-onboarding" element={
|
||||
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-fnf" element={<FinanceFnFPage onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} />} />
|
||||
<Route path="/finance-fnf/:id" element={<FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />} />
|
||||
<Route path="/finance-fnf" element={
|
||||
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/:id" element={<ConstitutionalChangeDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/constitutional-change')} currentUser={currentUser} />} />
|
||||
|
||||
@ -181,10 +181,13 @@ export function UserManagementPage() {
|
||||
});
|
||||
setShowUserModal(false);
|
||||
fetchData();
|
||||
} else {
|
||||
toast.error(res.message || 'Failed to create user');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Operation failed');
|
||||
const message = (error as any)?.response?.data?.message || (error as any)?.message || 'Operation failed';
|
||||
toast.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -87,6 +87,18 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
const [isSubmitting, setIsSubmitting] = 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(() => {
|
||||
fetchRequests();
|
||||
}, []);
|
||||
@ -248,22 +260,22 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
color: 'bg-blue-500',
|
||||
},
|
||||
{
|
||||
title: 'In Progress',
|
||||
value: requests.filter(r => r.status !== 'Completed' && !r.status.includes('Rejected')).length,
|
||||
title: 'Submitted / Review',
|
||||
value: requests.filter(r => isSubmittedRequest(r) || isPendingReviewRequest(r)).length,
|
||||
icon: Calendar,
|
||||
color: 'bg-yellow-500',
|
||||
},
|
||||
{
|
||||
title: 'Completed',
|
||||
value: requests.filter(r => r.status === 'Completed').length,
|
||||
value: requests.filter(r => isCompletedRequest(r)).length,
|
||||
icon: Shield,
|
||||
color: 'bg-green-500',
|
||||
},
|
||||
{
|
||||
title: 'Pending Action',
|
||||
value: requests.filter(r => r.status.includes('Review') || r.status.includes('Pending')).length,
|
||||
title: 'Rejected / Revoked',
|
||||
value: requests.filter(r => isRejectedRequest(r)).length,
|
||||
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">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="all">All Requests</TabsTrigger>
|
||||
<TabsTrigger value="pending">Pending</TabsTrigger>
|
||||
<TabsTrigger value="in-progress">In Progress</TabsTrigger>
|
||||
<TabsTrigger value="pending">Submitted / Review</TabsTrigger>
|
||||
<TabsTrigger value="in-progress">Rejected / Revoked</TabsTrigger>
|
||||
<TabsTrigger value="completed">Completed</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@ -612,7 +624,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{requests
|
||||
.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending'))
|
||||
.filter((r: any) => isSubmittedRequest(r) || isPendingReviewRequest(r))
|
||||
.map((request: any) => (
|
||||
<TableRow key={request.requestId}>
|
||||
<TableCell>
|
||||
@ -659,7 +671,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
</TableCell>
|
||||
</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>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-slate-500">
|
||||
No pending requests found
|
||||
@ -686,7 +698,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{requests
|
||||
.filter((r: any) => r.status !== 'Completed' && !r.status.includes('Rejected'))
|
||||
.filter((r: any) => isRejectedRequest(r))
|
||||
.map((request: any) => (
|
||||
<TableRow key={request.requestId}>
|
||||
<TableCell>
|
||||
@ -740,7 +752,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{requests.filter((r: any) => r.status !== 'Completed' && !r.status.includes('Rejected')).length === 0 && (
|
||||
{requests.filter((r: any) => isRejectedRequest(r)).length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-slate-500">
|
||||
No in-progress requests found
|
||||
@ -767,7 +779,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{requests
|
||||
.filter((r: any) => r.status === 'Completed' || r.status === 'Closed')
|
||||
.filter((r: any) => isCompletedRequest(r))
|
||||
.map((request: any) => (
|
||||
<TableRow key={request.requestId}>
|
||||
<TableCell>
|
||||
@ -812,7 +824,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{requests.filter((r: any) => r.status === 'Completed' || r.status === 'Closed').length === 0 && (
|
||||
{requests.filter((r: any) => isCompletedRequest(r)).length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-slate-500">
|
||||
No completed requests found
|
||||
|
||||
@ -16,13 +16,16 @@ interface FnFPageProps {
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'New':
|
||||
case 'Initiated':
|
||||
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';
|
||||
case 'Under Review':
|
||||
case 'Finance Approval':
|
||||
case 'Calculated':
|
||||
return 'bg-orange-100 text-orange-700 border-orange-300';
|
||||
case 'Completed':
|
||||
case 'Settled':
|
||||
return 'bg-green-100 text-green-700 border-green-300';
|
||||
default:
|
||||
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 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 (
|
||||
<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">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>New Cases</CardDescription>
|
||||
<CardDescription>Initiated</CardDescription>
|
||||
<CardTitle className="text-3xl text-blue-600">
|
||||
{displaySettlements.filter(c => c.status === 'Initiated' || c.status === 'New').length}
|
||||
{initiatedCases.length}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-slate-600">Just Arrived</p>
|
||||
<p className="text-slate-600">Newly created</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>In Progress</CardDescription>
|
||||
<CardDescription>Clearance</CardDescription>
|
||||
<CardTitle className="text-3xl text-yellow-600">
|
||||
{displaySettlements.filter(c => c.status === 'In Progress').length}
|
||||
{clearanceCases.length}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-slate-600">Awaiting Response</p>
|
||||
<p className="text-slate-600">Department / legal stage</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Under Review</CardDescription>
|
||||
<CardDescription>Finance Approval</CardDescription>
|
||||
<CardTitle className="text-3xl text-orange-600">
|
||||
{displaySettlements.filter(c => c.status === 'Under Review' || c.status === 'Calculated').length}
|
||||
{financeApprovalCases.length}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-slate-600">Discussion Ongoing</p>
|
||||
<p className="text-slate-600">Ready for finance review</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -171,18 +178,17 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
||||
<CardContent>
|
||||
<Tabs defaultValue="all" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="new">New Cases</TabsTrigger>
|
||||
<TabsTrigger value="all">All Cases</TabsTrigger>
|
||||
<TabsTrigger value="progress">In Progress</TabsTrigger>
|
||||
<TabsTrigger value="review">Under Review</TabsTrigger>
|
||||
<TabsTrigger value="initiated">Initiated</TabsTrigger>
|
||||
<TabsTrigger value="clearance">Clearance</TabsTrigger>
|
||||
<TabsTrigger value="finance">Finance Approval</TabsTrigger>
|
||||
<TabsTrigger value="completed">Completed</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* New Cases Tab */}
|
||||
<TabsContent value="new" className="mt-6">
|
||||
{/* Initiated Tab */}
|
||||
<TabsContent value="initiated" className="mt-6">
|
||||
<div className="space-y-4">
|
||||
{displaySettlements
|
||||
.filter(c => c.status === 'New' || c.status === 'Initiated')
|
||||
{initiatedCases
|
||||
.map((fnfCase) => (
|
||||
<Card key={fnfCase.id} className="border-slate-200">
|
||||
<CardContent className="pt-6">
|
||||
@ -261,10 +267,10 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
||||
</CardContent>
|
||||
</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">
|
||||
<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>
|
||||
@ -279,15 +285,15 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
<div className={`p-3 rounded-lg ${
|
||||
fnfCase.status === 'New' ? 'bg-blue-100' :
|
||||
fnfCase.status === 'In Progress' ? 'bg-yellow-100' :
|
||||
fnfCase.status === 'Under Review' ? 'bg-orange-100' :
|
||||
fnfCase.status === 'Initiated' ? 'bg-blue-100' :
|
||||
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
|
||||
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' :
|
||||
'bg-green-100'
|
||||
}`}>
|
||||
<IndianRupee className={`w-6 h-6 ${
|
||||
fnfCase.status === 'New' ? 'text-blue-600' :
|
||||
fnfCase.status === 'In Progress' ? 'text-yellow-600' :
|
||||
fnfCase.status === 'Under Review' ? 'text-orange-600' :
|
||||
fnfCase.status === 'Initiated' ? 'text-blue-600' :
|
||||
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
|
||||
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
|
||||
'text-green-600'
|
||||
}`} />
|
||||
</div>
|
||||
@ -322,7 +328,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{canSendToStakeholders && fnfCase.status === 'New' && (
|
||||
{canSendToStakeholders && fnfCase.status === 'Initiated' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@ -349,11 +355,10 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* In Progress Tab */}
|
||||
<TabsContent value="progress" className="mt-6">
|
||||
{/* Clearance Tab */}
|
||||
<TabsContent value="clearance" className="mt-6">
|
||||
<div className="space-y-4">
|
||||
{displaySettlements
|
||||
.filter(c => c.status === 'In Progress')
|
||||
{clearanceCases
|
||||
.map((fnfCase) => (
|
||||
<Card key={fnfCase.id} className="border-slate-200">
|
||||
<CardContent className="pt-6">
|
||||
@ -403,20 +408,19 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{displaySettlements.filter((c: any) => c.status === 'In Progress').length === 0 && (
|
||||
{clearanceCases.length === 0 && (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<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>
|
||||
</TabsContent>
|
||||
|
||||
{/* Under Review Tab */}
|
||||
<TabsContent value="review" className="mt-6">
|
||||
{/* Finance Approval Tab */}
|
||||
<TabsContent value="finance" className="mt-6">
|
||||
<div className="space-y-4">
|
||||
{displaySettlements
|
||||
.filter(c => c.status === 'Under Review' || c.status === 'Calculated')
|
||||
{financeApprovalCases
|
||||
.map((fnfCase) => (
|
||||
<Card key={fnfCase.id} className="border-slate-200">
|
||||
<CardContent className="pt-6">
|
||||
@ -468,10 +472,10 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
||||
</CardContent>
|
||||
</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">
|
||||
<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>
|
||||
@ -480,8 +484,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
||||
{/* Completed Tab */}
|
||||
<TabsContent value="completed" className="mt-6">
|
||||
<div className="space-y-4">
|
||||
{displaySettlements
|
||||
.filter(c => c.status === 'Completed' || c.status === 'Settled')
|
||||
{completedCases
|
||||
.map((fnfCase) => (
|
||||
<Card key={fnfCase.id} className="border-slate-200">
|
||||
<CardContent className="pt-6">
|
||||
@ -529,7 +532,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{displaySettlements.filter(c => c.status === 'Completed' || c.status === 'Settled').length === 0 && (
|
||||
{completedCases.length === 0 && (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<FileCheck className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
||||
<p>No completed cases</p>
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { API } from '../../api/API';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@ -23,6 +23,27 @@ interface 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 [loading, setLoading] = useState(true);
|
||||
const [documents, setDocuments] = useState<any[]>([]);
|
||||
@ -31,8 +52,36 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
||||
const [isUploading, setIsUploading] = 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
|
||||
const [form, setForm] = useState({
|
||||
const emptyForm = {
|
||||
panNumber: '',
|
||||
gstNumber: '',
|
||||
registeredAddress: '',
|
||||
@ -41,7 +90,16 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
||||
ifscCode: '',
|
||||
branchName: '',
|
||||
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(() => {
|
||||
fetchData();
|
||||
@ -57,8 +115,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
||||
|
||||
if (detailsRes.data?.success) {
|
||||
const data = detailsRes.data.data;
|
||||
setDetails(data);
|
||||
setForm({
|
||||
const nextForm = {
|
||||
panNumber: data.panNumber || '',
|
||||
gstNumber: data.gstNumber || '',
|
||||
registeredAddress: data.registeredAddress || data.address || '',
|
||||
@ -67,7 +124,10 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
||||
ifscCode: data.ifscCode || '',
|
||||
branchName: data.branchName || '',
|
||||
accountHolderName: data.accountHolderName || data.applicantName || ''
|
||||
});
|
||||
};
|
||||
setDetails(data);
|
||||
setForm(nextForm);
|
||||
setSavedForm(nextForm);
|
||||
}
|
||||
if (docsRes.data?.success || docsRes.ok) {
|
||||
setDocuments(docsRes.data.data || []);
|
||||
@ -277,7 +337,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
||||
<button
|
||||
onClick={handleSaveDetails}
|
||||
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" />}
|
||||
Save Business Info
|
||||
@ -389,34 +449,39 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
||||
<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="space-y-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label>
|
||||
<select
|
||||
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"
|
||||
value={selectedDocType}
|
||||
onChange={(e) => setSelectedDocType(e.target.value)}
|
||||
disabled={isUploading}
|
||||
{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'}`}>
|
||||
{selectedDocAlreadyUploaded ? 'Already uploaded' : 'Pending upload'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<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'}`} />
|
||||
<Select value={selectedDocType} onValueChange={setSelectedDocType} disabled={isUploading}>
|
||||
<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">
|
||||
<SelectValue placeholder="Choose document type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border-slate-200 shadow-lg">
|
||||
{requiredDocumentTypes.map((docType) => {
|
||||
const isUploaded = uploadedDocumentTypes.has(docType.toLowerCase());
|
||||
return (
|
||||
<SelectItem
|
||||
key={docType}
|
||||
value={docType}
|
||||
className="rounded-lg px-3 py-2 text-sm text-slate-700 focus:bg-amber-50 focus:text-slate-900"
|
||||
>
|
||||
<option value="">Select type...</option>
|
||||
<option value="PAN Card">PAN Card</option>
|
||||
<option value="GST Certificate">GST Certificate</option>
|
||||
<option value="Aadhaar Card">Aadhaar Card</option>
|
||||
<option value="Security Deposit Receipt">Security Deposit Receipt</option>
|
||||
<option value="First Fill Receipt">First Fill Receipt</option>
|
||||
<option value="Partnership Deed">Partnership Deed</option>
|
||||
<option value="LLP Agreement">LLP Agreement</option>
|
||||
<option value="Certificate of Incorporation">Certificate of Incorporation</option>
|
||||
<option value="MOA">MOA (Memorandum of Association)</option>
|
||||
<option value="AOA">AOA (Articles of Association)</option>
|
||||
<option value="Firm Registration">Firm Registration</option>
|
||||
<option value="Rental Agreement">Rental Agreement</option>
|
||||
<option value="Property Documents">Property Documents</option>
|
||||
<option value="Nodal Agreement">Nodal Agreement</option>
|
||||
<option value="Cancelled Check">Cancelled Check</option>
|
||||
<option value="LOI Acknowledgement">LOI Acknowledgement</option>
|
||||
<option value="Architecture Blueprint">Architecture Blueprint</option>
|
||||
<option value="Site Plan">Site Plan</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className={`h-4 w-4 ${isUploaded ? 'text-green-600' : 'text-slate-300'}`} />
|
||||
{docType}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Select File</label>
|
||||
@ -481,23 +546,35 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{details.statusHistory?.length > 0 ? (
|
||||
{(details.progressTracking || []).length > 0 ? (
|
||||
<div className="relative space-y-6">
|
||||
<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 || [])]
|
||||
.sort((a: any, b: any) => Number(a.stageOrder || 0) - Number(b.stageOrder || 0))
|
||||
.map((item: any, idx: number) => {
|
||||
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 border-amber-500 shadow-sm">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div>
|
||||
<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 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>
|
||||
)}
|
||||
<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 className="text-center py-6">
|
||||
|
||||
@ -27,6 +27,15 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
||||
const [requests, setRequests] = useState<any[]>([]);
|
||||
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(() => {
|
||||
fetchRequests();
|
||||
}, []);
|
||||
@ -57,22 +66,22 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
||||
color: 'bg-blue-500',
|
||||
},
|
||||
{
|
||||
title: 'In Progress',
|
||||
value: requests.filter((r: any) => r.status !== 'Completed' && r.status !== 'Closed' && !r.status.includes('Rejected') && !r.status.includes('Revoked')).length,
|
||||
title: 'Pending Review',
|
||||
value: requests.filter((r: any) => isPendingReviewRequest(r)).length,
|
||||
icon: Calendar,
|
||||
color: 'bg-yellow-500',
|
||||
},
|
||||
{
|
||||
title: 'Completed',
|
||||
value: requests.filter((r: any) => r.status === 'Completed' || r.status === 'Closed').length,
|
||||
value: requests.filter((r: any) => isCompletedRequest(r)).length,
|
||||
icon: MapPin,
|
||||
color: 'bg-green-500',
|
||||
},
|
||||
{
|
||||
title: 'Pending Action',
|
||||
value: requests.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending')).length,
|
||||
title: 'Rejected / Revoked',
|
||||
value: requests.filter((r: any) => isRejectedRequest(r)).length,
|
||||
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">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="all">All Requests</TabsTrigger>
|
||||
<TabsTrigger value="pending">Pending</TabsTrigger>
|
||||
<TabsTrigger value="in-progress">In Progress</TabsTrigger>
|
||||
<TabsTrigger value="pending">Pending Review</TabsTrigger>
|
||||
<TabsTrigger value="in-progress">Rejected / Revoked</TabsTrigger>
|
||||
<TabsTrigger value="completed">Completed</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@ -248,7 +257,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
||||
</TableRow>
|
||||
) : (
|
||||
requests
|
||||
.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending'))
|
||||
.filter((r: any) => isPendingReviewRequest(r))
|
||||
.map((request: any) => (
|
||||
<TableRow key={request.id}>
|
||||
<TableCell>
|
||||
@ -309,7 +318,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
||||
</TableRow>
|
||||
) : (
|
||||
requests
|
||||
.filter((r: any) => r.status !== 'Completed' && r.status !== 'Closed' && !r.status.includes('Rejected'))
|
||||
.filter((r: any) => isRejectedRequest(r))
|
||||
.map((request: any) => (
|
||||
<TableRow key={request.id}>
|
||||
<TableCell>
|
||||
@ -329,7 +338,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
|
||||
<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}%` }}
|
||||
/>
|
||||
</div>
|
||||
@ -380,7 +389,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
||||
</TableRow>
|
||||
) : (
|
||||
requests
|
||||
.filter((r: any) => r.status === 'Completed' || r.status === 'Closed')
|
||||
.filter((r: any) => isCompletedRequest(r))
|
||||
.map((request: any) => (
|
||||
<TableRow key={request.id}>
|
||||
<TableCell>
|
||||
|
||||
@ -74,6 +74,8 @@ interface ResignationDetailsProps {
|
||||
currentUser: UserType | null;
|
||||
}
|
||||
|
||||
export default ResignationDetails;
|
||||
|
||||
const STAGE_TO_ROLE_MAP: Record<string, string> = {
|
||||
'ASM': 'ASM',
|
||||
'RBM': 'RBM',
|
||||
@ -109,8 +111,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
...(stageKey ? (RESIGNATION_STAGE_ALIASES[stageKey] || []) : []),
|
||||
...(RESIGNATION_STAGE_ALIASES[stageName] || [])
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map((value: string) => value.trim().toLowerCase());
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map((value) => value.trim().toLowerCase());
|
||||
|
||||
return allDocs.filter((doc: any) => {
|
||||
if (!doc?.stage) return false;
|
||||
@ -136,7 +138,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||
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 fetchResignation = async () => {
|
||||
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 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 = () => {
|
||||
if (!resignationData || !currentUser) {
|
||||
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 canApprove = isCurrentlyAssigned &&
|
||||
!isFinalState &&
|
||||
!isSettlementPhase &&
|
||||
!(currentStage === 'Legal' && legalStageApproved);
|
||||
|
||||
return {
|
||||
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase,
|
||||
canApprove,
|
||||
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
|
||||
canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState,
|
||||
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 (stageKey === 'Legal' && legalStageApproved) return 'completed';
|
||||
|
||||
if (currentIndex === -1) return 'pending';
|
||||
if (stageIndex < currentIndex) return 'completed';
|
||||
if (stageIndex === currentIndex) return 'active';
|
||||
@ -277,11 +311,24 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
setAssignToUser('');
|
||||
setSelectedSpecificUser('');
|
||||
setAvailableUsers([]);
|
||||
setForceTriggerFnF(false);
|
||||
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) {
|
||||
console.error('Error submitting action:', error);
|
||||
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 {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@ -44,6 +44,9 @@ const getStatusColor = (status: string) => {
|
||||
|
||||
export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [dealers, setDealers] = useState<any[]>([]);
|
||||
const [selectedDealerId, setSelectedDealerId] = useState('');
|
||||
const [dialogDataLoading, setDialogDataLoading] = useState(false);
|
||||
const [dealerCode, setDealerCode] = useState('');
|
||||
const [autoFilledData, setAutoFilledData] = useState<any>(null);
|
||||
const [terminations, setTerminations] = useState<any[]>([]);
|
||||
@ -76,40 +79,97 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
fetchTerminations();
|
||||
}, []);
|
||||
|
||||
const handleDealerCodeChange = async (code: string) => {
|
||||
setDealerCode(code);
|
||||
if (code.length >= 5) {
|
||||
useEffect(() => {
|
||||
if (!isDialogOpen || !isDDLead) return;
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const response = await API.getOutletByCode(code);
|
||||
setDialogDataLoading(true);
|
||||
const response = await API.getDealers({ onboarded: 'true' });
|
||||
const data = response.data as any;
|
||||
if (data?.success) {
|
||||
setAutoFilledData(data.outlet);
|
||||
toast.success('Dealer details loaded');
|
||||
} else {
|
||||
setAutoFilledData(null);
|
||||
if (!cancelled && data?.success) {
|
||||
setDealers(Array.isArray(data.data) ? data.data : []);
|
||||
}
|
||||
} catch (error) {
|
||||
setAutoFilledData(null);
|
||||
if (!cancelled) {
|
||||
console.error('Error fetching dealers:', error);
|
||||
toast.error('Failed to load dealer list');
|
||||
}
|
||||
} else {
|
||||
setAutoFilledData(null);
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setDialogDataLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
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);
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
if (!autoFilledData) {
|
||||
toast.error('Please enter a valid dealer code');
|
||||
toast.error('Please select a dealer');
|
||||
return;
|
||||
}
|
||||
|
||||
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 = {
|
||||
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,
|
||||
reason: formData.reason,
|
||||
proposedLwd: formData.proposedLwd,
|
||||
@ -117,7 +177,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
};
|
||||
|
||||
if (!payload.dealerId) {
|
||||
toast.error('Dealer record not found for this code');
|
||||
toast.error('Dealer record not found for the selected dealer');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -128,7 +188,9 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
setIsDialogOpen(false);
|
||||
fetchTerminations();
|
||||
// Reset form
|
||||
setSelectedDealerId('');
|
||||
setDealerCode('');
|
||||
setDealers([]);
|
||||
setAutoFilledData(null);
|
||||
setFormData({
|
||||
terminationCategory: '',
|
||||
@ -259,14 +321,31 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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">
|
||||
<Label htmlFor="dealerCode">Dealer Code *</Label>
|
||||
<Input
|
||||
id="dealerCode"
|
||||
value={dealerCode}
|
||||
onChange={(e) => handleDealerCodeChange(e.target.value)}
|
||||
placeholder="e.g., DL-MH-025"
|
||||
placeholder="Type dealer code to auto-select"
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
<Label className="text-slate-600">Dealer Name (Legal)</Label>
|
||||
<p>{autoFilledData.Dealer?.legalName || 'N/A'}</p>
|
||||
<p>{autoFilledData.legalName || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Business Name</Label>
|
||||
<p>{autoFilledData.Dealer?.businessName || 'N/A'}</p>
|
||||
<p>{autoFilledData.businessName || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">GST</Label>
|
||||
<p>{autoFilledData.Dealer?.gstNumber || 'N/A'}</p>
|
||||
<p>{autoFilledData.gstNumber || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Address</Label>
|
||||
@ -295,8 +374,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
<p>{autoFilledData.city}, {autoFilledData.state}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Outlet Name</Label>
|
||||
<p>{autoFilledData.name}</p>
|
||||
<Label className="text-slate-600">Dealer Code</Label>
|
||||
<p>{autoFilledData.dealerCode || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Contact</Label>
|
||||
|
||||
@ -36,45 +36,54 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [offboardingExpanded, setOffboardingExpanded] = 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
|
||||
const menuItems = currentUser?.role === 'Finance' ? [
|
||||
const menuItems = currentRole === 'Finance' || currentRole === 'Finance Admin' ? [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ id: 'finance-onboarding', label: 'Onboarding', icon: FileText },
|
||||
{ id: 'finance-fnf', label: 'F&F', icon: UserMinus },
|
||||
] : currentUser?.role === 'Dealer' ? [
|
||||
] : currentRole === 'Dealer' ? [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus },
|
||||
{ id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw },
|
||||
{ id: 'dealer-relocation', label: 'Relocation Requests', icon: MapPin },
|
||||
] : currentUser?.role === 'FDD' ? [
|
||||
] : currentRole === 'FDD' ? [
|
||||
{ id: 'fdd-dashboard', label: 'FDD Dashboard', icon: LayoutDashboard },
|
||||
] : [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ id: 'applications', label: 'Dealership Requests', icon: FileText },
|
||||
{
|
||||
...(offboardingSubmenu.length > 0 ? [{
|
||||
id: 'offboarding',
|
||||
label: 'Offboarding',
|
||||
icon: UserMinus,
|
||||
hasSubmenu: true,
|
||||
submenuKey: 'offboarding',
|
||||
submenu: [
|
||||
{ id: 'resignation', label: 'Resignation' },
|
||||
{ id: 'termination', label: 'Termination' },
|
||||
{ id: 'fnf', label: 'F&F' }
|
||||
]
|
||||
},
|
||||
submenu: offboardingSubmenu
|
||||
}] : []),
|
||||
{ id: 'constitutional-change', label: 'Constitutional Change', icon: RefreshCcw },
|
||||
{ id: 'relocation-requests', label: 'Relocation Requests', icon: MapPin },
|
||||
];
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
// 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, {
|
||||
id: 'all-requests',
|
||||
label: 'All Requests',
|
||||
@ -89,11 +98,11 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
||||
}
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
if (currentUser?.role === 'Super Admin') {
|
||||
if (currentRole === 'Super Admin') {
|
||||
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
||||
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
||||
}
|
||||
|
||||
@ -2,6 +2,15 @@ import API from '../api/API';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const adminService = {
|
||||
extractErrorMessage(error: any, fallback: string) {
|
||||
return (
|
||||
error?.response?.data?.message ||
|
||||
error?.data?.message ||
|
||||
error?.message ||
|
||||
fallback
|
||||
);
|
||||
},
|
||||
|
||||
async getAllUsers() {
|
||||
try {
|
||||
const response = await API.getUsers() as any;
|
||||
@ -22,8 +31,9 @@ export const adminService = {
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error creating user:', error);
|
||||
toast.error(error.response?.data?.message || 'Failed to create user');
|
||||
return { success: false };
|
||||
const message = this.extractErrorMessage(error, 'Failed to create user');
|
||||
toast.error(message);
|
||||
return { success: false, message };
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user