enhanced the questionnaraire ui and added upto interview lvel 3 implemented

This commit is contained in:
laxmanhalaki 2026-02-18 20:03:11 +05:30
parent 0ff5412d04
commit e786ea12cf
11 changed files with 2017 additions and 422 deletions

View File

@ -2,15 +2,18 @@ import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { RootState } from './store'; import { RootState } from './store';
import { setCredentials, logout as logoutAction, initializeAuth } from './store/slices/authSlice'; import { setCredentials, logout as logoutAction, initializeAuth } from './store/slices/authSlice';
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { RoleGuard } from './components/auth/RoleGuard';
import { Routes, Route, Navigate, useLocation, useNavigate, Outlet } from 'react-router-dom';
import { ApplicationFormPage } from './components/public/ApplicationFormPage'; import { ApplicationFormPage } from './components/public/ApplicationFormPage';
import PublicQuestionnairePage from './pages/public/PublicQuestionnairePage'; import PublicQuestionnairePage from './pages/public/PublicQuestionnairePage';
import { LoginPage } from './components/auth/LoginPage'; import { LoginPage } from './components/auth/LoginPage';
import { ProspectiveLoginPage } from './components/auth/ProspectiveLoginPage';
import { Sidebar } from './components/layout/Sidebar'; import { Sidebar } from './components/layout/Sidebar';
import { Header } from './components/layout/Header'; import { Header } from './components/layout/Header';
import { Dashboard } from './components/dashboard/Dashboard'; import { Dashboard } from './components/dashboard/Dashboard';
import { FinanceDashboard } from './components/dashboard/FinanceDashboard'; import { FinanceDashboard } from './components/dashboard/FinanceDashboard';
import { DealerDashboard } from './components/dashboard/DealerDashboard'; import { DealerDashboard } from './components/dashboard/DealerDashboard';
import { ProspectiveDashboardPage } from './components/dashboard/ProspectiveDashboardPage';
import { ApplicationsPage } from './components/applications/ApplicationsPage'; import { ApplicationsPage } from './components/applications/ApplicationsPage';
import { AllApplicationsPage } from './components/applications/AllApplicationsPage'; import { AllApplicationsPage } from './components/applications/AllApplicationsPage';
import { OpportunityRequestsPage } from './components/applications/OpportunityRequestsPage'; import { OpportunityRequestsPage } from './components/applications/OpportunityRequestsPage';
@ -44,14 +47,14 @@ import { toast } from 'sonner';
import { API } from './api/API'; import { API } from './api/API';
// Layout Component // Layout Component
const AppLayout = ({ children, onLogout, title }: { children: React.ReactNode, onLogout: () => void, title: string }) => { const AppLayout = ({ onLogout, title }: { onLogout: () => void, title: string }) => {
return ( return (
<div className="flex h-screen bg-slate-50"> <div className="flex h-screen bg-slate-50">
<Sidebar onLogout={onLogout} /> <Sidebar onLogout={onLogout} />
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<Header title={title} onRefresh={() => window.location.reload()} /> <Header title={title} onRefresh={() => window.location.reload()} />
<main className="flex-1 overflow-y-auto p-6"> <main className="flex-1 overflow-y-auto p-6">
{children} <Outlet />
</main> </main>
</div> </div>
<Toaster /> <Toaster />
@ -103,6 +106,16 @@ export default function App() {
navigate('/'); navigate('/');
}; };
// Listen for 401 logout events from API client
useEffect(() => {
const onLogout = () => {
handleLogout();
toast.error('Session expired. Please login again.');
};
window.addEventListener('auth:logout', onLogout);
return () => window.removeEventListener('auth:logout', onLogout);
}, [dispatch, navigate]);
// Helper to determine page title based on path // Helper to determine page title based on path
const getPageTitle = (pathname: string) => { const getPageTitle = (pathname: string) => {
if (pathname.startsWith('/applications/') && pathname.length > 14) return 'Application Details'; if (pathname.startsWith('/applications/') && pathname.length > 14) return 'Application Details';
@ -148,6 +161,7 @@ export default function App() {
<> <>
<Routes> <Routes>
<Route path="/admin-login" element={<LoginPage onLogin={handleLogin} />} /> <Route path="/admin-login" element={<LoginPage onLogin={handleLogin} />} />
<Route path="/prospective-login" element={<ProspectiveLoginPage />} />
<Route path="/questionnaire/:applicationId" element={<PublicQuestionnairePage />} /> <Route path="/questionnaire/:applicationId" element={<PublicQuestionnairePage />} />
<Route path="*" element={showAdminLogin ? <LoginPage onLogin={handleLogin} /> : <Route path="*" element={showAdminLogin ? <LoginPage onLogin={handleLogin} /> :
<ApplicationFormPage onAdminLogin={() => setShowAdminLogin(true)} />} <ApplicationFormPage onAdminLogin={() => setShowAdminLogin(true)} />}
@ -160,8 +174,23 @@ export default function App() {
// Protected Routes // Protected Routes
return ( return (
<AppLayout onLogout={handleLogout} title={getPageTitle(location.pathname)}> <Routes>
<Routes> {/* Prospective Dealer Route - STRICTLY ISOLATED */}
<Route
path="/prospective-dashboard"
element={
<RoleGuard allowedRoles={['Prospective Dealer']}>
<ProspectiveDashboardPage />
</RoleGuard>
}
/>
{/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
<Route element={
<RoleGuard excludeRoles={['Prospective Dealer']} redirectTo="/prospective-dashboard">
<AppLayout onLogout={handleLogout} title={getPageTitle(location.pathname)} />
</RoleGuard>
}>
<Route path="/" element={<Navigate to="/dashboard" replace />} /> <Route path="/" element={<Navigate to="/dashboard" replace />} />
{/* Dashboards */} {/* Dashboards */}
@ -257,8 +286,7 @@ export default function App() {
{/* Fallback */} {/* Fallback */}
<Route path="*" element={<Navigate to="/dashboard" replace />} /> <Route path="*" element={<Navigate to="/dashboard" replace />} />
</Route>
</Routes> </Routes>
</AppLayout>
); );
} }

View File

@ -35,6 +35,12 @@ export const API = {
getAllQuestionnaires: () => client.get('/onboarding/questionnaires'), getAllQuestionnaires: () => client.get('/onboarding/questionnaires'),
getQuestionnaireById: (id: string) => client.get(`/onboarding/questionnaires/${id}`), getQuestionnaireById: (id: string) => client.get(`/onboarding/questionnaires/${id}`),
// Documents
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {
headers: { 'Content-Type': 'multipart/form-data' }
}),
getDocuments: (id: string) => client.get(`/onboarding/applications/${id}/documents`),
// Public Questionnaire // Public Questionnaire
getPublicQuestionnaire: (appId: string) => axios.get(`http://localhost:5000/api/questionnaire/public/${appId}`), // Direct axios to bypass interceptors if client has auth getPublicQuestionnaire: (appId: string) => axios.get(`http://localhost:5000/api/questionnaire/public/${appId}`), // Direct axios to bypass interceptors if client has auth
submitPublicResponse: (data: any) => axios.post('http://localhost:5000/api/questionnaire/public/submit', data), submitPublicResponse: (data: any) => axios.post('http://localhost:5000/api/questionnaire/public/submit', data),
@ -44,6 +50,9 @@ export const API = {
scheduleInterview: (data: any) => client.post('/assessment/interviews', data), scheduleInterview: (data: any) => client.post('/assessment/interviews', data),
updateInterview: (id: string, data: any) => client.put(`/assessment/interviews/${id}`, data), updateInterview: (id: string, data: any) => client.put(`/assessment/interviews/${id}`, data),
submitEvaluation: (id: string, data: any) => client.post(`/assessment/interviews/${id}/evaluation`, data), submitEvaluation: (id: string, data: any) => client.post(`/assessment/interviews/${id}/evaluation`, data),
submitKTMatrix: (data: any) => client.post('/assessment/kt-matrix', data),
submitLevel2Feedback: (data: any) => client.post('/assessment/level2-feedback', data),
getInterviews: (applicationId: string) => client.get(`/assessment/interviews/${applicationId}`),
// Collaboration & Participants // Collaboration & Participants
getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }), getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }),
@ -57,6 +66,10 @@ export const API = {
updateUser: (id: string, data: any) => client.put(`/admin/users/${id}`, data), updateUser: (id: string, data: any) => client.put(`/admin/users/${id}`, data),
updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data), updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data),
deleteUser: (id: string) => client.delete(`/admin/users/${id}`), deleteUser: (id: string) => client.delete(`/admin/users/${id}`),
// Prospective Login
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
verifyOtp: (phone: string, otp: string) => client.post('/prospective-login/verify-otp', { phone, otp }),
}; };
export default API; export default API;

View File

@ -24,7 +24,8 @@ client.addResponseTransform((response) => {
if (!response.ok) { if (!response.ok) {
if (response.status === 401) { if (response.status === 401) {
console.error('Unauthorized access - potential token expiration'); console.error('Unauthorized access - potential token expiration');
// Potential logic to logout user or refresh token // Dispatch global event for App to handle logout
window.dispatchEvent(new Event('auth:logout'));
} }
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@ -198,6 +198,25 @@ export function LoginPage({ onLogin }: LoginPageProps) {
'Login' 'Login'
)} )}
</Button> </Button>
<div className="text-center">
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-slate-200" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-slate-500">Or</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full border-amber-600 text-amber-600 hover:bg-amber-50 h-11"
onClick={() => window.location.href = '/prospective-login'}
>
Prospective User Login
</Button>
</div>
</form> </form>
) : ( ) : (
<form onSubmit={handleForgotPassword} className="space-y-6"> <form onSubmit={handleForgotPassword} className="space-y-6">

View File

@ -0,0 +1,235 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { AlertCircle, ArrowLeft, Smartphone } from 'lucide-react';
import { toast } from 'sonner';
import { API } from '../../api/API';
import { useDispatch } from 'react-redux';
import { setCredentials } from '../../store/slices/authSlice';
export function ProspectiveLoginPage() {
const navigate = useNavigate();
const dispatch = useDispatch();
const [step, setStep] = useState<'PHONE' | 'OTP'>('PHONE');
const [phone, setPhone] = useState('');
const [otp, setOtp] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const handleSendOtp = async (e: React.FormEvent) => {
e.preventDefault();
if (!phone || phone.length < 10) {
setError('Please enter a valid 10-digit phone number');
return;
}
setIsLoading(true);
setError('');
try {
const response = await API.sendOtp(phone) as any;
if (response.ok) {
setStep('OTP');
toast.success('OTP sent successfully!');
} else {
console.error('Send OTP error response:', response);
const errorMessage = response.data?.message || 'Failed to send OTP';
setError(errorMessage);
toast.error(errorMessage);
}
} catch (err: any) {
console.error('Send OTP network error:', err);
setError('Network error. Please try again.');
toast.error('Network error. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault();
if (!otp || otp.length < 6) {
setError('Please enter a valid 6-digit OTP');
return;
}
setIsLoading(true);
setError('');
try {
const response = await API.verifyOtp(phone, otp) as any;
if (response.ok && response.data) {
// Fix: Extract from nested data object
const { token, user } = response.data.data || response.data;
if (!token || !user) {
throw new Error('Invalid response format');
}
// Dispatch login action
dispatch(setCredentials({ user, token }));
// Store token in localStorage
localStorage.setItem('token', token);
toast.success('Logged in successfully!');
navigate('/prospective-dashboard');
} else {
const errorMessage = response.data?.message || 'Invalid OTP';
setError(errorMessage);
toast.error(errorMessage);
}
} catch (err: any) {
console.error('Verify OTP error:', err);
setError('An unexpected error occurred');
toast.error('An unexpected error occurred');
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-4 overflow-y-auto">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-600/10 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-amber-600/10 rounded-full blur-3xl"></div>
</div>
<div className="relative w-full max-w-md">
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 bg-amber-600 rounded-full mb-4">
<svg viewBox="0 0 24 24" className="w-12 h-12 text-white" fill="currentColor">
<path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</div>
<h1 className="text-white mb-2">Royal Enfield</h1>
<p className="text-slate-400">Dealer Login</p>
</div>
<div className="bg-white rounded-lg shadow-2xl p-8">
<div className="mb-6">
<Button
variant="ghost"
className="px-0 flex items-center gap-2 text-slate-600 hover:text-slate-900 mb-4 hover:bg-transparent"
onClick={() => step === 'OTP' ? setStep('PHONE') : navigate('/admin-login')}
>
<ArrowLeft className="w-4 h-4" />
{step === 'OTP' ? 'Change Phone Number' : 'Back to Login'}
</Button>
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-amber-100 rounded-lg">
<Smartphone className="w-6 h-6 text-amber-600" />
</div>
<div>
<h2 className="text-slate-900 text-lg font-semibold">Dealer Login</h2>
<p className="text-slate-600 text-sm">Login with your registered phone number</p>
</div>
</div>
</div>
{step === 'PHONE' ? (
<form onSubmit={handleSendOtp} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="phone">Registered Phone Number</Label>
<Input
id="phone"
type="tel"
placeholder="Enter 10-digit phone number"
maxLength={10}
value={phone}
onChange={(e) => setPhone(e.target.value.replace(/\D/g, ''))}
className="w-full"
disabled={isLoading}
/>
<p className="text-slate-500 text-xs">Enter the phone number you used during application</p>
</div>
{error && (
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-md">
<AlertCircle className="w-4 h-4 text-red-600" />
<span className="text-red-600 font-medium text-sm">{error}</span>
</div>
)}
<Button
type="submit"
className="w-full bg-amber-600 hover:bg-amber-700 h-9"
disabled={isLoading || phone.length < 10}
>
{isLoading ? 'Sending...' : 'Send OTP'}
</Button>
<div className="text-center text-slate-500 text-xs">
<p>You will receive a 6-digit OTP on your registered mobile number</p>
</div>
</form>
) : (
<form onSubmit={handleVerifyOtp} className="space-y-6">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="text-green-800 text-center text-sm">OTP sent to +91 {phone}</p>
</div>
<div className="space-y-2">
<Label htmlFor="otp">Enter OTP</Label>
<Input
id="otp"
type="text"
placeholder="Enter 6-digit OTP"
maxLength={6}
value={otp}
onChange={(e) => setOtp(e.target.value.replace(/\D/g, ''))}
className="w-full text-center text-2xl tracking-widest"
disabled={isLoading}
/>
<p className="text-slate-500 text-xs text-center">Check your SMS for the OTP</p>
</div>
{error && (
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-md">
<AlertCircle className="w-4 h-4 text-red-600" />
<span className="text-red-600 font-medium text-sm">{error}</span>
</div>
)}
<Button
type="submit"
className="w-full bg-amber-600 hover:bg-amber-700 h-9"
disabled={isLoading || otp.length < 6}
>
{isLoading ? 'Verifying...' : 'Verify OTP'}
</Button>
<div className="text-center text-sm">
<button
type="button"
className="text-amber-600 hover:text-amber-700 font-medium"
onClick={() => setStep('PHONE')}
>
Change Phone Number
</button>
<span className="mx-2 text-slate-400">|</span>
<button
type="button"
className="text-amber-600 hover:text-amber-700 font-medium"
onClick={handleSendOtp}
disabled={isLoading}
>
Resend OTP
</button>
</div>
</form>
)}
</div>
<div className="text-center mt-6 text-slate-400">
<p>© 2025 Royal Enfield. All rights reserved.</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,45 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { RootState } from '../../store';
interface RoleGuardProps {
children: React.ReactNode;
allowedRoles?: string[];
excludeRoles?: string[];
redirectTo?: string;
}
export const RoleGuard: React.FC<RoleGuardProps> = ({
children,
allowedRoles,
excludeRoles,
redirectTo
}) => {
const { user, isAuthenticated, loading } = useSelector((state: RootState) => state.auth);
const location = useLocation();
if (loading) {
return <div className="flex h-screen items-center justify-center">Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/admin-login" state={{ from: location }} replace />;
}
// Check excluded roles first (e.g. Block Prospective Dealer from main dashboard)
if (excludeRoles && user && excludeRoles.includes(user.role)) {
// If prospective dealer is excluded, redirect to their dashboard
if (user.role === 'Prospective Dealer') {
return <Navigate to="/prospective-dashboard" replace />;
}
return <Navigate to={redirectTo || '/dashboard'} replace />;
}
// Check allowed roles (e.g. Only Prospective Dealer can see their dashboard)
if (allowedRoles && user && !allowedRoles.includes(user.role)) {
// If regular dealer tries to access prospective dashboard
return <Navigate to={redirectTo || '/dashboard'} replace />;
}
return <>{children}</>;
};

View File

@ -0,0 +1,332 @@
import { useState, useEffect } from 'react';
import {
LayoutDashboard,
FileText,
UserMinus,
RefreshCcw,
MapPin,
LogOut,
Search,
ChevronLeft,
ChevronRight,
Bell,
User,
RefreshCw,
HelpCircle,
Upload,
Clock,
CheckCircle,
File,
X
} from 'lucide-react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { RootState } from '../../store';
import { logout } from '../../store/slices/authSlice';
import { toast } from 'sonner';
import { API } from '../../api/API';
export function ProspectiveDashboardPage() {
const dispatch = useDispatch();
const navigate = useNavigate();
const { user } = useSelector((state: RootState) => state.auth);
const [collapsed, setCollapsed] = useState(false);
const [activeTab, setActiveTab] = useState('applicant');
// Document State
const [documents, setDocuments] = useState<any[]>([]);
const [selectedDocType, setSelectedDocType] = useState('');
const [file, setFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
useEffect(() => {
if (user?.id) {
fetchDocuments();
}
}, [user?.id]);
const fetchDocuments = async () => {
try {
const response: any = await API.getDocuments(user.id);
if (response.ok && response.data) {
setDocuments(response.data.data);
}
} catch (error) {
console.error('Failed to fetch documents', error);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
}
};
const handleUpload = async () => {
if (!file || !selectedDocType) {
toast.error('Please select a document type and file');
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', selectedDocType);
setIsUploading(true);
try {
// Using user.id as it corresponds to Application ID (UUID) from login response
const response: any = await API.uploadDocument(user.id, formData);
if (response.ok) {
toast.success('Document uploaded successfully');
setFile(null);
setSelectedDocType('');
// Reset file input manually if needed, or rely on state
const fileInput = document.getElementById('file-upload') as HTMLInputElement;
if (fileInput) fileInput.value = '';
fetchDocuments();
} else {
toast.error(response.data?.message || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
toast.error('Upload failed');
} finally {
setIsUploading(false);
}
};
const handleLogout = () => {
dispatch(logout());
toast.info('Logged out successfully');
navigate('/');
};
return (
<div className="flex h-screen bg-slate-50">
{/* Sidebar */}
<div className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'}`}>
<div className="p-4 border-b border-slate-800">
<div className="flex items-center justify-between">
{!collapsed && (
<div className="flex items-center gap-2">
<div className="w-10 h-10 bg-amber-600 rounded-lg flex items-center justify-center">
<FileText className="w-6 h-6 text-white" />
</div>
<span className="text-amber-600 font-bold">Applicant Portal</span>
</div>
)}
<button
onClick={() => setCollapsed(!collapsed)}
className="p-1 hover:bg-slate-800 rounded transition-colors"
>
{collapsed ? <ChevronRight className="w-5 h-5" /> : <ChevronLeft className="w-5 h-5" />}
</button>
</div>
</div>
{!collapsed && (
<div className="p-4 border-b border-slate-800">
{/* Search Removed/hidden */}
</div>
)}
<nav className="flex-1 p-4 space-y-2">
<div>
<button
onClick={() => setActiveTab('applicant')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${activeTab === 'applicant' ? 'bg-amber-600 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`}
>
<FileText className="w-5 h-5 flex-shrink-0" />
{!collapsed && <span className="flex-1 text-left">My Application</span>}
</button>
</div>
</nav>
<div className="p-4 border-t border-slate-800 space-y-2">
{!collapsed && (
<div className="px-4 py-2 bg-slate-800 rounded-lg mb-2">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-600 rounded-full flex items-center justify-center">
<span>{user?.name?.charAt(0) || 'A'}</span>
</div>
<div className="flex-1 min-w-0">
<p className="truncate">{user?.name || 'Amit Sharma'}</p>
<p className="text-slate-400 truncate">{user?.role || 'Prospective'}</p>
</div>
</div>
</div>
)}
<button
onClick={handleLogout}
className={`w-full flex items-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all h-9 px-4 py-2 text-slate-300 hover:bg-slate-800 hover:text-white ${collapsed ? 'justify-center' : 'justify-start'}`}
>
<LogOut className="w-5 h-5 flex-shrink-0" />
{!collapsed && <span className="ml-3">Logout</span>}
</button>
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
<header className="bg-white border-b border-slate-200 px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-slate-900 text-xl font-semibold">Applicant Management</h1>
<p className="text-slate-600 text-sm">Manage and track dealership applications</p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
<div className="w-8 h-8 bg-amber-600 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-white" />
</div>
<div className="text-left">
<p className="text-slate-900 text-sm font-medium">{user?.name || 'Applicant'}</p>
<p className="text-slate-600 text-xs">{user?.role || 'User'}</p>
</div>
</div>
<button className="p-2 rounded-md hover:bg-slate-100" title="Refresh">
<RefreshCw className="w-4 h-4 text-slate-600" />
</button>
<button className="p-2 rounded-md hover:bg-slate-100" title="Help">
<HelpCircle className="w-4 h-4 text-slate-600" />
</button>
<button className="relative p-2 rounded-md hover:bg-slate-100">
<Bell className="w-4 h-4 text-slate-600" />
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
</button>
</div>
</div>
</header>
<main className="flex-1 overflow-y-auto p-6">
{activeTab === 'applicant' ? (
<div className="space-y-6">
<div>
<h1 className="text-slate-900 text-2xl font-bold mb-2">Applicant Portal</h1>
<p className="text-slate-600">Upload required documents for verification</p>
</div>
{/* Document Upload Card */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="p-6 border-b border-slate-200">
<h4 className="flex items-center gap-2 text-lg font-semibold text-slate-900">
<Upload className="w-5 h-5 text-blue-600" />
Document Upload
</h4>
<p className="text-slate-500 mt-1">Upload all required documents for your dealership application</p>
</div>
<div className="p-6 space-y-6">
<div className="space-y-4 p-4 bg-slate-50 rounded-lg border border-slate-200">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">Select Document Type</label>
<select
className="flex h-10 w-full items-center justify-between rounded-md border border-slate-300 bg-white px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 disabled:cursor-not-allowed disabled:opacity-50"
value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)}
disabled={isUploading}
>
<option value="">Choose document type...</option>
<option value="PAN Card">PAN Card</option>
<option value="GST Certificate">GST Certificate</option>
<option value="Aadhaar Card">Aadhaar Card</option>
<option value="Trade License">Trade License</option>
<option value="Bank Statement">Bank Statement</option>
<option value="Property Document">Property Document</option>
<option value="Other">Other</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">Upload File</label>
<div className="flex items-center gap-3">
<input
id="file-upload"
type="file"
className="block w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-amber-600 file:text-white hover:file:bg-amber-700 disabled:opacity-50 disabled:cursor-not-allowed"
onChange={handleFileChange}
disabled={isUploading}
/>
</div>
</div>
</div>
<div className="flex justify-end">
<button
onClick={handleUpload}
disabled={!file || !selectedDocType || isUploading}
className="px-4 py-2 bg-amber-600 text-white rounded-md hover:bg-amber-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{isUploading ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Uploading...
</>
) : (
<>
<Upload className="w-4 h-4" />
Upload Document
</>
)}
</button>
</div>
<p className="text-slate-500 text-sm">Accepted formats: PDF, JPG, PNG (Max size: 10MB)</p>
</div>
<div className="space-y-3">
<h3 className="font-medium text-slate-900">Uploaded Documents ({documents.length})</h3>
{documents.length === 0 ? (
<div className="text-center py-8 text-slate-500 bg-slate-50 rounded-lg border border-dashed border-slate-300">
<FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No documents uploaded yet</p>
</div>
) : (
<div className="space-y-2">
{documents.map((doc) => (
<div key={doc.id} className="flex items-center justify-between p-4 bg-white rounded-lg border border-slate-200 hover:border-amber-300 transition-colors">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<File className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="font-medium text-slate-900">{doc.documentType}</p>
<p className="text-slate-500 text-sm">{doc.fileName}</p>
<p className="text-slate-400 text-xs mt-1">Uploaded on {new Date(doc.createdAt).toLocaleDateString()}</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium ${doc.status === 'Approved' ? 'bg-green-100 text-green-700 border-green-200' :
doc.status === 'Rejected' ? 'bg-red-100 text-red-700 border-red-200' :
'bg-yellow-100 text-yellow-700 border-yellow-200'
}`}>
{doc.status === 'Approved' ? <CheckCircle className="w-3 h-3 mr-1" /> : <Clock className="w-3 h-3 mr-1" />}
{doc.status || 'Pending'}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<h2 className="text-2xl font-semibold text-slate-900 mb-2">
{activeTab === 'dashboard' ? 'Dashboard' :
activeTab === 'resignations' ? 'My Resignations' :
activeTab === 'constitutional' ? 'Constitutional Change' :
'Relocation Requests'}
</h2>
<p className="text-slate-500">Coming soon...</p>
</div>
</div>
)}
</main>
</div >
</div >
);
}

View File

@ -16,7 +16,8 @@ export type UserRole =
| 'DDL' | 'DDL'
| 'Finance' | 'Finance'
| 'Finance Admin' | 'Finance Admin'
| 'Dealer'; | 'Dealer'
| 'Prospective Dealer';
export type ApplicationStatus = export type ApplicationStatus =
| 'Submitted' | 'Submitted'
@ -48,6 +49,7 @@ export type ApplicationStatus =
| 'Statutory MSD' | 'Statutory MSD'
| 'Statutory LOI Ack' | 'Statutory LOI Ack'
| 'EOR In Progress' | 'EOR In Progress'
| 'EOR Complete'
| 'LOA Pending' | 'LOA Pending'
| 'Inauguration' | 'Inauguration'
| 'Approved' | 'Approved'

View File

@ -1,93 +1,368 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import QuestionnaireForm from '../../components/dealer/QuestionnaireForm';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useSelector } from 'react-redux';
import { RootState } from '../../store';
import {
User, RefreshCw, HelpCircle, Bell, ArrowLeft, Bike,
Users, Target, FileText, Award, ChevronLeft, ChevronRight,
CheckCircle, AlertCircle
} from 'lucide-react';
const PublicQuestionnairePage: React.FC = () => { const PublicQuestionnairePage: React.FC = () => {
const { applicationId } = useParams<{ applicationId: string }>(); const { applicationId } = useParams<{ applicationId: string }>();
const [isValid, setIsValid] = useState<boolean | null>(null); const navigate = useNavigate();
const [appName, setAppName] = useState(''); const { user } = useSelector((state: RootState) => state.auth);
const [loading, setLoading] = useState(true);
const [questions, setQuestions] = useState<any[]>([]); const [questions, setQuestions] = useState<any[]>([]);
const [sections, setSections] = useState<string[]>([]);
const [activeSection, setActiveSection] = useState<string>('');
const [responses, setResponses] = useState<Record<string, any>>({});
const [submitting, setSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
useEffect(() => { useEffect(() => {
const checkValidity = async () => { const fetchQuestionnaire = async () => {
if (!applicationId) return; if (!applicationId) return;
try { try {
// We use the public fetch to verify existence // Determine if we need public or private fetch based on auth
// For prospective users, we might want to use the public endpoint or a specific one
const res = await API.getPublicQuestionnaire(applicationId); const res = await API.getPublicQuestionnaire(applicationId);
if (res.data.success) { if (res.data.success) {
setIsValid(true); const fetchedQuestions = res.data.data.questions || [];
setAppName(res.data.data.applicationName); setQuestions(fetchedQuestions);
setQuestions(res.data.data.questions || []);
} else { // Extract unique sections
setIsValid(false); const uniqueSections = Array.from(new Set(fetchedQuestions.map((q: any) => q.sectionName)));
setSections(uniqueSections as string[]);
if (uniqueSections.length > 0) {
setActiveSection(uniqueSections[0] as string);
}
// Check if already submitted (optional, backend might handle)
} }
} catch (error: any) { } catch (error: any) {
console.error("Error fetching questionnaire:", error);
if (error.response?.data?.code === 'ALREADY_SUBMITTED') { if (error.response?.data?.code === 'ALREADY_SUBMITTED') {
setIsValid('submitted' as any); // Use a string or enum for distinct state setIsSubmitted(true);
} else { } else {
setIsValid(false); toast.error("Failed to load questionnaire");
} }
} finally {
setLoading(false);
} }
}; };
checkValidity(); fetchQuestionnaire();
}, [applicationId]); }, [applicationId]);
if (isValid === null) return <div className="p-8 text-center">Checking application link...</div>; const handleInputChange = (questionId: string, value: any) => {
setResponses(prev => ({ ...prev, [questionId]: value }));
};
if (isValid === 'submitted' as any) { const handleNextSection = () => {
const currentIndex = sections.indexOf(activeSection);
if (currentIndex < sections.length - 1) {
setActiveSection(sections[currentIndex + 1]);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
};
const handlePrevSection = () => {
const currentIndex = sections.indexOf(activeSection);
if (currentIndex > 0) {
setActiveSection(sections[currentIndex - 1]);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
};
const handleSubmit = async () => {
// Validate all mandatory questions
const missing = questions.filter(q => q.isMandatory && !responses[q.id]);
if (missing.length > 0) {
toast.error(`Please answer all mandatory questions. Missing: ${missing.length}`);
// Provide visual feedback or navigation to missing section?
return;
}
try {
setSubmitting(true);
const payload = Object.entries(responses).map(([qId, val]) => ({
questionId: qId,
value: val
}));
await API.submitPublicResponse({
applicationId: applicationId!,
responses: payload
});
toast.success('Responses submitted successfully');
setIsSubmitted(true);
setTimeout(() => navigate('/prospective-dashboard'), 3000);
} catch (error) {
console.error(error);
toast.error('Failed to submit responses');
} finally {
setSubmitting(false);
}
};
if (loading) return (
<div className="flex items-center justify-center h-screen bg-slate-50">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div>
</div>
);
if (isSubmitted) {
return ( return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-6"> <div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-6">
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full text-center"> <div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full text-center border-t-4 border-amber-600">
<div className="w-16 h-16 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto mb-4"> <div className="w-16 h-16 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CheckCircle className="w-8 h-8" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div> </div>
<h2 className="text-2xl font-bold mb-2">Assessment Submitted</h2> <h2 className="text-2xl font-bold mb-2 text-slate-900">Assessment Submitted</h2>
<p className="text-gray-600 mb-6"> <p className="text-slate-600 mb-6">
Thank you! Your assessment has already been submitted successfully. Thank you! Your assessment has been submitted successfully.
We will review your application and get back to you shortly. Redirecting to dashboard...
</p> </p>
<button
onClick={() => navigate('/prospective-dashboard')}
className="w-full py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors"
>
Return to Dashboard
</button>
</div> </div>
</div> </div>
); );
} }
if (isValid === false) return <div className="p-8 text-center text-red-500 text-xl font-bold">Invalid or Expired Link</div>; const activeQuestions = questions.filter(q => q.sectionName === activeSection);
const currentSectionIndex = sections.indexOf(activeSection);
const totalMarks = questions.reduce((sum, q) => sum + (Number(q.weight) || 0), 0);
return ( return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center py-10"> <div className="flex-1 flex flex-col overflow-hidden h-screen bg-slate-50">
<div className="w-full max-w-4xl bg-white rounded-lg shadow-lg overflow-hidden"> {/* Header */}
<div className="bg-black text-white p-6"> <header className="bg-white border-b border-slate-200 px-6 py-4 flex-shrink-0">
<h1 className="text-2xl font-bold">Dealer Application Assessment</h1> <div className="flex items-center justify-between">
<p className="opacity-80 mt-1">Applicant: {appName}</p> <div>
<p className="opacity-60 text-sm">ID: {applicationId}</p> <h1 className="text-slate-900 font-bold text-xl">Dealer Questionnaire Form</h1>
<p className="text-slate-600 text-sm">Manage and track dealership applications</p>
</div>
<div className="flex items-center gap-3">
{user && (
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
<div className="w-8 h-8 bg-amber-600 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-white" />
</div>
<div className="text-left">
<p className="text-slate-900 text-sm font-medium">{user.name || 'Applicant'}</p>
<p className="text-slate-600 text-xs">{user.role || 'Prospective Dealer'}</p>
</div>
</div>
)}
<button className="p-2 text-slate-500 hover:bg-slate-100 rounded-lg" title="Refresh" onClick={() => window.location.reload()}>
<RefreshCw className="w-4 h-4" />
</button>
<button className="p-2 text-slate-500 hover:bg-slate-100 rounded-lg" title="Help">
<HelpCircle className="w-4 h-4" />
</button>
</div>
</div>
</header>
<main className="flex-1 overflow-y-auto">
<div className="bg-white border-b border-slate-200 sticky top-0 z-20 shadow-sm">
<div className="max-w-5xl mx-auto px-8 py-3">
<button
onClick={() => navigate('/prospective-dashboard')}
className="inline-flex items-center gap-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors px-3 py-2 rounded-lg hover:bg-slate-100"
>
<ArrowLeft className="w-4 h-4" />
Back to Applicant Portal
</button>
</div>
</div> </div>
<div className="p-6"> <div className="max-w-5xl mx-auto py-8 px-6">
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6"> {/* Hero Section */}
<div className="flex"> <div className="bg-gradient-to-br from-slate-900 via-slate-800 to-amber-900 rounded-t-lg overflow-hidden shadow-xl">
<div className="ml-3"> <div className="relative px-8 py-12">
<p className="text-sm text-yellow-700"> <div className="absolute inset-0 opacity-10 pointer-events-none">
Please complete all mandatory fields. You can submit this form only once. <div className="absolute top-0 right-0 w-64 h-64 bg-amber-500 rounded-full blur-3xl"></div>
<div className="absolute bottom-0 left-0 w-64 h-64 bg-amber-600 rounded-full blur-3xl"></div>
</div>
<div className="relative z-10 text-center">
<div className="inline-flex items-center justify-center mb-6">
<div className="w-20 h-20 bg-amber-600 rounded-full flex items-center justify-center shadow-xl">
<Bike className="w-10 h-10 text-white" />
</div>
</div>
<h1 className="text-white text-3xl mb-3 font-serif tracking-wide">ROYAL ENFIELD</h1>
<div className="h-1 w-24 bg-amber-600 mx-auto mb-4"></div>
<h2 className="text-amber-400 text-xl mb-4 font-light">Dealership Partner Application</h2>
<p className="text-slate-300 max-w-2xl mx-auto leading-relaxed text-sm">
Thank you for your interest in becoming a Royal Enfield business partner.
Please complete this questionnaire to help us understand your profile and aspirations.
</p> </p>
<div className="flex items-center justify-center gap-8 mt-8 border-t border-slate-700/50 pt-6 inline-flex mx-auto">
<div className="text-center px-4">
<div className="text-amber-400 text-2xl font-bold">{questions.length}</div>
<div className="text-slate-400 text-xs uppercase tracking-wider">Questions</div>
</div>
<div className="h-10 w-px bg-slate-700"></div>
<div className="text-center px-4">
<div className="text-amber-400 text-2xl font-bold">{sections.length}</div>
<div className="text-slate-400 text-xs uppercase tracking-wider">Sections</div>
</div>
<div className="h-10 w-px bg-slate-700"></div>
<div className="text-center px-4">
<div className="text-amber-400 text-2xl font-bold">{totalMarks}</div>
<div className="text-slate-400 text-xs uppercase tracking-wider">Total Marks</div>
</div>
</div>
</div>
</div>
{/* Section Tabs */}
<div className="bg-slate-800/50 backdrop-blur-sm border-t border-slate-700">
<div className="flex items-center gap-2 overflow-x-auto scrollbar-hide px-8 py-4 no-scrollbar">
{sections.map((section, idx) => (
<button
key={section}
onClick={() => setActiveSection(section)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg whitespace-nowrap transition-all text-sm font-medium
${activeSection === section
? 'bg-amber-600 text-white shadow-lg'
: 'bg-slate-700/50 text-slate-300 hover:bg-slate-700 hover:text-white'
}`}
>
<FileText className="w-4 h-4" />
<span>{section}</span>
{activeSection === section && (
<span className="ml-2 bg-white/20 px-2 py-0.5 rounded text-xs">
{questions.filter(q => q.sectionName === section).length}
</span>
)}
</button>
))}
</div> </div>
</div> </div>
</div> </div>
<QuestionnaireForm {/* Question Content */}
applicationId={applicationId!} <div className="bg-white rounded-b-lg shadow-xl border border-slate-200 border-t-0 min-h-[400px]">
publicMode={true} <div className="p-8">
initialQuestions={questions} <div className="flex items-start gap-4 pb-6 border-b-2 border-amber-100 mb-8">
onComplete={() => { <div className="w-12 h-12 bg-amber-50 rounded-lg flex items-center justify-center flex-shrink-0 text-amber-600">
toast.success("Thank you! Your assessment has been submitted."); <Users className="w-6 h-6" />
setTimeout(() => window.location.reload(), 2000); </div>
}} <div className="flex-1">
/> <h3 className="text-slate-900 text-xl font-bold mb-1">{activeSection}</h3>
<p className="text-slate-500 text-sm">
Section {currentSectionIndex + 1} of {sections.length} {activeQuestions.length} questions
</p>
</div>
</div>
<div className="space-y-10">
{activeQuestions.map((q, idx) => (
<div key={q.id} className="group animate-in fade-in duration-500" style={{ animationDelay: `${idx * 100}ms` }}>
<div className="flex items-start gap-5">
<div className="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center flex-shrink-0 group-hover:bg-amber-100 transition-colors text-slate-600 group-hover:text-amber-700 font-semibold text-sm">
{idx + 1}
</div>
<div className="flex-1 space-y-3">
<div className="flex items-start justify-between gap-4">
<label className="text-sm font-medium text-slate-900 leading-relaxed block">
{q.questionText}
{q.isMandatory && <span className="text-red-500 ml-1">*</span>}
</label>
{q.weight > 0 && (
<span className="shrink-0 px-2 py-1 bg-slate-100 text-slate-600 text-xs font-semibold rounded border border-slate-200">
{q.weight} Marks
</span>
)}
</div>
<div className="max-w-xl">
{(q.inputType === 'text' || q.inputType === 'email' || q.inputType === 'number') && (
<input
type={q.inputType}
className="w-full h-10 px-3 rounded-lg border border-slate-300 focus:border-amber-500 focus:ring-2 focus:ring-amber-200 outline-none transition-all placeholder:text-slate-400"
placeholder="Type your answer here..."
value={responses[q.id] || ''}
onChange={(e) => handleInputChange(q.id, e.target.value)}
/>
)}
{(q.inputType === 'select' || q.inputType === 'yesno') && (
<select
className="w-full h-10 px-3 rounded-lg border border-slate-300 focus:border-amber-500 focus:ring-2 focus:ring-amber-200 outline-none transition-all bg-white"
value={responses[q.id] || ''}
onChange={(e) => handleInputChange(q.id, e.target.value)}
>
<option value="">Select an option...</option>
{(q.questionOptions || (q.inputType === 'yesno' ? [{ optionText: 'Yes' }, { optionText: 'No' }] : [])).map((opt: any, i: number) => (
<option key={i} value={opt.optionText || opt.text}>
{opt.optionText || opt.text}
</option>
))}
</select>
)}
</div>
</div>
</div>
</div>
))}
{activeQuestions.length === 0 && (
<div className="text-center py-10 text-slate-500 italic">
No questions in this section.
</div>
)}
</div>
<div className="flex items-center justify-between pt-10 mt-10 border-t border-slate-100">
<button
onClick={handlePrevSection}
disabled={currentSectionIndex === 0}
className="px-6 py-2.5 rounded-lg text-sm font-medium border border-slate-300 text-slate-700 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Previous Section
</button>
{currentSectionIndex < sections.length - 1 ? (
<button
onClick={handleNextSection}
className="px-6 py-2.5 rounded-lg text-sm font-medium bg-amber-600 text-white hover:bg-amber-700 flex items-center gap-2 transition-colors shadow-md hover:shadow-lg"
>
Next Section
<ChevronRight className="w-4 h-4" />
</button>
) : (
<button
onClick={handleSubmit}
disabled={submitting}
className="px-8 py-2.5 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 flex items-center gap-2 transition-colors shadow-md hover:shadow-lg disabled:bg-slate-400"
>
{submitting ? 'Submitting...' : 'Submit Application'}
{!submitting && <CheckCircle className="w-4 h-4" />}
</button>
)}
</div>
</div>
</div>
<div className="mt-8 text-center text-slate-500 text-sm">
© 2026 Royal Enfield. All rights reserved.
</div>
</div> </div>
</div> </main>
</div> </div>
); );
}; };

View File

@ -3,7 +3,7 @@ import { API } from '../api/API';
export const onboardingService = { export const onboardingService = {
submitApplication: async (data: any) => { submitApplication: async (data: any) => {
try { try {
const response = await API.submitApplication(data); const response: any = await API.submitApplication(data);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Submit application error:', error); console.error('Submit application error:', error);
@ -12,7 +12,7 @@ export const onboardingService = {
}, },
getApplications: async () => { getApplications: async () => {
try { try {
const response = await API.getApplications(); const response: any = await API.getApplications();
return response.data?.data || response.data; return response.data?.data || response.data;
} catch (error) { } catch (error) {
console.error('Get applications error:', error); console.error('Get applications error:', error);
@ -21,7 +21,7 @@ export const onboardingService = {
}, },
shortlistApplications: async (applicationIds: string[], assignedTo: string[], remarks?: string) => { shortlistApplications: async (applicationIds: string[], assignedTo: string[], remarks?: string) => {
try { try {
const response = await API.shortlistApplications({ applicationIds, assignedTo, remarks }); const response: any = await API.shortlistApplications({ applicationIds, assignedTo, remarks });
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Shortlist applications error:', error); console.error('Shortlist applications error:', error);
@ -30,7 +30,7 @@ export const onboardingService = {
}, },
getApplicationById: async (id: string) => { getApplicationById: async (id: string) => {
try { try {
const response = await API.getApplicationById(id); const response: any = await API.getApplicationById(id);
return response.data?.data || response.data; return response.data?.data || response.data;
} catch (error) { } catch (error) {
console.error('Get application by id error:', error); console.error('Get application by id error:', error);
@ -39,7 +39,7 @@ export const onboardingService = {
}, },
getUsers: async () => { getUsers: async () => {
try { try {
const response = await API.getUsers(); const response: any = await API.getUsers();
return response.data?.data || response.data; return response.data?.data || response.data;
} catch (error) { } catch (error) {
console.error('Get users error:', error); console.error('Get users error:', error);
@ -48,7 +48,7 @@ export const onboardingService = {
}, },
addParticipant: async (data: any) => { addParticipant: async (data: any) => {
try { try {
const response = await API.addParticipant(data); const response: any = await API.addParticipant(data);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Add participant error:', error); console.error('Add participant error:', error);
@ -57,11 +57,64 @@ export const onboardingService = {
}, },
scheduleInterview: async (data: any) => { scheduleInterview: async (data: any) => {
try { try {
const response = await API.scheduleInterview(data); const response: any = await API.scheduleInterview(data);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Schedule interview error:', error); console.error('Schedule interview error:', error);
throw error; throw error;
} }
},
getInterviews: async (applicationId: string) => {
try {
const response: any = await API.getInterviews(applicationId);
return response.data?.data || response.data;
} catch (error) {
console.error('Get interviews error:', error);
throw error;
}
},
getDocuments: async (id: string) => {
try {
const response: any = await API.getDocuments(id);
return response.data?.data || response.data;
} catch (error) {
console.error('Get documents error:', error);
throw error;
}
},
uploadDocument: async (id: string, data: any) => {
try {
const response: any = await API.uploadDocument(id, data);
return response.data;
} catch (error) {
console.error('Upload document error:', error);
throw error;
}
},
submitKTMatrix: async (data: any) => {
try {
const response: any = await API.submitKTMatrix(data);
if (response.ok) {
return response.data;
} else {
throw new Error(response.problem || 'Failed to submit KT Matrix');
}
} catch (error) {
console.error('Submit KT Matrix error:', error);
throw error;
}
},
submitLevel2Feedback: async (data: any) => {
try {
const response: any = await API.submitLevel2Feedback(data);
if (response.ok) {
return response.data;
} else {
throw new Error(response.problem || 'Failed to submit Level 2 Feedback');
}
} catch (error) {
console.error('Submit Level 2 Feedback error:', error);
throw error;
}
} }
}; };