hirarchy made stable and flow strted checking end to end level 3 completed

This commit is contained in:
laxman h 2026-03-31 21:09:31 +05:30
parent d2228543b1
commit e68f96a929
14 changed files with 309 additions and 160 deletions

View File

@ -47,6 +47,7 @@ export const API = {
updateArchitectureStatus: (applicationId: string, status: string, remarks?: string) => client.post(`/onboarding/applications/${applicationId}/architecture-status`, { status, remarks }), updateArchitectureStatus: (applicationId: string, status: string, remarks?: string) => client.post(`/onboarding/applications/${applicationId}/architecture-status`, { status, remarks }),
generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`), generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`),
updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data), updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data),
retriggerEvaluators: (id: string) => client.post(`/onboarding/applications/${id}/retrigger-evaluators`),
// Documents // Documents
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, { uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {

View File

@ -8,7 +8,7 @@ interface Question {
id?: string; id?: string;
sectionName: string; sectionName: string;
questionText: string; questionText: string;
inputType: 'text' | 'yesno' | 'file' | 'number' | 'select'; inputType: 'text' | 'yesno' | 'file' | 'number' | 'select' | 'mcq' | 'radio' | 'textarea' | 'email';
options?: { text: string; score: number }[]; options?: { text: string; score: number }[];
weight: number; weight: number;
order: number; order: number;
@ -58,7 +58,7 @@ const QuestionnaireBuilder: React.FC = () => {
if (normalizedType === 'mcq') normalizedType = 'select'; if (normalizedType === 'mcq') normalizedType = 'select';
// Fallback validity check // Fallback validity check
const validTypes = ['text', 'number', 'file', 'yesno', 'select']; const validTypes = ['text', 'number', 'file', 'yesno', 'select', 'radio', 'textarea', 'email', 'mcq'];
if (!validTypes.includes(normalizedType)) normalizedType = 'text'; if (!validTypes.includes(normalizedType)) normalizedType = 'text';
return { return {
@ -295,10 +295,13 @@ const QuestionnaireBuilder: React.FC = () => {
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none bg-white" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none bg-white"
> >
<option value="text">Text Input</option> <option value="text">Text Input</option>
<option value="email">Email Address</option>
<option value="textarea">Long Text (Textarea)</option>
<option value="number">Numeric</option> <option value="number">Numeric</option>
<option value="file">File Upload</option> <option value="file">File Upload</option>
<option value="yesno">Yes / No</option> <option value="yesno">Yes / No</option>
<option value="select">Dropdown / Multi-Choice</option> <option value="select">Multiple Choice (Dropdown)</option>
<option value="radio">Multiple Choice (Radio)</option>
</select> </select>
</div> </div>
@ -308,8 +311,8 @@ const QuestionnaireBuilder: React.FC = () => {
<div className="relative"> <div className="relative">
<input <input
type="number" type="number"
value={q.weight} value={isNaN(q.weight) ? 0 : q.weight}
onChange={(e) => updateQuestion(index, 'weight', parseFloat(e.target.value))} onChange={(e) => updateQuestion(index, 'weight', parseFloat(e.target.value) || 0)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none pl-3 pr-8" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none pl-3 pr-8"
title="Weightage" title="Weightage"
/> />
@ -330,7 +333,7 @@ const QuestionnaireBuilder: React.FC = () => {
</div> </div>
{/* Options Editor for Select/YesNo */} {/* Options Editor for Select/YesNo */}
{(q.inputType === 'select' || q.inputType === 'yesno') && ( {(q.inputType === 'select' || q.inputType === 'yesno' || q.inputType === 'radio' || q.inputType === 'mcq') && (
<div className="w-full mt-4 pl-4 md:pl-16 border-t border-slate-100 pt-4"> <div className="w-full mt-4 pl-4 md:pl-16 border-t border-slate-100 pt-4">
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2"> <label className="block text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">
Answer Options & Scores Answer Options & Scores
@ -349,11 +352,11 @@ const QuestionnaireBuilder: React.FC = () => {
<span className="text-xs text-slate-400 font-medium">Score:</span> <span className="text-xs text-slate-400 font-medium">Score:</span>
<input <input
type="number" type="number"
value={opt.score} value={isNaN(opt.score) ? 0 : opt.score}
max={q.weight} max={isNaN(q.weight) ? 0 : q.weight}
min={0} min={0}
onChange={(e) => updateOption(index, optIndex, 'score', e.target.value)} onChange={(e) => updateOption(index, optIndex, 'score', e.target.value)}
className={`w-20 border ${opt.score > q.weight ? 'border-red-500 text-red-600' : 'border-slate-300'} p-2 rounded-md text-sm focus:ring-1 focus:ring-amber-500 outline-none`} className={`w-20 border ${(opt.score > q.weight) || isNaN(opt.score) ? 'border-red-500 text-red-600' : 'border-slate-300'} p-2 rounded-md text-sm focus:ring-1 focus:ring-amber-500 outline-none`}
/> />
</div> </div>
<button <button

View File

@ -102,7 +102,7 @@ export function UserManagementPage() {
useEffect(() => { useEffect(() => {
if (formData.zoneId) { if (formData.zoneId) {
masterService.getStates(formData.zoneId).then((res: any) => { masterService.getStates(formData.zoneId).then((res: any) => {
if (res.success) setStates(normalizeList(res, 'states')); if (res && res.success) setStates(normalizeList(res, 'states'));
}); });
} else { } else {
setStates([]); setStates([]);
@ -113,7 +113,7 @@ export function UserManagementPage() {
useEffect(() => { useEffect(() => {
if (formData.stateId) { if (formData.stateId) {
masterService.getDistricts(formData.stateId).then((res: any) => { masterService.getDistricts(formData.stateId).then((res: any) => {
if (res.success) setDistricts(normalizeList(res, 'districts')); if (res && res.success) setDistricts(normalizeList(res, 'districts'));
}); });
} else { } else {
setDistricts([]); setDistricts([]);
@ -124,7 +124,7 @@ export function UserManagementPage() {
useEffect(() => { useEffect(() => {
if (formData.districtId) { if (formData.districtId) {
masterService.getAreas(formData.districtId).then((res: any) => { masterService.getAreas(formData.districtId).then((res: any) => {
if (res.success) setAreas(normalizeList(res, 'areas')); if (res && res.success) setAreas(normalizeList(res, 'areas'));
}); });
} else { } else {
setAreas([]); setAreas([]);

View File

@ -42,6 +42,8 @@ import { Progress } from '../ui/progress';
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
import { Label } from '../ui/label'; import { Label } from '../ui/label';
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
import { AlertCircle, RefreshCw } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '../ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '../ui/dialog';
import { ScrollArea } from '../ui/scroll-area'; import { ScrollArea } from '../ui/scroll-area';
import { import {
@ -313,6 +315,7 @@ export function ApplicationDetails() {
zoneId: data.zoneId, zoneId: data.zoneId,
regionId: data.regionId, regionId: data.regionId,
areaId: data.areaId, areaId: data.areaId,
districtId: data.districtId,
}; };
setApplication(mappedApp); setApplication(mappedApp);
} catch (error) { } catch (error) {
@ -336,21 +339,10 @@ export function ApplicationDetails() {
const resp = await eorService.getChecklist(applicationId); const resp = await eorService.getChecklist(applicationId);
if (resp.success && resp.data) { if (resp.success && resp.data) {
setEorData(resp.data); setEorData(resp.data);
} else {
// Auto-create if not found
await eorService.createChecklist(applicationId);
const retry = await eorService.getChecklist(applicationId);
if (retry.success) setEorData(retry.data);
} }
} catch (err) { } catch (err) {
console.log('EOR not found, attempting auto-create...'); console.log('EOR checklist not found or not yet initiated.');
try { setEorData(null);
await eorService.createChecklist(applicationId);
const retry = await eorService.getChecklist(applicationId);
if (retry.success) setEorData(retry.data);
} catch (createErr) {
console.error('Fetch/Create EOR error:', createErr);
}
} }
}; };
@ -384,9 +376,11 @@ export function ApplicationDetails() {
} }
}, [applicationId]); }, [applicationId]);
const [activeTab, setActiveTab] = useState('questionnaire'); const [activeTab, setActiveTab ] = useState('questionnaire');
const [showApproveModal, setShowApproveModal] = useState(false); const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false); const [showRejectModal, setShowRejectModal] = useState(false);
const [rejectionReason, setRejectionReason] = useState('');
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
const [showWorkNoteModal, setShowWorkNoteModal] = useState(false); const [showWorkNoteModal, setShowWorkNoteModal] = useState(false);
const [showScheduleModal, setShowScheduleModal] = useState(false); const [showScheduleModal, setShowScheduleModal] = useState(false);
@ -398,7 +392,6 @@ export function ApplicationDetails() {
const [selectedStage, setSelectedStage] = useState<string | null>(null); const [selectedStage, setSelectedStage] = useState<string | null>(null);
const [interviewMode, setInterviewMode] = useState('virtual'); const [interviewMode, setInterviewMode] = useState('virtual');
const [approvalRemark, setApprovalRemark] = useState(''); const [approvalRemark, setApprovalRemark] = useState('');
const [rejectionReason, setRejectionReason] = useState('');
const [workNote, setWorkNote] = useState(''); const [workNote, setWorkNote] = useState('');
const [expandedBranches, setExpandedBranches] = useState<{ [key: string]: boolean }>({ const [expandedBranches, setExpandedBranches] = useState<{ [key: string]: boolean }>({
'architectural-work': true, 'architectural-work': true,
@ -418,7 +411,6 @@ export function ApplicationDetails() {
const [approvalFile, setApprovalFile] = useState<File | null>(null); // State for approval modal file const [approvalFile, setApprovalFile] = useState<File | null>(null); // State for approval modal file
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [selectedInterviewerId, setSelectedInterviewerId] = useState<string>(''); const [selectedInterviewerId, setSelectedInterviewerId] = useState<string>('');
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
const [interviews, setInterviews] = useState<any[]>([]); const [interviews, setInterviews] = useState<any[]>([]);
const [isScheduling, setIsScheduling] = useState(false); const [isScheduling, setIsScheduling] = useState(false);
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false); const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
@ -481,7 +473,7 @@ export function ApplicationDetails() {
interviewId, interviewId,
criteriaScores, criteriaScores,
feedback: ktMatrixRemarks, feedback: ktMatrixRemarks,
recommendation: calculateKTScore() // Or separate recommendation field recommendation: null // No auto-decision
}); });
toast.success('KT Matrix submitted successfully'); toast.success('KT Matrix submitted successfully');
@ -490,7 +482,8 @@ export function ApplicationDetails() {
// Reset form // Reset form
setKtMatrixScores({}); setKtMatrixScores({});
setKtMatrixRemarks(''); setKtMatrixRemarks('');
await fetchInterviews(); // Silent refresh await fetchInterviews();
await fetchApplication(); // Refresh application status and progress
} catch (error) { } catch (error) {
toast.error('Failed to submit KT Matrix'); toast.error('Failed to submit KT Matrix');
} finally { } finally {
@ -506,8 +499,9 @@ export function ApplicationDetails() {
keyStrengths: '', keyStrengths: '',
areasOfConcern: '', areasOfConcern: '',
additionalComments: '', additionalComments: '',
recommendation: '', overallScore: '',
overallScore: '' interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
}); });
const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false); const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false);
@ -543,7 +537,6 @@ export function ApplicationDetails() {
await onboardingService.submitLevel2Feedback({ await onboardingService.submitLevel2Feedback({
interviewId, interviewId,
overallScore: Number(level2Feedback.overallScore), overallScore: Number(level2Feedback.overallScore),
recommendation: level2Feedback.recommendation,
feedbackItems feedbackItems
}); });
@ -558,10 +551,12 @@ export function ApplicationDetails() {
keyStrengths: '', keyStrengths: '',
areasOfConcern: '', areasOfConcern: '',
additionalComments: '', additionalComments: '',
recommendation: '', overallScore: '',
overallScore: '' interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
}); });
fetchInterviews(); // Refresh to show feedback fetchInterviews(); // Refresh to show feedback
fetchApplication(); // Refresh application status
} catch (error) { } catch (error) {
toast.error('Failed to submit Level 2 Feedback'); toast.error('Failed to submit Level 2 Feedback');
} finally { } finally {
@ -574,13 +569,14 @@ export function ApplicationDetails() {
strategicVision: '', strategicVision: '',
managementCapabilities: '', managementCapabilities: '',
operationalUnderstanding: '', operationalUnderstanding: '',
keyStrengths: '',
areasOfConcern: '',
brandAlignment: '', brandAlignment: '',
executiveSummary: '', executiveSummary: '',
keyStrengths: '',
areasOfConcern: '',
additionalComments: '', additionalComments: '',
recommendation: '', overallScore: '',
overallScore: '' interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
}); });
const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false); const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false);
@ -621,7 +617,6 @@ export function ApplicationDetails() {
await onboardingService.submitLevel2Feedback({ await onboardingService.submitLevel2Feedback({
interviewId, interviewId,
overallScore: Number(level3Feedback.overallScore), overallScore: Number(level3Feedback.overallScore),
recommendation: level3Feedback.recommendation,
feedbackItems feedbackItems
}); });
@ -633,15 +628,17 @@ export function ApplicationDetails() {
strategicVision: '', strategicVision: '',
managementCapabilities: '', managementCapabilities: '',
operationalUnderstanding: '', operationalUnderstanding: '',
keyStrengths: '',
areasOfConcern: '',
brandAlignment: '', brandAlignment: '',
executiveSummary: '', executiveSummary: '',
keyStrengths: '',
areasOfConcern: '',
additionalComments: '', additionalComments: '',
recommendation: '', overallScore: '',
overallScore: '' interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
}); });
fetchInterviews(); fetchInterviews();
fetchApplication();
} catch (error) { } catch (error) {
toast.error('Failed to submit Level 3 Feedback'); toast.error('Failed to submit Level 3 Feedback');
} finally { } finally {
@ -713,7 +710,7 @@ export function ApplicationDetails() {
// Include location from the application // Include location from the application
if (application) { if (application) {
params.locationId = application.locationId || application.areaId || application.regionId || application.zoneId; params.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
} }
} }
@ -735,12 +732,25 @@ export function ApplicationDetails() {
}; };
useEffect(() => { useEffect(() => {
if (showScheduleModal) { if (showScheduleModal && application) {
fetchUsers(interviewType); fetchUsers(interviewType);
// Auto-fill participants based on pre-assigned evaluators for this level
const levelNum = parseInt(interviewType.replace('level', '')) || 1;
const preAssigned = (application?.participants || [])
.filter((p: any) => p.metadata?.interviewLevel === levelNum || p.metadata?.interviewLevel === String(levelNum))
.map((p: any) => p.user)
.filter(Boolean);
if (preAssigned.length > 0) {
setScheduledInterviewParticipants(preAssigned);
} else { } else {
setScheduledInterviewParticipants([]);
}
} else if (showScheduleModal && application) {
fetchUsers(); // Default fetch for other modals like Assign fetchUsers(); // Default fetch for other modals like Assign
} }
}, [showScheduleModal, interviewType]); }, [showScheduleModal, interviewType, application?.participants]);
const handleScheduleInterview = async () => { const handleScheduleInterview = async () => {
if (!interviewDate) { if (!interviewDate) {
@ -754,8 +764,8 @@ export function ApplicationDetails() {
applicationId: application?.id, applicationId: application?.id,
level: interviewType, level: interviewType,
scheduledAt: interviewDate, scheduledAt: interviewDate,
type: interviewMode, // 'virtual' | 'physical' type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview',
location: interviewMode === 'physical' ? location : meetingLink, location: interviewMode === 'virtual' ? meetingLink : location,
participants: scheduledInterviewParticipants.map(p => p.id) participants: scheduledInterviewParticipants.map(p => p.id)
}; };
@ -763,8 +773,8 @@ export function ApplicationDetails() {
toast.success('Interview scheduled successfully'); toast.success('Interview scheduled successfully');
setShowScheduleModal(false); setShowScheduleModal(false);
// Refresh interviews // Refresh interviews
fetchInterviews(); await fetchInterviews();
fetchApplication(); // Refresh application status await fetchApplication(); // Refresh application status
} catch (error) { } catch (error) {
toast.error('Failed to schedule interview'); toast.error('Failed to schedule interview');
@ -842,7 +852,7 @@ export function ApplicationDetails() {
{ {
id: 3, id: 3,
name: 'Shortlist', name: 'Shortlist',
status: ['Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', '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 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Rejected'].includes(application.status) ? 'completed' : 'pending',
date: '2025-10-04', date: '2025-10-04',
description: 'Application shortlisted by DD', description: 'Application shortlisted by DD',
documentsUploaded: 2 documentsUploaded: 2
@ -850,28 +860,34 @@ export function ApplicationDetails() {
{ {
id: 4, id: 4,
name: '1st Level Interview', name: '1st Level Interview',
status: ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', '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 Interview Pending' ? 'active' : 'pending', status: ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', '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 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 40 ? 'completed' : 'pending',
date: application.level1InterviewDate, date: application.level1InterviewDate,
description: 'DD-ZM + RBM evaluation', description: 'DD-ZM + RBM evaluation',
evaluators: ['DD-ZM', 'RBM'], evaluators: (application.participants || [])
.filter((p: any) => p.metadata?.interviewLevel === 1 || (p.metadata?.interviewLevel === '1'))
.map((p: any) => `${p.user?.name} (${p.user?.role})`),
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
id: 5, id: 5,
name: '2nd Level Interview', name: '2nd Level Interview',
status: ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', '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 Interview Pending'].includes(application.status) ? 'active' : 'pending', status: ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', '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 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 55 ? 'completed' : 'pending',
date: application.level2InterviewDate, date: application.level2InterviewDate,
description: 'DD Lead + ZBH evaluation', description: 'DD Lead + ZBH evaluation',
evaluators: ['DD Lead', 'ZBH'], evaluators: (application.participants || [])
.filter((p: any) => p.metadata?.interviewLevel === 2 || (p.metadata?.interviewLevel === '2'))
.map((p: any) => `${p.user?.name} (${p.user?.role})`),
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
id: 6, id: 6,
name: '3rd Level Interview', name: '3rd Level Interview',
status: ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', '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 Interview Pending'].includes(application.status) ? 'active' : 'pending', status: ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', '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 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 70 ? 'completed' : 'pending',
date: application.level3InterviewDate, date: application.level3InterviewDate,
description: 'NBH + DD Head evaluation', description: 'NBH + DD Head evaluation',
evaluators: ['NBH', 'DD Head'], evaluators: (application.participants || [])
.filter((p: any) => p.metadata?.interviewLevel === 3 || (p.metadata?.interviewLevel === '3'))
.map((p: any) => `${p.user?.name} (${p.user?.role})`),
documentsUploaded: 2 documentsUploaded: 2
}, },
{ {
@ -888,6 +904,9 @@ export function ApplicationDetails() {
status: ['Payment Pending', 'LOI Issued', 'Statutory LOI Ack', '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 === 'LOI In Progress' ? 'active' : 'pending', status: ['Payment Pending', 'LOI Issued', 'Statutory LOI Ack', '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 === 'LOI In Progress' ? 'active' : 'pending',
date: application.loiApprovalDate, date: application.loiApprovalDate,
description: 'Letter of Intent approval', description: 'Letter of Intent approval',
evaluators: (application.participants || [])
.filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL')
.map((p: any) => `${p.user?.name} (${p.user?.role})`),
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
@ -1036,6 +1055,9 @@ export function ApplicationDetails() {
status: ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending', status: ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending',
date: application.loaDate, date: application.loaDate,
description: 'Letter of Authorization', description: 'Letter of Authorization',
evaluators: (application.participants || [])
.filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL')
.map((p: any) => `${p.user?.name} (${p.user?.role})`),
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
@ -1153,10 +1175,7 @@ export function ApplicationDetails() {
setApprovalFile(null); // Reset file setApprovalFile(null); // Reset file
fetchInterviews(); fetchInterviews();
// Refresh application to check if status updated // Refresh application to check if status updated
if (id) { fetchApplication();
const appData = await onboardingService.getApplicationById(id);
setApplication(appData);
}
return; return;
} catch (error) { } catch (error) {
toast.error('Failed to approve interview'); toast.error('Failed to approve interview');
@ -1268,10 +1287,7 @@ export function ApplicationDetails() {
setShowRejectModal(false); setShowRejectModal(false);
setRejectionReason(''); setRejectionReason('');
fetchInterviews(); fetchInterviews();
if (id) { fetchApplication();
const appData = await onboardingService.getApplicationById(id);
setApplication(appData);
}
return; return;
} catch (error) { } catch (error) {
toast.error('Failed to reject interview'); toast.error('Failed to reject interview');
@ -1362,16 +1378,28 @@ export function ApplicationDetails() {
requestId: applicationId, requestId: applicationId,
requestType: 'application', requestType: 'application',
userId: selectedUser, userId: selectedUser,
participantType participantType: 'contributor'
}); });
alert('User assigned successfully!'); toast.success('User assigned successfully!');
// Refresh application data // Refresh application data
const data = await onboardingService.getApplicationById(applicationId); fetchApplication();
setApplication({ ...application, participants: data.participants || [] });
setSelectedUser(''); setSelectedUser('');
setShowAssignModal(false); setShowAssignModal(false);
} catch (error) { } catch (error) {
alert('Failed to assign user'); toast.error('Failed to assign user');
}
};
const handleRetriggerEvaluators = async () => {
try {
setLoading(true);
await onboardingService.retriggerEvaluators(applicationId!);
toast.success('Evaluators re-assigned successfully');
await fetchApplication();
} catch (error) {
toast.error('Failed to re-assign evaluators');
} finally {
setLoading(false);
} }
}; };
@ -1401,6 +1429,11 @@ export function ApplicationDetails() {
(e: any) => e.evaluatorId === currentUser?.id (e: any) => e.evaluatorId === currentUser?.id
); );
// Helper to check interview level completion
const isInterviewCompleted = (level: number) => {
return interviewsList.some(i => (Number(i.level) === level) && i.status === 'Completed');
};
// Robust checks for feedback and decision // Robust checks for feedback and decision
// 1. If there's an active interview, feedback is required before Approve/Reject // 1. If there's an active interview, feedback is required before Approve/Reject
// 2. hasMadeDecision should check if the evaluation has a recommendation // 2. hasMadeDecision should check if the evaluation has a recommendation
@ -1673,11 +1706,50 @@ export function ApplicationDetails() {
{stage.description && ( {stage.description && (
<p className="text-slate-600 text-sm mt-0.5">{stage.description}</p> <p className="text-slate-600 text-sm mt-0.5">{stage.description}</p>
)} )}
{stage.evaluators && ( {stage.evaluators && stage.evaluators.length > 0 ? (
<p className="text-amber-600 text-sm mt-0.5"> <p className="text-amber-600 text-sm mt-0.5">
Evaluators: {stage.evaluators.join(' + ')} Evaluators: {stage.evaluators.join(' + ')}
</p> </p>
)} ) : (() => {
// Determine expected count for this stage
const expectedMap: Record<number, number> = {
4: 2, // L1 Interview (ZM + RBM)
5: 2, // L2 Interview (ZBH + DD Lead)
6: 2, // L3 Interview (NBH + DD Head)
8: 3, // LOI Approval (Finance + DD Head + NBH)
12: 2 // LOA Approval (DD Head + NBH)
};
const stageId = Number(stage.id);
const expectedCount = expectedMap[stageId];
const actualCount = stage.evaluators?.length || 0;
if (expectedCount && actualCount < expectedCount && application.status !== 'Rejected') {
return (
<div className="mt-2">
<Alert variant="destructive" className="py-2 px-3 border-amber-200 bg-amber-50 text-amber-800">
<AlertCircle className="h-4 w-4 text-amber-600" />
<AlertTitle className="text-xs font-semibold">Missing Evaluators</AlertTitle>
<AlertDescription className="text-xs">
{actualCount === 0
? "Respective role users were not found for this location."
: `Some roles (${actualCount}/${expectedCount}) are missing for this location.`
}
<Button
variant="link"
size="sm"
className="h-auto p-0 ml-1 text-xs text-amber-700 underline"
onClick={handleRetriggerEvaluators}
>
<RefreshCw className="w-3 h-3 mr-1" />
Re-trigger Assignment
</Button>
</AlertDescription>
</Alert>
</div>
);
}
return null;
})()}
{/* Stage Docs Link */} {/* Stage Docs Link */}
{(() => { {(() => {
const stageDocsCount = documents.filter(doc => const stageDocsCount = documents.filter(doc =>
@ -2344,7 +2416,8 @@ export function ApplicationDetails() {
Work Note Work Note
</Button> </Button>
{currentUser && ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) && ( {currentUser && ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) &&
!([1, 2, 3].every(level => interviews.some(i => i.level === level))) && (
<Button <Button
variant="outline" variant="outline"
className="w-full" className="w-full"
@ -2717,9 +2790,26 @@ export function ApplicationDetails() {
<SelectValue placeholder="Select interview type" /> <SelectValue placeholder="Select interview type" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="level1">Level 1</SelectItem> <SelectItem value="level1" disabled={isInterviewCompleted(1)}>
<SelectItem value="level2">Level 2</SelectItem> <div className="flex items-center justify-between w-full">
<SelectItem value="level3">Level 3</SelectItem> <span>Level 1</span>
{isInterviewCompleted(1) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}
</div>
</SelectItem>
<SelectItem value="level2" disabled={!isInterviewCompleted(1) || isInterviewCompleted(2)}>
<div className="flex items-center justify-between w-full">
<span>Level 2</span>
{!isInterviewCompleted(1) && <span className="text-[10px] text-slate-400 ml-2">(Prerequisite: L1)</span>}
{isInterviewCompleted(2) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}
</div>
</SelectItem>
<SelectItem value="level3" disabled={!isInterviewCompleted(2) || isInterviewCompleted(3)}>
<div className="flex items-center justify-between w-full">
<span>Level 3</span>
{!isInterviewCompleted(2) && <span className="text-[10px] text-slate-400 ml-2">(Prerequisite: L2)</span>}
{isInterviewCompleted(3) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}
</div>
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -2778,7 +2868,7 @@ export function ApplicationDetails() {
<SelectContent> <SelectContent>
{users.map((user) => ( {users.map((user) => (
<SelectItem key={user.id} value={user.id}> <SelectItem key={user.id} value={user.id}>
{user.fullName} ({user.role?.roleName || user.roleCode}) {user.fullName || user.name} ({user.role?.roleName || user.roleCode})
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@ -2795,7 +2885,7 @@ export function ApplicationDetails() {
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{scheduledInterviewParticipants.map((p) => ( {scheduledInterviewParticipants.map((p) => (
<div key={p.id} className="flex items-center gap-1 bg-secondary px-2 py-1 rounded text-sm"> <div key={p.id} className="flex items-center gap-1 bg-secondary px-2 py-1 rounded text-sm">
<span>{p.fullName}</span> <span>{p.fullName || p.name || 'Unknown'}</span>
<button <button
onClick={() => handleRemoveInterviewer(p.id)} onClick={() => handleRemoveInterviewer(p.id)}
className="text-muted-foreground hover:text-destructive" className="text-muted-foreground hover:text-destructive"
@ -2819,7 +2909,10 @@ export function ApplicationDetails() {
</Button> </Button>
<Button <Button
className="flex-1 bg-amber-600 hover:bg-amber-700" className="flex-1 bg-amber-600 hover:bg-amber-700"
onClick={handleScheduleInterview} onClick={() => {
// Ensure we pass participants to the schedule handler
handleScheduleInterview();
}}
disabled={isScheduling} disabled={isScheduling}
> >
{isScheduling ? 'Scheduling...' : 'Schedule'} {isScheduling ? 'Scheduling...' : 'Schedule'}
@ -3008,7 +3101,7 @@ export function ApplicationDetails() {
Cancel Cancel
</Button> </Button>
<Button <Button
className="flex-1 bg-amber-600 hover:bg-amber-700" className="flex-1 bg-black hover:bg-zinc-800 text-white"
onClick={handleSubmitKTMatrix} onClick={handleSubmitKTMatrix}
disabled={isSubmittingKT} disabled={isSubmittingKT}
> >
@ -3031,12 +3124,22 @@ export function ApplicationDetails() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Label>Interview Date</Label> <Label>Interview Date</Label>
<Input type="date" className="mt-2" /> <Input
type="date"
className="mt-2"
value={level2Feedback.interviewDate}
disabled
/>
</div> </div>
<div> <div>
<Label>Interviewer Name</Label> <Label>Interviewer Name</Label>
<Input placeholder="Enter your name" className="mt-2" /> <Input
placeholder="Enter your name"
className="mt-2"
value={level2Feedback.interviewerName}
disabled
/>
</div> </div>
<div> <div>
@ -3137,7 +3240,7 @@ export function ApplicationDetails() {
Cancel Cancel
</Button> </Button>
<Button <Button
className="flex-1 bg-blue-600 hover:bg-blue-700" className="flex-1 bg-black hover:bg-zinc-800 text-white"
onClick={handleSubmitLevel2Feedback} onClick={handleSubmitLevel2Feedback}
disabled={isSubmittingLevel2} disabled={isSubmittingLevel2}
> >
@ -3220,12 +3323,22 @@ export function ApplicationDetails() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Label>Interview Date</Label> <Label>Interview Date</Label>
<Input type="date" className="mt-2" /> <Input
type="date"
className="mt-2"
value={level3Feedback.interviewDate}
disabled
/>
</div> </div>
<div> <div>
<Label>Interviewer Name</Label> <Label>Interviewer Name</Label>
<Input placeholder="Enter your name" className="mt-2" /> <Input
placeholder="Enter your name"
className="mt-2"
value={level3Feedback.interviewerName}
disabled
/>
</div> </div>
<div> <div>
@ -3315,23 +3428,6 @@ export function ApplicationDetails() {
/> />
</div> </div>
<div>
<Label>Final Recommendation</Label>
<Select
value={level3Feedback.recommendation}
onValueChange={(value) => handleLevel3Change('recommendation', value)}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve for Onboarding</SelectItem>
<SelectItem value="Hold">Hold Decision</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
</SelectContent>
</Select>
</div>
<div> <div>
<Label>Executive Summary</Label> <Label>Executive Summary</Label>
<Textarea <Textarea
@ -3352,7 +3448,7 @@ export function ApplicationDetails() {
Cancel Cancel
</Button> </Button>
<Button <Button
className="flex-1 bg-green-700 hover:bg-green-800" className="flex-1 bg-black hover:bg-zinc-800 text-white"
onClick={handleSubmitLevel3Feedback} onClick={handleSubmitLevel3Feedback}
disabled={isSubmittingLevel3} disabled={isSubmittingLevel3}
> >
@ -3566,7 +3662,9 @@ export function ApplicationDetails() {
</div> </div>
)} )}
</DialogContent> </DialogContent>
</Dialog > </Dialog>
</div > </div>
); );
} };
export default ApplicationDetails;

View File

@ -98,7 +98,7 @@ export const MasterPage: React.FC = () => {
const [zoneName, setZoneName] = useState(''); const [zoneName, setZoneName] = useState('');
const [zoneCode, setZoneCode] = useState(''); const [zoneCode, setZoneCode] = useState('');
const [zoneDescription, setZoneDescription] = useState(''); const [zoneDescription, setZoneDescription] = useState('');
const [zonalBusinessHeadId, setZonalBusinessHeadId] = useState(''); const [zonalBusinessHeadId, setZonalBusinessHeadId] = useState('none');
// Form State (Region) // Form State (Region)
const [editingRegionId, setEditingRegionId] = useState<string | null>(null); const [editingRegionId, setEditingRegionId] = useState<string | null>(null);
@ -288,7 +288,13 @@ export const MasterPage: React.FC = () => {
const handleSaveZone = async () => { const handleSaveZone = async () => {
try { try {
const payload = { id: editingZoneId, name: zoneName, code: zoneCode, description: zoneDescription, managerId: zonalBusinessHeadId }; const payload = {
id: editingZoneId,
name: zoneName,
code: zoneCode,
description: zoneDescription,
managerId: zonalBusinessHeadId === 'none' ? null : zonalBusinessHeadId
};
const res = await masterService.saveZone(payload) as any; const res = await masterService.saveZone(payload) as any;
if (res.success) { if (res.success) {
toast.success('Zone saved successfully'); toast.success('Zone saved successfully');
@ -456,8 +462,8 @@ export const MasterPage: React.FC = () => {
<TabsContent value="hierarchy" className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300"> <TabsContent value="hierarchy" className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300">
<ZonesOverview selectedZone={selectedZone} onZoneClick={(id) => setSelectedZone(selectedZone === id ? 'all' : id)} /> <ZonesOverview selectedZone={selectedZone} onZoneClick={(id) => setSelectedZone(selectedZone === id ? 'all' : id)} />
<ZoneDetails selectedZone={selectedZone} onAddZone={() => { setEditingZoneId(null); setZoneName(''); setZoneCode(''); setZoneDescription(''); setZonalBusinessHeadId(''); setShowZoneDialog(true); }} <ZoneDetails selectedZone={selectedZone} onAddZone={() => { setEditingZoneId(null); setZoneName(''); setZoneCode(''); setZoneDescription(''); setZonalBusinessHeadId('none'); setShowZoneDialog(true); }}
onEditZone={(z) => { setEditingZoneId(z.id); setZoneName(z.name); setZoneCode(z.code); setZoneDescription(z.description); setZonalBusinessHeadId(z.zonalBusinessHead?.id || ''); setShowZoneDialog(true); }} /> onEditZone={(z) => { setEditingZoneId(z.id); setZoneName(z.name); setZoneCode(z.code); setZoneDescription(z.description); setZonalBusinessHeadId(z.zonalBusinessHead?.id || 'none'); setShowZoneDialog(true); }} />
<RegionalManagement selectedZone={selectedZone} onAddRegion={() => { setEditingRegionId(null); setRegionName(''); setRegionCode(''); setSelectedRegionZone(selectedZone === 'all' ? '' : selectedZone); setRegionalManagerId(''); setSelectedRegionDistricts([]); setShowRegionDialog(true); }} <RegionalManagement selectedZone={selectedZone} onAddRegion={() => { setEditingRegionId(null); setRegionName(''); setRegionCode(''); setSelectedRegionZone(selectedZone === 'all' ? '' : selectedZone); setRegionalManagerId(''); setSelectedRegionDistricts([]); setShowRegionDialog(true); }}
onEditRegion={(r) => { onEditRegion={(r) => {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
import { Badge } from '../../ui/badge'; import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { Shield, User, Mail, MapPin } from 'lucide-react'; import { Shield, User, Mail } from 'lucide-react';
interface UserManagementTableProps { interface UserManagementTableProps {
userAssignedData: any[]; userAssignedData: any[];
@ -22,7 +22,6 @@ export const UserManagementTable: React.FC<UserManagementTableProps> = ({ userAs
<TableHead>Role</TableHead> <TableHead>Role</TableHead>
<TableHead>Assigned Zone</TableHead> <TableHead>Assigned Zone</TableHead>
<TableHead>Assigned Region</TableHead> <TableHead>Assigned Region</TableHead>
<TableHead>Location Type</TableHead>
<TableHead>Status</TableHead> <TableHead>Status</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@ -57,12 +56,6 @@ export const UserManagementTable: React.FC<UserManagementTableProps> = ({ userAs
{user.region} {user.region}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell>
<div className="flex items-center gap-1.5 text-slate-600">
<MapPin className="w-3.5 h-3.5" />
<span className="text-sm capitalize">{user.locationType || 'N/A'}</span>
</div>
</TableCell>
<TableCell> <TableCell>
<Badge variant={user.status === 'Active' ? 'default' : 'secondary'} className={user.status === 'Active' ? 'bg-emerald-100 text-emerald-700' : ''}> <Badge variant={user.status === 'Active' ? 'default' : 'secondary'} className={user.status === 'Active' ? 'bg-emerald-100 text-emerald-700' : ''}>
{user.status} {user.status}

View File

@ -31,7 +31,7 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle>Zonal Managers (ZM)</CardTitle> <CardTitle>Zonal Managers (ZM)</CardTitle>
<CardDescription>Manage Zonal Managers and their district assignments</CardDescription> <CardDescription>Manage Zonal Managers and their region assignments</CardDescription>
</div> </div>
<Button onClick={onAddZM} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddZM} className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />

View File

@ -5,7 +5,7 @@ import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge'; import { Badge } from '../../ui/badge';
import { ScrollArea } from '../../ui/scroll-area'; import { ScrollArea } from '../../ui/scroll-area';
import { Label } from '../../ui/label'; import { Label } from '../../ui/label';
import { Globe, Plus, Edit2, Mail, Users, MapPin } from 'lucide-react'; import { Globe, Plus, Edit2, Mail, Users, Shield } from 'lucide-react';
import { RootState } from '../../../store'; import { RootState } from '../../../store';
interface ZoneDetailsProps { interface ZoneDetailsProps {
@ -81,6 +81,26 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
</div> </div>
</div> </div>
{zone.zonalBusinessHead && (
<div className="border-t pt-3">
<Label className="text-xs text-slate-600 mb-2 block">
Zonal Business Head (ZBH)
</Label>
<div className="bg-amber-50 border border-amber-100 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-amber-600" />
<span className="text-sm font-semibold text-slate-900">{zone.zonalBusinessHead.name}</span>
<Badge className="bg-amber-600 text-white text-[10px] ml-auto">ZBH</Badge>
</div>
<div className="flex items-center gap-2 ml-6 text-slate-600">
<Mail className="w-3 h-3" />
<span className="text-xs">{zone.zonalBusinessHead.email}</span>
</div>
</div>
</div>
)}
{zone.zonalManagers && zone.zonalManagers.length > 0 && ( {zone.zonalManagers && zone.zonalManagers.length > 0 && (
<div className="border-t pt-3"> <div className="border-t pt-3">
<Label className="text-xs text-slate-600 mb-2 block"> <Label className="text-xs text-slate-600 mb-2 block">
@ -108,20 +128,20 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
</div> </div>
)} )}
{zm.districts && zm.districts.length > 0 && ( {zm.regions && zm.regions.length > 0 && (
<div className="ml-6 mt-2"> <div className="ml-6 mt-2">
<Label className="text-xs text-slate-500 mb-1 block"> <Label className="text-xs text-slate-500 mb-1 block">
Assigned Districts ({zm.districts.length}) Managed Regions ({zm.regions.length})
</Label> </Label>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{zm.districts.map((district: string, dIdx: number) => ( {zm.regions.map((region: string, rIdx: number) => (
<Badge <Badge
key={dIdx} key={rIdx}
variant="outline" variant="outline"
className="text-xs bg-white text-foreground" className="text-xs bg-white text-foreground"
> >
<MapPin className="w-2.5 h-2.5 mr-1" /> <Globe className="w-2.5 h-2.5 mr-1" />
{district} {region}
</Badge> </Badge>
))} ))}
</div> </div>

View File

@ -28,11 +28,15 @@ export const ZoneDialog: React.FC<ZoneDialogProps> = ({
zonalBusinessHeadId, setZonalBusinessHeadId, userAssignedData, onSave zonalBusinessHeadId, setZonalBusinessHeadId, userAssignedData, onSave
}) => { }) => {
const filteredZBHUsers = (userAssignedData || []).filter((u: any) => { const filteredZBHUsers = (userAssignedData || []).filter((u: any) => {
// Always include the currently assigned head to ensure pre-filling works
if (zonalBusinessHeadId !== 'none' && u.id === zonalBusinessHeadId) return true;
const roles = u.allRoles || []; const roles = u.allRoles || [];
return roles.some((r: string) => { const topLevelRole = (u.roleCode || '').toUpperCase();
return topLevelRole === 'ZBH' || roles.some((r: string) => {
const roleStr = (r || '').toUpperCase(); const roleStr = (r || '').toUpperCase();
return ['ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD', 'RM', 'RBM', 'REGIONAL MANAGER', 'ASM', 'AREA SALES MANAGER'].includes(roleStr) || return roleStr === 'ZBH' || roleStr === 'ZONE BUSINESS HEAD' || roleStr === 'ZONAL BUSINESS HEAD';
roleStr.includes('ZONAL') || roleStr.includes('REGIONAL') || roleStr.includes('AREA SALES');
}); });
}); });

View File

@ -65,7 +65,7 @@ export const useMasterData = () => {
const zones = (bodyZones?.zones || bodyZones?.data || []).map((z: any) => { const zones = (bodyZones?.zones || bodyZones?.data || []).map((z: any) => {
const zoneName = (z.name || z.zoneName || '').toUpperCase(); const zoneName = (z.name || z.zoneName || '').toUpperCase();
const zoneUsers = users.filter((u: any) => u.allZones?.includes(zoneName)); // const zoneUsers = users.filter((u: any) => u.allZones?.includes(zoneName));
return { return {
id: z.id, name: zoneName, description: z.description || '', id: z.id, name: zoneName, description: z.description || '',
@ -75,17 +75,17 @@ export const useMasterData = () => {
regionalOfficerCount: z.regionalOfficerCount || 0, regionalOfficerCount: z.regionalOfficerCount || 0,
zmCount: z.zmCount || 0, zmCount: z.zmCount || 0,
states: z.states || [], states: z.states || [],
zbh: { zonalBusinessHead: {
name: z.zonalBusinessHead?.fullName || 'Not Assigned', name: z.zonalBusinessHead?.name || z.zonalBusinessHead?.fullName || 'Not Assigned',
email: z.zonalBusinessHead?.email || '', email: z.zonalBusinessHead?.email || '',
phone: z.zonalBusinessHead?.mobileNumber || '' phone: z.zonalBusinessHead?.mobileNumber || z.zonalBusinessHead?.phone || ''
}, },
zonalManagers: (z.zonalManagers || []).map((m: any) => ({ zonalManagers: (z.zonalManagers || []).map((m: any) => ({
id: m.id, id: m.id,
name: m.name || m.fullName || 'Unknown', name: m.name || m.fullName || 'Unknown',
email: m.email || '', email: m.email || '',
phone: m.phone || m.mobileNumber || '', phone: m.phone || m.mobileNumber || '',
districts: m.districts || [] regions: m.regions || []
})) }))
}; };
}); });

View File

@ -116,6 +116,7 @@ export interface Application {
zoneId?: string; zoneId?: string;
regionId?: string; regionId?: string;
areaId?: string; areaId?: string;
districtId?: string;
} }
export interface Participant { export interface Participant {

View File

@ -5,9 +5,9 @@ import { toast } from 'sonner';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../../store'; import { RootState } from '../../store';
import { import {
User, RefreshCw, HelpCircle, Bell, ArrowLeft, Bike, User, RefreshCw, HelpCircle, ArrowLeft, Bike,
Users, Target, FileText, Award, ChevronLeft, ChevronRight, Users, FileText, ChevronRight,
CheckCircle, AlertCircle CheckCircle
} from 'lucide-react'; } from 'lucide-react';
const PublicQuestionnairePage: React.FC = () => { const PublicQuestionnairePage: React.FC = () => {
@ -231,7 +231,7 @@ const PublicQuestionnairePage: React.FC = () => {
{/* Section Tabs */} {/* Section Tabs */}
<div className="bg-slate-800/50 backdrop-blur-sm border-t border-slate-700"> <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"> <div className="flex items-center gap-2 overflow-x-auto scrollbar-hide px-8 py-4 no-scrollbar">
{sections.map((section, idx) => ( {sections.map((section) => (
<button <button
key={section} key={section}
onClick={() => setActiveSection(section)} onClick={() => setActiveSection(section)}
@ -300,19 +300,33 @@ const PublicQuestionnairePage: React.FC = () => {
/> />
)} )}
{(q.inputType === 'select' || q.inputType === 'yesno') && ( {q.inputType === 'textarea' && (
<select <textarea
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" className="w-full h-32 p-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] || ''} value={responses[q.id] || ''}
onChange={(e) => handleInputChange(q.id, e.target.value)} 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}> {(q.inputType === 'select' || q.inputType === 'yesno' || q.inputType === 'radio' || q.inputType === 'mcq') && (
{opt.optionText || opt.text} <div className="space-y-2">
</option> {(q.questionOptions || (q.inputType === 'yesno' ? [{ optionText: 'Yes' }, { optionText: 'No' }] : [])).map((opt: any, i: number) => {
))} const val = opt.optionText || opt.text;
</select> return (
<label key={i} className="flex items-center gap-3 cursor-pointer group/opt">
<input
type="radio"
name={`q-${q.id}`}
className="w-4 h-4 text-amber-600 focus:ring-amber-500 border-slate-300"
checked={responses[q.id] === val}
onChange={() => handleInputChange(q.id, val)}
/>
<span className="text-slate-700 group-hover/opt:text-slate-900 transition-colors">{val}</span>
</label>
);
})}
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -188,5 +188,14 @@ export const onboardingService = {
console.error('Create dealer error:', error); console.error('Create dealer error:', error);
throw error; throw error;
} }
},
retriggerEvaluators: async (id: string) => {
try {
const response: any = await API.retriggerEvaluators(id);
return response.data;
} catch (error) {
console.error('Retrigger evaluators error:', error);
throw error;
}
} }
}; };

View File

@ -6,11 +6,10 @@ export interface Zone {
description?: string; description?: string;
code: string; code: string;
regionCount: number; regionCount: number;
districtCount: number;
states: string[];
zmCount: number; zmCount: number;
zonalBusinessHead: { id: string; name: string; email: string; phone: string } | null; states: string[];
zonalManagers: { id: string; name: string; email: string; phone: string; districts: string[] }[]; zonalBusinessHead?: { id: string; name: string; email: string; phone?: string };
zonalManagers: { id: string; name: string; email: string; phone: string; regions: string[] }[];
} }
export interface Region { export interface Region {
@ -21,6 +20,7 @@ export interface Region {
zoneName: string; zoneName: string;
districts: { id: string; name: string, stateId?: string }[]; districts: { id: string; name: string, stateId?: string }[];
states: string[]; states: string[];
cities: string[];
status: string; status: string;
regionalOfficerCount: number; regionalOfficerCount: number;
asmCount: number; asmCount: number;