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 }),
generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`),
updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data),
retriggerEvaluators: (id: string) => client.post(`/onboarding/applications/${id}/retrigger-evaluators`),
// Documents
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {

View File

@ -8,7 +8,7 @@ interface Question {
id?: string;
sectionName: string;
questionText: string;
inputType: 'text' | 'yesno' | 'file' | 'number' | 'select';
inputType: 'text' | 'yesno' | 'file' | 'number' | 'select' | 'mcq' | 'radio' | 'textarea' | 'email';
options?: { text: string; score: number }[];
weight: number;
order: number;
@ -58,7 +58,7 @@ const QuestionnaireBuilder: React.FC = () => {
if (normalizedType === 'mcq') normalizedType = 'select';
// 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';
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"
>
<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="file">File Upload</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>
</div>
@ -308,8 +311,8 @@ const QuestionnaireBuilder: React.FC = () => {
<div className="relative">
<input
type="number"
value={q.weight}
onChange={(e) => updateQuestion(index, 'weight', parseFloat(e.target.value))}
value={isNaN(q.weight) ? 0 : q.weight}
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"
title="Weightage"
/>
@ -330,7 +333,7 @@ const QuestionnaireBuilder: React.FC = () => {
</div>
{/* 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">
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">
Answer Options & Scores
@ -347,13 +350,13 @@ const QuestionnaireBuilder: React.FC = () => {
/>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-400 font-medium">Score:</span>
<input
<input
type="number"
value={opt.score}
max={q.weight}
value={isNaN(opt.score) ? 0 : opt.score}
max={isNaN(q.weight) ? 0 : q.weight}
min={0}
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>
<button

View File

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

View File

@ -42,6 +42,8 @@ import { Progress } from '../ui/progress';
import { Textarea } from '../ui/textarea';
import { Input } from '../ui/input';
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 { ScrollArea } from '../ui/scroll-area';
import {
@ -313,6 +315,7 @@ export function ApplicationDetails() {
zoneId: data.zoneId,
regionId: data.regionId,
areaId: data.areaId,
districtId: data.districtId,
};
setApplication(mappedApp);
} catch (error) {
@ -336,21 +339,10 @@ export function ApplicationDetails() {
const resp = await eorService.getChecklist(applicationId);
if (resp.success && 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) {
console.log('EOR not found, attempting auto-create...');
try {
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);
}
console.log('EOR checklist not found or not yet initiated.');
setEorData(null);
}
};
@ -384,9 +376,11 @@ export function ApplicationDetails() {
}
}, [applicationId]);
const [activeTab, setActiveTab] = useState('questionnaire');
const [activeTab, setActiveTab ] = useState('questionnaire');
const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
const [rejectionReason, setRejectionReason] = useState('');
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
const [showWorkNoteModal, setShowWorkNoteModal] = useState(false);
const [showScheduleModal, setShowScheduleModal] = useState(false);
@ -398,7 +392,6 @@ export function ApplicationDetails() {
const [selectedStage, setSelectedStage] = useState<string | null>(null);
const [interviewMode, setInterviewMode] = useState('virtual');
const [approvalRemark, setApprovalRemark] = useState('');
const [rejectionReason, setRejectionReason] = useState('');
const [workNote, setWorkNote] = useState('');
const [expandedBranches, setExpandedBranches] = useState<{ [key: string]: boolean }>({
'architectural-work': true,
@ -418,7 +411,6 @@ export function ApplicationDetails() {
const [approvalFile, setApprovalFile] = useState<File | null>(null); // State for approval modal file
const [isUploading, setIsUploading] = useState(false);
const [selectedInterviewerId, setSelectedInterviewerId] = useState<string>('');
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
const [interviews, setInterviews] = useState<any[]>([]);
const [isScheduling, setIsScheduling] = useState(false);
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
@ -481,7 +473,7 @@ export function ApplicationDetails() {
interviewId,
criteriaScores,
feedback: ktMatrixRemarks,
recommendation: calculateKTScore() // Or separate recommendation field
recommendation: null // No auto-decision
});
toast.success('KT Matrix submitted successfully');
@ -490,7 +482,8 @@ export function ApplicationDetails() {
// Reset form
setKtMatrixScores({});
setKtMatrixRemarks('');
await fetchInterviews(); // Silent refresh
await fetchInterviews();
await fetchApplication(); // Refresh application status and progress
} catch (error) {
toast.error('Failed to submit KT Matrix');
} finally {
@ -506,8 +499,9 @@ export function ApplicationDetails() {
keyStrengths: '',
areasOfConcern: '',
additionalComments: '',
recommendation: '',
overallScore: ''
overallScore: '',
interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
});
const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false);
@ -543,7 +537,6 @@ export function ApplicationDetails() {
await onboardingService.submitLevel2Feedback({
interviewId,
overallScore: Number(level2Feedback.overallScore),
recommendation: level2Feedback.recommendation,
feedbackItems
});
@ -558,10 +551,12 @@ export function ApplicationDetails() {
keyStrengths: '',
areasOfConcern: '',
additionalComments: '',
recommendation: '',
overallScore: ''
overallScore: '',
interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
});
fetchInterviews(); // Refresh to show feedback
fetchApplication(); // Refresh application status
} catch (error) {
toast.error('Failed to submit Level 2 Feedback');
} finally {
@ -574,13 +569,14 @@ export function ApplicationDetails() {
strategicVision: '',
managementCapabilities: '',
operationalUnderstanding: '',
keyStrengths: '',
areasOfConcern: '',
brandAlignment: '',
executiveSummary: '',
keyStrengths: '',
areasOfConcern: '',
additionalComments: '',
recommendation: '',
overallScore: ''
overallScore: '',
interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
});
const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false);
@ -621,7 +617,6 @@ export function ApplicationDetails() {
await onboardingService.submitLevel2Feedback({
interviewId,
overallScore: Number(level3Feedback.overallScore),
recommendation: level3Feedback.recommendation,
feedbackItems
});
@ -633,15 +628,17 @@ export function ApplicationDetails() {
strategicVision: '',
managementCapabilities: '',
operationalUnderstanding: '',
keyStrengths: '',
areasOfConcern: '',
brandAlignment: '',
executiveSummary: '',
keyStrengths: '',
areasOfConcern: '',
additionalComments: '',
recommendation: '',
overallScore: ''
overallScore: '',
interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
});
fetchInterviews();
fetchApplication();
} catch (error) {
toast.error('Failed to submit Level 3 Feedback');
} finally {
@ -713,7 +710,7 @@ export function ApplicationDetails() {
// Include location from the 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(() => {
if (showScheduleModal) {
if (showScheduleModal && application) {
fetchUsers(interviewType);
} else {
// 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 {
setScheduledInterviewParticipants([]);
}
} else if (showScheduleModal && application) {
fetchUsers(); // Default fetch for other modals like Assign
}
}, [showScheduleModal, interviewType]);
}, [showScheduleModal, interviewType, application?.participants]);
const handleScheduleInterview = async () => {
if (!interviewDate) {
@ -754,17 +764,17 @@ export function ApplicationDetails() {
applicationId: application?.id,
level: interviewType,
scheduledAt: interviewDate,
type: interviewMode, // 'virtual' | 'physical'
location: interviewMode === 'physical' ? location : meetingLink,
type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview',
location: interviewMode === 'virtual' ? meetingLink : location,
participants: scheduledInterviewParticipants.map(p => p.id)
};
await onboardingService.scheduleInterview(payload);
toast.success('Interview scheduled successfully');
setShowScheduleModal(false);
// Refresh interviews
fetchInterviews();
fetchApplication(); // Refresh application status
await onboardingService.scheduleInterview(payload);
toast.success('Interview scheduled successfully');
setShowScheduleModal(false);
// Refresh interviews
await fetchInterviews();
await fetchApplication(); // Refresh application status
} catch (error) {
toast.error('Failed to schedule interview');
@ -842,7 +852,7 @@ export function ApplicationDetails() {
{
id: 3,
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',
description: 'Application shortlisted by DD',
documentsUploaded: 2
@ -850,28 +860,34 @@ export function ApplicationDetails() {
{
id: 4,
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,
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
},
{
id: 5,
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,
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
},
{
id: 6,
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,
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
},
{
@ -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',
date: application.loiApprovalDate,
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
},
{
@ -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',
date: application.loaDate,
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
},
{
@ -1153,10 +1175,7 @@ export function ApplicationDetails() {
setApprovalFile(null); // Reset file
fetchInterviews();
// Refresh application to check if status updated
if (id) {
const appData = await onboardingService.getApplicationById(id);
setApplication(appData);
}
fetchApplication();
return;
} catch (error) {
toast.error('Failed to approve interview');
@ -1268,10 +1287,7 @@ export function ApplicationDetails() {
setShowRejectModal(false);
setRejectionReason('');
fetchInterviews();
if (id) {
const appData = await onboardingService.getApplicationById(id);
setApplication(appData);
}
fetchApplication();
return;
} catch (error) {
toast.error('Failed to reject interview');
@ -1362,16 +1378,28 @@ export function ApplicationDetails() {
requestId: applicationId,
requestType: 'application',
userId: selectedUser,
participantType
participantType: 'contributor'
});
alert('User assigned successfully!');
toast.success('User assigned successfully!');
// Refresh application data
const data = await onboardingService.getApplicationById(applicationId);
setApplication({ ...application, participants: data.participants || [] });
fetchApplication();
setSelectedUser('');
setShowAssignModal(false);
} 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
);
// 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
// 1. If there's an active interview, feedback is required before Approve/Reject
// 2. hasMadeDecision should check if the evaluation has a recommendation
@ -1673,11 +1706,50 @@ export function ApplicationDetails() {
{stage.description && (
<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">
Evaluators: {stage.evaluators.join(' + ')}
</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 */}
{(() => {
const stageDocsCount = documents.filter(doc =>
@ -2344,7 +2416,8 @@ export function ApplicationDetails() {
Work Note
</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
variant="outline"
className="w-full"
@ -2717,9 +2790,26 @@ export function ApplicationDetails() {
<SelectValue placeholder="Select interview type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="level1">Level 1</SelectItem>
<SelectItem value="level2">Level 2</SelectItem>
<SelectItem value="level3">Level 3</SelectItem>
<SelectItem value="level1" disabled={isInterviewCompleted(1)}>
<div className="flex items-center justify-between w-full">
<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>
</Select>
</div>
@ -2778,7 +2868,7 @@ export function ApplicationDetails() {
<SelectContent>
{users.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.fullName} ({user.role?.roleName || user.roleCode})
{user.fullName || user.name} ({user.role?.roleName || user.roleCode})
</SelectItem>
))}
</SelectContent>
@ -2795,7 +2885,7 @@ export function ApplicationDetails() {
<div className="flex flex-wrap gap-2">
{scheduledInterviewParticipants.map((p) => (
<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
onClick={() => handleRemoveInterviewer(p.id)}
className="text-muted-foreground hover:text-destructive"
@ -2819,7 +2909,10 @@ export function ApplicationDetails() {
</Button>
<Button
className="flex-1 bg-amber-600 hover:bg-amber-700"
onClick={handleScheduleInterview}
onClick={() => {
// Ensure we pass participants to the schedule handler
handleScheduleInterview();
}}
disabled={isScheduling}
>
{isScheduling ? 'Scheduling...' : 'Schedule'}
@ -3008,7 +3101,7 @@ export function ApplicationDetails() {
Cancel
</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}
disabled={isSubmittingKT}
>
@ -3031,12 +3124,22 @@ export function ApplicationDetails() {
<div className="space-y-4">
<div>
<Label>Interview Date</Label>
<Input type="date" className="mt-2" />
<Input
type="date"
className="mt-2"
value={level2Feedback.interviewDate}
disabled
/>
</div>
<div>
<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>
@ -3137,7 +3240,7 @@ export function ApplicationDetails() {
Cancel
</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}
disabled={isSubmittingLevel2}
>
@ -3220,12 +3323,22 @@ export function ApplicationDetails() {
<div className="space-y-4">
<div>
<Label>Interview Date</Label>
<Input type="date" className="mt-2" />
<Input
type="date"
className="mt-2"
value={level3Feedback.interviewDate}
disabled
/>
</div>
<div>
<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>
@ -3315,23 +3428,6 @@ export function ApplicationDetails() {
/>
</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>
<Label>Executive Summary</Label>
<Textarea
@ -3352,7 +3448,7 @@ export function ApplicationDetails() {
Cancel
</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}
disabled={isSubmittingLevel3}
>
@ -3566,7 +3662,9 @@ export function ApplicationDetails() {
</div>
)}
</DialogContent>
</Dialog >
</div >
</Dialog>
</div>
);
}
};
export default ApplicationDetails;

View File

@ -98,7 +98,7 @@ export const MasterPage: React.FC = () => {
const [zoneName, setZoneName] = useState('');
const [zoneCode, setZoneCode] = useState('');
const [zoneDescription, setZoneDescription] = useState('');
const [zonalBusinessHeadId, setZonalBusinessHeadId] = useState('');
const [zonalBusinessHeadId, setZonalBusinessHeadId] = useState('none');
// Form State (Region)
const [editingRegionId, setEditingRegionId] = useState<string | null>(null);
@ -288,7 +288,13 @@ export const MasterPage: React.FC = () => {
const handleSaveZone = async () => {
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;
if (res.success) {
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">
<ZonesOverview selectedZone={selectedZone} onZoneClick={(id) => setSelectedZone(selectedZone === id ? 'all' : id)} />
<ZoneDetails selectedZone={selectedZone} onAddZone={() => { setEditingZoneId(null); setZoneName(''); setZoneCode(''); setZoneDescription(''); setZonalBusinessHeadId(''); setShowZoneDialog(true); }}
onEditZone={(z) => { setEditingZoneId(z.id); setZoneName(z.name); setZoneCode(z.code); setZoneDescription(z.description); setZonalBusinessHeadId(z.zonalBusinessHead?.id || ''); 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 || 'none'); setShowZoneDialog(true); }} />
<RegionalManagement selectedZone={selectedZone} onAddRegion={() => { setEditingRegionId(null); setRegionName(''); setRegionCode(''); setSelectedRegionZone(selectedZone === 'all' ? '' : selectedZone); setRegionalManagerId(''); setSelectedRegionDistricts([]); setShowRegionDialog(true); }}
onEditRegion={(r) => {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
import { Badge } from '../../ui/badge';
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 {
userAssignedData: any[];
@ -22,7 +22,6 @@ export const UserManagementTable: React.FC<UserManagementTableProps> = ({ userAs
<TableHead>Role</TableHead>
<TableHead>Assigned Zone</TableHead>
<TableHead>Assigned Region</TableHead>
<TableHead>Location Type</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
@ -57,12 +56,6 @@ export const UserManagementTable: React.FC<UserManagementTableProps> = ({ userAs
{user.region}
</Badge>
</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>
<Badge variant={user.status === 'Active' ? 'default' : 'secondary'} className={user.status === 'Active' ? 'bg-emerald-100 text-emerald-700' : ''}>
{user.status}

View File

@ -31,7 +31,7 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
<div className="flex items-center justify-between">
<div>
<CardTitle>Zonal Managers (ZM)</CardTitle>
<CardDescription>Manage Zonal Managers and their district assignments</CardDescription>
<CardDescription>Manage Zonal Managers and their region assignments</CardDescription>
</div>
<Button onClick={onAddZM} className="bg-amber-600 hover:bg-amber-700">
<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 { ScrollArea } from '../../ui/scroll-area';
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';
interface ZoneDetailsProps {
@ -81,6 +81,26 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
</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 && (
<div className="border-t pt-3">
<Label className="text-xs text-slate-600 mb-2 block">
@ -108,20 +128,20 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
</div>
)}
{zm.districts && zm.districts.length > 0 && (
{zm.regions && zm.regions.length > 0 && (
<div className="ml-6 mt-2">
<Label className="text-xs text-slate-500 mb-1 block">
Assigned Districts ({zm.districts.length})
Managed Regions ({zm.regions.length})
</Label>
<div className="flex flex-wrap gap-1">
{zm.districts.map((district: string, dIdx: number) => (
{zm.regions.map((region: string, rIdx: number) => (
<Badge
key={dIdx}
key={rIdx}
variant="outline"
className="text-xs bg-white text-foreground"
>
<MapPin className="w-2.5 h-2.5 mr-1" />
{district}
<Globe className="w-2.5 h-2.5 mr-1" />
{region}
</Badge>
))}
</div>

View File

@ -28,11 +28,15 @@ export const ZoneDialog: React.FC<ZoneDialogProps> = ({
zonalBusinessHeadId, setZonalBusinessHeadId, userAssignedData, onSave
}) => {
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 || [];
return roles.some((r: string) => {
const topLevelRole = (u.roleCode || '').toUpperCase();
return topLevelRole === 'ZBH' || roles.some((r: string) => {
const roleStr = (r || '').toUpperCase();
return ['ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD', 'RM', 'RBM', 'REGIONAL MANAGER', 'ASM', 'AREA SALES MANAGER'].includes(roleStr) ||
roleStr.includes('ZONAL') || roleStr.includes('REGIONAL') || roleStr.includes('AREA SALES');
return roleStr === 'ZBH' || roleStr === 'ZONE BUSINESS HEAD' || roleStr === 'ZONAL BUSINESS HEAD';
});
});

View File

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

View File

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

View File

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

View File

@ -188,5 +188,14 @@ export const onboardingService = {
console.error('Create dealer error:', 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;
code: string;
regionCount: number;
districtCount: number;
states: string[];
zmCount: number;
zonalBusinessHead: { id: string; name: string; email: string; phone: string } | null;
zonalManagers: { id: string; name: string; email: string; phone: string; districts: string[] }[];
states: string[];
zonalBusinessHead?: { id: string; name: string; email: string; phone?: string };
zonalManagers: { id: string; name: string; email: string; phone: string; regions: string[] }[];
}
export interface Region {
@ -21,6 +20,7 @@ export interface Region {
zoneName: string;
districts: { id: string; name: string, stateId?: string }[];
states: string[];
cities: string[];
status: string;
regionalOfficerCount: number;
asmCount: number;