From 5170ab6c5a58b4b00ebaa823b9670bab26586ab5 Mon Sep 17 00:00:00 2001 From: laxman h Date: Mon, 27 Apr 2026 19:10:49 +0530 Subject: [PATCH] after the 3rd demo fe points covered like templates improvd ui enahanced to match original theme check revokemiddlware added stagwe transistion even after one rejection flow added --- src/api/API.ts | 2 + src/components/dealer/QuestionnaireForm.tsx | 15 +- src/components/layout/Header.tsx | 2 +- src/features/master/components/ASMDialog.tsx | 36 +-- .../master/components/ASMManagement.tsx | 8 +- .../master/components/DealerAsmAssignment.tsx | 232 ++++++++++++++++++ src/features/master/pages/MasterPage.tsx | 13 +- .../ApplicationDetailsActionModals.tsx | 59 +++++ .../ApplicationDetailsExtendedModals.tsx | 80 +++++- .../ApplicationDetailsTabs.tsx | 72 +++++- .../useApplicationDetailsAdminActions.ts | 104 ++++++-- .../hooks/useApplicationDetailsData.ts | 14 +- .../useApplicationDetailsFeedbackActions.ts | 127 ++++++++-- .../useApplicationDetailsLocalActions.ts | 6 +- .../hooks/useApplicationDetailsPermissions.ts | 13 +- .../hooks/useApplicationDetailsStageData.ts | 59 ++++- .../hooks/useApplicationDetailsUIState.ts | 14 +- .../onboarding/pages/ApplicationDetails.tsx | 31 ++- .../pages/OpportunityRequestsPage.tsx | 15 +- src/pages/public/PublicQuestionnairePage.tsx | 15 +- src/services/master.service.ts | 8 + src/styles/globals.css | 10 + 22 files changed, 786 insertions(+), 149 deletions(-) create mode 100644 src/features/master/components/DealerAsmAssignment.tsx diff --git a/src/api/API.ts b/src/api/API.ts index 639a554..4ac30ba 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -222,6 +222,8 @@ export const API = { // System Configs getSystemConfigs: (params?: any) => client.get('/master/system-configs', params), saveSystemConfig: (data: any) => client.post('/master/system-configs', data), + getDealerAsmMappings: () => client.get('/master/dealer-asm-mappings'), + saveDealerAsmMapping: (data: { dealerId: string; asmUserId?: string | null }) => client.post('/master/dealer-asm-mappings', data), // EOR Checklist getEorChecklistForApplication: (applicationId: string) => client.get(`/eor/application/${applicationId}`), diff --git a/src/components/dealer/QuestionnaireForm.tsx b/src/components/dealer/QuestionnaireForm.tsx index 8468bad..421509d 100644 --- a/src/components/dealer/QuestionnaireForm.tsx +++ b/src/components/dealer/QuestionnaireForm.tsx @@ -146,7 +146,20 @@ const QuestionnaireForm: React.FC = ({ return (
-

Dealership Assessment Questionnaire

+
+
+
+ Royal Enfield +
+

ROYAL ENFIELD

+

Dealership Partner Application

+
+
+
+
+

Dealership Assessment Questionnaire

+
+
{Object.entries(sections).map(([sectionName, sectionQuestions]) => (
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 0b2e931..b3975dc 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -108,7 +108,7 @@ export function Header({ title, onRefresh }: HeaderProps) { {/* Current User Info */} {currentUser && (
-
+
diff --git a/src/features/master/components/ASMDialog.tsx b/src/features/master/components/ASMDialog.tsx index cb72f62..34e5872 100644 --- a/src/features/master/components/ASMDialog.tsx +++ b/src/features/master/components/ASMDialog.tsx @@ -43,12 +43,9 @@ export const ASMDialog: React.FC = ({ const { zones, regionalOffices } = useSelector((state: RootState) => state.master); const filteredASMUsers = userAssignedData.filter(u => { - const roles = u.allRoles || []; - return roles.some((r: string) => { - const roleStr = (r || '').toUpperCase(); - return ['ASM', 'AREA SALES MANAGER', 'DD-AM', 'AREA MANAGER', 'RM', 'RBM', 'REGIONAL MANAGER', 'ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD'].includes(roleStr) || - roleStr.includes('AREA SALES') || roleStr.includes('REGIONAL') || roleStr.includes('ZONAL'); - }); + const roles = (u.allRoles || []).map((r: string) => String(r || '').toUpperCase()); + const roleCode = String(u.roleCode || '').toUpperCase(); + return roles.includes('DD-AM') || roleCode === 'DD-AM'; }); // PRE-FILLING LOGIC: When manager or role changes, pre-select their districts @@ -71,8 +68,8 @@ export const ASMDialog: React.FC = ({ - {editingASMId ? 'Edit' : 'Add'} Area Sales Manager - Configure ASM details and assignment + {editingASMId ? 'Edit' : 'Add'} DD Area Manager + Configure DD-AM details and district assignment
@@ -116,24 +113,11 @@ export const ASMDialog: React.FC = ({
-
- - -
-
- + { @@ -262,7 +246,7 @@ export const ASMDialog: React.FC = ({ disabled={!!editingASMId} > - + {filteredASMUsers.length > 0 ? ( @@ -296,7 +280,7 @@ export const ASMDialog: React.FC = ({
- +
diff --git a/src/features/master/components/ASMManagement.tsx b/src/features/master/components/ASMManagement.tsx index a83840f..45cca2f 100644 --- a/src/features/master/components/ASMManagement.tsx +++ b/src/features/master/components/ASMManagement.tsx @@ -38,12 +38,12 @@ export const ASMManagement: React.FC = ({
- Area Sales Managers (ASM) - Manage ASMs across all regions and zones + District Development Area Managers (DD-AM) + Manage DD-AM users across districts (multi-district)
@@ -51,7 +51,7 @@ export const ASMManagement: React.FC = ({ - ASM Code + DD-AM Code Name Zone Region diff --git a/src/features/master/components/DealerAsmAssignment.tsx b/src/features/master/components/DealerAsmAssignment.tsx new file mode 100644 index 0000000..aa889c9 --- /dev/null +++ b/src/features/master/components/DealerAsmAssignment.tsx @@ -0,0 +1,232 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; +import { Check, ChevronsUpDown } from 'lucide-react'; +import { toast } from 'sonner'; +import { masterService } from '@/services/master.service'; +import { cn } from '@/components/ui/utils'; + +type DealerRow = { + dealerId: string; + dealerName: string; + legalName: string; + dealerCode: string; + status: string; + assignedAsm: null | { + id: string; + fullName: string; + email: string; + employeeId?: string; + }; + assignedAt?: string | null; +}; + +type AsmUser = { + id: string; + fullName: string; + email: string; + employeeId?: string; +}; + +type AsmSearchSelectProps = { + value: string; + onChange: (value: string) => void; + asmUsers: AsmUser[]; + className?: string; +}; + +const AsmSearchSelect: React.FC = ({ value, onChange, asmUsers, className }) => { + const [open, setOpen] = useState(false); + const selectedAsm = asmUsers.find((u) => u.id === value); + + return ( + + + + + + + + + No ASM found. + + { + onChange('__none__'); + setOpen(false); + }} + > + + Unassign + + {asmUsers.map((asm) => ( + { + onChange(asm.id); + setOpen(false); + }} + > + + {asm.fullName} ({asm.employeeId || asm.email}) + + ))} + + + + + + ); +}; + +export const DealerAsmAssignment: React.FC = () => { + const [loading, setLoading] = useState(false); + const [dealers, setDealers] = useState([]); + const [asmUsers, setAsmUsers] = useState([]); + const [draft, setDraft] = useState>({}); + + const fetchData = async () => { + try { + setLoading(true); + const res: any = await (masterService as any).getDealerAsmMappings(); + if (res?.success) { + setDealers(res.data?.dealers || []); + setAsmUsers(res.data?.asmUsers || []); + } + } catch (error: any) { + toast.error(error?.response?.data?.message || 'Failed to load dealer ASM mappings'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + const sortedDealers = useMemo( + () => + [...dealers].sort((a, b) => { + const aActive = String(a.status || '').toLowerCase() === 'active'; + const bActive = String(b.status || '').toLowerCase() === 'active'; + if (aActive !== bActive) return aActive ? -1 : 1; + return String(a.dealerName || '').localeCompare(String(b.dealerName || '')); + }), + [dealers] + ); + + const saveMapping = async (dealerId: string) => { + const selectedAsm = draft[dealerId] || ''; + try { + const res: any = await (masterService as any).saveDealerAsmMapping({ + dealerId, + asmUserId: selectedAsm === '__none__' ? null : selectedAsm || null, + }); + if (res?.success) { + toast.success(res.message || 'Dealer ASM mapping updated'); + await fetchData(); + } else { + toast.error(res?.message || 'Failed to save mapping'); + } + } catch (error: any) { + toast.error(error?.response?.data?.message || 'Failed to save mapping'); + } + }; + + return ( + + + Dealer-Level ASM Assignment + + Assign Sales ASM to onboarded dealers. DD-AM remains district-level in the section above. + + + + {loading ? ( +

Loading mappings...

+ ) : ( +
+ + + Dealer + Dealer Code + Status + Current ASM + Assign ASM + + + + {sortedDealers.length === 0 && ( + + + No dealers available for ASM mapping yet. + + + )} + {sortedDealers.map((dealer) => ( + + +
+ {dealer.dealerName} + {dealer.legalName} +
+
+ {dealer.dealerCode || 'N/A'} + + + {dealer.status || 'Unknown'} + + + + {dealer.assignedAsm ? ( +
+ {dealer.assignedAsm.fullName} + {dealer.assignedAsm.employeeId || dealer.assignedAsm.email} +
+ ) : ( + Unassigned + )} +
+ +
+ setDraft((prev) => ({ ...prev, [dealer.dealerId]: val }))} + className="flex-1 min-w-[180px]" + /> + +
+
+
+ ))} +
+
+ )} + + + ); +}; + diff --git a/src/features/master/pages/MasterPage.tsx b/src/features/master/pages/MasterPage.tsx index 0f16fe4..b30ae30 100644 --- a/src/features/master/pages/MasterPage.tsx +++ b/src/features/master/pages/MasterPage.tsx @@ -24,6 +24,7 @@ import { AddRoleDialog } from '@/features/master/components/AddRoleDialog'; import { EmailTemplates } from '@/features/master/components/EmailTemplates'; import { LocationManagement } from '@/features/master/components/LocationManagement'; import { ASMDialog } from '@/features/master/components/ASMDialog'; +import { DealerAsmAssignment } from '@/features/master/components/DealerAsmAssignment'; import { ZMDialog } from '@/features/master/components/ZMDialog'; import { ZoneDialog } from '@/features/master/components/ZoneDialog'; import { RegionDialog } from '@/features/master/components/RegionDialog'; @@ -65,7 +66,7 @@ export const MasterPage: React.FC = () => { const [selectedASMRegion, setSelectedASMRegion] = useState(''); const [selectedASMStates, setSelectedASMStates] = useState([]); const [selectedASMDistricts, setSelectedASMDistricts] = useState([]); - const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('ASM'); + const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('DD-AM'); // ZM Management State const [showZMDialog, setShowZMDialog] = useState(false); @@ -156,7 +157,7 @@ export const MasterPage: React.FC = () => { // Handlers const handleSaveASM = async () => { if (!asmManagerId) { - toast.error('Please select an ASM user'); + toast.error('Please select a DD-AM user'); return; } try { @@ -168,7 +169,7 @@ export const MasterPage: React.FC = () => { }; const res = await masterService.saveASM(payload) as any; if (res.success) { - toast.success(`${asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'} ${editingASMId ? 'updated' : 'assigned'} successfully`); + toast.success(`DD Area Manager ${editingASMId ? 'updated' : 'assigned'} successfully`); setShowASMDialog(false); fetchInitialData(); } else { @@ -188,7 +189,7 @@ export const MasterPage: React.FC = () => { setSelectedASMRegion(asm.regionId); setSelectedASMStates(asm.stateNames || []); setSelectedASMDistricts(asm.areasManaged?.map((a: any) => a.id) || []); - setAsmRoleCode(asm.roleCode === 'DD-AM' ? 'DD-AM' : 'ASM'); + setAsmRoleCode('DD-AM'); setShowASMDialog(true); }; @@ -500,9 +501,11 @@ export const MasterPage: React.FC = () => { onEditZM={handleEditZM} onDeleteZM={() => toast.error('ZM deletion restricted')} /> - { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setShowASMDialog(true); }} + { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setAsmRoleCode('DD-AM'); setShowASMDialog(true); }} onEditASM={handleEditASM} onDeleteASM={() => toast.error('ASM deletion restricted')} /> + + 0 ? users : asms} /> diff --git a/src/features/onboarding/components/application-details/ApplicationDetailsActionModals.tsx b/src/features/onboarding/components/application-details/ApplicationDetailsActionModals.tsx index f54df2f..48695e4 100644 --- a/src/features/onboarding/components/application-details/ApplicationDetailsActionModals.tsx +++ b/src/features/onboarding/components/application-details/ApplicationDetailsActionModals.tsx @@ -30,6 +30,11 @@ interface ApplicationDetailsActionModalsProps { handleReject: () => void; showScheduleModal: boolean; setShowScheduleModal: (value: boolean) => void; + showCancelInterviewModal: boolean; + setShowCancelInterviewModal: (value: boolean) => void; + setInterviewIdToCancel: (value: string) => void; + isCancellingInterview: boolean; + handleConfirmCancelInterview: () => void; interviewType: string; setInterviewType: (value: string) => void; interviewMode: string; @@ -89,6 +94,11 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo handleReject, showScheduleModal, setShowScheduleModal, + showCancelInterviewModal, + setShowCancelInterviewModal, + setInterviewIdToCancel, + isCancellingInterview, + handleConfirmCancelInterview, interviewType, setInterviewType, interviewMode, @@ -125,6 +135,14 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo handleUpdateArchitectureStatus, } = props; + const participantRoleLabel = (participant: any) => + participant?.__stageRole || + participant?.role?.roleName || + participant?.role?.roleCode || + participant?.roleCode || + participant?.role || + 'Panelist'; + return ( <> @@ -278,6 +296,7 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo {scheduledInterviewParticipants.map((p) => (
{p.fullName || p.name || 'Unknown'} + ({participantRoleLabel(p)})
))} @@ -293,6 +312,46 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
+ { + setShowCancelInterviewModal(open); + if (!open) setInterviewIdToCancel(''); + }} + > + + + Cancel Interview + + Are you sure you want to cancel this interview? + + +
+ + +
+
+
+ diff --git a/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx b/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx index c46866c..42bf2c3 100644 --- a/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx +++ b/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx @@ -36,6 +36,8 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend handleKTMatrixChange, ktMatrixRemarks, setKtMatrixRemarks, + ktMatrixRecommendation, + setKtMatrixRecommendation, calculateKTScore, handleSubmitKTMatrix, isSubmittingKT, @@ -43,15 +45,20 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend setShowLevel2FeedbackModal, level2Feedback, handleLevel2Change, + level2Recommendation, + setLevel2Recommendation, handleSubmitLevel2Feedback, isSubmittingLevel2, showFeedbackDetailsModal, setShowFeedbackDetailsModal, selectedEvaluationForView, + selectedInterviewForFeedback, showLevel3FeedbackModal, setShowLevel3FeedbackModal, level3Feedback, handleLevel3Change, + level3Recommendation, + setLevel3Recommendation, handleSubmitLevel3Feedback, isSubmittingLevel3, showDocumentsModal, @@ -95,6 +102,11 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend handleUpdateFirmType, } = props; + const selectedInterviewDate = selectedInterviewForFeedback?.scheduleDate + ? new Date(selectedInterviewForFeedback.scheduleDate).toISOString().split('T')[0] + : ''; + const interviewerDisplayName = currentUser?.fullName || currentUser?.name || ''; + return ( <> @@ -113,6 +125,11 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
+ {ktCriteria.length === 0 && ( +
+ KT Matrix configuration is not available. Configure it in Master > Interview Configurations. +
+ )} {ktCriteria.map((criterion: any, idx: number) => (
+
+ + +

Weighted total {calculateKTScore()} / 100

- +
@@ -169,8 +199,8 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend Provide detailed feedback from the Level 2 interview (DD Lead + ZBH evaluation).
-
-
+
+
+
+ + +
+ {l2Fields.length === 0 && ( +
+ Level 2 feedback configuration is not available. Configure it in Master > Interview Configurations. +
+ )} {(l2Fields || []).map((field: any, idx: number) => (
@@ -254,8 +302,8 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend Provide detailed feedback from the Level 3 interview (NBH + DD-Head evaluation).
-
-
+
+
+
+ + +
+ {l3Fields.length === 0 && ( +
+ Level 3 feedback configuration is not available. Configure it in Master > Interview Configurations. +
+ )} {(l3Fields || []).map((field: any, idx: number) => (
diff --git a/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx b/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx index 8133766..20ed457 100644 --- a/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx +++ b/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx @@ -105,6 +105,22 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) { auditLogActionBadgeClass, } = props; + const normalizeRole = (value: unknown): string => + String(value || '') + .trim() + .toLowerCase() + .replace(/[_\s-]+/g, ' '); + + const participantHasAnyRole = (participant: any, expectedRoles: string[]) => { + const participantRoles = [ + participant?.user?.role, + participant?.user?.roleCode, + participant?.metadata?.role, + ].map(normalizeRole); + const normalizedExpected = expectedRoles.map(normalizeRole); + return participantRoles.some((role) => normalizedExpected.includes(role)); + }; + return ( @@ -139,13 +155,38 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
{(() => { + const interviewRoleMap: Record = { + 1: ['DD-ZM', 'RBM'], + 2: ['DD Lead', 'ZBH'], + 3: ['NBH', 'DD Head'], + }; + + const stageRoleMap: Record = { + LOI_APPROVAL: ['DD Head', 'NBH'], + LOA_APPROVAL: ['DD Head', 'NBH'], + }; + const getApproverStatus = (stageCode: string | number) => { - const stageParticipants = (application.participants || []).filter((p: any) => - p.metadata?.stageCode === stageCode || - p.metadata?.allAssignments?.includes(stageCode) || - (typeof stageCode === 'number' && (p.metadata?.interviewLevel === stageCode || p.metadata?.allAssignments?.includes(stageCode))) || - (typeof stageCode === 'string' && !isNaN(Number(stageCode)) && (p.metadata?.interviewLevel === Number(stageCode) || p.metadata?.allAssignments?.includes(Number(stageCode)))) - ); + const stageParticipants = (application.participants || []).filter((p: any) => { + const metadataMatch = + p.metadata?.stageCode === stageCode || + p.metadata?.allAssignments?.includes(stageCode) || + (typeof stageCode === 'number' && + (p.metadata?.interviewLevel === stageCode || + p.metadata?.interviewLevel === String(stageCode) || + p.metadata?.allAssignments?.includes(stageCode) || + p.metadata?.allAssignments?.includes(String(stageCode)))) || + (typeof stageCode === 'string' && + !isNaN(Number(stageCode)) && + (p.metadata?.interviewLevel === Number(stageCode) || + p.metadata?.allAssignments?.includes(Number(stageCode)))); + + if (metadataMatch) return true; + if (typeof stageCode === 'number') { + return participantHasAnyRole(p, interviewRoleMap[stageCode] || []); + } + return participantHasAnyRole(p, stageRoleMap[stageCode] || []); + }); return stageParticipants.map((p: any) => { const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode; @@ -155,8 +196,8 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) { ); return { - name: p.user?.name || 'Unknown', - role: p.user?.role || 'Reviewer', + name: p.user?.name || p.user?.fullName || 'Unknown', + role: p.user?.role || p.user?.roleCode || p.metadata?.role || 'Reviewer', status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending' }; }); @@ -278,11 +319,16 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) { const stageId = Number(stage.id); const expectedCount = expectedMap[stageId]; - let actualCount = stage.evaluators?.length || 0; - if (stageId === 3) { - const l1Evaluators = (application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 1 || p.metadata?.interviewLevel === '1'); - actualCount = l1Evaluators.length; - } + const stageCodeById: Record = { + 3: 1, // shortlist depends on L1 evaluators + 4: 1, + 5: 2, + 6: 3, + 8: 'LOI_APPROVAL', + 12: 'LOA_APPROVAL', + }; + const mappedStageCode = stageCodeById[stageId]; + const actualCount = mappedStageCode ? getApproverStatus(mappedStageCode).length : (stage.evaluators?.length || 0); const isEligibleForWarning = stageId === 3 ? (stage.status === 'completed') : (stage.status !== 'pending'); diff --git a/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts b/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts index df808d8..8aaf9f1 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction } from 'react'; +import { Dispatch, SetStateAction, useCallback } from 'react'; import { toast } from 'sonner'; import { onboardingService } from '@/services/onboarding.service'; @@ -42,6 +42,10 @@ interface UseApplicationDetailsAdminActionsParams { setLoading: Dispatch>; setIsScheduling: Dispatch>; setShowScheduleModal: Dispatch>; + setShowCancelInterviewModal: Dispatch>; + interviewIdToCancel: string; + setInterviewIdToCancel: Dispatch>; + setIsCancellingInterview: Dispatch>; setIsUploading: Dispatch>; setShowUploadForm: Dispatch>; setUploadFile: Dispatch>; @@ -100,6 +104,10 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA setLoading, setIsScheduling, setShowScheduleModal, + setShowCancelInterviewModal, + interviewIdToCancel, + setInterviewIdToCancel, + setIsCancellingInterview, setIsUploading, setShowUploadForm, setUploadFile, @@ -131,7 +139,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA setScheduledInterviewParticipants(scheduledInterviewParticipants.filter((p) => p.id !== userId)); }; - const fetchUsers = async (type?: string) => { + 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 = {}; @@ -141,34 +149,77 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA level2: ['DD Lead', 'ZBH'], level3: ['NBH', 'DD Head'], }; - reqParams.roleCode = roleMapping[type]; + // 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); - if (Array.isArray(response)) setUsers(response); - else if (response && Array.isArray(response.data)) setUsers(response.data); - else if (response && Array.isArray(response.users)) setUsers(response.users); - else setUsers([]); + 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 = () => { + const prefillInterviewParticipants = useCallback(() => { if (!showScheduleModal || !application) return; const levelNum = parseInt(interviewType.replace('level', '')) || 1; + const requiredRolesByLevel: Record = { + 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)) + 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) => p.user) - .filter(Boolean); + .map((p: any) => { + const user = p.user || {}; + return { + ...user, + __stageRole: deriveDisplayRole(p, user), + }; + }) + .filter((u: any) => !!u?.id); if (preAssigned.length === 0) { setScheduledInterviewParticipants([]); return; @@ -182,7 +233,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA } }); setScheduledInterviewParticipants(unique); - }; + }, [showScheduleModal, application, interviewType, setScheduledInterviewParticipants]); const handleScheduleInterview = async () => { if (!interviewDate) { @@ -211,13 +262,23 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA }; const handleCancelInterview = async (interviewId: string) => { - if (!window.confirm('Are you sure you want to cancel this interview?')) return; + setInterviewIdToCancel(interviewId); + setShowCancelInterviewModal(true); + }; + + const handleConfirmCancelInterview = async () => { + if (!interviewIdToCancel) return; try { - await onboardingService.updateInterview(interviewId, { status: 'Cancelled' }); + 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); } }; @@ -520,7 +581,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA } }; - const maybeFetchUsersForModal = async () => { + const maybeFetchUsersForModal = useCallback(async () => { if (showScheduleModal && application) { await fetchUsers(interviewType); prefillInterviewParticipants(); @@ -529,7 +590,15 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA if ((showAssignArchitectureModal || showAssignModal) && application) { await fetchUsers(); } - }; + }, [ + showScheduleModal, + showAssignArchitectureModal, + showAssignModal, + application, + interviewType, + fetchUsers, + prefillInterviewParticipants, + ]); return { handleAddInterviewer, @@ -538,6 +607,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA maybeFetchUsersForModal, handleScheduleInterview, handleCancelInterview, + handleConfirmCancelInterview, handleUpload, handleApprove, handleReject, diff --git a/src/features/onboarding/hooks/useApplicationDetailsData.ts b/src/features/onboarding/hooks/useApplicationDetailsData.ts index 869b642..8c88e4e 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsData.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsData.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Application, ApplicationStatus } from '@/lib/mock-data'; import { onboardingService } from '@/services/onboarding.service'; import { eorService } from '@/services/eor.service'; @@ -20,16 +20,16 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai const [deposits, setDeposits] = useState([]); const [paymentConfigs, setPaymentConfigs] = useState({}); - const refreshDocuments = async () => { + const refreshDocuments = useCallback(async () => { try { const docs = await onboardingService.getDocuments(applicationId); setDocuments(docs || []); } catch (error) { console.error('Failed to refresh documents:', error); } - }; + }, [applicationId]); - const fetchApplication = async (silent = false) => { + const fetchApplication = useCallback(async (silent = false) => { try { if (!silent) setLoading(true); const data = await onboardingService.getApplicationById(applicationId); @@ -123,9 +123,9 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai } finally { setLoading(false); } - }; + }, [applicationId]); - const fetchEorData = async () => { + const fetchEorData = useCallback(async () => { if (!applicationId) return; try { const resp = await eorService.getChecklist(applicationId); @@ -133,7 +133,7 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai } catch { setEorData(null); } - }; + }, [applicationId]); const getDeposit = (type: string) => deposits.find((d) => d.depositType === type); diff --git a/src/features/onboarding/hooks/useApplicationDetailsFeedbackActions.ts b/src/features/onboarding/hooks/useApplicationDetailsFeedbackActions.ts index eb59674..d7adb03 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsFeedbackActions.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsFeedbackActions.ts @@ -1,7 +1,6 @@ import { toast } from 'sonner'; import { Dispatch, SetStateAction } from 'react'; import { onboardingService } from '@/services/onboarding.service'; -import { KT_MATRIX_CRITERIA } from '@/features/onboarding/components/application-details/applicationDetails.shared'; import type { InterviewConfig } from './useInterviewConfigs'; interface UseApplicationDetailsFeedbackActionsParams { @@ -10,16 +9,22 @@ interface UseApplicationDetailsFeedbackActionsParams { setKtMatrixSelectedValues: Dispatch>>; ktMatrixRemarks: string; setKtMatrixRemarks: Dispatch>; + ktMatrixRecommendation: string; + setKtMatrixRecommendation: Dispatch>; selectedInterviewForFeedback: any; interviews: any[]; setIsSubmittingKT: Dispatch>; setShowKTMatrixModal: Dispatch>; level2Feedback: any; setLevel2Feedback: Dispatch>; + level2Recommendation: string; + setLevel2Recommendation: Dispatch>; setIsSubmittingLevel2: Dispatch>; setShowLevel2FeedbackModal: Dispatch>; level3Feedback: any; setLevel3Feedback: Dispatch>; + level3Recommendation: string; + setLevel3Recommendation: Dispatch>; setIsSubmittingLevel3: Dispatch>; setShowLevel3FeedbackModal: Dispatch>; currentUser: any; @@ -64,16 +69,22 @@ export function useApplicationDetailsFeedbackActions({ setKtMatrixSelectedValues, ktMatrixRemarks, setKtMatrixRemarks, + ktMatrixRecommendation, + setKtMatrixRecommendation, selectedInterviewForFeedback, interviews, setIsSubmittingKT, setShowKTMatrixModal, level2Feedback, setLevel2Feedback, + level2Recommendation, + setLevel2Recommendation, setIsSubmittingLevel2, setShowLevel2FeedbackModal, level3Feedback, setLevel3Feedback, + level3Recommendation, + setLevel3Recommendation, setIsSubmittingLevel3, setShowLevel3FeedbackModal, currentUser, @@ -83,6 +94,17 @@ export function useApplicationDetailsFeedbackActions({ level2Config, level3Config, }: UseApplicationDetailsFeedbackActionsParams) { + const mapRecommendationForFeedback = (value: string) => { + if (value === 'Approve') return 'Recommended'; + if (value === 'Reject') return 'Not Recommended'; + return 'Hold'; + }; + + const mapRecommendationForDecision = (value: string) => { + if (value === 'Approve') return 'Approved'; + if (value === 'Reject') return 'Rejected'; + return null; + }; // Resolve active criteria/fields from config or fallback to hardcoded defaults const getKtCriteria = () => { if (ktMatrixConfig?.items && ktMatrixConfig.items.length > 0) { @@ -97,37 +119,21 @@ export function useApplicationDetailsFeedbackActions({ })), })); } - return KT_MATRIX_CRITERIA; + return []; }; const getLevel2Fields = () => { if (level2Config?.items && level2Config.items.length > 0) { return level2Config.items; } - return [ - { itemKey: 'strategicVision', label: 'Strategic Vision', isRequired: true }, - { itemKey: 'managementCapabilities', label: 'Management Capabilities', isRequired: true }, - { itemKey: 'operationalUnderstanding', label: 'Operational Understanding', isRequired: true }, - { itemKey: 'keyStrengths', label: 'Key Strengths', isRequired: true }, - { itemKey: 'areasOfConcern', label: 'Areas of Concern', isRequired: true }, - { itemKey: 'additionalComments', label: 'Additional Comments', isRequired: false }, - ]; + return []; }; const getLevel3Fields = () => { if (level3Config?.items && level3Config.items.length > 0) { return level3Config.items; } - return [ - { itemKey: 'strategicVision', label: 'Business Vision & Strategy', isRequired: true }, - { itemKey: 'managementCapabilities', label: 'Leadership & Decision Making', isRequired: true }, - { itemKey: 'operationalUnderstanding', label: 'Operational & Financial Readiness', isRequired: true }, - { itemKey: 'brandAlignment', label: 'Brand Alignment', isRequired: true }, - { itemKey: 'keyStrengths', label: 'Key Strengths', isRequired: true }, - { itemKey: 'areasOfConcern', label: 'Areas of Concern', isRequired: true }, - { itemKey: 'executiveSummary', label: 'Executive Summary', isRequired: false }, - { itemKey: 'additionalComments', label: 'Additional Comments', isRequired: false }, - ]; + return []; }; const ktCriteria = getKtCriteria(); @@ -151,6 +157,10 @@ export function useApplicationDetailsFeedbackActions({ }; const handleSubmitKTMatrix = async () => { + if (ktCriteria.length === 0) { + toast.error('KT Matrix configuration is missing. Please configure it in Master > Interview Configurations.'); + return; + } if (Object.keys(ktMatrixScores).length < ktCriteria.length) { toast.warning('Please fill all fields in the KT Matrix'); return; @@ -168,12 +178,32 @@ export function useApplicationDetailsFeedbackActions({ maxScore: c.maxScore || 10, weightage: c.weight || 0, })); - await onboardingService.submitKTMatrix({ interviewId, criteriaScores, feedback: ktMatrixRemarks, recommendation: null }); - toast.success('KT Matrix submitted successfully'); + await onboardingService.submitKTMatrix({ + interviewId, + criteriaScores, + feedback: ktMatrixRemarks, + recommendation: mapRecommendationForFeedback(ktMatrixRecommendation) + }); + + const decision = mapRecommendationForDecision(ktMatrixRecommendation); + if (decision) { + await onboardingService.updateInterviewDecision({ + interviewId, + decision, + remarks: ktMatrixRemarks || `Level 1 ${decision.toLowerCase()} via KT Matrix` + }); + } + + toast.success( + decision + ? `KT Matrix submitted and interview ${decision.toLowerCase()}` + : 'KT Matrix submitted and interview kept on hold' + ); setShowKTMatrixModal(false); setKtMatrixScores({}); setKtMatrixSelectedValues({}); setKtMatrixRemarks(''); + setKtMatrixRecommendation('Approve'); await fetchInterviews(); await fetchApplication(); } catch { @@ -188,6 +218,10 @@ export function useApplicationDetailsFeedbackActions({ }; const handleSubmitLevel2Feedback = async () => { + if (l2Fields.length === 0) { + toast.error('Level 2 feedback configuration is missing. Please configure it in Master > Interview Configurations.'); + return; + } if (!level2Feedback.overallScore) { toast.warning('Please provide an overall score.'); return; @@ -202,10 +236,27 @@ export function useApplicationDetailsFeedbackActions({ const feedbackItems = l2Fields .map((f: any) => ({ type: f.label, comments: level2Feedback[f.itemKey] || '' })) .filter((item) => item.comments.trim() !== ''); - await onboardingService.submitLevel2Feedback({ interviewId, overallScore: Number(level2Feedback.overallScore), feedbackItems }); - toast.success('Level 2 Feedback submitted successfully'); + await onboardingService.submitLevel2Feedback({ + interviewId, + overallScore: Number(level2Feedback.overallScore), + feedbackItems, + recommendation: mapRecommendationForFeedback(level2Recommendation) + }); + + const decision = mapRecommendationForDecision(level2Recommendation); + const remarks = level2Feedback.additionalComments || 'Level 2 decision submitted via feedback modal'; + if (decision) { + await onboardingService.updateInterviewDecision({ interviewId, decision, remarks }); + } + + toast.success( + decision + ? `Level 2 feedback submitted and interview ${decision.toLowerCase()}` + : 'Level 2 feedback submitted and interview kept on hold' + ); setShowLevel2FeedbackModal(false); setLevel2Feedback(createInitialLevel2Feedback(currentUser)); + setLevel2Recommendation('Approve'); await fetchInterviews(); await fetchApplication(); } catch { @@ -220,6 +271,10 @@ export function useApplicationDetailsFeedbackActions({ }; const handleSubmitLevel3Feedback = async () => { + if (l3Fields.length === 0) { + toast.error('Level 3 feedback configuration is missing. Please configure it in Master > Interview Configurations.'); + return; + } if (!level3Feedback.overallScore) { toast.warning('Please provide an overall score.'); return; @@ -234,10 +289,30 @@ export function useApplicationDetailsFeedbackActions({ const feedbackItems = l3Fields .map((f: any) => ({ type: f.label, comments: level3Feedback[f.itemKey] || '' })) .filter((item) => item.comments.trim() !== ''); - await onboardingService.submitLevel2Feedback({ interviewId, overallScore: Number(level3Feedback.overallScore), feedbackItems }); - toast.success('Level 3 Feedback submitted successfully'); + await onboardingService.submitLevel2Feedback({ + interviewId, + overallScore: Number(level3Feedback.overallScore), + feedbackItems, + recommendation: mapRecommendationForFeedback(level3Recommendation) + }); + + const decision = mapRecommendationForDecision(level3Recommendation); + const remarks = + level3Feedback.executiveSummary || + level3Feedback.additionalComments || + 'Level 3 decision submitted via feedback modal'; + if (decision) { + await onboardingService.updateInterviewDecision({ interviewId, decision, remarks }); + } + + toast.success( + decision + ? `Level 3 feedback submitted and interview ${decision.toLowerCase()}` + : 'Level 3 feedback submitted and interview kept on hold' + ); setShowLevel3FeedbackModal(false); setLevel3Feedback(createInitialLevel3Feedback(currentUser)); + setLevel3Recommendation('Approve'); await fetchInterviews(); await fetchApplication(); } catch { diff --git a/src/features/onboarding/hooks/useApplicationDetailsLocalActions.ts b/src/features/onboarding/hooks/useApplicationDetailsLocalActions.ts index d78a5a3..74ea93c 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsLocalActions.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsLocalActions.ts @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction } from 'react'; +import { Dispatch, SetStateAction, useCallback } from 'react'; import { toast } from 'sonner'; import { onboardingService } from '@/services/onboarding.service'; @@ -75,14 +75,14 @@ export function useApplicationDetailsLocalActions({ } }; - const fetchFddAgencies = async () => { + const fetchFddAgencies = useCallback(async () => { try { const agencies = await onboardingService.getUsers({ roleCode: 'FDD' }); setFddAgencies(Array.isArray(agencies) ? agencies : []); } catch { setFddAgencies([]); } - }; + }, [setFddAgencies]); const handleAssignAgency = async () => { if (!selectedAgencyId) { diff --git a/src/features/onboarding/hooks/useApplicationDetailsPermissions.ts b/src/features/onboarding/hooks/useApplicationDetailsPermissions.ts index 75c4828..48eef10 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsPermissions.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsPermissions.ts @@ -80,10 +80,6 @@ export function useApplicationDetailsPermissions({ getDeposit('SECURITY_DEPOSIT')?.status !== 'Verified'; const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected'; - const hasFeedbackForActive = !!(activeInterviewForUser || lastInterviewForUser)?.evaluations?.find( - (e: any) => e.evaluatorId === currentUser?.id - ); - const ddHeadApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'); const ddHeadLoaApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'); @@ -105,11 +101,10 @@ export function useApplicationDetailsPermissions({ const canApproveReject = !isFinalState && !isDecisionMade && - ((!!activeInterviewForUser && !!hasFeedbackForActive) || - (isAdminRole && - isAdministrativeStage && - sequenceMet && - (!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100))); + (isAdminRole && + isAdministrativeStage && + sequenceMet && + (!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100)); return { canApprove: canApproveReject && !isLoaLocked && !isSecurityDetailsLocked, diff --git a/src/features/onboarding/hooks/useApplicationDetailsStageData.ts b/src/features/onboarding/hooks/useApplicationDetailsStageData.ts index a44f1ff..5de2ff6 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsStageData.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsStageData.ts @@ -15,6 +15,25 @@ export function useApplicationDetailsStageData({ eorData, getDeposit, }: UseApplicationDetailsStageDataParams) { + const normalizeRole = (value: unknown): string => + String(value || '') + .trim() + .toLowerCase() + .replace(/[_\s-]+/g, ' '); + + const hasAnyRole = (participant: any, expectedRoles: string[]) => { + const userRoles = [ + participant?.user?.role, + participant?.user?.roleCode, + participant?.metadata?.role, + ].map(normalizeRole); + const target = expectedRoles.map(normalizeRole); + return userRoles.some((r) => target.includes(r)); + }; + + const participantLabel = (participant: any) => + `${participant?.user?.fullName || participant?.user?.name || 'User'} (${participant?.user?.role || participant?.user?.roleCode || participant?.metadata?.role || participant?.participantType || 'participant'})`; + const isDocumentUploaded = (docType: string) => { return (documents || []).some((d) => d.documentType === docType); }; @@ -47,26 +66,56 @@ export function useApplicationDetailsStageData({ { id: 4, name: '1st Level Interview', status: getStageStatus('1st Level Interview', () => ['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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 1 Interview Pending' && isInterviewScheduled(1)) ? 'active' : 'pending'), date: application.level1InterviewDate, description: 'DD-ZM + RBM evaluation', - evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 1 || p.metadata?.interviewLevel === '1' || p.metadata?.allAssignments?.includes(1)).map((p: any) => `${p.user?.name} (${p.user?.role})`))), + evaluators: Array.from(new Set( + (application.participants || []) + .filter((p: any) => + p.metadata?.interviewLevel === 1 || + p.metadata?.interviewLevel === '1' || + p.metadata?.allAssignments?.includes(1) || + p.metadata?.allAssignments?.includes('1') || + hasAnyRole(p, ['DD-ZM', 'RBM']) + ) + .map(participantLabel) + )), documentsUploaded: 1 }, { id: 5, name: '2nd Level Interview', status: getStageStatus('2nd Level Interview', () => ['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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 2 Interview Pending' && isInterviewScheduled(2)) ? 'active' : 'pending'), date: application.level2InterviewDate, description: 'DD Lead + ZBH evaluation', - evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 2 || p.metadata?.interviewLevel === '2' || p.metadata?.allAssignments?.includes(2)).map((p: any) => `${p.user?.name} (${p.user?.role})`))), + evaluators: Array.from(new Set( + (application.participants || []) + .filter((p: any) => + p.metadata?.interviewLevel === 2 || + p.metadata?.interviewLevel === '2' || + p.metadata?.allAssignments?.includes(2) || + p.metadata?.allAssignments?.includes('2') || + hasAnyRole(p, ['DD Lead', 'ZBH']) + ) + .map(participantLabel) + )), documentsUploaded: 1 }, { id: 6, name: '3rd Level Interview', status: getStageStatus('3rd Level Interview', () => ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 3 Interview Pending' && isInterviewScheduled(3)) ? 'active' : 'pending'), date: application.level3InterviewDate, description: 'NBH + DD Head evaluation', - evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 3 || p.metadata?.interviewLevel === '3' || p.metadata?.allAssignments?.includes(3)).map((p: any) => `${p.user?.name} (${p.user?.role})`))), + evaluators: Array.from(new Set( + (application.participants || []) + .filter((p: any) => + p.metadata?.interviewLevel === 3 || + p.metadata?.interviewLevel === '3' || + p.metadata?.allAssignments?.includes(3) || + p.metadata?.allAssignments?.includes('3') || + hasAnyRole(p, ['NBH', 'DD Head']) + ) + .map(participantLabel) + )), documentsUploaded: 2 }, { id: 7, name: 'FDD', status: getStageStatus('FDD', () => ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending'), date: application.fddDate, description: 'Financial Due Diligence', documentsUploaded: 5 }, { id: 8, name: 'LOI Approval', status: getStageStatus('LOI Approval', () => ['Security Details', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'), date: application.loiApprovalDate, description: 'Letter of Intent approval', - evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL')).map((p: any) => `${p.user?.name} (${p.user?.role})`))), + evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL')).map(participantLabel))), documentsUploaded: 1 }, { @@ -105,7 +154,7 @@ export function useApplicationDetailsStageData({ id: 12, name: 'LOA', status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'), isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified', lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.', - evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map((p: any) => `${p.user?.name} (${p.user?.role})`))), + evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map(participantLabel))), description: 'Letter of Authorization' }, { id: 13, name: 'EOR Complete', status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'), description: 'Essential Operating Requirements' }, diff --git a/src/features/onboarding/hooks/useApplicationDetailsUIState.ts b/src/features/onboarding/hooks/useApplicationDetailsUIState.ts index 40699b6..006381d 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsUIState.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsUIState.ts @@ -17,13 +17,15 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U const [rejectionReason, setRejectionReason] = useState(''); const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState([]); const [showScheduleModal, setShowScheduleModal] = useState(false); + const [showCancelInterviewModal, setShowCancelInterviewModal] = useState(false); + const [interviewIdToCancel, setInterviewIdToCancel] = useState(''); const [showKTMatrixModal, setShowKTMatrixModal] = useState(false); const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false); const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false); const [showDocumentsModal, setShowDocumentsModal] = useState(false); const [showAssignModal, setShowAssignModal] = useState(false); const [selectedStage, setSelectedStage] = useState(null); - const [interviewMode, setInterviewMode] = useState('physical'); + const [interviewMode, setInterviewMode] = useState('virtual'); const [approvalRemark, setApprovalRemark] = useState(''); const [expandedBranches, setExpandedBranches] = useState>({}); const [users, setUsers] = useState([]); @@ -46,6 +48,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U const [isSavingStatutory, setIsSavingStatutory] = useState(false); const [interviews, setInterviews] = useState([]); const [isScheduling, setIsScheduling] = useState(false); + const [isCancellingInterview, setIsCancellingInterview] = useState(false); const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false); const [architectureLeadId, setArchitectureLeadId] = useState(''); const [isAssigningArchitecture, setIsAssigningArchitecture] = useState(false); @@ -63,6 +66,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U const [ktMatrixScores, setKtMatrixScores] = useState>({}); const [ktMatrixSelectedValues, setKtMatrixSelectedValues] = useState>({}); const [ktMatrixRemarks, setKtMatrixRemarks] = useState(''); + const [ktMatrixRecommendation, setKtMatrixRecommendation] = useState('Approve'); const [isSubmittingKT, setIsSubmittingKT] = useState(false); const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState(null); const [showFddFinalizeModal, setShowFddFinalizeModal] = useState(false); @@ -72,8 +76,10 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U const [isFinalizingFdd, setIsFinalizingFdd] = useState(false); const [isFddFlagging, setIsFddFlagging] = useState(false); const [level2Feedback, setLevel2Feedback] = useState({}); + const [level2Recommendation, setLevel2Recommendation] = useState('Approve'); const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false); const [level3Feedback, setLevel3Feedback] = useState({}); + const [level3Recommendation, setLevel3Recommendation] = useState('Approve'); const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false); const [selectedEvaluationForView, setSelectedEvaluationForView] = useState(null); const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false); @@ -90,6 +96,8 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U rejectionReason, setRejectionReason, scheduledInterviewParticipants, setScheduledInterviewParticipants, showScheduleModal, setShowScheduleModal, + showCancelInterviewModal, setShowCancelInterviewModal, + interviewIdToCancel, setInterviewIdToCancel, showKTMatrixModal, setShowKTMatrixModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal, @@ -119,6 +127,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U isSavingStatutory, setIsSavingStatutory, interviews, setInterviews, isScheduling, setIsScheduling, + isCancellingInterview, setIsCancellingInterview, showAssignArchitectureModal, setShowAssignArchitectureModal, architectureLeadId, setArchitectureLeadId, isAssigningArchitecture, setIsAssigningArchitecture, @@ -136,6 +145,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U ktMatrixScores, setKtMatrixScores, ktMatrixSelectedValues, setKtMatrixSelectedValues, ktMatrixRemarks, setKtMatrixRemarks, + ktMatrixRecommendation, setKtMatrixRecommendation, isSubmittingKT, setIsSubmittingKT, selectedInterviewForFeedback, setSelectedInterviewForFeedback, showFddFinalizeModal, setShowFddFinalizeModal, @@ -145,8 +155,10 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U isFinalizingFdd, setIsFinalizingFdd, isFddFlagging, setIsFddFlagging, level2Feedback, setLevel2Feedback, + level2Recommendation, setLevel2Recommendation, isSubmittingLevel2, setIsSubmittingLevel2, level3Feedback, setLevel3Feedback, + level3Recommendation, setLevel3Recommendation, isSubmittingLevel3, setIsSubmittingLevel3, selectedEvaluationForView, setSelectedEvaluationForView, showFeedbackDetailsModal, setShowFeedbackDetailsModal, diff --git a/src/features/onboarding/pages/ApplicationDetails.tsx b/src/features/onboarding/pages/ApplicationDetails.tsx index f775bb1..d38ceea 100644 --- a/src/features/onboarding/pages/ApplicationDetails.tsx +++ b/src/features/onboarding/pages/ApplicationDetails.tsx @@ -60,6 +60,8 @@ export const ApplicationDetails = () => { rejectionReason, setRejectionReason, scheduledInterviewParticipants, setScheduledInterviewParticipants, showScheduleModal, setShowScheduleModal, + showCancelInterviewModal, setShowCancelInterviewModal, + interviewIdToCancel, setInterviewIdToCancel, showKTMatrixModal, setShowKTMatrixModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal, @@ -89,6 +91,7 @@ export const ApplicationDetails = () => { isSavingStatutory, setIsSavingStatutory, interviews, setInterviews, isScheduling, setIsScheduling, + isCancellingInterview, setIsCancellingInterview, showAssignArchitectureModal, setShowAssignArchitectureModal, architectureLeadId, setArchitectureLeadId, isAssigningArchitecture, setIsAssigningArchitecture, @@ -106,6 +109,7 @@ export const ApplicationDetails = () => { ktMatrixScores, setKtMatrixScores, ktMatrixSelectedValues, setKtMatrixSelectedValues, ktMatrixRemarks, setKtMatrixRemarks, + ktMatrixRecommendation, setKtMatrixRecommendation, isSubmittingKT, setIsSubmittingKT, selectedInterviewForFeedback, setSelectedInterviewForFeedback, showFddFinalizeModal, setShowFddFinalizeModal, @@ -115,8 +119,10 @@ export const ApplicationDetails = () => { isFinalizingFdd, setIsFinalizingFdd, isFddFlagging, setIsFddFlagging, level2Feedback, setLevel2Feedback, + level2Recommendation, setLevel2Recommendation, isSubmittingLevel2, setIsSubmittingLevel2, level3Feedback, setLevel3Feedback, + level3Recommendation, setLevel3Recommendation, isSubmittingLevel3, setIsSubmittingLevel3, showFeedbackDetailsModal, setShowFeedbackDetailsModal, selectedEvaluationForView, setSelectedEvaluationForView, @@ -220,16 +226,22 @@ export const ApplicationDetails = () => { setKtMatrixSelectedValues, ktMatrixRemarks, setKtMatrixRemarks, + ktMatrixRecommendation, + setKtMatrixRecommendation, selectedInterviewForFeedback, interviews, setIsSubmittingKT, setShowKTMatrixModal, level2Feedback, setLevel2Feedback, + level2Recommendation, + setLevel2Recommendation, setIsSubmittingLevel2, setShowLevel2FeedbackModal, level3Feedback, setLevel3Feedback, + level3Recommendation, + setLevel3Recommendation, setIsSubmittingLevel3, setShowLevel3FeedbackModal, currentUser, @@ -255,6 +267,7 @@ export const ApplicationDetails = () => { maybeFetchUsersForModal, handleScheduleInterview, handleCancelInterview, + handleConfirmCancelInterview, handleUpload, handleApprove, handleReject, @@ -303,6 +316,10 @@ export const ApplicationDetails = () => { setLoading, setIsScheduling, setShowScheduleModal, + setShowCancelInterviewModal, + interviewIdToCancel, + setInterviewIdToCancel, + setIsCancellingInterview, setIsUploading, setShowUploadForm, setUploadFile, @@ -322,7 +339,7 @@ export const ApplicationDetails = () => { useEffect(() => { maybeFetchUsersForModal(); - }, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants, maybeFetchUsersForModal]); + }, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.id, maybeFetchUsersForModal]); if (loading && !application) { return ( @@ -510,6 +527,11 @@ export const ApplicationDetails = () => { handleReject={handleReject} showScheduleModal={showScheduleModal} setShowScheduleModal={setShowScheduleModal} + showCancelInterviewModal={showCancelInterviewModal} + setShowCancelInterviewModal={setShowCancelInterviewModal} + setInterviewIdToCancel={setInterviewIdToCancel} + isCancellingInterview={isCancellingInterview} + handleConfirmCancelInterview={handleConfirmCancelInterview} interviewType={interviewType} setInterviewType={setInterviewType} interviewMode={interviewMode} @@ -557,6 +579,8 @@ export const ApplicationDetails = () => { handleKTMatrixChange={handleKTMatrixChange} ktMatrixRemarks={ktMatrixRemarks} setKtMatrixRemarks={setKtMatrixRemarks} + ktMatrixRecommendation={ktMatrixRecommendation} + setKtMatrixRecommendation={setKtMatrixRecommendation} calculateKTScore={calculateKTScore} handleSubmitKTMatrix={handleSubmitKTMatrix} isSubmittingKT={isSubmittingKT} @@ -564,15 +588,20 @@ export const ApplicationDetails = () => { setShowLevel2FeedbackModal={setShowLevel2FeedbackModal} level2Feedback={level2Feedback} handleLevel2Change={handleLevel2Change} + level2Recommendation={level2Recommendation} + setLevel2Recommendation={setLevel2Recommendation} handleSubmitLevel2Feedback={handleSubmitLevel2Feedback} isSubmittingLevel2={isSubmittingLevel2} showFeedbackDetailsModal={showFeedbackDetailsModal} setShowFeedbackDetailsModal={setShowFeedbackDetailsModal} selectedEvaluationForView={selectedEvaluationForView} + selectedInterviewForFeedback={selectedInterviewForFeedback} showLevel3FeedbackModal={showLevel3FeedbackModal} setShowLevel3FeedbackModal={setShowLevel3FeedbackModal} level3Feedback={level3Feedback} handleLevel3Change={handleLevel3Change} + level3Recommendation={level3Recommendation} + setLevel3Recommendation={setLevel3Recommendation} handleSubmitLevel3Feedback={handleSubmitLevel3Feedback} isSubmittingLevel3={isSubmittingLevel3} showDocumentsModal={showDocumentsModal} diff --git a/src/features/onboarding/pages/OpportunityRequestsPage.tsx b/src/features/onboarding/pages/OpportunityRequestsPage.tsx index e6feb5b..f0e011d 100644 --- a/src/features/onboarding/pages/OpportunityRequestsPage.tsx +++ b/src/features/onboarding/pages/OpportunityRequestsPage.tsx @@ -214,18 +214,9 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa const response = await onboardingService.shortlistApplications(selectedIds, [], shortlistRemark); if (response && response.success) { - // Update local state and show success only if API succeeded - const updatedApplications = applicationsData.map(app => { - if (selectedIds.includes(app.id)) { - return { - ...app, - ddLeadShortlisted: true - } as any; - } - return app; - }); - - setApplicationsData(updatedApplications); + // Refresh data from server to ensure correct filtering and pagination + await fetchApplications(); + setSelectedIds([]); setShowShortlistModal(false); setShortlistRemark(''); diff --git a/src/pages/public/PublicQuestionnairePage.tsx b/src/pages/public/PublicQuestionnairePage.tsx index 25d051f..0827af1 100644 --- a/src/pages/public/PublicQuestionnairePage.tsx +++ b/src/pages/public/PublicQuestionnairePage.tsx @@ -5,7 +5,7 @@ import { toast } from 'sonner'; import { useSelector } from 'react-redux'; import { RootState } from '../../store'; import { - User, RefreshCw, HelpCircle, ArrowLeft, Bike, + User, RefreshCw, HelpCircle, ArrowLeft, Users, FileText, ChevronRight, CheckCircle } from 'lucide-react'; @@ -188,19 +188,12 @@ const PublicQuestionnairePage: React.FC = () => {
{/* Hero Section */} -
+
-
-
-
-
-
-
- -
+
+ Royal Enfield
-

ROYAL ENFIELD

Dealership Partner Application

diff --git a/src/services/master.service.ts b/src/services/master.service.ts index e8c32d6..caff085 100644 --- a/src/services/master.service.ts +++ b/src/services/master.service.ts @@ -162,5 +162,13 @@ export const masterService = { saveSystemConfig: async (data: any) => { const response = await API.saveSystemConfig(data); return response.data; + }, + getDealerAsmMappings: async () => { + const response = await (API as any).getDealerAsmMappings(); + return response.data; + }, + saveDealerAsmMapping: async (data: { dealerId: string; asmUserId?: string | null }) => { + const response = await (API as any).saveDealerAsmMapping(data); + return response.data; } }; diff --git a/src/styles/globals.css b/src/styles/globals.css index 4a5045c..5133591 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -148,6 +148,16 @@ } } +@layer utilities { + button.bg-amber-600 { + background-color: var(--color-re-red) !important; + } + + button.hover\:bg-amber-700:hover { + background-color: var(--color-re-red) !important; + } +} + /* RE Branding Utilities */ .re-heading { font-family: 'Montserrat', sans-serif;