293 lines
16 KiB
TypeScript
293 lines
16 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { RootState } from './store';
|
|
import { setCredentials, logout as logoutAction, initializeAuth } from './store/slices/authSlice';
|
|
import { RoleGuard } from './components/auth/RoleGuard';
|
|
import { Routes, Route, Navigate, useLocation, useNavigate, Outlet } from 'react-router-dom';
|
|
import { ApplicationFormPage } from './components/public/ApplicationFormPage';
|
|
import PublicQuestionnairePage from './pages/public/PublicQuestionnairePage';
|
|
import { LoginPage } from './components/auth/LoginPage';
|
|
import { ProspectiveLoginPage } from './components/auth/ProspectiveLoginPage';
|
|
import { Sidebar } from './components/layout/Sidebar';
|
|
import { Header } from './components/layout/Header';
|
|
import { Dashboard } from './components/dashboard/Dashboard';
|
|
import { FinanceDashboard } from './components/dashboard/FinanceDashboard';
|
|
import { DealerDashboard } from './components/dashboard/DealerDashboard';
|
|
import { ProspectiveDashboardPage } from './components/dashboard/ProspectiveDashboardPage';
|
|
import { ApplicationsPage } from './components/applications/ApplicationsPage';
|
|
import { AllApplicationsPage } from './components/applications/AllApplicationsPage';
|
|
import { OpportunityRequestsPage } from './components/applications/OpportunityRequestsPage';
|
|
import { UnopportunityRequestsPage } from './components/applications/UnopportunityRequestsPage';
|
|
import { ApplicationDetails } from './components/applications/ApplicationDetails';
|
|
import { ResignationPage } from './components/applications/ResignationPage';
|
|
import { TerminationPage } from './components/applications/TerminationPage';
|
|
import { FnFPage } from './components/applications/FnFPage';
|
|
import { ResignationDetails } from './components/applications/ResignationDetails';
|
|
import { TerminationDetails } from './components/applications/TerminationDetails';
|
|
import { FnFDetails } from './components/applications/FnFDetails';
|
|
import { FinanceOnboardingPage } from './components/applications/FinanceOnboardingPage';
|
|
import { FinanceFnFPage } from './components/applications/FinanceFnFPage';
|
|
import { FinancePaymentDetailsPage } from './components/applications/FinancePaymentDetailsPage';
|
|
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
|
|
import { MasterPage } from './components/applications/MasterPage';
|
|
import { UserManagementPage } from './components/admin/UserManagementPage';
|
|
import { ConstitutionalChangePage } from './components/applications/ConstitutionalChangePage';
|
|
import { ConstitutionalChangeDetails } from './components/applications/ConstitutionalChangeDetails';
|
|
import { RelocationRequestPage } from './components/applications/RelocationRequestPage';
|
|
import { RelocationRequestDetails } from './components/applications/RelocationRequestDetails';
|
|
import { WorknotePage } from './components/applications/WorknotePage';
|
|
import { DealerResignationPage } from './components/dealer/DealerResignationPage';
|
|
import { DealerConstitutionalChangePage } from './components/dealer/DealerConstitutionalChangePage';
|
|
import { DealerRelocationPage } from './components/dealer/DealerRelocationPage';
|
|
import QuestionnaireBuilder from './components/admin/QuestionnaireBuilder';
|
|
import QuestionnaireList from './components/admin/QuestionnaireList';
|
|
import { Toaster } from './components/ui/sonner';
|
|
import { User } from './lib/mock-data';
|
|
import { toast } from 'sonner';
|
|
import { API } from './api/API';
|
|
|
|
// Layout Component
|
|
const AppLayout = ({ onLogout, title }: { onLogout: () => void, title: string }) => {
|
|
return (
|
|
<div className="flex h-screen bg-slate-50">
|
|
<Sidebar onLogout={onLogout} />
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
<Header title={title} onRefresh={() => window.location.reload()} />
|
|
<main className="flex-1 overflow-y-auto p-6">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
<Toaster />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default function App() {
|
|
const dispatch = useDispatch<any>();
|
|
const { user: currentUser, isAuthenticated, loading } = useSelector((state: RootState) => state.auth);
|
|
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
useEffect(() => {
|
|
dispatch(initializeAuth());
|
|
}, [dispatch]);
|
|
|
|
const handleLogin = async (email: string, password: string) => {
|
|
try {
|
|
const response = await API.login({ email, password });
|
|
if (response.ok && response.data) {
|
|
const { token, user } = response.data as any;
|
|
localStorage.setItem('token', token);
|
|
const simplifiedUser: User = {
|
|
id: user.id,
|
|
name: user.fullName || email.split('@')[0],
|
|
email: user.email,
|
|
password: password, // Note: storing password in state is not ideal, but keeping existing structure
|
|
role: typeof user.role === 'string' ? user.role : (user.roleCode || 'User')
|
|
};
|
|
dispatch(setCredentials({ user: simplifiedUser, token }));
|
|
toast.success(`Welcome back, ${simplifiedUser.name}!`);
|
|
setShowAdminLogin(false);
|
|
} else {
|
|
const errorMsg = (response.data as any)?.message || 'Invalid credentials';
|
|
toast.error(errorMsg);
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
toast.error('Something went wrong. Please try again.');
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
dispatch(logoutAction());
|
|
setShowAdminLogin(false);
|
|
toast.info('Logged out successfully');
|
|
navigate('/');
|
|
};
|
|
|
|
// Listen for 401 logout events from API client
|
|
useEffect(() => {
|
|
const onLogout = () => {
|
|
handleLogout();
|
|
toast.error('Session expired. Please login again.');
|
|
};
|
|
window.addEventListener('auth:logout', onLogout);
|
|
return () => window.removeEventListener('auth:logout', onLogout);
|
|
}, [dispatch, navigate]);
|
|
|
|
// Helper to determine page title based on path
|
|
const getPageTitle = (pathname: string) => {
|
|
if (pathname.startsWith('/applications/') && pathname.length > 14) return 'Application Details';
|
|
if (pathname.includes('/resignation/') && pathname.length > 13) return 'Resignation Details';
|
|
// ... Add more dynamic title logic as needed
|
|
const titles: Record<string, string> = {
|
|
'/dashboard': 'Dashboard',
|
|
'/applications': 'Dealership Requests',
|
|
'/all-applications': 'All Applications',
|
|
'/opportunity-requests': 'Opportunity Requests',
|
|
'/unopportunity-requests': 'Unopportunity Requests',
|
|
'/tasks': 'My Tasks',
|
|
'/reports': 'Reports & Analytics',
|
|
'/settings': 'Settings',
|
|
'/users': 'User Management',
|
|
'/resignation': 'Resignation Management',
|
|
'/termination': 'Termination Management',
|
|
'/fnf': 'Full & Final Settlement',
|
|
'/finance-onboarding': 'Payment Verification',
|
|
'/finance-fnf': 'F&F Financial Settlement',
|
|
'/master': 'Master Configuration',
|
|
'/constitutional-change': 'Constitutional Change',
|
|
'/relocation-requests': 'Relocation Requests',
|
|
'/dealer-resignation': 'Dealer Resignation Management',
|
|
'/dealer-constitutional': 'Dealer Constitutional Change',
|
|
'/dealer-relocation': 'Dealer Relocation Requests',
|
|
'/questionnaire-builder': 'Questionnaire Builder',
|
|
};
|
|
return titles[pathname] || 'Dashboard';
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-screen bg-slate-50">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-red-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Public Routes
|
|
if (!isAuthenticated) {
|
|
return (
|
|
<>
|
|
<Routes>
|
|
<Route path="/admin-login" element={<LoginPage onLogin={handleLogin} />} />
|
|
<Route path="/prospective-login" element={<ProspectiveLoginPage />} />
|
|
<Route path="/questionnaire/:applicationId" element={<PublicQuestionnairePage />} />
|
|
<Route path="*" element={showAdminLogin ? <LoginPage onLogin={handleLogin} /> :
|
|
<ApplicationFormPage onAdminLogin={() => setShowAdminLogin(true)} />}
|
|
/>
|
|
</Routes>
|
|
<Toaster />
|
|
</>
|
|
)
|
|
}
|
|
|
|
// Protected Routes
|
|
return (
|
|
<Routes>
|
|
{/* Prospective Dealer Route - STRICTLY ISOLATED */}
|
|
<Route
|
|
path="/prospective-dashboard"
|
|
element={
|
|
<RoleGuard allowedRoles={['Prospective Dealer']}>
|
|
<ProspectiveDashboardPage />
|
|
</RoleGuard>
|
|
}
|
|
/>
|
|
|
|
{/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
|
|
<Route element={
|
|
<RoleGuard excludeRoles={['Prospective Dealer']} redirectTo="/prospective-dashboard">
|
|
<AppLayout onLogout={handleLogout} title={getPageTitle(location.pathname)} />
|
|
</RoleGuard>
|
|
}>
|
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
|
|
{/* Dashboards */}
|
|
<Route path="/dashboard" element={
|
|
currentUser?.role === 'Finance Admin' || currentUser?.role === 'Finance' ?
|
|
<FinanceDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> :
|
|
currentUser?.role === 'Dealer' ?
|
|
<DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> :
|
|
<Dashboard onNavigate={(path) => navigate(`/${path}`)} />
|
|
} />
|
|
|
|
{/* Applications */}
|
|
<Route path="/applications" element={<ApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" />} />
|
|
<Route path="/applications/:id" element={<ApplicationDetails applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/applications')} />} />
|
|
|
|
<Route path="/all-applications" element={
|
|
currentUser?.role === 'DD' ? <AllApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" /> : <Navigate to="/dashboard" />
|
|
} />
|
|
|
|
{/* Admin/Lead Routes */}
|
|
<Route path="/opportunity-requests" element={<OpportunityRequestsPage onViewDetails={(id) => navigate(`/applications/${id}`)} />} />
|
|
<Route path="/unopportunity-requests" element={<UnopportunityRequestsPage onViewDetails={(id) => navigate(`/applications/${id}`)} />} />
|
|
|
|
{/* Other Modules */}
|
|
<Route path="/users" element={<UserManagementPage />} />
|
|
<Route path="/master" element={<MasterPage />} />
|
|
<Route path="/questions" element={<QuestionnaireList />} />
|
|
<Route path="/questionnaire-builder" element={<QuestionnaireBuilder />} />
|
|
<Route path="/questionnaire-builder/:id" element={<QuestionnaireBuilder />} />
|
|
<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="/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="/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="/finance-onboarding" element={<FinanceOnboardingPage onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} />} />
|
|
<Route path="/finance-onboarding/:id" element={<FinancePaymentDetailsPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />} />
|
|
|
|
<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="/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} onOpenWorknote={() => { }} />} />
|
|
|
|
<Route path="/relocation-requests" element={<RelocationRequestPage currentUser={currentUser} onViewDetails={(id) => navigate(`/relocation-requests/${id}`)} />} />
|
|
<Route path="/relocation-requests/:id" element={<RelocationRequestDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/relocation-requests')} currentUser={currentUser} onOpenWorknote={() => { }} />} />
|
|
|
|
{/* Dealer Routes */}
|
|
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />} />
|
|
<Route path="/dealer-constitutional" element={<DealerConstitutionalChangePage currentUser={currentUser} onViewDetails={(id) => navigate(`/constitutional-change/${id}`)} />} />
|
|
<Route path="/dealer-relocation" element={<DealerRelocationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/relocation-requests/${id}`)} />} />
|
|
|
|
{/* Placeholder Routes */}
|
|
<Route path="/tasks" element={
|
|
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
|
|
<h2 className="text-slate-900 mb-2">My Tasks</h2>
|
|
<p className="text-slate-600">Task management interface would be displayed here</p>
|
|
<p className="text-slate-500 mt-4">Shows applications assigned to the current user</p>
|
|
</div>
|
|
} />
|
|
<Route path="/reports" element={
|
|
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
|
|
<h2 className="text-slate-900 mb-2">Reports & Analytics</h2>
|
|
<p className="text-slate-600">Advanced reporting and analytics dashboard</p>
|
|
<p className="text-slate-500 mt-4">Charts, export capabilities, and custom filters</p>
|
|
</div>
|
|
} />
|
|
<Route path="/settings" element={
|
|
<div className="bg-white rounded-lg border border-slate-200 p-8">
|
|
<h2 className="text-slate-900 mb-4">Settings</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-slate-900 mb-2">Profile Settings</h3>
|
|
<p className="text-slate-600">Update your profile information and preferences</p>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-slate-900 mb-2">Notification Preferences</h3>
|
|
<p className="text-slate-600">Configure email and system notifications</p>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-slate-900 mb-2">Security</h3>
|
|
<p className="text-slate-600">Change password and manage security settings</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
} />
|
|
|
|
{/* Fallback */}
|
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
|
</Route>
|
|
</Routes>
|
|
);
|
|
}
|