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 [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} />} />
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user