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

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

View File

@ -72,6 +72,11 @@ export default function App() {
const [showAdminLogin, setShowAdminLogin] = useState(false);
const 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} />} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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