diff --git a/src/App.tsx b/src/App.tsx
index 9db7228..b17098b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,15 +2,18 @@ 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 { RoleGuard } from './components/auth/RoleGuard';
+import { Routes, Route, Navigate, useLocation, useNavigate, Outlet } from 'react-router-dom';
import { ApplicationFormPage } from './components/public/ApplicationFormPage';
import PublicQuestionnairePage from './pages/public/PublicQuestionnairePage';
import { LoginPage } from './components/auth/LoginPage';
+import { ProspectiveLoginPage } from './components/auth/ProspectiveLoginPage';
import { Sidebar } from './components/layout/Sidebar';
import { Header } from './components/layout/Header';
import { Dashboard } from './components/dashboard/Dashboard';
import { FinanceDashboard } from './components/dashboard/FinanceDashboard';
import { DealerDashboard } from './components/dashboard/DealerDashboard';
+import { ProspectiveDashboardPage } from './components/dashboard/ProspectiveDashboardPage';
import { ApplicationsPage } from './components/applications/ApplicationsPage';
import { AllApplicationsPage } from './components/applications/AllApplicationsPage';
import { OpportunityRequestsPage } from './components/applications/OpportunityRequestsPage';
@@ -44,14 +47,14 @@ import { toast } from 'sonner';
import { API } from './api/API';
// Layout Component
-const AppLayout = ({ children, onLogout, title }: { children: React.ReactNode, onLogout: () => void, title: string }) => {
+const AppLayout = ({ onLogout, title }: { onLogout: () => void, title: string }) => {
return (
window.location.reload()} />
- {children}
+
@@ -103,6 +106,16 @@ export default function App() {
navigate('/');
};
+ // Listen for 401 logout events from API client
+ useEffect(() => {
+ const onLogout = () => {
+ handleLogout();
+ toast.error('Session expired. Please login again.');
+ };
+ window.addEventListener('auth:logout', onLogout);
+ return () => window.removeEventListener('auth:logout', onLogout);
+ }, [dispatch, navigate]);
+
// Helper to determine page title based on path
const getPageTitle = (pathname: string) => {
if (pathname.startsWith('/applications/') && pathname.length > 14) return 'Application Details';
@@ -148,6 +161,7 @@ export default function App() {
<>
} />
+ } />
} />
:
setShowAdminLogin(true)} />}
@@ -160,8 +174,23 @@ export default function App() {
// Protected Routes
return (
-
-
+
+ {/* Prospective Dealer Route - STRICTLY ISOLATED */}
+
+
+
+ }
+ />
+
+ {/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
+
+
+
+ }>
} />
{/* Dashboards */}
@@ -257,8 +286,7 @@ export default function App() {
{/* Fallback */}
} />
-
-
-
+
+
);
}
diff --git a/src/api/API.ts b/src/api/API.ts
index 0e67935..c6ee256 100644
--- a/src/api/API.ts
+++ b/src/api/API.ts
@@ -35,6 +35,12 @@ export const API = {
getAllQuestionnaires: () => client.get('/onboarding/questionnaires'),
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
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),
@@ -44,6 +50,9 @@ export const API = {
scheduleInterview: (data: any) => client.post('/assessment/interviews', data),
updateInterview: (id: string, data: any) => client.put(`/assessment/interviews/${id}`, 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
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),
updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data),
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;
diff --git a/src/api/client.ts b/src/api/client.ts
index 6175426..205d432 100644
--- a/src/api/client.ts
+++ b/src/api/client.ts
@@ -24,7 +24,8 @@ client.addResponseTransform((response) => {
if (!response.ok) {
if (response.status === 401) {
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'));
}
}
});
diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx
index 823da9f..f91cb40 100644
--- a/src/components/applications/ApplicationDetails.tsx
+++ b/src/components/applications/ApplicationDetails.tsx
@@ -1,6 +1,7 @@
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 { toast } from 'sonner';
+import { mockApplications, mockAuditLogs, mockDocuments, mockWorkNotes, mockQuestionnaireResponses, Application, ApplicationStatus } from '../../lib/mock-data';
import { onboardingService } from '../../services/onboarding.service';
import { WorkNotesPage } from './WorkNotesPage';
import QuestionnaireForm from '../dealer/QuestionnaireForm';
@@ -81,6 +82,157 @@ interface ProcessStage {
}[];
}
+
+
+const KT_MATRIX_CRITERIA = [
+ {
+ name: "Age",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "20 to 40 years old", value: "20-40", score: 10 },
+ { label: "40 to 50 years old", value: "40-50", score: 5 },
+ { label: "Above 50 years old", value: "above-50", score: 0 }
+ ]
+ },
+ {
+ name: "Qualification",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "Post Graduate", value: "post-graduate", score: 10 },
+ { label: "Graduate", value: "graduate", score: 5 },
+ { label: "SSLC", value: "sslc", score: 0 }
+ ]
+ },
+ {
+ name: "Local Knowledge and Influence",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "Excellent PR", value: "excellent", score: 10 },
+ { label: "Good PR", value: "good", score: 5 },
+ { label: "Poor PR", value: "poor", score: 0 }
+ ]
+ },
+ {
+ name: "Base Location vs Applied Location",
+ weight: 10,
+ maxScore: 10,
+ options: [
+ { label: "Native of the Applied location", value: "native", score: 10 },
+ { label: "Willing to relocate", value: "relocate", score: 5 },
+ { label: "Will manage remotely with occasional visits", value: "remote", score: 0 }
+ ]
+ },
+ {
+ name: "Why Interested in Royal Enfield Business?",
+ weight: 10,
+ maxScore: 10,
+ options: [
+ { label: "Passion", value: "passion", score: 10 },
+ { label: "Business expansion / Status symbol", value: "business", score: 5 }
+ ]
+ },
+ {
+ name: "Passion for Royal Enfield",
+ weight: 10,
+ maxScore: 10,
+ options: [
+ { label: "Currently owns a Royal Enfield", value: "owns", score: 10 },
+ { label: "Owned by Immediate Relative", value: "relative", score: 5 },
+ { label: "Does not own Royal Enfield", value: "none", score: 0 }
+ ]
+ },
+ {
+ name: "Passion For Rides",
+ weight: 10,
+ maxScore: 10,
+ options: [
+ { label: "Goes for long rides regularly", value: "regular", score: 10 },
+ { label: "Goes for long rides rarely", value: "rarely", score: 5 },
+ { label: "Doesn't go for rides", value: "never", score: 0 }
+ ]
+ },
+ {
+ name: "With Whom Partnering?",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "Within family", value: "family", score: 10 },
+ { label: "Outside family", value: "outside", score: 0 }
+ ]
+ },
+ {
+ name: "Who Will Manage the Firm?",
+ weight: 10,
+ maxScore: 10,
+ options: [
+ { label: "Owner managed", value: "owner", score: 10 },
+ { label: "Partly owner / partly manager model", value: "partly", score: 5 },
+ { label: "Fully manager model", value: "manager", score: 0 }
+ ]
+ },
+ {
+ name: "Business Acumen",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "Has similar automobile experience", value: "automobile", score: 10 },
+ { label: "Has successful business but not automobile", value: "other-business", score: 5 },
+ { label: "No business experience", value: "no-experience", score: 0 }
+ ]
+ },
+ {
+ name: "Time Availability",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "Full Time Availability for RE Business", value: "full-time", score: 10 },
+ { label: "Part Time Availability for RE Business", value: "part-time", score: 5 },
+ { label: "Not Available personally, Manager will handle", value: "manager", score: 0 }
+ ]
+ },
+ {
+ name: "Property Ownership",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "Has own property in proposed location", value: "own", score: 10 },
+ { label: "Will rent / lease", value: "rent", score: 0 }
+ ]
+ },
+ {
+ name: "Investment in the Business",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "Full own funds", value: "own-funds", score: 10 },
+ { label: "Partially from the bank", value: "partial-bank", score: 5 },
+ { label: "Completely bank funded", value: "full-bank", score: 0 }
+ ]
+ },
+ {
+ name: "Will Expand to Other 2W/4W OEMs?",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "No", value: "no", score: 10 },
+ { label: "Yes", value: "yes", score: 0 }
+ ]
+ },
+ {
+ name: "Plans of Expansion with RE",
+ weight: 5,
+ maxScore: 10,
+ options: [
+ { label: "Immediate blood relation will join & expand", value: "blood-relation", score: 10 },
+ { label: "Wants to expand by himself into more clusters", value: "self-expand", score: 5 },
+ { label: "No plans for expansion", value: "no-plans", score: 0 }
+ ]
+ }
+];
+
export function ApplicationDetails() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
@@ -90,78 +242,78 @@ export function ApplicationDetails() {
const [application, setApplication] = useState
(null);
const [loading, setLoading] = useState(true);
+ 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: data.score || data.questionnaireMarks || 0, // Read from score or correct field
+ questionnaireResponses: data.questionnaireResponses || [], // Map responses
+ 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'),
+ participants: data.participants || [],
+ };
+ setApplication(mappedApp);
+ } catch (error) {
+ console.error('Failed to fetch application details', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
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: data.score || data.questionnaireMarks || 0, // Read from score or correct field
- questionnaireResponses: data.questionnaireResponses || [], // Map responses
- 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'),
- participants: data.participants || [],
- };
- setApplication(mappedApp);
- } catch (error) {
- console.error('Failed to fetch application details', error);
- } finally {
- setLoading(false);
- }
- };
-
if (applicationId) {
fetchApplication();
}
@@ -193,6 +345,279 @@ export function ApplicationDetails() {
const [interviewType, setInterviewType] = useState('level1');
const [meetingLink, setMeetingLink] = useState('');
const [location, setLocation] = useState('');
+ const [documents, setDocuments] = useState([]);
+ const [showUploadModal, setShowUploadModal] = useState(false);
+ const [uploadFile, setUploadFile] = useState(null);
+ const [uploadDocType, setUploadDocType] = useState('');
+ const [isUploading, setIsUploading] = useState(false);
+ const [selectedInterviewerId, setSelectedInterviewerId] = useState('');
+ const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState([]);
+ const [interviews, setInterviews] = useState([]);
+ const [isScheduling, setIsScheduling] = useState(false);
+
+ // KT Matrix State
+ const [selectedInterviewIdForFeedback, setSelectedInterviewIdForFeedback] = useState('');
+ const [ktMatrixScores, setKtMatrixScores] = useState>({});
+ const [ktMatrixRemarks, setKtMatrixRemarks] = useState('');
+ const [isSubmittingKT, setIsSubmittingKT] = useState(false);
+
+ const handleKTMatrixChange = (criterionName: string, score: number) => {
+ setKtMatrixScores(prev => ({
+ ...prev,
+ [criterionName]: score
+ }));
+ };
+
+ const calculateKTScore = () => {
+ let totalWeightedScore = 0;
+ KT_MATRIX_CRITERIA.forEach(criterion => {
+ const score = ktMatrixScores[criterion.name] || 0;
+ const weightedScore = (score / criterion.maxScore) * criterion.weight;
+ totalWeightedScore += weightedScore;
+ });
+ return totalWeightedScore.toFixed(2);
+ };
+
+ const handleSubmitKTMatrix = async () => {
+ if (Object.keys(ktMatrixScores).length < KT_MATRIX_CRITERIA.length) {
+ alert('Please fill all fields in the KT Matrix');
+ return;
+ }
+
+ // Use the selected interview ID or fallback (though UI now forces selection)
+ const interviewId = selectedInterviewIdForFeedback || interviews.find(i => i.status !== 'Completed')?.id || interviews[0]?.id;
+
+ if (!interviewId) {
+ alert('No active interview found to link this KT Matrix to.');
+ return;
+ }
+
+ try {
+ setIsSubmittingKT(true);
+
+ const criteriaScores = KT_MATRIX_CRITERIA.map(c => ({
+ criterionName: c.name,
+ score: ktMatrixScores[c.name] || 0,
+ maxScore: c.maxScore,
+ weightage: c.weight
+ }));
+
+ await onboardingService.submitKTMatrix({
+ interviewId,
+ criteriaScores,
+ feedback: ktMatrixRemarks,
+ recommendation: calculateKTScore() // Or separate recommendation field
+ });
+
+ toast.success('KT Matrix submitted successfully');
+ setShowKTMatrixModal(false);
+
+ // Reset form
+ setKtMatrixScores({});
+ setKtMatrixRemarks('');
+ } catch (error) {
+ toast.error('Failed to submit KT Matrix');
+ } finally {
+ setIsSubmittingKT(false);
+ }
+ };
+
+ // Level 2 Feedback State
+ const [level2Feedback, setLevel2Feedback] = useState({
+ strategicVision: '',
+ managementCapabilities: '',
+ operationalUnderstanding: '',
+ keyStrengths: '',
+ areasOfConcern: '',
+ additionalComments: '',
+ recommendation: '',
+ overallScore: ''
+ });
+ const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false);
+
+ const handleLevel2Change = (field: string, value: string) => {
+ setLevel2Feedback(prev => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmitLevel2Feedback = async () => {
+ if (!level2Feedback.recommendation || !level2Feedback.overallScore) {
+ alert('Please provide an overall score and recommendation.');
+ return;
+ }
+
+ const interviewId = selectedInterviewIdForFeedback || interviews.find(i => i.status !== 'Completed' && i.level === 2)?.id;
+
+ if (!interviewId) {
+ alert('No active Level 2 interview found to link this feedback to.');
+ return;
+ }
+
+ try {
+ setIsSubmittingLevel2(true);
+
+ const feedbackItems = [
+ { type: 'Strategic Vision', comments: level2Feedback.strategicVision },
+ { type: 'Management Capabilities', comments: level2Feedback.managementCapabilities },
+ { type: 'Operational Understanding', comments: level2Feedback.operationalUnderstanding },
+ { type: 'Key Strengths', comments: level2Feedback.keyStrengths },
+ { type: 'Areas of Concern', comments: level2Feedback.areasOfConcern },
+ { type: 'Additional Comments', comments: level2Feedback.additionalComments }
+ ].filter(item => item.comments.trim() !== '');
+
+ await onboardingService.submitLevel2Feedback({
+ interviewId,
+ overallScore: Number(level2Feedback.overallScore),
+ recommendation: level2Feedback.recommendation,
+ feedbackItems
+ });
+
+ toast.success('Level 2 Feedback submitted successfully');
+ setShowLevel2FeedbackModal(false);
+
+ // Reset form
+ setLevel2Feedback({
+ strategicVision: '',
+ managementCapabilities: '',
+ operationalUnderstanding: '',
+ keyStrengths: '',
+ areasOfConcern: '',
+ additionalComments: '',
+ recommendation: '',
+ overallScore: ''
+ });
+ fetchInterviews(); // Refresh to show feedback
+ } catch (error) {
+ toast.error('Failed to submit Level 2 Feedback');
+ } finally {
+ setIsSubmittingLevel2(false);
+ }
+ };
+
+ // Level 3 Feedback State
+ const [level3Feedback, setLevel3Feedback] = useState({
+ strategicVision: '',
+ managementCapabilities: '',
+ operationalUnderstanding: '',
+ keyStrengths: '',
+ areasOfConcern: '',
+ brandAlignment: '',
+ executiveSummary: '',
+ additionalComments: '',
+ recommendation: '',
+ overallScore: ''
+ });
+ const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false);
+
+ const handleLevel3Change = (field: string, value: string) => {
+ setLevel3Feedback(prev => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmitLevel3Feedback = async () => {
+ if (!level3Feedback.recommendation || !level3Feedback.overallScore) {
+ alert('Please provide an overall score and recommendation.');
+ return;
+ }
+
+ const interviewId = selectedInterviewIdForFeedback || interviews.find(i => i.status !== 'Completed' && i.level === 3)?.id;
+
+ if (!interviewId) {
+ alert('No active Level 3 interview found to link this feedback to.');
+ return;
+ }
+
+ try {
+ setIsSubmittingLevel3(true);
+
+ // Level 3 might have slightly different fields or same structure. Assuming same for now.
+ const feedbackItems = [
+ { type: 'Business Vision & Strategy', comments: level3Feedback.strategicVision },
+ { type: 'Leadership & Decision Making', comments: level3Feedback.managementCapabilities },
+ { type: 'Operational & Financial Readiness', comments: level3Feedback.operationalUnderstanding },
+ { type: 'Brand Alignment', comments: level3Feedback.brandAlignment },
+ { type: 'Key Strengths', comments: level3Feedback.keyStrengths },
+ { type: 'Areas of Concern', comments: level3Feedback.areasOfConcern },
+ { type: 'Executive Summary', comments: level3Feedback.executiveSummary },
+ { type: 'Additional Comments', comments: level3Feedback.additionalComments }
+ ].filter(item => item.comments.trim() !== '');
+
+ // Reusing submitLevel2Feedback endpoint as it maps to InterviewFeedback table generic enough for Level 3 too
+ // Or we can create specific one if needed, but logic is identical so reusing service method
+ await onboardingService.submitLevel2Feedback({
+ interviewId,
+ overallScore: Number(level3Feedback.overallScore),
+ recommendation: level3Feedback.recommendation,
+ feedbackItems
+ });
+
+ toast.success('Level 3 Feedback submitted successfully');
+ setShowLevel3FeedbackModal(false);
+
+ // Reset form
+ setLevel3Feedback({
+ strategicVision: '',
+ managementCapabilities: '',
+ operationalUnderstanding: '',
+ keyStrengths: '',
+ areasOfConcern: '',
+ brandAlignment: '',
+ executiveSummary: '',
+ additionalComments: '',
+ recommendation: '',
+ overallScore: ''
+ });
+ fetchInterviews();
+ } catch (error) {
+ toast.error('Failed to submit Level 3 Feedback');
+ } finally {
+ setIsSubmittingLevel3(false);
+ }
+ };
+
+ // Feedback Details Modal State
+ const [selectedEvaluationForView, setSelectedEvaluationForView] = useState(null);
+ const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false);
+
+ const fetchInterviews = async () => {
+ if (applicationId) {
+ try {
+ const data = await onboardingService.getInterviews(applicationId);
+ setInterviews(data || []);
+ } catch (error) {
+ console.error('Failed to fetch interviews', error);
+ }
+ }
+ };
+
+ useEffect(() => {
+ fetchInterviews();
+ }, [applicationId]);
+
+ const handleAddInterviewer = () => {
+ if (!selectedInterviewerId) return;
+ const userToAdd = users.find(u => u.id === selectedInterviewerId);
+ if (userToAdd && !scheduledInterviewParticipants.find(p => p.id === userToAdd.id)) {
+ setScheduledInterviewParticipants([...scheduledInterviewParticipants, userToAdd]);
+ setSelectedInterviewerId('');
+ }
+ };
+
+ const handleRemoveInterviewer = (userId: string) => {
+ setScheduledInterviewParticipants(scheduledInterviewParticipants.filter(p => p.id !== userId));
+ };
+
+ useEffect(() => {
+ if (activeTab === 'documents' && applicationId) {
+ const fetchDocuments = async () => {
+ try {
+ const docs = await onboardingService.getDocuments(applicationId);
+ setDocuments(docs || []);
+ } catch (error) {
+ console.error('Failed to fetch documents', error);
+ }
+ };
+ fetchDocuments();
+ }
+ }, [activeTab, applicationId]);
useEffect(() => {
const fetchUsers = async () => {
@@ -234,7 +659,7 @@ export function ApplicationDetails() {
{
id: 3,
name: 'Shortlist',
- status: ['Shortlisted', 'Level 1 Pending', 'Level 1 Approved', 'Level 2 Pending', 'Level 2 Approved', 'Level 3 Pending', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : 'pending',
+ status: ['Shortlisted', 'Level 1 Pending', 'Level 1 Approved', 'Level 2 Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Pending', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : 'pending',
date: '2025-10-04',
description: 'Application shortlisted by DD',
documentsUploaded: 2
@@ -242,7 +667,7 @@ export function ApplicationDetails() {
{
id: 4,
name: '1st Level Interview',
- status: ['Level 1 Approved', 'Level 2 Pending', 'Level 2 Approved', 'Level 3 Pending', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 1 Pending' ? 'active' : 'pending',
+ status: ['Level 1 Approved', 'Level 2 Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Pending', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 1 Pending' ? 'active' : 'pending',
date: application.level1InterviewDate,
description: 'DD-ZM + RBM evaluation',
evaluators: ['DD-ZM', 'RBM'],
@@ -251,7 +676,7 @@ export function ApplicationDetails() {
{
id: 5,
name: '2nd Level Interview',
- status: ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Pending', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 2 Pending' ? 'active' : 'pending',
+ status: ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Pending', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : ['Level 2 Pending', 'Level 2 Interview Pending'].includes(application.status) ? 'active' : 'pending',
date: application.level2InterviewDate,
description: 'DD Lead + ZBH evaluation',
evaluators: ['DD Lead', 'ZBH'],
@@ -260,7 +685,7 @@ export function ApplicationDetails() {
{
id: 6,
name: '3rd Level Interview',
- status: ['FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 3 Pending' ? 'active' : 'pending',
+ status: ['Level 3 Approved', 'FDD Verification', 'Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : ['Level 3 Pending', 'Level 3 Interview Pending'].includes(application.status) ? 'active' : 'pending',
date: application.level3InterviewDate,
description: 'NBH + DD-Head evaluation',
evaluators: ['NBH', 'DD-Head'],
@@ -625,18 +1050,57 @@ export function ApplicationDetails() {
return;
}
try {
+ setIsScheduling(true);
await onboardingService.scheduleInterview({
applicationId,
level: interviewType,
scheduledAt: interviewDate,
type: interviewMode,
- location: interviewMode === 'virtual' ? meetingLink : location,
- participants: [] // Optional: add multi-participant support if needed
+ location: interviewMode === 'physical' ? location : meetingLink,
+ participants: scheduledInterviewParticipants.map(u => u.id)
});
- alert('Interview scheduled successfully!');
+ toast.success('Interview scheduled successfully');
setShowScheduleModal(false);
+ setScheduledInterviewParticipants([]); // Reset
+ setInterviewDate('');
+ setMeetingLink('');
+ setLocation('');
+ fetchInterviews(); // Refresh list
+ fetchApplication(); // Refresh application status
} catch (error) {
alert('Failed to schedule interview');
+ } finally {
+ setIsScheduling(false);
+ }
+ };
+
+ const handleUpload = async () => {
+ if (!uploadFile || !uploadDocType) {
+ alert('Please select a file and document type');
+ return;
+ }
+
+ try {
+ setIsUploading(true);
+ const formData = new FormData();
+ formData.append('file', uploadFile);
+ formData.append('documentType', uploadDocType);
+
+ await onboardingService.uploadDocument(applicationId, formData);
+
+ alert('Document uploaded successfully');
+ setShowUploadModal(false);
+ setUploadFile(null);
+ setUploadDocType('');
+
+ // Refresh documents
+ const docs = await onboardingService.getDocuments(applicationId);
+ setDocuments(docs || []);
+ } catch (error) {
+ console.error('Upload failed', error);
+ alert('Failed to upload document');
+ } finally {
+ setIsUploading(false);
}
};
@@ -1019,7 +1483,7 @@ export function ApplicationDetails() {
Uploaded Documents
-
+
+
+
+ {/* Upload Document Modal */}
+
@@ -1877,7 +2375,10 @@ export function ApplicationDetails() {