add location add asm and deealer form submition along with api integrated upto application detail screen
This commit is contained in:
parent
cb793354bf
commit
5bb4b481c2
650
src/App.tsx
650
src/App.tsx
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}`),
|
||||
|
||||
175
src/components/admin/QuestionnaireBuilder.tsx
Normal file
175
src/components/admin/QuestionnaireBuilder.tsx
Normal 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;
|
||||
@ -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');
|
||||
|
||||
@ -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,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">
|
||||
@ -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" />
|
||||
)}
|
||||
|
||||
@ -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
173
src/components/dealer/QuestionnaireForm.tsx
Normal file
173
src/components/dealer/QuestionnaireForm.tsx
Normal 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;
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>,
|
||||
)
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
31
src/services/onboarding.service.ts
Normal file
31
src/services/onboarding.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user