add location add asm and deealer form submition along with api integrated upto application detail screen

This commit is contained in:
laxmanhalaki 2026-01-28 21:36:59 +05:30
parent cb793354bf
commit 5bb4b481c2
15 changed files with 1983 additions and 1231 deletions

View File

@ -2,6 +2,7 @@ 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 { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
import { ApplicationFormPage } from './components/public/ApplicationFormPage';
import { LoginPage } from './components/auth/LoginPage';
import { Sidebar } from './components/layout/Sidebar';
@ -34,32 +35,34 @@ 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 { Toaster } from './components/ui/sonner';
import { User } from './lib/mock-data';
import { toast } from 'sonner';
import { API } from './api/API';
type View = 'dashboard' | 'applications' | 'all-applications' | 'opportunity-requests' | 'unopportunity-requests' | 'tasks' | 'reports' | 'settings' | 'users' | 'resignation' | 'termination' | 'fnf' | 'finance-onboarding' | 'finance-fnf' | 'master' | 'constitutional-change' | 'relocation-requests' | 'worknote' | 'dealer-resignation' | 'dealer-constitutional' | 'dealer-relocation';
// Layout Component
const AppLayout = ({ children, onLogout, title }: { children: React.ReactNode, 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">
{children}
</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 [currentView, setCurrentView] = useState<View>('dashboard');
const [selectedApplicationId, setSelectedApplicationId] = useState<string | null>(null);
const [selectedResignationId, setSelectedResignationId] = useState<string | null>(null);
const [selectedTerminationId, setSelectedTerminationId] = useState<string | null>(null);
const [selectedFnFId, setSelectedFnFId] = useState<string | null>(null);
const [selectedPaymentId, setSelectedPaymentId] = useState<string | null>(null);
const [selectedFinanceFnFId, setSelectedFinanceFnFId] = useState<string | null>(null);
const [selectedConstitutionalChangeId, setSelectedConstitutionalChangeId] = useState<string | null>(null);
const [selectedRelocationRequestId, setSelectedRelocationRequestId] = useState<string | null>(null);
const [applicationFilter, setApplicationFilter] = useState<string>('all');
const [worknoteContext, setWorknoteContext] = useState<{
requestId: string;
requestType: 'relocation' | 'constitutional-change' | 'fnf' | 'resignation' | 'termination';
requestTitle: string;
} | null>(null);
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
dispatch(initializeAuth());
@ -68,27 +71,19 @@ export default function App() {
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;
// Store token for persistence
localStorage.setItem('token', token);
// Use backend user data
const simplifiedUser: User = {
id: user.id,
name: user.fullName || email.split('@')[0],
email: user.email,
password: password,
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
}));
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);
@ -101,181 +96,42 @@ export default function App() {
const handleLogout = () => {
dispatch(logoutAction());
setCurrentView('dashboard');
setSelectedApplicationId(null);
setShowAdminLogin(false);
toast.info('Logged out successfully');
navigate('/');
};
const handleShowAdminLogin = () => {
setShowAdminLogin(true);
// 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';
};
const handleNavigate = (view: string, filter?: string) => {
setCurrentView(view as View);
setSelectedApplicationId(null);
setSelectedResignationId(null);
setSelectedTerminationId(null);
setSelectedFnFId(null);
setSelectedPaymentId(null);
setSelectedFinanceFnFId(null);
if (filter) {
setApplicationFilter(filter);
}
};
const handleViewDetails = (id: string) => {
setSelectedApplicationId(id);
};
const handleViewResignationDetails = (id: string) => {
setSelectedResignationId(id);
};
const handleViewTerminationDetails = (id: string) => {
setSelectedTerminationId(id);
};
const handleViewFnFDetails = (id: string) => {
setSelectedFnFId(id);
};
const handleViewPaymentDetails = (id: string) => {
setSelectedPaymentId(id);
};
const handleViewFinanceFnFDetails = (id: string) => {
setSelectedFinanceFnFId(id);
};
const handleViewConstitutionalChangeDetails = (id: string) => {
setSelectedConstitutionalChangeId(id);
};
const handleViewRelocationRequestDetails = (id: string) => {
setSelectedRelocationRequestId(id);
};
const handleBackFromDetails = () => {
setSelectedApplicationId(null);
};
const handleBackFromPaymentDetails = () => {
setSelectedPaymentId(null);
};
const handleBackFromFinanceFnFDetails = () => {
setSelectedFinanceFnFId(null);
};
const handleBackFromResignation = () => {
setSelectedResignationId(null);
};
const handleBackFromTermination = () => {
setSelectedTerminationId(null);
};
const handleBackFromFnF = () => {
setSelectedFnFId(null);
};
const handleBackFromConstitutionalChange = () => {
setSelectedConstitutionalChangeId(null);
};
const handleBackFromRelocationRequest = () => {
setSelectedRelocationRequestId(null);
};
const handleOpenWorknote = (requestId: string, requestType: 'relocation' | 'constitutional-change' | 'fnf' | 'resignation' | 'termination', requestTitle: string) => {
setWorknoteContext({ requestId, requestType, requestTitle });
setCurrentView('worknote');
};
const handleBackFromWorknote = () => {
setWorknoteContext(null);
// Return to the previous view based on request type
if (worknoteContext) {
if (worknoteContext.requestType === 'relocation') {
setSelectedRelocationRequestId(worknoteContext.requestId);
setCurrentView('relocation-requests');
} else if (worknoteContext.requestType === 'constitutional-change') {
setSelectedConstitutionalChangeId(worknoteContext.requestId);
setCurrentView('constitutional-change');
}
// Add other request types as needed
}
};
const getPageTitle = () => {
if (selectedApplicationId) {
return 'Application Details';
}
if (selectedResignationId) {
return 'Resignation Details';
}
if (selectedTerminationId) {
return 'Termination Details';
}
if (selectedFnFId) {
return 'F&F Case Details';
}
if (selectedConstitutionalChangeId) {
return 'Constitutional Change Details';
}
if (selectedRelocationRequestId) {
return 'Relocation Request Details';
}
switch (currentView) {
case 'dashboard':
return 'Dashboard';
case 'all-applications':
return 'All Applications';
case 'opportunity-requests':
return 'Opportunity Requests';
case 'unopportunity-requests':
return 'Unopportunity Requests';
case 'applications':
return 'Dealership Requests';
case 'tasks':
return 'My Tasks';
case 'reports':
return 'Reports & Analytics';
case 'settings':
return 'Settings';
case 'users':
return 'User Management';
case 'resignation':
return 'Resignation Management';
case 'termination':
return 'Termination Management';
case 'fnf':
return 'Full & Final Settlement';
case 'finance-onboarding':
return 'Payment Verification';
case 'finance-fnf':
return 'F&F Financial Settlement';
case 'master':
return 'Master Configuration';
case 'constitutional-change':
return 'Constitutional Change';
case 'relocation-requests':
return 'Relocation Requests';
case 'worknote':
return 'Worknote Management';
case 'dealer-resignation':
return 'Dealer Resignation Management';
case 'dealer-constitutional':
return 'Dealer Constitutional Change';
case 'dealer-relocation':
return 'Dealer Relocation Requests';
default:
return 'Dashboard';
}
};
// Show loading state while initializing auth
if (loading) {
return (
<div className="flex items-center justify-center h-screen bg-slate-50">
@ -284,180 +140,94 @@ export default function App() {
);
}
// Show public application form if not authenticated and not trying to log in as admin
if (!isAuthenticated && !showAdminLogin) {
// Public Routes
if (!isAuthenticated) {
return (
<>
<ApplicationFormPage onAdminLogin={handleShowAdminLogin} />
<Toaster />
</>
);
<Routes>
<Route path="/admin-login" element={<LoginPage onLogin={handleLogin} />} />
<Route path="*" element={showAdminLogin ? <LoginPage onLogin={handleLogin} /> :
<><ApplicationFormPage onAdminLogin={() => setShowAdminLogin(true)} /><Toaster /></>}
/>
</Routes>
)
}
// Show admin login page if user clicked admin login but hasn't authenticated yet
if (!isAuthenticated && showAdminLogin) {
// Protected Routes
return (
<>
<LoginPage onLogin={handleLogin} />
<Toaster />
</>
);
}
<AppLayout onLogout={handleLogout} title={getPageTitle(location.pathname)}>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
return (
<div className="flex h-screen bg-slate-50">
<Sidebar
activeView={currentView}
onNavigate={handleNavigate}
onLogout={handleLogout}
/>
{/* 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}`)} />
} />
<div className="flex-1 flex flex-col overflow-hidden">
<Header
title={getPageTitle()}
onRefresh={() => window.location.reload()}
/>
{/* 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')} />} />
<main className={`flex-1 overflow-y-auto ${currentView === 'worknote' ? '' : 'p-6'}`}>
{currentView === 'worknote' && worknoteContext ? (
<WorknotePage
requestId={worknoteContext.requestId}
requestType={worknoteContext.requestType}
requestTitle={worknoteContext.requestTitle}
onBack={handleBackFromWorknote}
currentUser={currentUser}
/>
) : selectedPaymentId ? (
<FinancePaymentDetailsPage
applicationId={selectedPaymentId}
onBack={handleBackFromPaymentDetails}
/>
) : selectedFinanceFnFId ? (
<FinanceFnFDetailsPage
fnfId={selectedFinanceFnFId}
onBack={handleBackFromFinanceFnFDetails}
/>
) : selectedApplicationId ? (
<ApplicationDetails
applicationId={selectedApplicationId}
onBack={handleBackFromDetails}
/>
) : selectedResignationId ? (
<ResignationDetails
resignationId={selectedResignationId}
onBack={handleBackFromResignation}
currentUser={currentUser}
/>
) : selectedTerminationId ? (
<TerminationDetails
terminationId={selectedTerminationId}
onBack={handleBackFromTermination}
currentUser={currentUser}
/>
) : selectedFnFId ? (
<FnFDetails
fnfId={selectedFnFId}
onBack={handleBackFromFnF}
currentUser={currentUser}
/>
) : selectedConstitutionalChangeId ? (
<ConstitutionalChangeDetails
requestId={selectedConstitutionalChangeId}
onBack={handleBackFromConstitutionalChange}
currentUser={currentUser}
onOpenWorknote={handleOpenWorknote}
/>
) : selectedRelocationRequestId ? (
<RelocationRequestDetails
requestId={selectedRelocationRequestId}
onBack={handleBackFromRelocationRequest}
currentUser={currentUser}
onOpenWorknote={handleOpenWorknote}
/>
) : (
<>
{currentView === 'dashboard' && (
currentUser?.role === 'Finance Admin' || currentUser?.role === 'Finance' ? (
<FinanceDashboard
currentUser={currentUser}
onNavigate={handleNavigate}
onViewPaymentDetails={handleViewPaymentDetails}
onViewFnFDetails={handleViewFinanceFnFDetails}
/>
) : currentUser?.role === 'Dealer' ? (
<DealerDashboard
currentUser={currentUser}
onNavigate={handleNavigate}
/>
) : (
<Dashboard onNavigate={handleNavigate} />
)
)}
<Route path="/all-applications" element={
currentUser?.role === 'DD' ? <AllApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" /> : <Navigate to="/dashboard" />
} />
{currentView === 'all-applications' && (
currentUser?.role === 'DD' ? (
<AllApplicationsPage
onViewDetails={handleViewDetails}
initialFilter={applicationFilter}
/>
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to DD users.</p>
</div>
)
)}
{/* 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}`)} />} />
{currentView === 'opportunity-requests' && (
currentUser?.role === 'DD Lead' ? (
<OpportunityRequestsPage
onViewDetails={handleViewDetails}
/>
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to DD Lead users.</p>
</div>
)
)}
{/* Other Modules */}
<Route path="/users" element={<UserManagementPage />} />
<Route path="/master" element={<MasterPage />} />
<Route path="/questions" element={<QuestionnaireBuilder />} />
<Route path="/questionnaire-builder" element={<QuestionnaireBuilder />} />
{currentView === 'unopportunity-requests' && (
currentUser?.role === 'DD Lead' ? (
<UnopportunityRequestsPage
onViewDetails={handleViewDetails}
/>
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to DD Lead users.</p>
</div>
)
)}
{/* 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} />} />
{currentView === 'applications' && (
<ApplicationsPage
onViewDetails={handleViewDetails}
initialFilter={applicationFilter}
/>
)}
<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} />} />
{currentView === 'tasks' && (
<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>
)}
{currentView === 'reports' && (
} />
<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>
)}
{currentView === 'settings' && (
} />
<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">
@ -475,142 +245,12 @@ export default function App() {
</div>
</div>
</div>
)}
} />
{currentView === 'users' && (
<UserManagementPage />
)}
{/* Fallback */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
{currentView === 'resignation' && (
<ResignationPage
currentUser={currentUser}
onViewDetails={handleViewResignationDetails}
/>
)}
{currentView === 'termination' && (
<TerminationPage
currentUser={currentUser}
onViewDetails={handleViewTerminationDetails}
/>
)}
{currentView === 'fnf' && (
<FnFPage
currentUser={currentUser}
onViewDetails={handleViewFnFDetails}
/>
)}
{currentView === 'finance-onboarding' && (
currentUser?.role === 'Finance' ? (
<FinanceOnboardingPage onViewPaymentDetails={handleViewPaymentDetails} />
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to Finance users.</p>
</div>
)
)}
{currentView === 'finance-fnf' && (
currentUser?.role === 'Finance' ? (
<FinanceFnFPage onViewFnFDetails={handleViewFinanceFnFDetails} />
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to Finance users.</p>
</div>
)
)}
{currentView === 'master' && (
currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' || currentUser?.role === 'DD Lead' ? (
<MasterPage />
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to Super Admin, DD Admin, and DD Lead users.</p>
</div>
)
)}
{currentView === 'constitutional-change' && (
currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' || currentUser?.role === 'DD Lead' ? (
<ConstitutionalChangePage
currentUser={currentUser}
onViewDetails={handleViewConstitutionalChangeDetails}
/>
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to Super Admin, DD Admin, and DD Lead users.</p>
</div>
)
)}
{currentView === 'relocation-requests' && (
currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' || currentUser?.role === 'DD Lead' ? (
<RelocationRequestPage
currentUser={currentUser}
onViewDetails={handleViewRelocationRequestDetails}
/>
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to Super Admin, DD Admin, and DD Lead users.</p>
</div>
)
)}
{/* Dealer-specific views */}
{currentView === 'dealer-resignation' && (
currentUser?.role === 'Dealer' ? (
<DealerResignationPage
currentUser={currentUser}
onViewDetails={handleViewResignationDetails}
/>
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to Dealer users.</p>
</div>
)
)}
{currentView === 'dealer-constitutional' && (
currentUser?.role === 'Dealer' ? (
<DealerConstitutionalChangePage
currentUser={currentUser}
onViewDetails={handleViewConstitutionalChangeDetails}
/>
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to Dealer users.</p>
</div>
)
)}
{currentView === 'dealer-relocation' && (
currentUser?.role === 'Dealer' ? (
<DealerRelocationPage
currentUser={currentUser}
onViewDetails={handleViewRelocationRequestDetails}
/>
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">This page is only accessible to Dealer users.</p>
</div>
)
)}
</>
)}
</main>
</div>
<Toaster />
</div>
</Routes>
</AppLayout>
);
}

View File

@ -12,12 +12,28 @@ export const API = {
updateRole: (id: string, data: any) => client.put(`/admin/roles/${id}`, data),
getZones: () => client.get('/master/zones'),
updateZone: (id: string, data: any) => client.put(`/master/zones/${id}`, data),
createRegion: (data: any) => client.post('/master/regions', data),
updateRegion: (id: string, data: any) => client.put(`/master/regions/${id}`, data),
getRegions: () => client.get('/master/regions'),
getStates: (zoneId?: string) => client.get('/master/states', { zoneId }),
getDistricts: (stateId?: string) => client.get('/master/districts', { stateId }),
getAreas: (districtId?: string) => client.get('/master/areas', { districtId }),
updateArea: (id: string, data: any) => client.put(`/master/areas/${id}`, data),
createArea: (data: any) => client.post('/master/areas', data),
getAreaManagers: () => client.get('/master/area-managers'),
// Onboarding
submitApplication: (data: any) => client.post('/onboarding/apply', data),
getApplications: () => client.get('/onboarding/applications'),
getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`),
getLatestQuestionnaire: () => client.get('/questionnaire/latest'),
createQuestionnaireVersion: (data: any) => client.post('/questionnaire/version', data),
submitQuestionnaireResponse: (data: any) => client.post('/questionnaire/response', data),
// User management routes
getUsers: () => client.get('/admin/users'),
createUser: (data: any) => client.post('/admin/users', data),
updateUser: (id: string, data: any) => client.put(`/admin/users/${id}`, data),
updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data),
deleteUser: (id: string) => client.delete(`/admin/users/${id}`),

View File

@ -0,0 +1,175 @@
import React, { useState, useEffect } from 'react';
import { API } from '../../api/API';
import { toast } from 'sonner'; // Assuming hot-toast is used
import { Trash2, Plus, Save } from 'lucide-react'; // Assuming lucide-react icons
interface Question {
courseId?: string; // Legacy?
sectionName: string;
questionText: string;
inputType: 'text' | 'yesno' | 'file' | 'number';
options?: any;
weight: number;
order: number;
isMandatory: boolean;
}
const SECTIONS = ['General', 'Financial', 'Infrastructure', 'Experience', 'Market Knowledge'];
const QuestionnaireBuilder: React.FC = () => {
const [version, setVersion] = useState<string>(`v${new Date().toISOString().split('T')[0]}`);
const [questions, setQuestions] = useState<Question[]>([
{ sectionName: 'General', questionText: '', inputType: 'text', weight: 0, order: 1, isMandatory: true }
]);
const [loading, setLoading] = useState(false);
const addQuestion = () => {
setQuestions([...questions, {
sectionName: 'General',
questionText: '',
inputType: 'text',
weight: 0,
order: questions.length + 1,
isMandatory: true
}]);
};
const removeQuestion = (index: number) => {
const newQuestions = questions.filter((_, i) => i !== index);
// Re-order
const reOrdered = newQuestions.map((q, i) => ({ ...q, order: i + 1 }));
setQuestions(reOrdered);
};
const updateQuestion = (index: number, field: keyof Question, value: any) => {
const newQuestions = [...questions];
newQuestions[index] = { ...newQuestions[index], [field]: value };
setQuestions(newQuestions);
};
const handleSave = async () => {
if (questions.some(q => !q.questionText)) {
toast.error('All questions must have text');
return;
}
try {
setLoading(true);
await API.createQuestionnaireVersion({
version,
questions
});
toast.success('Questionnaire version created successfully');
} catch (error) {
console.error(error);
toast.error('Failed to create questionnaire');
} finally {
setLoading(false);
}
};
return (
<div className="p-6 bg-white rounded-lg shadow-md">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">Questionnaire Builder</h2>
<div className="flex gap-4 items-center">
<input
type="text"
value={version}
onChange={(e) => setVersion(e.target.value)}
className="border p-2 rounded"
placeholder="Version Name"
/>
<button
onClick={handleSave}
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded flex items-center gap-2 hover:bg-blue-700"
>
<Save size={18} /> {loading ? 'Saving...' : 'Publish Version'}
</button>
</div>
</div>
<div className="space-y-4">
{questions.map((q, index) => (
<div key={index} className="border p-4 rounded-md bg-gray-50 flex gap-4 items-start">
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Question Text</label>
<input
type="text"
value={q.questionText}
onChange={(e) => updateQuestion(index, 'questionText', e.target.value)}
className="w-full border p-2 rounded"
placeholder="Enter question..."
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Section</label>
<select
value={q.sectionName}
onChange={(e) => updateQuestion(index, 'sectionName', e.target.value)}
className="w-full border p-2 rounded"
>
{SECTIONS.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Type</label>
<select
value={q.inputType}
onChange={(e) => updateQuestion(index, 'inputType', e.target.value as any)}
className="w-full border p-2 rounded"
>
<option value="text">Text</option>
<option value="yesno">Yes/No</option>
<option value="number">Number</option>
<option value="file">File Upload</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Weightage</label>
<input
type="number"
value={q.weight}
onChange={(e) => updateQuestion(index, 'weight', parseFloat(e.target.value))}
className="w-full border p-2 rounded"
/>
</div>
<div className="flex items-center pt-6">
<input
type="checkbox"
checked={q.isMandatory}
onChange={(e) => updateQuestion(index, 'isMandatory', e.target.checked)}
className="mr-2"
/>
<label className="text-sm">Mandatory</label>
</div>
</div>
<button
onClick={() => removeQuestion(index)}
className="text-red-500 hover:text-red-700 mt-8"
title="Remove Question"
>
<Trash2 size={20} />
</button>
</div>
))}
</div>
<button
onClick={addQuestion}
className="mt-6 w-full border-2 border-dashed border-gray-300 p-4 rounded text-gray-500 hover:border-blue-500 hover:text-blue-500 flex justify-center items-center gap-2"
>
<Plus size={20} /> Add Question
</button>
</div>
);
};
export default QuestionnaireBuilder;

View File

@ -98,8 +98,23 @@ export function UserManagementPage() {
fetchData();
}
} else {
// Implementation for Create User can be added here
toast.info('Create user functionality coming soon');
const res = await adminService.createUser(formData);
if (res.success) {
toast.success('User created successfully');
setFormData({
fullName: '',
email: '',
roleCode: '',
status: 'active',
isActive: true,
mobileNumber: '',
department: '',
designation: '',
employeeId: ''
});
setShowUserModal(false);
fetchData();
}
}
} catch (error) {
toast.error('Operation failed');

View File

@ -1,6 +1,9 @@
import { useState } from 'react';
import { mockApplications, mockAuditLogs, mockDocuments, mockWorkNotes, mockLevel1Scores, mockQuestionnaireResponses } from '../../lib/mock-data';
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { mockApplications, mockAuditLogs, mockDocuments, mockWorkNotes, mockLevel1Scores, mockQuestionnaireResponses, Application, ApplicationStatus } from '../../lib/mock-data';
import { onboardingService } from '../../services/onboarding.service';
import { WorkNotesPage } from './WorkNotesPage';
import QuestionnaireForm from '../dealer/QuestionnaireForm';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
@ -59,10 +62,7 @@ import {
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
interface ApplicationDetailsProps {
applicationId: string;
onBack: () => void;
}
interface ProcessStage {
id: number | string;
@ -80,8 +80,89 @@ interface ProcessStage {
}[];
}
export function ApplicationDetails({ applicationId, onBack }: ApplicationDetailsProps) {
const application = mockApplications.find(app => app.id === applicationId);
export function ApplicationDetails() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const applicationId = id || '';
const onBack = () => navigate(-1);
// const application = mockApplications.find(app => app.id === applicationId);
const [application, setApplication] = useState<Application | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchApplication = async () => {
try {
setLoading(true);
const data = await onboardingService.getApplicationById(applicationId);
// Helper to find stage date
const getStageDate = (stageName: string) => {
const stage = data.progressTracking?.find((p: any) => p.stageName === stageName);
return stage?.stageCompletedAt ? new Date(stage.stageCompletedAt).toISOString().split('T')[0] :
stage?.stageStartedAt ? new Date(stage.stageStartedAt).toISOString().split('T')[0] : undefined;
};
// Map backend data to frontend Application interface
const mappedApp: Application = {
id: data.id,
registrationNumber: data.applicationId || 'N/A',
name: data.applicantName,
email: data.email,
phone: data.phone,
age: data.age,
education: data.education,
residentialAddress: data.address || data.city || '',
businessAddress: data.address || '',
preferredLocation: data.preferredLocation,
state: data.state,
ownsBike: data.ownRoyalEnfield === 'yes',
pastExperience: data.experienceYears ? `${data.experienceYears} years` : (data.description || ''),
status: data.overallStatus as ApplicationStatus,
questionnaireMarks: 0,
rank: 0,
totalApplicantsAtLocation: 0,
submissionDate: data.createdAt,
assignedUsers: [],
progress: data.progressPercentage || 0,
isShortlisted: data.isShortlisted || true, // Default to true for now
// Add other fields to match interface
companyName: data.companyName,
source: data.source,
existingDealer: data.existingDealer,
royalEnfieldModel: data.royalEnfieldModel,
description: data.description,
pincode: data.pincode,
locationType: data.locationType,
ownRoyalEnfield: data.ownRoyalEnfield,
address: data.address,
// Map timeline dates from progressTracking
level1InterviewDate: getStageDate('1st Level Interview'),
level2InterviewDate: getStageDate('2nd Level Interview'),
level3InterviewDate: getStageDate('3rd Level Interview'),
fddDate: getStageDate('FDD'),
loiApprovalDate: getStageDate('LOI Approval'),
securityDetailsDate: getStageDate('Security Details'),
loiIssueDate: getStageDate('LOI Issue'),
dealerCodeDate: getStageDate('Dealer Code Generation'),
architectureAssignedDate: getStageDate('Architecture Team Assigned'),
architectureDocumentDate: getStageDate('Architecture Document Upload'),
architectureCompletionDate: getStageDate('Architecture Team Completion'),
loaDate: getStageDate('LOA'),
eorCompleteDate: getStageDate('EOR Complete'),
inaugurationDate: getStageDate('Inauguration'),
};
setApplication(mappedApp);
} catch (error) {
console.error('Failed to fetch application details', error);
} finally {
setLoading(false);
}
};
if (applicationId) {
fetchApplication();
}
}, [applicationId]);
const [activeTab, setActiveTab] = useState('questionnaire');
const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
@ -102,6 +183,10 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
'statutory-documents': true
});
if (loading) {
return <div>Loading application details...</div>;
}
if (!application) {
return <div>Application not found</div>;
}
@ -554,7 +639,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<User className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Age</p>
<p className="text-slate-900">{application.age} years</p>
<p className="text-slate-900">{application.age ? `${application.age} years` : 'N/A'}</p>
</div>
</div>
@ -562,7 +647,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<GraduationCap className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Education</p>
<p className="text-slate-900">{application.education}</p>
<p className="text-slate-900">{application.education || 'N/A'}</p>
</div>
</div>
@ -570,7 +655,15 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<MapPin className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Preferred Location</p>
<p className="text-slate-900">{application.preferredLocation}</p>
<p className="text-slate-900">{application.preferredLocation || 'N/A'}</p>
</div>
</div>
<div className="flex items-start gap-3">
<MapPin className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Location Type</p>
<p className="text-slate-900">{application.locationType || 'N/A'}</p>
</div>
</div>
@ -578,7 +671,43 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<Bike className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Owns Bike</p>
<p className="text-slate-900">{application.ownsBike ? 'Yes' : 'No'}</p>
<p className="text-slate-900">{application.ownRoyalEnfield === 'yes' ? 'Yes' : 'No'}</p>
</div>
</div>
{application.ownRoyalEnfield === 'yes' && (
<div className="flex items-start gap-3">
<Bike className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Bike Model</p>
<p className="text-slate-900">{application.royalEnfieldModel || 'N/A'}</p>
</div>
</div>
)}
<div className="flex items-start gap-3">
<User className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Existing Dealer</p>
<p className="text-slate-900">{application.existingDealer === 'yes' ? 'Yes' : 'No'}</p>
</div>
</div>
{application.existingDealer === 'yes' && (
<div className="flex items-start gap-3">
<User className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Company Name</p>
<p className="text-slate-900">{application.companyName || 'N/A'}</p>
</div>
</div>
)}
<div className="flex items-start gap-3">
<ClipboardList className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Source</p>
<p className="text-slate-900">{application.source || 'N/A'}</p>
</div>
</div>
@ -596,18 +725,23 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<Separator />
<div>
<p className="text-slate-600 mb-2">Residential Address</p>
<p className="text-slate-900">{application.residentialAddress}</p>
<p className="text-slate-600 mb-2">Address</p>
<p className="text-slate-900">{application.address || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600 mb-2">Business Address</p>
<p className="text-slate-900">{application.businessAddress}</p>
<p className="text-slate-600 mb-2">Pincode</p>
<p className="text-slate-900">{application.pincode || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600 mb-2">Description</p>
<p className="text-slate-900">{application.description || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600 mb-2">Past Experience</p>
<p className="text-slate-900">{application.pastExperience}</p>
<p className="text-slate-900">{application.pastExperience || 'N/A'}</p>
</div>
</CardContent>
</Card>
@ -645,42 +779,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
)}
</div>
{application.questionnaireMarks ? (
<div className="space-y-6">
{mockQuestionnaireResponses.map((response, index) => (
<div key={response.id} className="border border-slate-200 rounded-lg p-5 hover:border-amber-300 transition-colors">
<div className="flex items-start gap-3 mb-3">
<div className="w-8 h-8 rounded-full bg-amber-100 flex items-center justify-center flex-shrink-0">
<span className="text-amber-600">{index + 1}</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline">{response.category}</Badge>
<Badge className="bg-green-600">
{response.marksScored}/{response.totalMarks}
</Badge>
</div>
<h4 className="text-slate-900">{response.question}</h4>
</div>
</div>
<div className="ml-11">
<p className="text-slate-600 leading-relaxed">{response.answer}</p>
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<ClipboardList className="w-16 h-16 text-slate-300 mb-4" />
<h3 className="text-slate-900 mb-2">Questionnaire Not Completed</h3>
<p className="text-slate-600 mb-4">The applicant has not yet completed the questionnaire.</p>
{application.deadline && (
<p className="text-slate-500">
Deadline: {new Date(application.deadline).toLocaleDateString()}
</p>
)}
</div>
)}
<QuestionnaireForm applicationId={application.id} readOnly={true} />
</TabsContent>
{/* Progress Tab */}
@ -693,13 +792,12 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<Progress value={application.progress} className="h-3 mb-6" />
</div>
<div className="relative">
<div className="relative">
{processStages.map((stage, index) => (
<div key={stage.id}>
<div className="flex gap-4 pb-8">
<div className="relative">
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 ${
stage.status === 'completed'
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 ${stage.status === 'completed'
? 'bg-green-500 border-green-500'
: stage.status === 'active'
? 'bg-amber-500 border-amber-500'
@ -722,8 +820,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
)}
</div>
{index < processStages.length - 1 && !stage.isParallel && (
<div className={`absolute top-10 left-1/2 -translate-x-1/2 w-0.5 h-full ${
stage.status === 'completed' ? 'bg-green-500' : 'bg-slate-300'
<div className={`absolute top-10 left-1/2 -translate-x-1/2 w-0.5 h-full ${stage.status === 'completed' ? 'bg-green-500' : 'bg-slate-300'
}`}></div>
)}
</div>
@ -772,8 +869,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
...prev,
[branchKey]: !prev[branchKey]
}))}
className={`w-full flex items-center gap-3 p-4 rounded-lg border-2 transition-all hover:shadow-md ${
branchColor === 'blue'
className={`w-full flex items-center gap-3 p-4 rounded-lg border-2 transition-all hover:shadow-md ${branchColor === 'blue'
? 'border-blue-300 bg-blue-50 hover:bg-blue-100'
: 'border-green-300 bg-green-50 hover:bg-green-100'
}`}
@ -783,8 +879,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
) : (
<ChevronRight className={`w-5 h-5 ${branchColor === 'blue' ? 'text-blue-600' : 'text-green-600'}`} />
)}
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
branchColor === 'blue' ? 'bg-blue-200' : 'bg-green-200'
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${branchColor === 'blue' ? 'bg-blue-200' : 'bg-green-200'
}`}>
<GitBranch className={`w-4 h-4 ${branchColor === 'blue' ? 'text-blue-700' : 'text-green-700'}`} />
</div>
@ -805,8 +900,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<div key={branchStage.id} className="relative">
<div className="flex gap-4">
<div className="relative">
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
branchStage.status === 'completed'
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${branchStage.status === 'completed'
? `${branchColor === 'blue' ? 'bg-blue-500 border-blue-500' : 'bg-green-500 border-green-500'}`
: branchStage.status === 'active'
? 'bg-amber-500 border-amber-500'

View File

@ -1,5 +1,6 @@
import { useState } from 'react';
import { mockApplications, locations, ApplicationStatus } from '../../lib/mock-data';
import { useState, useEffect } from 'react';
import { mockApplications, locations, ApplicationStatus, Application } from '../../lib/mock-data';
import { onboardingService } from '../../services/onboarding.service';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import {
@ -44,11 +45,67 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
const [sortBy, setSortBy] = useState<'date'>('date');
const [showNewApplicationModal, setShowNewApplicationModal] = useState(false);
// Real Data Integration
const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchApplications = async () => {
try {
setLoading(true);
const response = await onboardingService.getApplications();
// Check if response is array or wrapped in data property
const applicationsData = response.data || (Array.isArray(response) ? response : []);
// Map backend data to frontend Application interface
const mappedApps = applicationsData.map((app: any) => ({
id: app.id,
registrationNumber: app.applicationId || 'N/A',
name: app.applicantName,
email: app.email,
phone: app.phone,
age: app.age,
education: app.education,
residentialAddress: app.address || app.city || '',
businessAddress: app.address || '',
preferredLocation: app.preferredLocation,
state: app.state,
ownsBike: app.ownRoyalEnfield === 'yes',
pastExperience: app.experienceYears ? `${app.experienceYears} years` : (app.description || ''),
status: app.overallStatus as ApplicationStatus,
questionnaireMarks: 0,
rank: 0,
totalApplicantsAtLocation: 0,
submissionDate: app.createdAt,
assignedUsers: [],
progress: app.progressPercentage || 0,
isShortlisted: true, // Show all for admin view
// Add other fields to match interface
companyName: app.companyName,
source: app.source,
existingDealer: app.existingDealer,
royalEnfieldModel: app.royalEnfieldModel,
description: app.description,
pincode: app.pincode,
locationType: app.locationType,
ownRoyalEnfield: app.ownRoyalEnfield,
address: app.address
}));
setApplications(mappedApps);
} catch (error) {
console.error('Failed to fetch applications', error);
} finally {
setLoading(false);
}
};
fetchApplications();
}, []);
// Filter and sort applications - ONLY show shortlisted applications
// Exclude specific applications (APP-005, APP-006, APP-007, APP-008) from Dealership Requests page
const excludedApplicationIds = ['5', '6', '7', '8'];
const filteredApplications = mockApplications
const filteredApplications = applications
.filter((app) => {
const matchesSearch =
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,173 @@
import React, { useState, useEffect } from 'react';
import { API } from '../../api/API';
import { toast } from 'sonner';
interface Question {
id: string;
sectionName: string;
questionText: string;
inputType: 'text' | 'yesno' | 'file' | 'number';
options?: any;
weight: number;
order: number;
isMandatory: boolean;
}
interface QuestionnaireFormProps {
applicationId: string;
onComplete?: () => void;
readOnly?: boolean;
existingResponses?: any[];
}
const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({ applicationId, onComplete, readOnly = false, existingResponses }) => {
const [questions, setQuestions] = useState<Question[]>([]);
const [responses, setResponses] = useState<Record<string, any>>({});
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
fetchQuestionnaire();
if (existingResponses) {
const initialResponses: any = {};
existingResponses.forEach(r => {
initialResponses[r.questionId] = r.responseValue;
});
setResponses(initialResponses);
}
}, [existingResponses]);
const fetchQuestionnaire = async () => {
try {
const res = await API.getLatestQuestionnaire();
if (res.data && res.data.data && res.data.data.questions) {
setQuestions(res.data.data.questions);
}
} catch (error) {
console.error(error);
toast.error('Failed to load questionnaire');
} finally {
setLoading(false);
}
};
const handleInputChange = (questionId: string, value: any) => {
if (readOnly) return;
setResponses(prev => ({ ...prev, [questionId]: value }));
};
const handleSubmit = async () => {
const missing = questions.filter(q => q.isMandatory && !responses[q.id]);
if (missing.length > 0) {
toast.error(`Please answer all mandatory questions. Missing: ${missing.length}`);
return;
}
try {
setSubmitting(true);
const payload = Object.entries(responses).map(([qId, val]) => ({
questionId: qId,
value: val
}));
await API.submitQuestionnaireResponse({
applicationId,
responses: payload
});
toast.success('Responses submitted successfully');
if (onComplete) onComplete();
} catch (error) {
console.error(error);
toast.error('Failed to submit responses');
} finally {
setSubmitting(false);
}
};
if (loading) return <div>Loading questionnaire...</div>;
if (questions.length === 0) return <div>No active questionnaire found.</div>;
const sections = questions.reduce((acc, q) => {
if (!acc[q.sectionName]) acc[q.sectionName] = [];
acc[q.sectionName].push(q);
return acc;
}, {} as Record<string, Question[]>);
return (
<div className="space-y-6">
<h3 className="text-xl font-semibold">Dealership Assessment Questionnaire</h3>
{Object.entries(sections).map(([sectionName, sectionQuestions]) => (
<div key={sectionName} className="border p-4 rounded bg-white shadow-sm">
<h4 className="font-medium text-lg mb-4 border-b pb-2">{sectionName}</h4>
<div className="space-y-4">
{sectionQuestions.map(q => (
<div key={q.id}>
<label className="block text-sm font-medium mb-1">
{q.questionText} {q.isMandatory && !readOnly && <span className="text-red-500">*</span>}
</label>
{q.inputType === 'text' && (
<input
type="text"
className="w-full border p-2 rounded disabled:bg-gray-100"
onChange={(e) => handleInputChange(q.id, e.target.value)}
value={responses[q.id] || ''}
disabled={readOnly}
/>
)}
{q.inputType === 'number' && (
<input
type="number"
className="w-full border p-2 rounded disabled:bg-gray-100"
onChange={(e) => handleInputChange(q.id, e.target.value)}
value={responses[q.id] || ''}
disabled={readOnly}
/>
)}
{q.inputType === 'yesno' && (
<div className="flex gap-4">
<label className="flex items-center gap-2">
<input
type="radio"
name={`q-${q.id}`}
value="yes"
checked={responses[q.id] === 'yes'}
onChange={() => handleInputChange(q.id, 'yes')}
disabled={readOnly}
/> Yes
</label>
<label className="flex items-center gap-2">
<input
type="radio"
name={`q-${q.id}`}
value="no"
checked={responses[q.id] === 'no'}
onChange={() => handleInputChange(q.id, 'no')}
disabled={readOnly}
/> No
</label>
</div>
)}
</div>
))}
</div>
</div>
))}
{!readOnly && (
<button
onClick={handleSubmit}
disabled={submitting}
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 w-full md:w-auto"
>
{submitting ? 'Submitting...' : 'Submit Assessment'}
</button>
)}
</div>
);
};
export default QuestionnaireForm;

View File

@ -16,18 +16,20 @@ import {
MapPin
} from 'lucide-react';
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { RootState } from '../../store';
import { Input } from '../ui/input';
import { Button } from '../ui/button';
interface SidebarProps {
activeView: string;
onNavigate: (view: string) => void;
onLogout: () => void;
}
export function Sidebar({ activeView, onNavigate, onLogout }: SidebarProps) {
export function Sidebar({ onLogout }: SidebarProps) {
const navigate = useNavigate();
const location = useLocation();
const activeView = location.pathname.substring(1) || 'dashboard'; // Simple mapping for now
const { user: currentUser } = useSelector((state: RootState) => state.auth);
const [collapsed, setCollapsed] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
@ -96,7 +98,7 @@ export function Sidebar({ activeView, onNavigate, onLogout }: SidebarProps) {
e.preventDefault();
if (searchQuery.trim()) {
// Navigate to applications with search query
onNavigate('applications');
navigate('/applications');
// In real app, would pass search query
}
};
@ -173,7 +175,7 @@ export function Sidebar({ activeView, onNavigate, onLogout }: SidebarProps) {
setAllRequestsExpanded(!allRequestsExpanded);
}
} else {
onNavigate(item.id);
navigate(`/${item.id}`);
}
}}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive || isSubmenuActive
@ -205,7 +207,7 @@ export function Sidebar({ activeView, onNavigate, onLogout }: SidebarProps) {
return (
<button
key={subItem.id}
onClick={() => onNavigate(subItem.id)}
onClick={() => navigate(`/${subItem.id}`)}
className={`w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-colors text-sm ${isSubActive
? 'bg-amber-600 text-white'
: 'text-slate-400 hover:bg-slate-800 hover:text-white'

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
@ -9,6 +9,8 @@ import { Checkbox } from '../ui/checkbox';
import { CheckCircle, Users, Star, Shield, LogIn, Award, TrendingUp, Handshake } from 'lucide-react';
import { toast } from 'sonner';
// import backgroundImage from 'figma:asset/ee01d864b6e23a8197b42f3168c98eedec9d2440.png';
import { onboardingService } from '../../services/onboarding.service';
import { masterService } from '../../services/master.service';
interface ApplicationFormPageProps {
onAdminLogin: () => void;
@ -16,7 +18,7 @@ interface ApplicationFormPageProps {
export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps) {
const [formData, setFormData] = useState({
country: '',
country: 'India', // Default to India
state: '',
district: '',
name: '',
@ -36,7 +38,54 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
acceptTerms: false
});
const handleSubmit = (e: React.FormEvent) => {
const [states, setStates] = useState<any[]>([]);
const [districts, setDistricts] = useState<any[]>([]);
const [fetchingStates, setFetchingStates] = useState(false);
const [fetchingDistricts, setFetchingDistricts] = useState(false);
useEffect(() => {
fetchStates();
}, []);
const fetchStates = async () => {
setFetchingStates(true);
try {
// ZoneID is optional, public API should return all states if no zone override
const response: any = await masterService.getStates();
if (response && response.states) {
setStates(response.states);
}
} catch (error) {
console.error('Error fetching states:', error);
toast.error('Failed to load states. Please refresh the page.');
} finally {
setFetchingStates(false);
}
};
const handleStateChange = async (selectedState: any) => {
if (!selectedState) return;
setFormData(prev => ({ ...prev, state: selectedState.stateName, district: '' }));
setDistricts([]);
setFetchingDistricts(true);
try {
const response: any = await masterService.getDistricts(selectedState.id);
if (response && response.districts) {
setDistricts(response.districts);
}
} catch (error) {
console.error('Error fetching districts:', error);
toast.error('Failed to load districts.');
} finally {
setFetchingDistricts(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate required fields
@ -49,42 +98,58 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
return;
}
// Validate Royal Enfield model if they own one
// Validate Royal Enfield model
if (formData.ownRoyalEnfield === 'yes' && !formData.royalEnfieldModel) {
toast.error('Please select your Royal Enfield model');
return;
}
// Validate terms acceptance
if (!formData.acceptTerms) {
toast.error('Please accept the terms and conditions to continue');
toast.error('Please accept the terms and conditions');
return;
}
// Success message
try {
// Map form data to backend expected format
const payload = {
applicantName: formData.name,
email: formData.email,
phone: formData.mobile,
state: formData.state,
city: formData.interestedCity, // Or district?
district: formData.district, // Crucial for auto-assignment
preferredLocation: `${formData.interestedCity}, ${formData.state}`,
businessType: 'Dealership', // Default or derived
locationType: 'Urban', // Default or need field
address: formData.address, // Need backend support?
pincode: formData.pincode, // Need backend support?
age: formData.age,
education: formData.education,
companyName: formData.companyName,
source: formData.source,
existingDealer: formData.existingDealer,
ownRoyalEnfield: formData.ownRoyalEnfield,
royalEnfieldModel: formData.royalEnfieldModel,
description: formData.description,
// flexible fields for now
experienceYears: 0, // Not in form
investmentCapacity: 'Unknown' // Not in form
};
await onboardingService.submitApplication(payload);
toast.success('Application submitted successfully! We will contact you soon.');
// Reset form
setFormData({
country: '',
state: '',
district: '',
name: '',
interestedCity: '',
email: '',
pincode: '',
mobile: '',
ownRoyalEnfield: '',
royalEnfieldModel: '',
age: '',
education: '',
companyName: '',
source: '',
existingDealer: '',
description: '',
address: '',
acceptTerms: false
country: '', state: '', district: '', name: '', interestedCity: '',
email: '', pincode: '', mobile: '', ownRoyalEnfield: '', royalEnfieldModel: '',
age: '', education: '', companyName: '', source: '', existingDealer: '',
description: '', address: '', acceptTerms: false
});
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to submit application.');
}
};
return (
@ -215,11 +280,6 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
<section className="relative py-24 overflow-hidden">
{/* Background Image with Overlay */}
<div className="absolute inset-0">
{/* <img
src={backgroundImage}
alt="Royal Enfield Showroom"
className="w-full h-full object-cover"
/> */}
<div className="absolute inset-0 bg-gradient-to-br from-slate-950/95 via-slate-900/90 to-slate-950/95 backdrop-blur-sm"></div>
</div>
@ -312,15 +372,15 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
<span className="text-amber-400">🌍</span>
Country <span className="text-amber-500">*</span>
</Label>
<Select value={formData.country} onValueChange={(value) => setFormData({ ...formData, country: value })}>
<SelectTrigger className="bg-slate-800/50 border-slate-600/50 text-white focus:border-amber-500/50 focus:ring-amber-500/20">
<Select value={formData.country} onValueChange={(value) => setFormData({ ...formData, country: value })} disabled>
<SelectTrigger className="bg-slate-800/50 border-slate-600/50 text-white focus:border-amber-500/50 focus:ring-amber-500/20 disabled:opacity-70 disabled:cursor-not-allowed">
<SelectValue placeholder="Select country" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-white">
<SelectItem value="india">India</SelectItem>
<SelectItem value="nepal">Nepal</SelectItem>
<SelectItem value="bangladesh">Bangladesh</SelectItem>
<SelectItem value="sri-lanka">Sri Lanka</SelectItem>
<SelectItem value="India">India</SelectItem>
<SelectItem value="Nepal">Nepal</SelectItem>
<SelectItem value="Bangladesh">Bangladesh</SelectItem>
<SelectItem value="Sri Lanka">Sri Lanka</SelectItem>
</SelectContent>
</Select>
</div>
@ -329,26 +389,23 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
<span className="text-amber-400">🏛</span>
State <span className="text-amber-500">*</span>
</Label>
<Select value={formData.state} onValueChange={(value) => setFormData({ ...formData, state: value })}>
<Select
value={formData.state}
onValueChange={(value) => {
const selectedState = states.find((s: any) => s.stateName === value);
handleStateChange(selectedState);
}}
disabled={fetchingStates}
>
<SelectTrigger className="bg-slate-800/50 border-slate-600/50 text-white focus:border-amber-500/50 focus:ring-amber-500/20">
<SelectValue placeholder="Select state" />
<SelectValue placeholder={fetchingStates ? "Loading states..." : "Select state"} />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-white">
<SelectItem value="maharashtra">Maharashtra</SelectItem>
<SelectItem value="karnataka">Karnataka</SelectItem>
<SelectItem value="tamil-nadu">Tamil Nadu</SelectItem>
<SelectItem value="delhi">Delhi</SelectItem>
<SelectItem value="rajasthan">Rajasthan</SelectItem>
<SelectItem value="uttar-pradesh">Uttar Pradesh</SelectItem>
<SelectItem value="gujarat">Gujarat</SelectItem>
<SelectItem value="west-bengal">West Bengal</SelectItem>
<SelectItem value="andhra-pradesh">Andhra Pradesh</SelectItem>
<SelectItem value="telangana">Telangana</SelectItem>
<SelectItem value="kerala">Kerala</SelectItem>
<SelectItem value="punjab">Punjab</SelectItem>
<SelectItem value="haryana">Haryana</SelectItem>
<SelectItem value="madhya-pradesh">Madhya Pradesh</SelectItem>
<SelectItem value="odisha">Odisha</SelectItem>
<SelectContent className="bg-slate-800 border-slate-700 text-white h-64">
{states.map((state: any) => (
<SelectItem key={state.id} value={state.stateName}>
{state.stateName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
@ -357,14 +414,22 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
<span className="text-amber-400">📍</span>
District <span className="text-amber-500">*</span>
</Label>
<Input
id="district"
placeholder="Enter district"
<Select
value={formData.district}
onChange={(e) => setFormData({ ...formData, district: e.target.value })}
className="bg-slate-800/50 border-slate-600/50 text-white placeholder:text-slate-500 focus:border-amber-500/50 focus:ring-amber-500/20"
required
/>
onValueChange={(value) => setFormData({ ...formData, district: value })}
disabled={!formData.state || fetchingDistricts}
>
<SelectTrigger className="bg-slate-800/50 border-slate-600/50 text-white focus:border-amber-500/50 focus:ring-amber-500/20">
<SelectValue placeholder={fetchingDistricts ? "Loading districts..." : "Select district"} />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-white h-64">
{districts.map((district: any) => (
<SelectItem key={district.id} value={district.districtName}>
{district.districtName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>

View File

@ -61,12 +61,21 @@ export interface Application {
email: string;
phone: string;
age: number;
education: 'Graduate' | 'Undergraduate' | 'Postgraduate';
education: string;
residentialAddress: string;
businessAddress: string;
preferredLocation: string;
state: string;
ownsBike: boolean;
ownsBike?: boolean;
ownRoyalEnfield?: string;
royalEnfieldModel?: string;
existingDealer?: string;
companyName?: string;
source?: string;
description?: string;
address?: string;
pincode?: string;
locationType?: string;
pastExperience: string;
status: ApplicationStatus;
questionnaireMarks?: number;

View File

@ -5,10 +5,14 @@ import { store } from './store'
import App from './App.tsx'
import './styles/globals.css'
import { BrowserRouter } from 'react-router-dom'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
)

View File

@ -13,13 +13,27 @@ export const adminService = {
}
},
async createUser(userData: any) {
try {
const response = await API.createUser(userData) as any;
if (response.ok && response.data?.success) {
// Toast handled in component
}
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 };
}
},
async updateUser(id: string, userData: any) {
try {
const response = await API.updateUser(id, userData) as any;
if (response.success) {
toast.success(response.message || 'User updated successfully');
if (response.ok && response.data?.success) {
toast.success(response.data.message || 'User updated successfully');
}
return response;
return response.data;
} catch (error: any) {
console.error('Error updating user:', error);
toast.error(error.response?.data?.message || 'Failed to update user');
@ -30,10 +44,10 @@ export const adminService = {
async updateUserStatus(id: string, status: string, isActive: boolean) {
try {
const response = await API.updateUserStatus(id, { status, isActive }) as any;
if (response.success) {
toast.success(response.message || 'User status updated');
if (response.ok && response.data?.success) {
toast.success(response.data.message || 'User status updated');
}
return response;
return response.data;
} catch (error: any) {
console.error('Error updating status:', error);
toast.error(error.response?.data?.message || 'Failed to update status');

View File

@ -20,6 +20,18 @@ export const masterService = {
const response = await API.getZones();
return response.data;
},
updateZone: async (id: string, data: any) => {
const response = await API.updateZone(id, data);
return response.data;
},
createRegion: async (data: any) => {
const response = await API.createRegion(data);
return response.data;
},
updateRegion: async (id: string, data: any) => {
const response = await API.updateRegion(id, data);
return response.data;
},
getRegions: async () => {
const response = await API.getRegions();
return response.data;
@ -32,6 +44,22 @@ export const masterService = {
const response = await API.getDistricts(stateId);
return response.data;
},
getAreas: async (districtId?: string) => {
const response = await API.getAreas(districtId);
return response.data;
},
updateArea: async (id: string, data: any) => {
const response = await API.updateArea(id, data);
return response.data;
},
createArea: async (data: any) => {
const response = await API.createArea(data);
return response.data;
},
getAreaManagers: async () => {
const response = await API.getAreaManagers();
return response.data;
},
// User Management
getUsers: async () => {

View File

@ -0,0 +1,31 @@
import { API } from '../api/API';
export const onboardingService = {
submitApplication: async (data: any) => {
try {
const response = await API.submitApplication(data);
return response.data;
} catch (error) {
console.error('Submit application error:', error);
throw error;
}
},
getApplications: async () => {
try {
const response = await API.getApplications();
return response.data?.data || response.data;
} catch (error) {
console.error('Get applications error:', error);
throw error;
}
},
getApplicationById: async (id: string) => {
try {
const response = await API.getApplicationById(id);
return response.data?.data || response.data;
} catch (error) {
console.error('Get application by id error:', error);
throw error;
}
}
};