From e786ea12cf33c43042fa33b7f2f4fa990b59b1bf Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Wed, 18 Feb 2026 20:03:11 +0530 Subject: [PATCH] enhanced the questionnaraire ui and added upto interview lvel 3 implemented --- src/App.tsx | 44 +- src/api/API.ts | 13 + src/api/client.ts | 3 +- .../applications/ApplicationDetails.tsx | 1306 ++++++++++++----- src/components/auth/LoginPage.tsx | 19 + src/components/auth/ProspectiveLoginPage.tsx | 235 +++ src/components/auth/RoleGuard.tsx | 45 + .../dashboard/ProspectiveDashboardPage.tsx | 332 +++++ src/lib/mock-data.ts | 4 +- src/pages/public/PublicQuestionnairePage.tsx | 371 ++++- src/services/onboarding.service.ts | 67 +- 11 files changed, 2017 insertions(+), 422 deletions(-) create mode 100644 src/components/auth/ProspectiveLoginPage.tsx create mode 100644 src/components/auth/RoleGuard.tsx create mode 100644 src/components/dashboard/ProspectiveDashboardPage.tsx 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

- @@ -1036,56 +1500,159 @@ export function ApplicationDetails() { - {mockDocuments.map((doc) => ( - - - - {doc.name} - - {doc.type} - {new Date(doc.uploadDate).toLocaleDateString()} - {doc.uploader || '-'} - -
- -
+ {documents.length === 0 ? ( + + + No documents uploaded yet - ))} + ) : ( + documents.map((doc) => ( + + + + {doc.fileName} + + {doc.documentType} + {new Date(doc.createdAt).toLocaleDateString()} + + {doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')} + + +
+ +
+
+
+ )))}
+ {/* Interviews Tab */} {/* Interviews Tab */}
-

Level 1 Interview - KT Matrix

+

Scheduled Interviews

- Interviewer - Role - Score - Remarks - Feedback + Level + Date & Time + Type + Location/Link + Status + Scheduled By - {mockLevel1Scores.map((score, idx) => ( - - {score.user} - {score.role} - {score.score}/50 - {score.remarks} - {score.feedback} + {interviews.length === 0 ? ( + + + No interviews scheduled yet + - ))} + ) : ( + interviews.map((interview) => ( + + Level {interview.level} + {interview.scheduleDate ? new Date(interview.scheduleDate).toLocaleString() : 'N/A'} + {interview.interviewType} + + {interview.interviewType === 'virtual' ? ( + + Join Meeting + + ) : ( + interview.linkOrLocation + )} + + + + {interview.status} + + + {interview.scheduler?.fullName || interview.scheduledBy || 'N/A'} + + )) + )}
+
+

Interview Feedback

+ {interviews.length === 0 ? ( +

No interviews scheduled.

+ ) : ( + interviews.map((interview) => ( +
+

+ Level {interview.level} Interview + + ({new Date(interview.scheduleDate).toLocaleDateString()} - {interview.interviewType}) + +

+ {interview.evaluations && interview.evaluations.length > 0 ? ( + + + + Interviewer + Role + + {interview.level === 1 ? 'Score (KT Matrix)' : 'Overall Score'} + + Remarks + Recommendation + + + + {interview.evaluations.map((evalItem: any) => ( + + {evalItem.evaluator?.fullName} + {evalItem.evaluator?.role?.roleName || 'N/A'} + + {evalItem.ktMatrixScore ? ( + = 50 ? 'outline' : 'destructive') + : (Number(evalItem.ktMatrixScore) >= 5 ? 'outline' : 'destructive') + }> + {evalItem.ktMatrixScore}/{interview.level === 1 ? '100' : '10'} + + ) : 'N/A'} + + + {evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 ? ( + + ) : ( + evalItem.qualitativeFeedback || '-' + )} + + {evalItem.recommendation || '-'} + + ))} + +
+ ) : ( +

No feedback recorded yet.

+ )} +
+ )) + )} +
+ {['Level 2 Approved', 'Level 3 Pending', 'Approved'].includes(application.status) && (

Level 2 Interview Summary

@@ -1272,15 +1839,23 @@ export function ApplicationDetails() { - setShowKTMatrixModal(true)}> - Fill KT Matrix - - setShowLevel2FeedbackModal(true)}> - Level 2 Feedback - - setShowLevel3FeedbackModal(true)}> - Level 3 Feedback - + {interviews.length === 0 ? ( + No interviews scheduled + ) : ( + interviews.map(interview => ( + { + setSelectedInterviewIdForFeedback(interview.id); // Storing interview ID here + if (interview.level === 1) setShowKTMatrixModal(true); + else if (interview.level === 2) setShowLevel2FeedbackModal(true); + else setShowLevel3FeedbackModal(true); + }} + > + Level {interview.level} - {interview.interviewType} + + )) + )} @@ -1576,19 +2151,119 @@ export function ApplicationDetails() { />
)} + + {/* Interviewer Selection */} +
+ +
+ + +
+ + {/* Selected Interviewers List */} + {scheduledInterviewParticipants.length > 0 && ( +
+ +
+ {scheduledInterviewParticipants.map((p) => ( +
+ {p.fullName} + +
+ ))} +
+
+ )} +
+
+
+ + + + {/* Upload Document Modal */} + + + + Upload Document + + Select the document type and upload the file. + + +
+
+ + +
+
+ + setUploadFile(e.target.files ? e.target.files[0] : null)} + /> +
+
+ +
@@ -1606,211 +2281,30 @@ export function ApplicationDetails() {
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
+ {KT_MATRIX_CRITERIA.map((criterion) => ( +
+ + +
+ ))}
@@ -1819,6 +2313,9 @@ export function ApplicationDetails() {

Total Weightage: 100%

+

+ Current Score: {calculateKTScore()}/100 +

All parameters will be scored and calculated automatically based on the selected options.

@@ -1830,6 +2327,8 @@ export function ApplicationDetails() { placeholder="Enter additional remarks, observations, and recommendations..." className="mt-2" rows={4} + value={ktMatrixRemarks} + onChange={(e) => setKtMatrixRemarks(e.target.value)} />
@@ -1838,17 +2337,16 @@ export function ApplicationDetails() { variant="outline" className="flex-1" onClick={() => setShowKTMatrixModal(false)} + disabled={isSubmittingKT} > Cancel
@@ -1877,7 +2375,10 @@ export function ApplicationDetails() {
- handleLevel2Change('overallScore', value)} + > @@ -1899,6 +2400,8 @@ export function ApplicationDetails() { placeholder="Evaluate the candidate's strategic thinking and long-term vision..." className="mt-2" rows={3} + value={level2Feedback.strategicVision} + onChange={(e) => handleLevel2Change('strategicVision', e.target.value)} />
@@ -1908,6 +2411,8 @@ export function ApplicationDetails() { placeholder="Assess leadership and team management potential..." className="mt-2" rows={3} + value={level2Feedback.managementCapabilities} + onChange={(e) => handleLevel2Change('managementCapabilities', e.target.value)} /> @@ -1917,6 +2422,8 @@ export function ApplicationDetails() { placeholder="Review understanding of dealership operations and processes..." className="mt-2" rows={3} + value={level2Feedback.operationalUnderstanding} + onChange={(e) => handleLevel2Change('operationalUnderstanding', e.target.value)} /> @@ -1926,6 +2433,8 @@ export function ApplicationDetails() { placeholder="List the candidate's key strengths and positive attributes..." className="mt-2" rows={3} + value={level2Feedback.keyStrengths} + onChange={(e) => handleLevel2Change('keyStrengths', e.target.value)} /> @@ -1935,19 +2444,24 @@ export function ApplicationDetails() { placeholder="Highlight any concerns or areas needing improvement..." className="mt-2" rows={3} + value={level2Feedback.areasOfConcern} + onChange={(e) => handleLevel2Change('areasOfConcern', e.target.value)} />
- handleLevel2Change('recommendation', value)} + > - Proceed to Level 3 - Hold Decision - Reject + Proceed to Level 3 + Hold Decision + Reject
@@ -1958,6 +2472,8 @@ export function ApplicationDetails() { placeholder="Any additional observations or comments..." className="mt-2" rows={3} + value={level2Feedback.additionalComments} + onChange={(e) => handleLevel2Change('additionalComments', e.target.value)} /> @@ -1971,18 +2487,76 @@ export function ApplicationDetails() { + {/* Feedback Details Modal */} + + + + Interview Feedback Details + + {selectedEvaluationForView && ( +
+
+
+

Interviewer

+

{selectedEvaluationForView.evaluator?.fullName}

+
+
+

Role

+

{selectedEvaluationForView.evaluator?.role?.roleName || 'N/A'}

+
+
+

+ {selectedEvaluationForView.interview?.level === 1 ? 'Score (KT Matrix)' : 'Overall Score'} +

+

+ {selectedEvaluationForView.ktMatrixScore ? + `${selectedEvaluationForView.ktMatrixScore}/${selectedEvaluationForView.interview?.level === 1 ? '100' : '10'}` + : 'N/A'} +

+
+
+

Recommendation

+ + {selectedEvaluationForView.recommendation || 'N/A'} + +
+
+ + + +
+

Detailed Feedback

+ {selectedEvaluationForView.feedbackDetails?.length > 0 ? ( +
+ {selectedEvaluationForView.feedbackDetails.map((detail: any, index: number) => ( +
+

{detail.feedbackType}

+

{detail.comments}

+
+ ))} +
+ ) : ( +

No detailed feedback available.

+ )} +
+
+ )} +
+
+ {/* Level 3 Feedback Modal */} @@ -2005,7 +2579,10 @@ export function ApplicationDetails() {
- handleLevel3Change('overallScore', value)} + > @@ -2027,6 +2604,8 @@ export function ApplicationDetails() { placeholder="Evaluate the candidate's long-term business vision and strategic planning..." className="mt-2" rows={3} + value={level3Feedback.strategicVision} + onChange={(e) => handleLevel3Change('strategicVision', e.target.value)} />
@@ -2036,15 +2615,19 @@ export function ApplicationDetails() { placeholder="Assess leadership qualities and decision-making capabilities..." className="mt-2" rows={3} + value={level3Feedback.managementCapabilities} + onChange={(e) => handleLevel3Change('managementCapabilities', e.target.value)} />
- +