contitutional and relocation changes done based on document alignment
This commit is contained in:
parent
b357dbdcbb
commit
c23593bb11
@ -77,7 +77,6 @@ export default function App() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const currentRole = currentUser?.role || currentUser?.roleCode || '';
|
const currentRole = currentUser?.role || currentUser?.roleCode || '';
|
||||||
const normalizedRole = String(currentRole).trim().toLowerCase();
|
|
||||||
const hasRole = (roles: string[]) => {
|
const hasRole = (roles: string[]) => {
|
||||||
const normalizedTargetRoles = roles.map((r) => r.toLowerCase());
|
const normalizedTargetRoles = roles.map((r) => r.toLowerCase());
|
||||||
const userRole = String(currentUser?.role || '').toLowerCase();
|
const userRole = String(currentUser?.role || '').toLowerCase();
|
||||||
|
|||||||
@ -47,7 +47,6 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
const hoverTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const hoverTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const currentRole = currentUser?.role || currentUser?.roleCode || '';
|
const currentRole = currentUser?.role || currentUser?.roleCode || '';
|
||||||
const normalizedRole = String(currentRole).trim().toLowerCase();
|
|
||||||
const hasRole = (roles: string[]) => {
|
const hasRole = (roles: string[]) => {
|
||||||
const normalizedTargetRoles = roles.map((r) => r.toLowerCase());
|
const normalizedTargetRoles = roles.map((r) => r.toLowerCase());
|
||||||
const userRole = String(currentUser?.role || '').toLowerCase();
|
const userRole = String(currentUser?.role || '').toLowerCase();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { render, screen, waitFor } from "@testing-library/react"
|
import { render, screen, waitFor } from "@testing-library/react"
|
||||||
import userEvent from "@testing-library/user-event"
|
import userEvent from "@testing-library/user-event"
|
||||||
import { ConstitutionalChangePage } from "../pages/ConstitutionalChangePage"
|
import { ConstitutionalChangePage } from "../pages/ConstitutionalChangePage"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
jest.mock("sonner", () => ({
|
jest.mock("sonner", () => ({
|
||||||
toast: {
|
toast: {
|
||||||
@ -138,4 +139,54 @@ describe("ConstitutionalChangePage", () => {
|
|||||||
|
|
||||||
expect(screen.getByText(/^Dealer \*$/i)).toBeInTheDocument()
|
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")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const formatStageRole = (role: string) =>
|
|||||||
// Document requirements mapping (same as in ConstitutionalChangePage) — SRS §12.2.4 by target constitution
|
// Document requirements mapping (same as in ConstitutionalChangePage) — SRS §12.2.4 by target constitution
|
||||||
const documentRequirements: Record<string, number[]> = {
|
const documentRequirements: Record<string, number[]> = {
|
||||||
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
|
'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],
|
'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16],
|
||||||
'Pvt Ltd': [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]
|
'Proprietorship': [1, 2, 3, 10, 16]
|
||||||
@ -151,6 +151,8 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||||
|
/** Set when POST /action returns 4xx (apisauce does not throw — must check response.ok). */
|
||||||
|
const [actionDialogError, setActionDialogError] = useState<string | null>(null);
|
||||||
const [isUploadingDoc, setIsUploadingDoc] = useState(false);
|
const [isUploadingDoc, setIsUploadingDoc] = useState(false);
|
||||||
const [rejectDocDialogOpen, setRejectDocDialogOpen] = useState(false);
|
const [rejectDocDialogOpen, setRejectDocDialogOpen] = useState(false);
|
||||||
const [rejectDocIndex, setRejectDocIndex] = useState<number | null>(null);
|
const [rejectDocIndex, setRejectDocIndex] = useState<number | null>(null);
|
||||||
@ -415,6 +417,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
|
|
||||||
const handleAction = (type: 'approve' | 'reject' | 'sendBack' | 'revoke') => {
|
const handleAction = (type: 'approve' | 'reject' | 'sendBack' | 'revoke') => {
|
||||||
setActionType(type);
|
setActionType(type);
|
||||||
|
setActionDialogError(null);
|
||||||
setIsActionDialogOpen(true);
|
setIsActionDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -436,6 +439,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsActionLoading(true);
|
setIsActionLoading(true);
|
||||||
|
setActionDialogError(null);
|
||||||
const actionPayload =
|
const actionPayload =
|
||||||
actionType === 'approve'
|
actionType === 'approve'
|
||||||
? OFFBOARDING_ACTIONS.APPROVE
|
? OFFBOARDING_ACTIONS.APPROVE
|
||||||
@ -448,7 +452,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
comments
|
comments
|
||||||
}) as any;
|
}) 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 =
|
const actionText =
|
||||||
actionType === 'approve' ? 'approved' :
|
actionType === 'approve' ? 'approved' :
|
||||||
actionType === 'reject' ? 'rejected' :
|
actionType === 'reject' ? 'rejected' :
|
||||||
@ -457,12 +463,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
toast.success(`Request ${actionText} successfully`);
|
toast.success(`Request ${actionText} successfully`);
|
||||||
setIsActionDialogOpen(false);
|
setIsActionDialogOpen(false);
|
||||||
setComments('');
|
setComments('');
|
||||||
|
setActionDialogError(null);
|
||||||
await fetchRequestDetails();
|
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) {
|
} catch (error) {
|
||||||
console.error('Submit action error:', error);
|
console.error('Submit action error:', error);
|
||||||
const message = (error as any)?.response?.data?.message || 'Failed to submit action';
|
const message =
|
||||||
toast.error(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 {
|
} finally {
|
||||||
setIsActionLoading(false);
|
setIsActionLoading(false);
|
||||||
}
|
}
|
||||||
@ -1261,7 +1281,13 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Dialog */}
|
{/* Action Dialog */}
|
||||||
<Dialog open={isActionDialogOpen} onOpenChange={setIsActionDialogOpen}>
|
<Dialog
|
||||||
|
open={isActionDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setIsActionDialogOpen(open);
|
||||||
|
if (!open) setActionDialogError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
@ -1278,6 +1304,24 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmitAction} className="space-y-4">
|
<form onSubmit={handleSubmitAction} className="space-y-4">
|
||||||
|
{actionDialogError && (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-900 flex gap-2"
|
||||||
|
>
|
||||||
|
<AlertCircle className="w-5 h-5 shrink-0 text-red-600" aria-hidden />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium">This action was not completed</p>
|
||||||
|
<p className="mt-1 whitespace-pre-wrap break-words">{actionDialogError}</p>
|
||||||
|
{/mandatory documents/i.test(actionDialogError) && (
|
||||||
|
<p className="mt-2 text-red-800">
|
||||||
|
Use the <strong>Documents</strong> tab to upload every required file for this constitution
|
||||||
|
type, then approve again.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="comments">
|
<Label htmlFor="comments">
|
||||||
{actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks (required) *' : 'Comments *'}
|
{actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks (required) *' : 'Comments *'}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ interface ConstitutionalChangePageProps {
|
|||||||
// Document requirements mapping (keys = DB `changeType` ENUM values)
|
// Document requirements mapping (keys = DB `changeType` ENUM values)
|
||||||
const documentRequirements: Record<string, number[]> = {
|
const documentRequirements: Record<string, number[]> = {
|
||||||
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
|
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
|
||||||
'LLP': [1, 2, 3, 7, 8, 9, 10, 16],
|
'LLP': [1, 2, 3, 7, 8, 10, 11, 16],
|
||||||
'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16],
|
'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16],
|
||||||
'Proprietorship': [1, 2, 3, 10, 16]
|
'Proprietorship': [1, 2, 3, 10, 16]
|
||||||
};
|
};
|
||||||
@ -73,10 +73,8 @@ const getTypeColor = (type: string) => {
|
|||||||
case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300';
|
case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300';
|
||||||
case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300';
|
case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300';
|
||||||
case 'LLP':
|
case 'LLP':
|
||||||
case 'LLP Conversion':
|
|
||||||
return 'bg-indigo-100 text-indigo-700 border-indigo-300';
|
return 'bg-indigo-100 text-indigo-700 border-indigo-300';
|
||||||
case 'Private Limited':
|
case 'Private Limited':
|
||||||
case 'Pvt Ltd':
|
|
||||||
return 'bg-cyan-100 text-cyan-700 border-cyan-300';
|
return 'bg-cyan-100 text-cyan-700 border-cyan-300';
|
||||||
default: return 'bg-slate-100 text-slate-700 border-slate-300';
|
default: return 'bg-slate-100 text-slate-700 border-slate-300';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -270,12 +270,13 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
|
|||||||
<ul className="text-blue-800 text-sm space-y-1">
|
<ul className="text-blue-800 text-sm space-y-1">
|
||||||
<li>• GST Registration Certificate</li>
|
<li>• GST Registration Certificate</li>
|
||||||
<li>• Firm PAN Copy</li>
|
<li>• Firm PAN Copy</li>
|
||||||
<li>• Partnership Deed (if applicable)</li>
|
<li>• Self-attested KYC documents</li>
|
||||||
<li>• LLP Agreement (if applicable)</li>
|
<li>• Business Purchase Agreement (BPA)</li>
|
||||||
<li>• Certificate of Incorporation (if applicable)</li>
|
<li>• Partnership Agreement / Firm Registration (if target is Partnership)</li>
|
||||||
<li>• MOA & AOA (if applicable)</li>
|
<li>• LLP Agreement / COI (if target is LLP)</li>
|
||||||
<li>• Board Resolution</li>
|
<li>• MOA, AOA, COI (if target is Private Limited)</li>
|
||||||
<li>• Aadhaar & PAN of all partners/directors</li>
|
<li>• Cancelled Cheque</li>
|
||||||
|
<li>• Declaration / Authorization Letter</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {
|
|||||||
Tabs, TabsContent, TabsList, TabsTrigger
|
Tabs, TabsContent, TabsList, TabsTrigger
|
||||||
} from '@/components/ui/tabs';
|
} from '@/components/ui/tabs';
|
||||||
import { Globe, Shield, Mail, MapPin, SlidersHorizontal, Settings, FileText, Settings2 } from 'lucide-react';
|
import { Globe, Shield, Mail, MapPin, SlidersHorizontal, Settings, FileText, Settings2 } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
// Services & Hooks
|
// Services & Hooks
|
||||||
|
|||||||
@ -50,7 +50,6 @@ interface ApplicationDetailsTabsProps {
|
|||||||
setShowDocumentsModal: (value: boolean) => void;
|
setShowDocumentsModal: (value: boolean) => void;
|
||||||
setShowUploadForm: (value: boolean) => void;
|
setShowUploadForm: (value: boolean) => void;
|
||||||
handleRetriggerEvaluators: () => void;
|
handleRetriggerEvaluators: () => void;
|
||||||
handleCancelInterview: (interviewId: any) => void;
|
|
||||||
handleRescheduleInterview: (interview: any) => void;
|
handleRescheduleInterview: (interview: any) => void;
|
||||||
setSelectedEvaluationForView: (value: any) => void;
|
setSelectedEvaluationForView: (value: any) => void;
|
||||||
setShowFeedbackDetailsModal: (value: boolean) => void;
|
setShowFeedbackDetailsModal: (value: boolean) => void;
|
||||||
@ -86,7 +85,6 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
setShowDocumentsModal,
|
setShowDocumentsModal,
|
||||||
setShowUploadForm,
|
setShowUploadForm,
|
||||||
handleRetriggerEvaluators,
|
handleRetriggerEvaluators,
|
||||||
handleCancelInterview,
|
|
||||||
handleRescheduleInterview,
|
handleRescheduleInterview,
|
||||||
setSelectedEvaluationForView,
|
setSelectedEvaluationForView,
|
||||||
setShowFeedbackDetailsModal,
|
setShowFeedbackDetailsModal,
|
||||||
|
|||||||
@ -35,7 +35,6 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
Grid3x3,
|
Grid3x3,
|
||||||
List,
|
List,
|
||||||
AlertCircle,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
Calendar,
|
Calendar,
|
||||||
ArrowUpDown
|
ArrowUpDown
|
||||||
|
|||||||
@ -29,11 +29,9 @@ const workflowStages = [
|
|||||||
{ id: 3, name: 'DD ZM Review', key: 'DD_ZM_REVIEW', role: 'DD-ZM' },
|
{ id: 3, name: 'DD ZM Review', key: 'DD_ZM_REVIEW', role: 'DD-ZM' },
|
||||||
{ id: 4, name: 'ZBH Review', key: 'ZBH_REVIEW', role: 'ZBH' },
|
{ id: 4, name: 'ZBH Review', key: 'ZBH_REVIEW', role: 'ZBH' },
|
||||||
{ id: 5, name: 'DD Lead Review', key: 'DD_LEAD_REVIEW', role: 'DD Lead' },
|
{ 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: 6, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' },
|
||||||
{ id: 7, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' },
|
{ id: 7, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' },
|
||||||
{ id: 8, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' },
|
{ id: 8, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' }
|
||||||
{ id: 9, name: 'NBH Clearance with EOR', key: 'NBH_CLEARANCE_EOR', role: 'NBH' },
|
|
||||||
{ id: 10, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Map API stage / status label to 1-based workflow row index; 0 = unknown; length+1 = finished */
|
/** 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) {
|
if (response.data.success) {
|
||||||
const req = response.data.request;
|
const req = response.data.request;
|
||||||
setRequest(req);
|
setRequest(req);
|
||||||
|
|
||||||
const currentStage = req.currentStage;
|
|
||||||
if (
|
|
||||||
currentStage === 'NBH_CLEARANCE_EOR' ||
|
|
||||||
currentStage === 'NBH Clearance with EOR' ||
|
|
||||||
req.status === 'Completed'
|
|
||||||
) {
|
|
||||||
fetchEorChecklist(req.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch relocation request details error:', 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 timelineResolvedOrdinal = relocationTimelineResolvedOrdinal(timelineEntries);
|
||||||
const auditResolvedOrdinal = relocationAuditResolvedOrdinal(auditLogs);
|
const auditResolvedOrdinal = relocationAuditResolvedOrdinal(auditLogs);
|
||||||
const dbOrdinal = request ? getDbStageOrdinal() : 1;
|
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 =
|
const workflowProgressMismatch =
|
||||||
Boolean(request) &&
|
Boolean(request) &&
|
||||||
Math.max(timelineResolvedOrdinal, auditResolvedOrdinal) > dbOrdinal &&
|
Math.max(timelineResolvedOrdinal, auditResolvedOrdinal) > dbOrdinal &&
|
||||||
@ -365,7 +354,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
|
|||||||
request?.status === 'Completed' ||
|
request?.status === 'Completed' ||
|
||||||
request?.currentStage === 'Completed' ||
|
request?.currentStage === 'Completed' ||
|
||||||
dbOrdinal >= workflowStages.length + 1;
|
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
|
const timelineProgressPct = allWorkflowComplete
|
||||||
? 100
|
? 100
|
||||||
: Math.min(100, Math.round((dbOrdinal / workflowStages.length) * 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 &&
|
||||||
request.currentStage !== 'ASM Review' &&
|
request.currentStage !== 'ASM Review' &&
|
||||||
request.currentStage !== 'Rejected';
|
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 requiresDocGate = request?.currentStage === 'NBH Approval' || request?.currentStage === 'Legal Clearance';
|
||||||
const canApprove = showActions && (!requiresDocGate || (missingRequiredDocs.length === 0 && pendingVerificationDocs.length === 0));
|
const canApprove = showActions && (!requiresDocGate || (missingRequiredDocs.length === 0 && pendingVerificationDocs.length === 0));
|
||||||
|
|
||||||
@ -696,9 +685,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
|
|||||||
<TabsList className="w-max min-w-full justify-start">
|
<TabsList className="w-max min-w-full justify-start">
|
||||||
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
|
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
|
||||||
<TabsTrigger value="documents">Documents</TabsTrigger>
|
<TabsTrigger value="documents">Documents</TabsTrigger>
|
||||||
{(request.currentStage === 'NBH Clearance with EOR' || request.status === 'Completed' || request.currentStage === 'NBH_CLEARANCE_EOR') && (
|
|
||||||
<TabsTrigger value="eor">EOR Checklist</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="history">History & Audit Trail</TabsTrigger>
|
<TabsTrigger value="history">History & Audit Trail</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@ -1039,7 +1025,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
|
|||||||
{doc.status === 'Pending Verification' && (() => {
|
{doc.status === 'Pending Verification' && (() => {
|
||||||
const role = currentUser?.role || currentUser?.roleCode || '';
|
const role = currentUser?.role || currentUser?.roleCode || '';
|
||||||
// SRS — only authorized review roles can verify relocation documents
|
// 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);
|
||||||
})() && (
|
})() && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@ -1134,7 +1120,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
|
|||||||
{(!eorChecklist.items || eorChecklist.items.length === 0) ? (
|
{(!eorChecklist.items || eorChecklist.items.length === 0) ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={4} className="text-center text-slate-500 py-8 text-sm">
|
<TableCell colSpan={4} className="text-center text-slate-500 py-8 text-sm">
|
||||||
No checklist rows returned. Use "Try Refreshing" above or reload the page; rows are created when the request enters NBH Clearance with EOR.
|
No checklist rows returned. Use "Try Refreshing" above or reload the page.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -213,14 +213,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
const isDDLeadStage = currentStage === 'DD Lead' || currentStage === 'DD Lead Review';
|
const isDDLeadStage = currentStage === 'DD Lead' || currentStage === 'DD Lead Review';
|
||||||
const isDDLead = userRoleCode === 'DD_LEAD' || userRoleCode === 'DD LEAD';
|
const isDDLead = userRoleCode === 'DD_LEAD' || userRoleCode === 'DD LEAD';
|
||||||
|
|
||||||
const canApprove = isCurrentlyAssigned &&
|
|
||||||
!isFinalState &&
|
|
||||||
!isSettlementPhase &&
|
|
||||||
!hasAlreadyPartiallyApproved &&
|
|
||||||
!(currentStage === 'Legal' && legalStageApproved) &&
|
|
||||||
!(isDDLead && isDDLeadStage && !hasUploadedPPT) &&
|
|
||||||
!(currentStage === 'DD Admin' && !isLwdReached);
|
|
||||||
|
|
||||||
const isLwdReached = (() => {
|
const isLwdReached = (() => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
@ -231,6 +223,14 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
return today >= lwd;
|
return today >= lwd;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const canApprove = isCurrentlyAssigned &&
|
||||||
|
!isFinalState &&
|
||||||
|
!isSettlementPhase &&
|
||||||
|
!hasAlreadyPartiallyApproved &&
|
||||||
|
!(currentStage === 'Legal' && legalStageApproved) &&
|
||||||
|
!(isDDLead && isDDLeadStage && !hasUploadedPPT) &&
|
||||||
|
!(currentStage === 'DD Admin' && !isLwdReached);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canApprove,
|
canApprove,
|
||||||
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
|
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
|
||||||
|
|||||||
@ -4,24 +4,14 @@
|
|||||||
|
|
||||||
const PRIVATE_LIMITED = 'Private Limited';
|
const PRIVATE_LIMITED = 'Private Limited';
|
||||||
const LLP = 'LLP';
|
const LLP = 'LLP';
|
||||||
const LLP_CONVERSION = 'LLP Conversion';
|
|
||||||
const PARTNERSHIP = 'Partnership';
|
const PARTNERSHIP = 'Partnership';
|
||||||
const PARTNERSHIP_CHANGE = 'Partnership Change';
|
|
||||||
const PROPRIETORSHIP = 'Proprietorship';
|
const PROPRIETORSHIP = 'Proprietorship';
|
||||||
const DIRECTOR_CHANGE = 'Director Change';
|
|
||||||
const OWNERSHIP_TRANSFER = 'Ownership Transfer';
|
|
||||||
const COMPANY_FORMATION = 'Company Formation';
|
|
||||||
|
|
||||||
const ALL: string[] = [
|
const ALL: string[] = [
|
||||||
PROPRIETORSHIP,
|
PROPRIETORSHIP,
|
||||||
PARTNERSHIP,
|
PARTNERSHIP,
|
||||||
LLP_CONVERSION,
|
|
||||||
LLP,
|
LLP,
|
||||||
PRIVATE_LIMITED,
|
PRIVATE_LIMITED
|
||||||
COMPANY_FORMATION,
|
|
||||||
OWNERSHIP_TRANSFER,
|
|
||||||
PARTNERSHIP_CHANGE,
|
|
||||||
DIRECTOR_CHANGE
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function isRegisteredConstitutionalChangeType(value: string): boolean {
|
export function isRegisteredConstitutionalChangeType(value: string): boolean {
|
||||||
@ -44,14 +34,9 @@ export function normalizeToConstitutionalChangeType(raw: string | null | undefin
|
|||||||
) {
|
) {
|
||||||
return PRIVATE_LIMITED;
|
return PRIVATE_LIMITED;
|
||||||
}
|
}
|
||||||
if (compact.includes('llp') && compact.includes('conversion')) return LLP_CONVERSION;
|
|
||||||
if (compact.includes('llp')) return LLP;
|
if (compact.includes('llp')) return LLP;
|
||||||
if (compact.includes('partnership') && compact.includes('change')) return PARTNERSHIP_CHANGE;
|
|
||||||
if (compact.includes('partnership')) return PARTNERSHIP;
|
if (compact.includes('partnership')) return PARTNERSHIP;
|
||||||
if (compact.includes('proprietorship') || compact === 'sole proprietorship') return PROPRIETORSHIP;
|
if (compact.includes('proprietorship') || compact === 'sole proprietorship') return PROPRIETORSHIP;
|
||||||
if (compact.includes('director')) return DIRECTOR_CHANGE;
|
|
||||||
if (compact.includes('ownership') && compact.includes('transfer')) return OWNERSHIP_TRANSFER;
|
|
||||||
if (compact.includes('company') && compact.includes('formation')) return COMPANY_FORMATION;
|
|
||||||
const exact = ALL.find((v) => v.toLowerCase() === s.toLowerCase());
|
const exact = ALL.find((v) => v.toLowerCase() === s.toLowerCase());
|
||||||
return exact || null;
|
return exact || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,20 +2,27 @@
|
|||||||
|
|
||||||
export type UserRole =
|
export type UserRole =
|
||||||
| 'DD-ZM'
|
| 'DD-ZM'
|
||||||
|
| 'DD_ZM'
|
||||||
| 'RBM'
|
| 'RBM'
|
||||||
| 'DD'
|
| 'DD'
|
||||||
| 'ZBH'
|
| 'ZBH'
|
||||||
| 'DD Lead'
|
| 'DD Lead'
|
||||||
|
| 'DD_LEAD'
|
||||||
| 'DD Head'
|
| 'DD Head'
|
||||||
|
| 'DD_HEAD'
|
||||||
| 'NBH'
|
| 'NBH'
|
||||||
| 'DD Admin'
|
| 'DD Admin'
|
||||||
|
| 'DD_ADMIN'
|
||||||
| 'Legal Admin'
|
| 'Legal Admin'
|
||||||
|
| 'LEGAL_ADMIN'
|
||||||
| 'Super Admin'
|
| 'Super Admin'
|
||||||
|
| 'SUPER_ADMIN'
|
||||||
| 'DD AM'
|
| 'DD AM'
|
||||||
| 'FDD'
|
| 'FDD'
|
||||||
| 'DDL'
|
| 'DDL'
|
||||||
| 'Finance'
|
| 'Finance'
|
||||||
| 'Finance Admin'
|
| 'Finance Admin'
|
||||||
|
| 'FINANCE_ADMIN'
|
||||||
| 'Dealer'
|
| 'Dealer'
|
||||||
| 'ASM'
|
| 'ASM'
|
||||||
| 'CCO'
|
| 'CCO'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user