Dealer_Onboard_Frontend/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts
2026-05-15 20:20:15 +05:30

667 lines
24 KiB
TypeScript

import { Dispatch, SetStateAction, useCallback } from 'react';
import { toast } from 'sonner';
import { onboardingService } from '@/services/onboarding.service';
interface UseApplicationDetailsAdminActionsParams {
application: any;
applicationId: string;
currentUser: any;
interviews: any[];
approvalFile: File | null;
approvalRemark: string;
rejectionReason: string;
architectureLeadId: string;
architectureStatus: string;
architectureRemarks: string;
selectedUser: string;
participantType: string;
users: any[];
interviewDate: string;
setInterviewDate: Dispatch<SetStateAction<string>>;
interviewType: string;
setInterviewType: Dispatch<SetStateAction<string>>;
interviewMode: string;
setInterviewMode: Dispatch<SetStateAction<string>>;
meetingLink: string;
setMeetingLink: Dispatch<SetStateAction<string>>;
location: string;
setLocation: Dispatch<SetStateAction<string>>;
scheduledInterviewParticipants: any[];
uploadFile: File | null;
uploadDocType: string;
selectedStage: string | null;
setIsApproving: Dispatch<SetStateAction<boolean>>;
setShowApproveModal: Dispatch<SetStateAction<boolean>>;
setApprovalRemark: Dispatch<SetStateAction<string>>;
setApprovalFile: Dispatch<SetStateAction<File | null>>;
setIsRejecting: Dispatch<SetStateAction<boolean>>;
setShowRejectModal: Dispatch<SetStateAction<boolean>>;
setRejectionReason: Dispatch<SetStateAction<string>>;
setIsAssigningArchitecture: Dispatch<SetStateAction<boolean>>;
setShowAssignArchitectureModal: Dispatch<SetStateAction<boolean>>;
setIsUpdatingArchitecture: Dispatch<SetStateAction<boolean>>;
setShowArchitectureStatusModal: Dispatch<SetStateAction<boolean>>;
setIsAssigningParticipant: Dispatch<SetStateAction<boolean>>;
setSelectedUser: Dispatch<SetStateAction<string>>;
setShowAssignModal: Dispatch<SetStateAction<boolean>>;
setLoading: Dispatch<SetStateAction<boolean>>;
setIsScheduling: Dispatch<SetStateAction<boolean>>;
setShowScheduleModal: Dispatch<SetStateAction<boolean>>;
setShowCancelInterviewModal: Dispatch<SetStateAction<boolean>>;
interviewIdToCancel: string;
setInterviewIdToCancel: Dispatch<SetStateAction<string>>;
interviewToReschedule: any;
setInterviewToReschedule: Dispatch<SetStateAction<any>>;
setIsCancellingInterview: Dispatch<SetStateAction<boolean>>;
setIsUploading: Dispatch<SetStateAction<boolean>>;
setShowUploadForm: Dispatch<SetStateAction<boolean>>;
setUploadFile: Dispatch<SetStateAction<File | null>>;
setUploadDocType: Dispatch<SetStateAction<string>>;
setDocuments: Dispatch<SetStateAction<any[]>>;
selectedInterviewerId: string;
setSelectedInterviewerId: Dispatch<SetStateAction<string>>;
setScheduledInterviewParticipants: Dispatch<SetStateAction<any[]>>;
setUsers: Dispatch<SetStateAction<any[]>>;
showScheduleModal: boolean;
showAssignArchitectureModal: boolean;
showAssignModal: boolean;
fetchApplication: (silent?: boolean) => Promise<void>;
fetchInterviews: () => Promise<void>;
fetchEorData: () => Promise<void>;
}
export function useApplicationDetailsAdminActions(params: UseApplicationDetailsAdminActionsParams) {
const {
application,
applicationId,
currentUser,
interviews,
approvalFile,
approvalRemark,
rejectionReason,
architectureLeadId,
architectureStatus,
architectureRemarks,
selectedUser,
participantType,
users,
interviewDate,
setInterviewDate,
interviewType,
setInterviewType,
interviewMode,
setInterviewMode,
meetingLink,
setMeetingLink,
location,
setLocation,
scheduledInterviewParticipants,
uploadFile,
uploadDocType,
selectedStage,
setIsApproving,
setShowApproveModal,
setApprovalRemark,
setApprovalFile,
setIsRejecting,
setShowRejectModal,
setRejectionReason,
setIsAssigningArchitecture,
setShowAssignArchitectureModal,
setIsUpdatingArchitecture,
setShowArchitectureStatusModal,
setIsAssigningParticipant,
setSelectedUser,
setShowAssignModal,
setLoading,
setIsScheduling,
setShowScheduleModal,
setShowCancelInterviewModal,
interviewIdToCancel,
setInterviewIdToCancel,
interviewToReschedule,
setInterviewToReschedule,
setIsCancellingInterview,
setIsUploading,
setShowUploadForm,
setUploadFile,
setUploadDocType,
setDocuments,
selectedInterviewerId,
setSelectedInterviewerId,
setScheduledInterviewParticipants,
setUsers,
showScheduleModal,
showAssignArchitectureModal,
showAssignModal,
fetchApplication,
fetchInterviews,
fetchEorData,
} = params;
const handleAddInterviewer = () => {
if (!selectedInterviewerId) return;
const usersList = Array.isArray(users) ? users : [];
const userToAdd = usersList.find((u) => u.id === selectedInterviewerId);
if (userToAdd && !scheduledInterviewParticipants.find((p) => p.id === userToAdd.id)) {
setScheduledInterviewParticipants([...scheduledInterviewParticipants, userToAdd]);
setSelectedInterviewerId('');
}
};
const handleRemoveInterviewer = (userId: string) => {
setScheduledInterviewParticipants(scheduledInterviewParticipants.filter((p) => p.id !== userId));
};
const fetchUsers = useCallback(async (type?: string) => {
if (!currentUser || !['DD Admin', 'Super Admin', 'DD Lead', 'DD Head', 'NBH'].includes(currentUser.role)) return;
try {
const reqParams: any = {};
if (type) {
const roleMapping: any = {
level1: ['DD-ZM', 'RBM'],
level2: ['DD Lead', 'ZBH'],
level3: ['NBH', 'DD Head'],
};
// Keep stage roles as preferred default, but allow broader user pool
// so admins can add extra panelists for the same interview.
if (roleMapping[type]) {
reqParams.preferredRoleCode = roleMapping[type];
}
if (application) {
reqParams.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
}
}
reqParams.isExternal = false;
const response = await onboardingService.getUsers(reqParams);
const rawUsers = Array.isArray(response)
? response
: response && Array.isArray(response.data)
? response.data
: response && Array.isArray(response.users)
? response.users
: [];
// Exclude inactive users and keep deterministic sorting.
const activeUsers = rawUsers.filter((u: any) => (u.status || '').toLowerCase() !== 'inactive');
setUsers(activeUsers.sort((a: any, b: any) => String(a.fullName || a.name || '').localeCompare(String(b.fullName || b.name || ''))));
} catch {
setUsers([]);
}
}, [currentUser, application, setUsers]);
const prefillInterviewParticipants = useCallback(() => {
if (!showScheduleModal || !application || interviewToReschedule) return;
const levelNum = parseInt(interviewType.replace('level', '')) || 1;
const requiredRolesByLevel: Record<number, string[]> = {
1: ['DD-ZM', 'RBM'],
2: ['DD Lead', 'ZBH'],
3: ['NBH', 'DD Head'],
};
const normalizeRole = (value: unknown) =>
String(value || '')
.trim()
.toLowerCase()
.replace(/[_\s-]+/g, ' ');
const expectedRoles = (requiredRolesByLevel[levelNum] || []).map(normalizeRole);
const deriveDisplayRole = (participant: any, user: any): string => {
const candidateRoles = [
participant?.metadata?.role,
user?.role?.roleName,
user?.role?.roleCode,
user?.roleCode,
user?.role,
].filter(Boolean);
const matched = candidateRoles.find((r: any) => expectedRoles.includes(normalizeRole(r)));
return String(matched || candidateRoles[0] || 'Panelist');
};
const preAssigned = (application?.participants || [])
.filter((p: any) =>
p.metadata?.interviewLevel === levelNum ||
p.metadata?.interviewLevel === String(levelNum) ||
p.metadata?.allAssignments?.includes(levelNum) ||
p.metadata?.allAssignments?.includes(String(levelNum)) ||
expectedRoles.includes(normalizeRole(p.user?.role)) ||
expectedRoles.includes(normalizeRole(p.user?.roleCode)) ||
expectedRoles.includes(normalizeRole(p.metadata?.role))
)
.map((p: any) => {
const user = p.user || {};
return {
...user,
__stageRole: deriveDisplayRole(p, user),
};
})
.filter((u: any) => !!u?.id);
if (preAssigned.length === 0) {
setScheduledInterviewParticipants([]);
return;
}
const unique: any[] = [];
const seen = new Set();
preAssigned.forEach((u: any) => {
if (u.id && !seen.has(u.id)) {
seen.add(u.id);
unique.push(u);
}
});
setScheduledInterviewParticipants(unique);
}, [showScheduleModal, application, interviewType, interviewToReschedule, setScheduledInterviewParticipants]);
const handleScheduleInterview = async () => {
if (!interviewDate) {
toast.warning('Please select date and time');
return;
}
try {
setIsScheduling(true);
const payload = {
applicationId: application?.id,
level: interviewType,
scheduledAt: interviewDate,
type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview',
location: interviewMode === 'virtual' ? meetingLink : location,
participants: scheduledInterviewParticipants.map((p) => p.id),
};
if (interviewToReschedule) {
await onboardingService.updateInterview(interviewToReschedule.id, {
...payload,
status: 'Scheduled',
});
toast.success('Interview rescheduled successfully');
} else {
await onboardingService.scheduleInterview(payload);
toast.success('Interview scheduled successfully');
}
setShowScheduleModal(false);
setInterviewToReschedule(null);
await fetchInterviews();
await fetchApplication();
} catch {
toast.error(interviewToReschedule ? 'Failed to reschedule interview' : 'Failed to schedule interview');
} finally {
setIsScheduling(false);
}
};
const handleCancelInterview = async (interviewId: string) => {
setInterviewIdToCancel(interviewId);
setShowCancelInterviewModal(true);
};
const handleRescheduleInterview = async (interview: any) => {
setInterviewToReschedule(interview);
setInterviewType(`level${interview.level}`);
setInterviewMode(interview.interviewType?.toLowerCase().includes('virtual') ? 'virtual' : 'physical');
setInterviewDate(interview.scheduleDate ? (() => {
const d = new Date(interview.scheduleDate);
return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
})() : '');
if (interview.interviewType?.toLowerCase().includes('virtual')) {
setMeetingLink(interview.linkOrLocation || '');
} else {
setLocation(interview.linkOrLocation || '');
}
const participants = (interview.participants || []).map((p: any) => p.user || p).filter(Boolean);
setScheduledInterviewParticipants(participants);
setShowScheduleModal(true);
};
const handleConfirmCancelInterview = async () => {
if (!interviewIdToCancel) return;
try {
setIsCancellingInterview(true);
await onboardingService.updateInterview(interviewIdToCancel, { status: 'Cancelled' });
toast.success('Interview cancelled successfully');
setShowCancelInterviewModal(false);
setInterviewIdToCancel('');
await fetchInterviews();
} catch {
toast.error('Failed to cancel interview');
} finally {
setIsCancellingInterview(false);
}
};
const handleUpload = async () => {
if (!uploadFile || !uploadDocType) {
toast.warning('Please select a file and document type');
return;
}
try {
setIsUploading(true);
const formData = new FormData();
formData.append('file', uploadFile);
formData.append('documentType', uploadDocType);
if (selectedStage) formData.append('stage', selectedStage);
await onboardingService.uploadDocument(applicationId, formData);
toast.success('Document uploaded successfully');
setShowUploadForm(false);
setUploadFile(null);
setUploadDocType('');
const docs = await onboardingService.getDocuments(applicationId);
setDocuments(docs || []);
await fetchEorData();
} catch {
toast.error('Failed to upload document');
} finally {
setIsUploading(false);
}
};
const handleApprove = async () => {
try {
setIsApproving(true);
const activeInterview = interviews.find((i) =>
i.status !== 'Completed' &&
i.status !== 'Cancelled' &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
if (approvalFile && applicationId) {
try {
const formData = new FormData();
formData.append('file', approvalFile);
formData.append('documentType', 'Approval Attachment');
let stageName: string | null = null;
if (activeInterview) {
if (activeInterview.level === 1 || activeInterview.level === '1') stageName = '1st Level Interview';
else if (activeInterview.level === 2 || activeInterview.level === '2') stageName = '2nd Level Interview';
else if (activeInterview.level === 3 || activeInterview.level === '3') stageName = '3rd Level Interview';
}
if (!stageName) {
if (application.status === 'Shortlisted' || application.status === 'Level 1 Interview Pending') stageName = '1st Level Interview';
else if (application.status === 'Level 1 Approved' || application.status === 'Level 2 Interview Pending') stageName = '2nd Level Interview';
else if (application.status === 'Level 2 Approved' || application.status === 'Level 3 Interview Pending') stageName = '3rd Level Interview';
}
if (stageName) formData.append('stage', stageName);
await onboardingService.uploadDocument(applicationId, formData);
} catch {
toast.error('Failed to upload document');
}
}
if (activeInterview) {
try {
await onboardingService.updateInterviewDecision({ interviewId: activeInterview.id, decision: 'Approved', remarks: approvalRemark });
toast.success('Interview approved successfully');
setShowApproveModal(false);
setApprovalRemark('');
setApprovalFile(null);
await fetchInterviews();
await fetchApplication();
return;
} catch {
toast.error('Failed to approve interview');
return;
}
}
if (!approvalRemark.trim()) {
toast.warning('Please enter a remark');
return;
}
let newStatus = application.status;
switch (application.status) {
case 'Shortlisted':
case 'Level 1 Interview Pending': newStatus = 'Level 1 Approved'; break;
case 'Level 1 Approved':
case 'Level 2 Interview Pending': newStatus = 'Level 2 Approved'; break;
case 'Level 2 Approved':
case 'Level 3 Interview Pending': newStatus = 'Level 3 Approved'; break;
case 'Level 3 Approved': newStatus = 'FDD Verification'; break;
case 'FDD Verification': newStatus = 'LOI In Progress'; break;
case 'LOI In Progress': newStatus = 'Security Deposit'; break;
case 'Security Deposit':
case 'Security Details':
case 'Payment Pending': newStatus = 'LOI Issued'; break;
case 'LOI Issued': newStatus = 'Dealer Code Generation'; break;
case 'Dealer Code Generation':
case 'Architecture Team Assigned':
case 'Architecture Document Upload':
case 'Architecture Team Completion':
case 'Statutory GST':
case 'Statutory PAN':
case 'Statutory Nodal':
case 'Statutory Check':
case 'Statutory Partnership':
case 'Statutory Firm Reg':
case 'Statutory Rental':
case 'Statutory Virtual Code':
case 'Statutory Domain':
case 'Statutory MSD':
case 'Statutory LOI Ack': newStatus = 'LOA Pending'; break;
case 'LOA Pending': newStatus = 'EOR In Progress'; break;
case 'EOR In Progress': newStatus = 'EOR Complete'; break;
case 'EOR Complete': newStatus = 'Inauguration'; break;
case 'Inauguration':
case 'Approved': newStatus = 'Onboarded'; break;
default: newStatus = 'Onboarded';
}
const policyManagedStages: Record<string, string> = {
'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1',
'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2',
'Level 2 Recommended': 'INTERVIEW_LEVEL_2',
'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3',
'LOI In Progress': 'LOI_APPROVAL',
'LOA Pending': 'LOA_APPROVAL',
};
const stageCodeForPolicy = policyManagedStages[application.status];
if (stageCodeForPolicy) {
const response = await onboardingService.submitStageDecision({
applicationId: application.id,
stageCode: stageCodeForPolicy,
decision: 'Approved',
remarks: approvalRemark,
nextStatus: newStatus,
});
if (response.data?.statusUpdated) toast.success(response.message || 'Stage completed and moved to next step');
else toast.info(response.message || 'Approval recorded. Waiting for other mandatory approvers.');
} else {
await onboardingService.updateApplicationStatus(applicationId, { status: newStatus, remarks: approvalRemark });
}
if (newStatus === 'Onboarded') {
await onboardingService.createDealer({ applicationId });
toast.success('Application finalized and Dealer profile created!');
} else {
toast.success(`Application moved to ${newStatus}`);
}
setShowApproveModal(false);
setApprovalRemark('');
setApprovalFile(null);
await fetchApplication();
} catch (error: any) {
toast.error(error.message || 'Failed to process approval');
} finally {
setIsApproving(false);
}
};
const handleReject = async () => {
try {
setIsRejecting(true);
const activeInterview = interviews.find((i) =>
i.status !== 'Completed' &&
i.status !== 'Cancelled' &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
if (activeInterview) {
try {
await onboardingService.updateInterviewDecision({ interviewId: activeInterview.id, decision: 'Rejected', remarks: rejectionReason });
toast.success('Interview rejected');
setShowRejectModal(false);
setRejectionReason('');
await fetchInterviews();
await fetchApplication();
return;
} catch {
toast.error('Failed to reject interview');
return;
}
}
if (!rejectionReason.trim()) {
toast.warning('Please enter a reason for rejection');
return;
}
const policyManagedStages: Record<string, string> = {
'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1',
'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2',
'Level 2 Recommended': 'INTERVIEW_LEVEL_2',
'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3',
'LOI In Progress': 'LOI_APPROVAL',
'LOA Pending': 'LOA_APPROVAL',
};
const stageCodeForPolicy = policyManagedStages[application.status];
if (stageCodeForPolicy) {
await onboardingService.submitStageDecision({
applicationId: application.id,
stageCode: stageCodeForPolicy,
decision: 'Rejected',
remarks: rejectionReason,
interviewId: activeInterview?.id,
});
} else {
await onboardingService.updateApplicationStatus(applicationId, { status: 'Rejected', remarks: rejectionReason });
}
toast.success('Application rejected');
setShowRejectModal(false);
setRejectionReason('');
await fetchApplication();
} catch (error: any) {
toast.error(error.message || 'Failed to process rejection');
} finally {
setIsRejecting(false);
}
};
const handleGenerateDealerCodes = async () => {
try {
await onboardingService.generateDealerCodes(applicationId);
toast.success('Dealer codes generated successfully');
await fetchApplication();
} catch (error: any) {
toast.error(error.message || 'Failed to generate dealer codes');
}
};
const handleAssignArchitecture = async () => {
if (!architectureLeadId) {
toast.warning('Please select an architecture lead');
return;
}
try {
setIsAssigningArchitecture(true);
await onboardingService.assignArchitectureTeam(applicationId, architectureLeadId);
toast.success('Architecture team assigned successfully');
setShowAssignArchitectureModal(false);
await fetchApplication();
} catch (error: any) {
toast.error(error.message || 'Failed to assign architecture team');
} finally {
setIsAssigningArchitecture(false);
}
};
const handleUpdateArchitectureStatus = async () => {
try {
setIsUpdatingArchitecture(true);
await onboardingService.updateArchitectureStatus(applicationId, architectureStatus, architectureRemarks);
toast.success('Architecture status updated successfully');
setShowArchitectureStatusModal(false);
await fetchApplication();
} catch {
toast.error('Failed to update architecture status');
} finally {
setIsUpdatingArchitecture(false);
}
};
const handleAddParticipant = async () => {
if (!selectedUser) {
toast.warning('Please select a user');
return;
}
try {
setIsAssigningParticipant(true);
const u = Array.isArray(users) ? users.find((user) => user.id === selectedUser) : null;
if (u && (u.role === 'FDD' || u.roleCode === 'FDD')) {
await onboardingService.assignFddAgency({ applicationId, assignedToAgency: selectedUser });
toast.info(`${u.fullName || u.name} assigned as FDD Agency based on role.`);
}
await onboardingService.addParticipant({
requestId: applicationId,
requestType: 'application',
userId: selectedUser,
participantType: participantType || 'contributor',
});
toast.success('User assigned successfully!');
await fetchApplication();
setSelectedUser('');
setShowAssignModal(false);
} catch {
toast.error('Failed to assign user');
} finally {
setIsAssigningParticipant(false);
}
};
const handleRetriggerEvaluators = async () => {
try {
setLoading(true);
await onboardingService.retriggerEvaluators(applicationId);
toast.success('Evaluators re-assigned successfully');
await fetchApplication();
} catch {
toast.error('Failed to re-assign evaluators');
} finally {
setLoading(false);
}
};
const maybeFetchUsersForModal = useCallback(async () => {
if (showScheduleModal && application) {
await fetchUsers(interviewType);
prefillInterviewParticipants();
return;
}
if ((showAssignArchitectureModal || showAssignModal) && application) {
await fetchUsers();
}
}, [
showScheduleModal,
showAssignArchitectureModal,
showAssignModal,
application,
interviewType,
fetchUsers,
prefillInterviewParticipants,
]);
return {
handleAddInterviewer,
handleRemoveInterviewer,
fetchUsers,
maybeFetchUsersForModal,
handleScheduleInterview,
handleRescheduleInterview,
handleCancelInterview,
handleConfirmCancelInterview,
handleUpload,
handleApprove,
handleReject,
handleGenerateDealerCodes,
handleAssignArchitecture,
handleUpdateArchitectureStatus,
handleAddParticipant,
handleRetriggerEvaluators,
};
}