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,333 +140,117 @@ 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 />
</>
);
}
// Show admin login page if user clicked admin login but hasn't authenticated yet
if (!isAuthenticated && showAdminLogin) {
return (
<>
<LoginPage onLogin={handleLogin} />
<Toaster />
</>
);
}
return (
<div className="flex h-screen bg-slate-50">
<Sidebar
activeView={currentView}
onNavigate={handleNavigate}
onLogout={handleLogout}
/>
<div className="flex-1 flex flex-col overflow-hidden">
<Header
title={getPageTitle()}
onRefresh={() => window.location.reload()}
<Routes>
<Route path="/admin-login" element={<LoginPage onLogin={handleLogin} />} />
<Route path="*" element={showAdminLogin ? <LoginPage onLogin={handleLogin} /> :
<><ApplicationFormPage onAdminLogin={() => setShowAdminLogin(true)} /><Toaster /></>}
/>
</Routes>
)
}
<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} />
)
)}
// Protected Routes
return (
<AppLayout onLogout={handleLogout} title={getPageTitle(location.pathname)}>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
{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>
)
)}
{/* 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}`)} />
} />
{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>
)
)}
{/* 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')} />} />
{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>
)
)}
<Route path="/all-applications" element={
currentUser?.role === 'DD' ? <AllApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" /> : <Navigate to="/dashboard" />
} />
{currentView === 'applications' && (
<ApplicationsPage
onViewDetails={handleViewDetails}
initialFilter={applicationFilter}
/>
)}
{/* 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 === 'tasks' && (
<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>
)}
{/* Other Modules */}
<Route path="/users" element={<UserManagementPage />} />
<Route path="/master" element={<MasterPage />} />
<Route path="/questions" element={<QuestionnaireBuilder />} />
<Route path="/questionnaire-builder" element={<QuestionnaireBuilder />} />
{currentView === 'reports' && (
<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>
)}
{/* 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 === 'settings' && (
<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>
)}
<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 === 'users' && (
<UserManagementPage />
)}
<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} />} />
{currentView === 'resignation' && (
<ResignationPage
currentUser={currentUser}
onViewDetails={handleViewResignationDetails}
/>
)}
<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')} />} />
{currentView === 'termination' && (
<TerminationPage
currentUser={currentUser}
onViewDetails={handleViewTerminationDetails}
/>
)}
<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')} />} />
{currentView === 'fnf' && (
<FnFPage
currentUser={currentUser}
onViewDetails={handleViewFnFDetails}
/>
)}
<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={() => { }} />} />
{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>
)
)}
<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={() => { }} />} />
{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>
)
)}
{/* 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}`)} />} />
{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>
)
)}
{/* 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>
} />
{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>
)
)}
{/* Fallback */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
{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,14 +1,17 @@
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';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import {
ArrowLeft,
CheckCircle,
XCircle,
import {
ArrowLeft,
CheckCircle,
XCircle,
MessageSquare,
Calendar,
Clock,
@ -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,97 +183,101 @@ 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>;
}
const processStages: ProcessStage[] = [
{
id: 1,
name: 'Submitted',
status: 'completed',
{
id: 1,
name: 'Submitted',
status: 'completed',
date: application.submissionDate,
description: 'Application submitted',
documentsUploaded: 3
},
{
id: 2,
name: 'Questionnaire',
status: application.questionnaireMarks ? 'completed' : 'pending',
{
id: 2,
name: 'Questionnaire',
status: application.questionnaireMarks ? 'completed' : 'pending',
date: '2025-10-03',
description: 'Questionnaire completed',
documentsUploaded: 0
},
{
id: 3,
name: 'Shortlist',
status: ['Shortlisted', 'Level 1 Pending', 'Level 1 Approved', 'Level 2 Pending', 'Level 2 Approved', 'Level 3 Pending', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : 'pending',
{
id: 3,
name: 'Shortlist',
status: ['Shortlisted', 'Level 1 Pending', 'Level 1 Approved', 'Level 2 Pending', 'Level 2 Approved', 'Level 3 Pending', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : 'pending',
date: '2025-10-04',
description: 'Application shortlisted by DD',
documentsUploaded: 2
},
{
id: 4,
name: '1st Level Interview',
{
id: 4,
name: '1st Level Interview',
status: ['Level 1 Approved', 'Level 2 Pending', 'Level 2 Approved', 'Level 3 Pending', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 1 Pending' ? 'active' : 'pending',
date: application.level1InterviewDate,
description: 'DD-ZM + RBM evaluation',
evaluators: ['DD-ZM', 'RBM'],
documentsUploaded: 1
},
{
id: 5,
name: '2nd Level Interview',
{
id: 5,
name: '2nd Level Interview',
status: ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Pending', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 2 Pending' ? 'active' : 'pending',
date: application.level2InterviewDate,
description: 'DD Lead + ZBH evaluation',
evaluators: ['DD Lead', 'ZBH'],
documentsUploaded: 1
},
{
id: 6,
name: '3rd Level Interview',
{
id: 6,
name: '3rd Level Interview',
status: ['FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 3 Pending' ? 'active' : 'pending',
date: application.level3InterviewDate,
description: 'NBH + DD-Head evaluation',
evaluators: ['NBH', 'DD-Head'],
documentsUploaded: 2
},
{
id: 7,
name: 'FDD',
{
id: 7,
name: 'FDD',
status: ['Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending',
date: application.fddDate,
description: 'Financial Due Diligence',
documentsUploaded: 5
},
{
id: 8,
name: 'LOI Approval',
{
id: 8,
name: 'LOI Approval',
status: ['Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : 'pending',
date: application.loiApprovalDate,
description: 'Letter of Intent approval',
documentsUploaded: 1
},
{
id: 9,
name: 'Security Details',
{
id: 9,
name: 'Security Details',
status: ['Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : 'pending',
date: application.securityDetailsDate,
description: 'Security verification',
documentsUploaded: 3
},
{
id: 10,
name: 'LOI Issue',
{
id: 10,
name: 'LOI Issue',
status: ['Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Payment Pending' ? 'active' : 'pending',
date: application.loiIssueDate,
description: 'Letter of Intent issued',
documentsUploaded: 1
},
{
id: 11,
name: 'Dealer Code Generation',
{
id: 11,
name: 'Dealer Code Generation',
status: ['Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Dealer Code Generation' ? 'active' : 'pending',
date: application.dealerCodeDate,
description: 'Dealer code generated and assigned',
@ -314,25 +399,25 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
}
]
},
{
id: 12,
name: 'LOA',
{
id: 12,
name: 'LOA',
status: ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending',
date: application.loaDate,
description: 'Letter of Authorization',
documentsUploaded: 1
},
{
id: 13,
name: 'EOR Complete',
{
id: 13,
name: 'EOR Complete',
status: ['Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending',
date: application.eorCompleteDate,
description: 'Essential Operating Requirements completed',
documentsUploaded: 6
},
{
id: 14,
name: 'Inauguration',
{
id: 14,
name: 'Inauguration',
status: application.status === 'Approved' ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending',
date: application.inaugurationDate,
description: 'Dealership inauguration ceremony',
@ -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,18 +792,17 @@ 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'
? 'bg-green-500 border-green-500'
: stage.status === 'active'
<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'
: 'bg-slate-200 border-slate-300'
}`}>
}`}>
{stage.isParallel ? (
<GitBranch className="w-5 h-5 text-white" />
) : (
@ -722,9 +820,8 @@ 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>
<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>
<div className="flex-1 pt-1">
@ -738,7 +835,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
</p>
)}
{stage.documentsUploaded !== undefined && stage.documentsUploaded > 0 && (
<p
<p
className="text-blue-600 text-sm mt-0.5 cursor-pointer hover:underline hover:text-blue-700"
onClick={() => {
setSelectedStage(stage.name);
@ -763,7 +860,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
const branchKey = branch.name.toLowerCase().replace(/\s+/g, '-');
const isExpanded = expandedBranches[branchKey];
const branchColor = branch.color === 'blue' ? 'blue' : 'green';
return (
<div key={branchIndex} className="mb-6 last:mb-0">
{/* Branch Header - Clickable */}
@ -772,20 +869,18 @@ 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'
? 'border-blue-300 bg-blue-50 hover:bg-blue-100'
: 'border-green-300 bg-green-50 hover:bg-green-100'
}`}
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'
}`}
>
{isExpanded ? (
<ChevronDown className={`w-5 h-5 ${branchColor === 'blue' ? 'text-blue-600' : 'text-green-600'}`} />
) : (
<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>
<div className="flex-1 text-left">
@ -805,13 +900,12 @@ 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'
? `${branchColor === 'blue' ? 'bg-blue-500 border-blue-500' : 'bg-green-500 border-green-500'}`
: branchStage.status === 'active'
<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'
: 'bg-slate-200 border-slate-300'
}`}>
}`}>
{branchStage.status === 'completed' && (
<CheckCircle className="w-4 h-4 text-white" />
)}
@ -829,7 +923,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<p className="text-slate-600 text-sm mt-0.5">{branchStage.description}</p>
)}
{branchStage.documentsUploaded !== undefined && branchStage.documentsUploaded > 0 && (
<p
<p
className="text-blue-600 text-sm mt-0.5 cursor-pointer hover:underline hover:text-blue-700"
onClick={() => {
setSelectedStage(branchStage.name);
@ -971,7 +1065,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
{/* Payments Tab */}
<TabsContent value="payments" className="space-y-4">
<h3 className="text-slate-900 mb-4">Payment Information</h3>
<div className="space-y-4">
<div className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-center justify-between mb-2">
@ -1044,7 +1138,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<div>
<p className="text-slate-600">Rank</p>
<p className="text-slate-900">
{application.rank} of {application.totalApplicantsAtLocation}
{application.rank} of {application.totalApplicantsAtLocation}
<span className="text-slate-500"> in {application.preferredLocation}</span>
</p>
</div>
@ -1074,7 +1168,7 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={() => setShowApproveModal(true)}
>
@ -1082,8 +1176,8 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
Approve
</Button>
<Button
variant="destructive"
<Button
variant="destructive"
className="w-full"
onClick={() => setShowRejectModal(true)}
>
@ -1093,8 +1187,8 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<Separator />
<Button
variant="outline"
<Button
variant="outline"
className="w-full"
onClick={() => setShowWorkNotesPage(true)}
>
@ -1102,8 +1196,8 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
Work Note
</Button>
<Button
variant="outline"
<Button
variant="outline"
className="w-full"
onClick={() => setShowScheduleModal(true)}
>
@ -1212,14 +1306,14 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<Input type="file" className="mt-2" />
</div>
<div className="flex gap-3">
<Button
variant="outline"
<Button
variant="outline"
className="flex-1"
onClick={() => setShowApproveModal(false)}
>
Cancel
</Button>
<Button
<Button
className="flex-1 bg-green-600 hover:bg-green-700"
onClick={handleApprove}
>
@ -1251,14 +1345,14 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
/>
</div>
<div className="flex gap-3">
<Button
variant="outline"
<Button
variant="outline"
className="flex-1"
onClick={() => setShowRejectModal(false)}
>
Cancel
</Button>
<Button
<Button
variant="destructive"
className="flex-1"
onClick={handleReject}
@ -1295,14 +1389,14 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<Input type="file" className="mt-2" multiple />
</div>
<div className="flex gap-3">
<Button
variant="outline"
<Button
variant="outline"
className="flex-1"
onClick={() => setShowWorkNoteModal(false)}
>
Cancel
</Button>
<Button
<Button
className="flex-1 bg-amber-600 hover:bg-amber-700"
onClick={handleWorkNote}
>
@ -1369,14 +1463,14 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
</div>
)}
<div className="flex gap-3">
<Button
variant="outline"
<Button
variant="outline"
className="flex-1"
onClick={() => setShowScheduleModal(false)}
>
Cancel
</Button>
<Button
<Button
className="flex-1 bg-amber-600 hover:bg-amber-700"
onClick={() => {
alert('Interview scheduled! Calendar invites will be sent.');
@ -1629,14 +1723,14 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
</div>
<div className="flex gap-3">
<Button
variant="outline"
<Button
variant="outline"
className="flex-1"
onClick={() => setShowKTMatrixModal(false)}
>
Cancel
</Button>
<Button
<Button
className="flex-1 bg-amber-600 hover:bg-amber-700"
onClick={() => {
alert('KT Matrix submitted successfully!');
@ -1757,14 +1851,14 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
</div>
<div className="flex gap-3">
<Button
variant="outline"
<Button
variant="outline"
className="flex-1"
onClick={() => setShowLevel2FeedbackModal(false)}
>
Cancel
</Button>
<Button
<Button
className="flex-1 bg-blue-600 hover:bg-blue-700"
onClick={() => {
alert('Level 2 feedback submitted successfully!');
@ -1894,14 +1988,14 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
</div>
<div className="flex gap-3">
<Button
variant="outline"
<Button
variant="outline"
className="flex-1"
onClick={() => setShowLevel3FeedbackModal(false)}
>
Cancel
</Button>
<Button
<Button
className="flex-1 bg-purple-600 hover:bg-purple-700"
onClick={() => {
alert('Level 3 feedback submitted successfully!');
@ -1951,8 +2045,8 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<TableCell>{new Date(doc.uploadDate).toLocaleDateString()}</TableCell>
<TableCell>{doc.uploader}</TableCell>
<TableCell className="text-right">
<Button
variant="outline"
<Button
variant="outline"
size="sm"
onClick={() => alert(`Downloading ${doc.name}`)}
>
@ -1975,14 +2069,14 @@ export function ApplicationDetails({ applicationId, onBack }: ApplicationDetails
<Separator />
<div className="flex gap-3">
<Button
variant="outline"
<Button
variant="outline"
className="flex-1"
>
<Upload className="w-4 h-4 mr-2" />
Upload Document
</Button>
<Button
<Button
variant="outline"
onClick={() => setShowDocumentsModal(false)}
>

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 {
@ -9,10 +10,10 @@ import {
SelectTrigger,
SelectValue,
} from '../ui/select';
import {
Search,
Filter,
Download,
import {
Search,
Filter,
Download,
Mail,
Plus
} from 'lucide-react';
@ -44,22 +45,78 @@ 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 =
const matchesSearch =
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.registrationNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.email.toLowerCase().includes(searchQuery.toLowerCase());
const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter;
const matchesStatus = statusFilter === 'all' || app.status === statusFilter;
const isShortlisted = app.isShortlisted === true; // Only show shortlisted applications
const notExcluded = !excludedApplicationIds.includes(app.id); // Exclude APP-005, 006, 007, 008
return matchesSearch && matchesLocation && matchesStatus && isShortlisted && notExcluded;
})
.sort((a, b) => {
@ -70,7 +127,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
});
const toggleSelection = (id: string) => {
setSelectedIds(prev =>
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
@ -134,7 +191,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
<div className="space-y-6">
{/* Info Banner - Only visible for DD users */}
{/* Note: This page shows only applications that have been shortlisted */}
{/* Filters and Actions Bar */}
<div className="bg-white rounded-lg border border-slate-200 p-4">
<div className="flex flex-col lg:flex-row gap-4">

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
toast.success('Application submitted successfully! We will contact you soon.');
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
};
// Reset form
setFormData({
country: '',
state: '',
district: '',
name: '',
interestedCity: '',
email: '',
pincode: '',
mobile: '',
ownRoyalEnfield: '',
royalEnfieldModel: '',
age: '',
education: '',
companyName: '',
source: '',
existingDealer: '',
description: '',
address: '',
acceptTerms: false
});
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
});
} 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}>
<App />
<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;
}
}
};