hirarchy made stable and flow strted checking end to end level 3 completed
This commit is contained in:
parent
d2228543b1
commit
e68f96a929
@ -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, {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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([]);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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';
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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 || []
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
@ -116,6 +116,7 @@ export interface Application {
|
||||
zoneId?: string;
|
||||
regionId?: string;
|
||||
areaId?: string;
|
||||
districtId?: string;
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user