contitutional and relocation changes done based on document alignment

This commit is contained in:
laxmanhalaki 2026-05-06 10:45:55 +05:30
parent b357dbdcbb
commit c23593bb11
13 changed files with 132 additions and 66 deletions

View File

@ -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();

View File

@ -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();

View File

@ -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")
)
})
})
}) })

View File

@ -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 *'}

View File

@ -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';
} }

View File

@ -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>

View File

@ -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

View File

@ -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,

View File

@ -35,7 +35,6 @@ import {
Mail, Mail,
Grid3x3, Grid3x3,
List, List,
AlertCircle,
Loader2, Loader2,
Calendar, Calendar,
ArrowUpDown ArrowUpDown

View File

@ -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 &quot;Try Refreshing&quot; above or reload the page; rows are created when the request enters NBH Clearance with EOR. No checklist rows returned. Use &quot;Try Refreshing&quot; above or reload the page.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (

View File

@ -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,

View File

@ -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;
} }

View File

@ -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'