diff --git a/src/App.tsx b/src/App.tsx index d7ac911..87182f8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -77,7 +77,6 @@ export default function App() { const navigate = useNavigate(); const location = useLocation(); const currentRole = currentUser?.role || currentUser?.roleCode || ''; - const normalizedRole = String(currentRole).trim().toLowerCase(); const hasRole = (roles: string[]) => { const normalizedTargetRoles = roles.map((r) => r.toLowerCase()); const userRole = String(currentUser?.role || '').toLowerCase(); diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index f31329e..2b69eaf 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -47,7 +47,6 @@ export function Sidebar({ onLogout }: SidebarProps) { const hoverTimeout = useRef | null>(null); const currentRole = currentUser?.role || currentUser?.roleCode || ''; - const normalizedRole = String(currentRole).trim().toLowerCase(); const hasRole = (roles: string[]) => { const normalizedTargetRoles = roles.map((r) => r.toLowerCase()); const userRole = String(currentUser?.role || '').toLowerCase(); diff --git a/src/features/constitutional/__tests__/ConstitutionalChangePage.test.tsx b/src/features/constitutional/__tests__/ConstitutionalChangePage.test.tsx index ba51e30..b8ac15e 100644 --- a/src/features/constitutional/__tests__/ConstitutionalChangePage.test.tsx +++ b/src/features/constitutional/__tests__/ConstitutionalChangePage.test.tsx @@ -1,6 +1,7 @@ import { render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { ConstitutionalChangePage } from "../pages/ConstitutionalChangePage" +import { toast } from "sonner" jest.mock("sonner", () => ({ toast: { @@ -138,4 +139,54 @@ describe("ConstitutionalChangePage", () => { expect(screen.getByText(/^Dealer \*$/i)).toBeInTheDocument() }) + + it("shows backend duplicate-open message on create conflict", async () => { + const user = userEvent.setup() + const { API } = await import("@/api/API") + ;(API.getDealers as jest.Mock).mockResolvedValueOnce({ + data: { + success: true, + data: [ + { + user: { id: "dealer-user-1" }, + constitutionType: "Proprietorship", + businessName: "Dealer A", + legalName: "Dealer A Pvt", + dealerCode: { dealerCode: "DLR-1" }, + }, + ], + }, + }) + ;(API.createConstitutionalChange as jest.Mock).mockRejectedValueOnce({ + response: { + data: { + message: + "Open constitutional request CCR-1 already exists at ASM Review. Complete it before creating a new one.", + }, + }, + }) + + setup() + await user.click(screen.getByRole("button", { name: /new request/i })) + await screen.findByRole("heading", { + name: /create constitutional change request/i, + }) + + await user.click(screen.getByRole("combobox", { name: /dealer/i })) + await user.click(await screen.findByText(/DLR-1 — Dealer A/i)) + + await user.click(screen.getByRole("combobox", { name: /proposed constitution/i })) + await user.click(await screen.findByText(/^Partnership$/i)) + + const reasonField = screen.getByLabelText(/reason for constitutional change/i) + await user.type(reasonField, "Need to onboard new partner") + + await user.click(screen.getByRole("button", { name: /submit request/i })) + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + expect.stringContaining("Open constitutional request CCR-1 already exists") + ) + }) + }) }) diff --git a/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx b/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx index a250897..b3e0a26 100644 --- a/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx +++ b/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx @@ -44,7 +44,7 @@ const formatStageRole = (role: string) => // Document requirements mapping (same as in ConstitutionalChangePage) — SRS §12.2.4 by target constitution const documentRequirements: Record = { 'Partnership': [1, 2, 3, 4, 8, 9, 10, 16], - 'LLP': [1, 2, 3, 7, 8, 9, 10, 11, 16], + 'LLP': [1, 2, 3, 7, 8, 10, 11, 16], 'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16], 'Pvt Ltd': [1, 2, 3, 5, 6, 7, 8, 10, 16], 'Proprietorship': [1, 2, 3, 10, 16] @@ -151,6 +151,8 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: const [auditLogs, setAuditLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isActionLoading, setIsActionLoading] = useState(false); + /** Set when POST /action returns 4xx (apisauce does not throw — must check response.ok). */ + const [actionDialogError, setActionDialogError] = useState(null); const [isUploadingDoc, setIsUploadingDoc] = useState(false); const [rejectDocDialogOpen, setRejectDocDialogOpen] = useState(false); const [rejectDocIndex, setRejectDocIndex] = useState(null); @@ -415,6 +417,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: const handleAction = (type: 'approve' | 'reject' | 'sendBack' | 'revoke') => { setActionType(type); + setActionDialogError(null); setIsActionDialogOpen(true); }; @@ -436,6 +439,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: try { setIsActionLoading(true); + setActionDialogError(null); const actionPayload = actionType === 'approve' ? OFFBOARDING_ACTIONS.APPROVE @@ -448,7 +452,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: comments }) as any; - if (response.data.success) { + const payload = response?.data as { success?: boolean; message?: string } | undefined; + /** apisauce returns { ok: false } on 4xx without throwing — must branch on this. */ + if (response?.ok && payload?.success) { const actionText = actionType === 'approve' ? 'approved' : actionType === 'reject' ? 'rejected' : @@ -457,12 +463,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: toast.success(`Request ${actionText} successfully`); setIsActionDialogOpen(false); setComments(''); + setActionDialogError(null); await fetchRequestDetails(); + return; } + + const message = + payload?.message || + (response as any)?.data?.error || + 'Failed to submit action'; + setActionDialogError(message); + const docGate = /mandatory documents/i.test(message); + toast.error(message, { duration: docGate ? 14000 : 8000 }); } catch (error) { console.error('Submit action error:', error); - const message = (error as any)?.response?.data?.message || 'Failed to submit action'; - toast.error(message); + const message = + (error as any)?.response?.data?.message || + (error as any)?.message || + 'Failed to submit action'; + setActionDialogError(message); + toast.error(message, { duration: /mandatory documents/i.test(message) ? 14000 : 8000 }); } finally { setIsActionLoading(false); } @@ -1261,7 +1281,13 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: {/* Action Dialog */} - + { + setIsActionDialogOpen(open); + if (!open) setActionDialogError(null); + }} + > @@ -1278,6 +1304,24 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
+ {actionDialogError && ( +
+ +
+

This action was not completed

+

{actionDialogError}

+ {/mandatory documents/i.test(actionDialogError) && ( +

+ Use the Documents tab to upload every required file for this constitution + type, then approve again. +

+ )} +
+
+ )}
diff --git a/src/features/master/pages/MasterPage.tsx b/src/features/master/pages/MasterPage.tsx index c7415b9..ec86f9f 100644 --- a/src/features/master/pages/MasterPage.tsx +++ b/src/features/master/pages/MasterPage.tsx @@ -4,7 +4,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Globe, Shield, Mail, MapPin, SlidersHorizontal, Settings, FileText, Settings2 } from 'lucide-react'; -import { Badge } from '@/components/ui/badge'; import { toast } from 'sonner'; // Services & Hooks diff --git a/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx b/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx index 041e5c5..385099c 100644 --- a/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx +++ b/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx @@ -50,7 +50,6 @@ interface ApplicationDetailsTabsProps { setShowDocumentsModal: (value: boolean) => void; setShowUploadForm: (value: boolean) => void; handleRetriggerEvaluators: () => void; - handleCancelInterview: (interviewId: any) => void; handleRescheduleInterview: (interview: any) => void; setSelectedEvaluationForView: (value: any) => void; setShowFeedbackDetailsModal: (value: boolean) => void; @@ -86,7 +85,6 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) { setShowDocumentsModal, setShowUploadForm, handleRetriggerEvaluators, - handleCancelInterview, handleRescheduleInterview, setSelectedEvaluationForView, setShowFeedbackDetailsModal, diff --git a/src/features/onboarding/pages/OpportunityRequestsPage.tsx b/src/features/onboarding/pages/OpportunityRequestsPage.tsx index 4683bcb..da65331 100644 --- a/src/features/onboarding/pages/OpportunityRequestsPage.tsx +++ b/src/features/onboarding/pages/OpportunityRequestsPage.tsx @@ -35,7 +35,6 @@ import { Mail, Grid3x3, List, - AlertCircle, Loader2, Calendar, ArrowUpDown diff --git a/src/features/relocation/pages/RelocationRequestDetails.tsx b/src/features/relocation/pages/RelocationRequestDetails.tsx index 646b3b7..42047f6 100644 --- a/src/features/relocation/pages/RelocationRequestDetails.tsx +++ b/src/features/relocation/pages/RelocationRequestDetails.tsx @@ -29,11 +29,9 @@ const workflowStages = [ { id: 3, name: 'DD ZM Review', key: 'DD_ZM_REVIEW', role: 'DD-ZM' }, { id: 4, name: 'ZBH Review', key: 'ZBH_REVIEW', role: 'ZBH' }, { id: 5, name: 'DD Lead Review', key: 'DD_LEAD_REVIEW', role: 'DD Lead' }, - { id: 6, name: 'DD Head Approval', key: 'DD_HEAD_APPROVAL', role: 'DD Head' }, - { id: 7, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' }, - { id: 8, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' }, - { id: 9, name: 'NBH Clearance with EOR', key: 'NBH_CLEARANCE_EOR', role: 'NBH' }, - { id: 10, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' } + { id: 6, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' }, + { id: 7, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' }, + { id: 8, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' } ]; /** Map API stage / status label to 1-based workflow row index; 0 = unknown; length+1 = finished */ @@ -296,15 +294,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel if (response.data.success) { const req = response.data.request; setRequest(req); - - const currentStage = req.currentStage; - if ( - currentStage === 'NBH_CLEARANCE_EOR' || - currentStage === 'NBH Clearance with EOR' || - req.status === 'Completed' - ) { - fetchEorChecklist(req.id); - } } } catch (error) { console.error('Fetch relocation request details error:', error); @@ -351,7 +340,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel const timelineResolvedOrdinal = relocationTimelineResolvedOrdinal(timelineEntries); const auditResolvedOrdinal = relocationAuditResolvedOrdinal(auditLogs); const dbOrdinal = request ? getDbStageOrdinal() : 1; - /** Audit/timeline can reference later steps (e.g. NBH EOR) while the request still sits at NBH Approval — do not use that to drive the active step. */ + /** Audit/timeline can reference later steps while the request still sits in a prior stage — do not use that to drive the active step. */ const workflowProgressMismatch = Boolean(request) && Math.max(timelineResolvedOrdinal, auditResolvedOrdinal) > dbOrdinal && @@ -365,7 +354,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel request?.status === 'Completed' || request?.currentStage === 'Completed' || dbOrdinal >= workflowStages.length + 1; - /** Match backend: N/10 while on pipeline (NBH EOR = 9 → 90%); 100% only when completed — avoids stale API 100% at NBH EOR. */ + /** Match backend: N/(pipeline+1) while in flight; 100% only when completed. */ const timelineProgressPct = allWorkflowComplete ? 100 : Math.min(100, Math.round((dbOrdinal / workflowStages.length) * 100)); @@ -422,7 +411,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel request.currentStage && request.currentStage !== 'ASM Review' && request.currentStage !== 'Rejected'; - const canRevoke = showActions && ['ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Super Admin'].includes(currentUser?.role || ''); + const canRevoke = showActions && ['ZBH', 'DD Lead', 'NBH', 'Legal Admin', 'Super Admin'].includes(currentUser?.role || ''); const requiresDocGate = request?.currentStage === 'NBH Approval' || request?.currentStage === 'Legal Clearance'; const canApprove = showActions && (!requiresDocGate || (missingRequiredDocs.length === 0 && pendingVerificationDocs.length === 0)); @@ -696,9 +685,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel Workflow Progress Documents - {(request.currentStage === 'NBH Clearance with EOR' || request.status === 'Completed' || request.currentStage === 'NBH_CLEARANCE_EOR') && ( - EOR Checklist - )} History & Audit Trail @@ -1039,7 +1025,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel {doc.status === 'Pending Verification' && (() => { const role = currentUser?.role || currentUser?.roleCode || ''; // SRS — only authorized review roles can verify relocation documents - return ['DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'DD Admin', 'Super Admin', 'SUPER_ADMIN', 'DD_ADMIN'].includes(role); + return ['DD Lead', 'NBH', 'Legal Admin', 'DD Admin', 'Super Admin', 'SUPER_ADMIN', 'DD_ADMIN'].includes(role); })() && ( <>