new table changes done for the vrersioning for the department clearence and conflict is concluded between department and finacne due entry before refactoring application detail sceen
This commit is contained in:
parent
873a097185
commit
2a0fe71584
44
src/App.tsx
44
src/App.tsx
@ -40,6 +40,7 @@ import { ConstitutionalChangeDetails } from './components/applications/Constitut
|
|||||||
import { RelocationRequestPage } from './components/applications/RelocationRequestPage';
|
import { RelocationRequestPage } from './components/applications/RelocationRequestPage';
|
||||||
import { RelocationRequestDetails } from './components/applications/RelocationRequestDetails';
|
import { RelocationRequestDetails } from './components/applications/RelocationRequestDetails';
|
||||||
import { DealerResignationPage } from './components/dealer/DealerResignationPage';
|
import { DealerResignationPage } from './components/dealer/DealerResignationPage';
|
||||||
|
import { DealerResignationDetailsPage } from './components/dealer/DealerResignationDetailsPage';
|
||||||
import { DealerConstitutionalChangePage } from './components/dealer/DealerConstitutionalChangePage';
|
import { DealerConstitutionalChangePage } from './components/dealer/DealerConstitutionalChangePage';
|
||||||
import { DealerRelocationPage } from './components/dealer/DealerRelocationPage';
|
import { DealerRelocationPage } from './components/dealer/DealerRelocationPage';
|
||||||
import QuestionnaireBuilder from './components/admin/QuestionnaireBuilder';
|
import QuestionnaireBuilder from './components/admin/QuestionnaireBuilder';
|
||||||
@ -72,9 +73,11 @@ export default function App() {
|
|||||||
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const currentRole = currentUser?.role || '';
|
const currentRole = currentUser?.role || currentUser?.roleCode || '';
|
||||||
const resignationRoles = ['DD Admin', 'ASM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Super Admin'];
|
const normalizedRole = String(currentRole).trim().toLowerCase();
|
||||||
const terminationRoles = ['ASM', 'DD Lead', 'DD Admin', 'Super Admin'];
|
const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole);
|
||||||
|
const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin'];
|
||||||
|
const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin'];
|
||||||
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin'];
|
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin'];
|
||||||
const financeRoles = ['Finance', 'Finance Admin'];
|
const financeRoles = ['Finance', 'Finance Admin'];
|
||||||
|
|
||||||
@ -210,9 +213,9 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Dashboards */}
|
{/* Dashboards */}
|
||||||
<Route path="/dashboard" element={
|
<Route path="/dashboard" element={
|
||||||
financeRoles.includes(currentRole) ?
|
hasRole(financeRoles) ?
|
||||||
<FinanceDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> :
|
<FinanceDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> :
|
||||||
currentRole === 'Dealer' ?
|
hasRole(['Dealer']) ?
|
||||||
<DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> :
|
<DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> :
|
||||||
<Dashboard onNavigate={(path) => navigate(`/${path}`)} />
|
<Dashboard onNavigate={(path) => navigate(`/${path}`)} />
|
||||||
} />
|
} />
|
||||||
@ -229,7 +232,7 @@ export default function App() {
|
|||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/all-applications" element={
|
<Route path="/all-applications" element={
|
||||||
currentUser?.role === 'DD' ? <AllApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" /> : <Navigate to="/dashboard" />
|
hasRole(['DD']) ? <AllApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" /> : <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
|
|
||||||
{/* FDD Routes - Integrated into Layout */}
|
{/* FDD Routes - Integrated into Layout */}
|
||||||
@ -243,7 +246,7 @@ export default function App() {
|
|||||||
{/* Other Modules */}
|
{/* Other Modules */}
|
||||||
<Route path="/users" element={<UserManagementPage />} />
|
<Route path="/users" element={<UserManagementPage />} />
|
||||||
<Route path="/approval-policies" element={
|
<Route path="/approval-policies" element={
|
||||||
(currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin')
|
(hasRole(['Super Admin', 'DD Admin']))
|
||||||
? <ApprovalPoliciesPage />
|
? <ApprovalPoliciesPage />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
@ -255,57 +258,57 @@ export default function App() {
|
|||||||
|
|
||||||
{/* HR/Finance Modules (Simplified for brevity, following pattern) */}
|
{/* HR/Finance Modules (Simplified for brevity, following pattern) */}
|
||||||
<Route path="/resignation" element={
|
<Route path="/resignation" element={
|
||||||
resignationRoles.includes(currentRole)
|
hasRole(resignationRoles)
|
||||||
? <ResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />
|
? <ResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
<Route path="/resignation/:id" element={
|
<Route path="/resignation/:id" element={
|
||||||
resignationRoles.includes(currentRole)
|
hasRole(resignationRoles)
|
||||||
? <ResignationDetails resignationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/resignation')} currentUser={currentUser} />
|
? <ResignationDetails resignationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/resignation')} currentUser={currentUser} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/termination" element={
|
<Route path="/termination" element={
|
||||||
terminationRoles.includes(currentRole)
|
hasRole(terminationRoles)
|
||||||
? <TerminationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/termination/${id}`)} />
|
? <TerminationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/termination/${id}`)} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
<Route path="/termination/:id" element={
|
<Route path="/termination/:id" element={
|
||||||
terminationRoles.includes(currentRole)
|
hasRole(terminationRoles)
|
||||||
? <TerminationDetails terminationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/termination')} currentUser={currentUser} />
|
? <TerminationDetails terminationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/termination')} currentUser={currentUser} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/fnf" element={
|
<Route path="/fnf" element={
|
||||||
fnfRoles.includes(currentRole)
|
hasRole(fnfRoles)
|
||||||
? <FnFPage currentUser={currentUser} onViewDetails={(id) => navigate(`/fnf/${id}`)} />
|
? <FnFPage currentUser={currentUser} onViewDetails={(id) => navigate(`/fnf/${id}`)} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
<Route path="/fnf/:id" element={
|
<Route path="/fnf/:id" element={
|
||||||
fnfRoles.includes(currentRole)
|
hasRole(fnfRoles)
|
||||||
? <FnFDetails fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/fnf')} currentUser={currentUser} />
|
? <FnFDetails fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/fnf')} currentUser={currentUser} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/finance-onboarding" element={
|
<Route path="/finance-onboarding" element={
|
||||||
financeRoles.includes(currentRole)
|
hasRole(financeRoles)
|
||||||
? <FinanceOnboardingPage onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} />
|
? <FinanceOnboardingPage onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
<Route path="/finance-onboarding/:id" element={
|
<Route path="/finance-onboarding/:id" element={
|
||||||
financeRoles.includes(currentRole)
|
hasRole(financeRoles)
|
||||||
? <FinancePaymentDetailsPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />
|
? <FinancePaymentDetailsPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
<Route path="/finance-audit/:id" element={<ApplicationDetails />} />
|
<Route path="/finance-audit/:id" element={<ApplicationDetails />} />
|
||||||
|
|
||||||
<Route path="/finance-fnf" element={
|
<Route path="/finance-fnf" element={
|
||||||
financeRoles.includes(currentRole)
|
hasRole(financeRoles)
|
||||||
? <FinanceFnFPage onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} />
|
? <FinanceFnFPage onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
<Route path="/finance-fnf/:id" element={
|
<Route path="/finance-fnf/:id" element={
|
||||||
financeRoles.includes(currentRole)
|
hasRole(financeRoles)
|
||||||
? <FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />
|
? <FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
@ -317,7 +320,12 @@ export default function App() {
|
|||||||
<Route path="/relocation-requests/:id" element={<RelocationRequestDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/relocation-requests')} currentUser={currentUser} />} />
|
<Route path="/relocation-requests/:id" element={<RelocationRequestDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/relocation-requests')} currentUser={currentUser} />} />
|
||||||
|
|
||||||
{/* Dealer Routes */}
|
{/* Dealer Routes */}
|
||||||
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />} />
|
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/dealer-resignation/${id}`)} />} />
|
||||||
|
<Route path="/dealer-resignation/:id" element={
|
||||||
|
hasRole(['Dealer'])
|
||||||
|
? <DealerResignationDetailsPage resignationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/dealer-resignation')} />
|
||||||
|
: <Navigate to="/dashboard" />
|
||||||
|
} />
|
||||||
<Route path="/dealer-constitutional" element={<DealerConstitutionalChangePage currentUser={currentUser} onViewDetails={(id) => navigate(`/constitutional-change/${id}`)} />} />
|
<Route path="/dealer-constitutional" element={<DealerConstitutionalChangePage currentUser={currentUser} onViewDetails={(id) => navigate(`/constitutional-change/${id}`)} />} />
|
||||||
<Route path="/dealer-relocation" element={<DealerRelocationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/relocation-requests/${id}`)} />} />
|
<Route path="/dealer-relocation" element={<DealerRelocationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/relocation-requests/${id}`)} />} />
|
||||||
|
|
||||||
|
|||||||
@ -98,7 +98,7 @@ export const API = {
|
|||||||
deleteUser: (id: string) => client.delete(`/admin/users/${id}`),
|
deleteUser: (id: string) => client.delete(`/admin/users/${id}`),
|
||||||
|
|
||||||
// Dealer & Outlets
|
// Dealer & Outlets
|
||||||
getDealers: (params?: { onboarded?: string }) => client.get('/dealer', { params }),
|
getDealers: (params?: { onboarded?: string; activeOnly?: string }) => client.get('/dealer', { params }),
|
||||||
createDealer: (data: any) => client.post('/dealer', data),
|
createDealer: (data: any) => client.post('/dealer', data),
|
||||||
getDealerById: (id: string) => client.get(`/dealer/${id}`),
|
getDealerById: (id: string) => client.get(`/dealer/${id}`),
|
||||||
updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data),
|
updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data),
|
||||||
|
|||||||
@ -34,6 +34,12 @@ const workflowStages = [
|
|||||||
{ id: 9, name: 'Completed', key: 'completed', role: 'System' }
|
{ id: 9, name: 'Completed', key: 'completed', role: 'System' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const formatStageLabel = (label: string) =>
|
||||||
|
label === 'ZM/RBM Review' ? 'ZM+RBM Review' : label;
|
||||||
|
|
||||||
|
const formatStageRole = (role: string) =>
|
||||||
|
role === 'ZM/RBM' ? 'ZM+RBM' : role;
|
||||||
|
|
||||||
// Document requirements mapping (same as in ConstitutionalChangePage)
|
// Document requirements mapping (same as in ConstitutionalChangePage)
|
||||||
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],
|
||||||
@ -238,6 +244,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
'Submitted': 1,
|
'Submitted': 1,
|
||||||
'ASM Review': 2,
|
'ASM Review': 2,
|
||||||
'ZM/RBM Review': 3,
|
'ZM/RBM Review': 3,
|
||||||
|
'ZM+RBM Review': 3,
|
||||||
'ZBH Review': 4,
|
'ZBH Review': 4,
|
||||||
'DD Lead Review': 5,
|
'DD Lead Review': 5,
|
||||||
'DD Head Review': 6,
|
'DD Head Review': 6,
|
||||||
@ -263,7 +270,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
const aliases: Record<string, string[]> = {
|
const aliases: Record<string, string[]> = {
|
||||||
'Submitted': ['Submitted', 'Draft'],
|
'Submitted': ['Submitted', 'Draft'],
|
||||||
'ASM Review': ['ASM Review'],
|
'ASM Review': ['ASM Review'],
|
||||||
'ZM/RBM Review': ['ZM/RBM Review', 'ZM Review', 'RBM Review'],
|
'ZM/RBM Review': ['ZM/RBM Review', 'ZM+RBM Review', 'ZM Review', 'RBM Review'],
|
||||||
'ZBH Review': ['ZBH Review'],
|
'ZBH Review': ['ZBH Review'],
|
||||||
'DD Lead Review': ['DD Lead Review', 'Lead Review'],
|
'DD Lead Review': ['DD Lead Review', 'Lead Review'],
|
||||||
'DD Head Review': ['DD Head Review', 'Head Review'],
|
'DD Head Review': ['DD Head Review', 'Head Review'],
|
||||||
@ -283,12 +290,13 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
// Centralized Permissions Utility (Fixes security gap where buttons showed for everyone)
|
// Centralized Permissions Utility (Fixes security gap where buttons showed for everyone)
|
||||||
const getConstitutionalPermissions = () => {
|
const getConstitutionalPermissions = () => {
|
||||||
if (!request || !currentUser) {
|
if (!request || !currentUser) {
|
||||||
return { canApprove: false, canReject: false, canSendBack: false, canRevoke: false, isFinalState: false };
|
return { canApprove: false, canReject: false, canSendBack: false, canRevoke: false, isFinalState: false, hasCurrentUserApprovedZmRbm: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentStage = request.currentStage;
|
const currentStage = request.currentStage;
|
||||||
const status = request.status;
|
const status = request.status;
|
||||||
const userRole = currentUser.role;
|
const userRole = currentUser.role || currentUser.roleCode;
|
||||||
|
const userRoleCode = String(currentUser.roleCode || '').toUpperCase();
|
||||||
|
|
||||||
const isFinalState = ['Completed', 'Rejected', 'Revoked'].includes(String(status || '')) ||
|
const isFinalState = ['Completed', 'Rejected', 'Revoked'].includes(String(status || '')) ||
|
||||||
['Rejected', 'Revoked', 'Completed'].includes(String(currentStage || ''));
|
['Rejected', 'Revoked', 'Completed'].includes(String(currentStage || ''));
|
||||||
@ -306,7 +314,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
(!atSubmittedDbStage &&
|
(!atSubmittedDbStage &&
|
||||||
!!(
|
!!(
|
||||||
(stageDef?.role === 'ASM' && userRole === 'ASM') ||
|
(stageDef?.role === 'ASM' && userRole === 'ASM') ||
|
||||||
(stageDef?.role === 'ZM/RBM' && (userRole === 'DD-ZM' || userRole === 'RBM')) ||
|
((stageDef?.role === 'ZM/RBM' || stageDef?.role === 'ZM+RBM') && (userRole === 'DD-ZM' || userRole === 'RBM')) ||
|
||||||
(stageDef?.role === 'ZBH' && userRole === 'ZBH') ||
|
(stageDef?.role === 'ZBH' && userRole === 'ZBH') ||
|
||||||
(stageDef?.role === 'DD Lead' && userRole === 'DD Lead') ||
|
(stageDef?.role === 'DD Lead' && userRole === 'DD Lead') ||
|
||||||
(stageDef?.role === 'DD Head' && userRole === 'DD Head') ||
|
(stageDef?.role === 'DD Head' && userRole === 'DD Head') ||
|
||||||
@ -323,12 +331,33 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
currentStage !== 'Legal Review' &&
|
currentStage !== 'Legal Review' &&
|
||||||
currentStage !== 'Submitted';
|
currentStage !== 'Submitted';
|
||||||
|
|
||||||
|
const jointZmRbmMeta = (request.metadata as any)?.jointApprovals?.zmRbm || {};
|
||||||
|
const isZmRbmStage = currentStage === 'ZM/RBM Review' || currentStage === 'ZM+RBM Review';
|
||||||
|
const actorKey = userRoleCode === 'RBM' ? 'RBM' : (userRoleCode === 'DD-ZM' ? 'DD-ZM' : null);
|
||||||
|
const approvedFromMetadata = actorKey ? Boolean(jointZmRbmMeta?.[actorKey]?.approvedByUserId) : false;
|
||||||
|
const approvedFromTimeline = isZmRbmStage && Boolean(
|
||||||
|
(request.timeline || []).some((entry: any) => {
|
||||||
|
const stage = String(entry?.stage || '').trim();
|
||||||
|
const action = String(entry?.action || '').toLowerCase();
|
||||||
|
const actor = String(entry?.user || '').trim().toLowerCase();
|
||||||
|
const me = String(currentUser?.name || '').trim().toLowerCase();
|
||||||
|
return (
|
||||||
|
(stage === 'ZM/RBM Review' || stage === 'ZM+RBM Review' || stage === 'RBM Review' || stage === 'ZM Review') &&
|
||||||
|
action.includes('approved') &&
|
||||||
|
me.length > 0 &&
|
||||||
|
actor === me
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const hasCurrentUserApprovedZmRbm = isZmRbmStage && (approvedFromMetadata || approvedFromTimeline);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canApprove: isCurrentlyAssigned && !isFinalState,
|
canApprove: isCurrentlyAssigned && !isFinalState && !hasCurrentUserApprovedZmRbm,
|
||||||
canReject: isCurrentlyAssigned && !isFinalState,
|
canReject: isCurrentlyAssigned && !isFinalState && !hasCurrentUserApprovedZmRbm,
|
||||||
canSendBack: canSendBackOrRevoke,
|
canSendBack: canSendBackOrRevoke,
|
||||||
canRevoke: canSendBackOrRevoke,
|
canRevoke: canSendBackOrRevoke,
|
||||||
isFinalState
|
isFinalState,
|
||||||
|
hasCurrentUserApprovedZmRbm
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -384,7 +413,8 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submit action error:', error);
|
console.error('Submit action error:', error);
|
||||||
toast.error('Failed to submit action');
|
const message = (error as any)?.response?.data?.message || 'Failed to submit action';
|
||||||
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsActionLoading(false);
|
setIsActionLoading(false);
|
||||||
}
|
}
|
||||||
@ -406,7 +436,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
fileName: uploadFile.name,
|
fileName: uploadFile.name,
|
||||||
status: 'Pending Verification',
|
status: 'Pending Verification',
|
||||||
uploadedOn: new Date().toISOString(),
|
uploadedOn: new Date().toISOString(),
|
||||||
uploadedBy: currentUser?.fullName || 'Dealer'
|
uploadedBy: currentUser?.name || 'Dealer'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingIndex >= 0) existingDocs[existingIndex] = { ...existingDocs[existingIndex], ...payloadDoc };
|
if (existingIndex >= 0) existingDocs[existingIndex] = { ...existingDocs[existingIndex], ...payloadDoc };
|
||||||
@ -437,7 +467,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
const isTargetByIndex = index === targetIndex;
|
const isTargetByIndex = index === targetIndex;
|
||||||
const isTargetByDocNumber = targetDoc.docNumber && doc.docNumber === targetDoc.docNumber;
|
const isTargetByDocNumber = targetDoc.docNumber && doc.docNumber === targetDoc.docNumber;
|
||||||
if (!(isTargetByIndex || isTargetByDocNumber)) return doc;
|
if (!(isTargetByIndex || isTargetByDocNumber)) return doc;
|
||||||
return { ...doc, status: 'Verified', verifiedOn: new Date().toISOString(), verifiedBy: currentUser?.fullName || 'System' };
|
return { ...doc, status: 'Verified', verifiedOn: new Date().toISOString(), verifiedBy: currentUser?.name || 'System' };
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await API.uploadConstitutionalDocuments(requestId, updatedDocs) as any;
|
const response = await API.uploadConstitutionalDocuments(requestId, updatedDocs) as any;
|
||||||
@ -636,7 +666,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<ul className="list-disc space-y-1 pl-5 text-sm text-slate-600">
|
<ul className="list-disc space-y-1 pl-5 text-sm text-slate-600">
|
||||||
{workflowStages.map((stage) => (
|
{workflowStages.map((stage) => (
|
||||||
<li key={stage.id}>
|
<li key={stage.id}>
|
||||||
<span className="text-slate-900">{stage.name}</span> — {stage.role}
|
<span className="text-slate-900">{formatStageLabel(stage.name)}</span> — {formatStageRole(stage.role)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -656,6 +686,17 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
(atSubmittedGate ? index === 1 : index === currentStageIndex - 1);
|
(atSubmittedGate ? index === 1 : index === currentStageIndex - 1);
|
||||||
const timelineEntry = getLatestStageTimelineEntry(stage.name);
|
const timelineEntry = getLatestStageTimelineEntry(stage.name);
|
||||||
const explicitFeedback = timelineEntry?.comments || timelineEntry?.feedback || timelineEntry?.remarks;
|
const explicitFeedback = timelineEntry?.comments || timelineEntry?.feedback || timelineEntry?.remarks;
|
||||||
|
const isJointZmRbmStage = stage.name === 'ZM/RBM Review';
|
||||||
|
const jointZmRbmMeta = (request.metadata as any)?.jointApprovals?.zmRbm || {};
|
||||||
|
const rbmApproval = jointZmRbmMeta?.RBM;
|
||||||
|
const ddZmApproval = jointZmRbmMeta?.['DD-ZM'];
|
||||||
|
const currentRoleNormalized = String(currentUser?.roleCode || currentUser?.role || '').toUpperCase();
|
||||||
|
const currentRoleApproval =
|
||||||
|
currentRoleNormalized === 'RBM'
|
||||||
|
? rbmApproval
|
||||||
|
: (currentRoleNormalized === 'DD-ZM' || currentRoleNormalized === 'DD ZM' || currentRoleNormalized === 'ZM')
|
||||||
|
? ddZmApproval
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={stage.id} className="flex items-start gap-4">
|
<div key={stage.id} className="flex items-start gap-4">
|
||||||
@ -686,12 +727,12 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className={`${isCurrent ? 'text-amber-900' : 'text-slate-900'}`}>
|
<h4 className={`${isCurrent ? 'text-amber-900' : 'text-slate-900'}`}>
|
||||||
{stage.name}
|
{formatStageLabel(stage.name)}
|
||||||
</h4>
|
</h4>
|
||||||
<p className={`text-sm ${isCurrent ? 'text-amber-700' : 'text-slate-600'}`}>
|
<p className={`text-sm ${isCurrent ? 'text-amber-700' : 'text-slate-600'}`}>
|
||||||
{atSubmittedGate && index === 0
|
{atSubmittedGate && index === 0
|
||||||
? 'Dealer action: filing complete (no further step here).'
|
? 'Dealer action: filing complete (no further step here).'
|
||||||
: `Responsible: ${stage.role}`}
|
: `Responsible: ${formatStageRole(stage.role)}`}
|
||||||
{atSubmittedGate && index === 1 ? (
|
{atSubmittedGate && index === 1 ? (
|
||||||
<span className="block mt-0.5 text-amber-800/90">
|
<span className="block mt-0.5 text-amber-800/90">
|
||||||
ASM approves to advance the request (first workflow action after submission).
|
ASM approves to advance the request (first workflow action after submission).
|
||||||
@ -722,6 +763,36 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isJointZmRbmStage && (
|
||||||
|
<div className="mt-3 rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||||
|
<p className="text-xs text-slate-600 mb-2">Joint approval status</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge className={rbmApproval?.approvedByUserId ? 'bg-green-100 text-green-700 border-green-300' : 'bg-slate-100 text-slate-500 border-slate-300'}>
|
||||||
|
RBM: {rbmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
|
||||||
|
</Badge>
|
||||||
|
<Badge className={ddZmApproval?.approvedByUserId ? 'bg-green-100 text-green-700 border-green-300' : 'bg-slate-100 text-slate-500 border-slate-300'}>
|
||||||
|
DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
|
||||||
|
</Badge>
|
||||||
|
{currentRoleApproval?.approvedByUserId && (
|
||||||
|
<Badge className="bg-blue-100 text-blue-700 border-blue-300">
|
||||||
|
Approved by you
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{(rbmApproval?.remarks || ddZmApproval?.remarks) && (
|
||||||
|
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
<div className="rounded border border-slate-200 bg-white p-2">
|
||||||
|
<p className="text-[11px] text-slate-500 mb-1">RBM Comment</p>
|
||||||
|
<p className="text-sm text-slate-700">{rbmApproval?.remarks || '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded border border-slate-200 bg-white p-2">
|
||||||
|
<p className="text-[11px] text-slate-500 mb-1">DD-ZM Comment</p>
|
||||||
|
<p className="text-sm text-slate-700">{ddZmApproval?.remarks || '-'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -771,7 +842,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<option value="">Select document type</option>
|
<option value="">Select document type</option>
|
||||||
{requiredDocs.map((docNum) => (
|
{requiredDocs.map((docNum) => (
|
||||||
<option key={docNum} value={String(docNum)}>
|
<option key={docNum} value={String(docNum)}>
|
||||||
{isDocTypeUploaded(docNum) ? '✅ ' : ''}
|
{isDocTypeUploaded(docNum) ? '✓ ' : ''}
|
||||||
{documentNames[docNum]}
|
{documentNames[docNum]}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
@ -1018,6 +1089,17 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!permissions.canApprove && permissions.hasCurrentUserApprovedZmRbm && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-green-300 text-green-700 bg-green-50 cursor-not-allowed"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||||
|
Approved by you
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{permissions.canReject && (
|
{permissions.canReject && (
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
|||||||
@ -62,6 +62,9 @@ const ALL_DEPARTMENTS = [
|
|||||||
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
|
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DEPARTMENT_CLAIM_PREFIX = '[DEPARTMENT_CLAIM]';
|
||||||
|
const FINANCE_VALIDATED_PREFIX = '[FINANCE_VALIDATED]';
|
||||||
|
|
||||||
interface FinanceFnFDetailsPageProps {
|
interface FinanceFnFDetailsPageProps {
|
||||||
fnfId: string;
|
fnfId: string;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
@ -88,6 +91,7 @@ interface FinancialLineItem {
|
|||||||
export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPageProps) {
|
export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPageProps) {
|
||||||
const [fnfCase, setFnfCase] = useState<any>(null);
|
const [fnfCase, setFnfCase] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState('overview');
|
||||||
|
|
||||||
// Initialize editable line items
|
// Initialize editable line items
|
||||||
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]);
|
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]);
|
||||||
@ -167,9 +171,24 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
return name;
|
return name;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFnFDetails = async () => {
|
const isDepartmentClaimLine = (description?: string, sourceType?: string) =>
|
||||||
|
sourceType === 'DepartmentClaim' ||
|
||||||
|
(typeof description === 'string' &&
|
||||||
|
(description.startsWith(DEPARTMENT_CLAIM_PREFIX) || description.includes('Clearance:')));
|
||||||
|
|
||||||
|
const isFinanceValidatedLine = (description?: string, sourceType?: string) =>
|
||||||
|
sourceType === 'FinanceValidated' ||
|
||||||
|
(typeof description === 'string' && description.startsWith(FINANCE_VALIDATED_PREFIX));
|
||||||
|
|
||||||
|
const cleanLineItemDescription = (description?: string) =>
|
||||||
|
(description || '')
|
||||||
|
.replace(DEPARTMENT_CLAIM_PREFIX, '')
|
||||||
|
.replace(FINANCE_VALIDATED_PREFIX, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const fetchFnFDetails = async (showLoader: boolean = true) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
if (showLoader) setLoading(true);
|
||||||
const response = await API.getFnFSettlementById(fnfId);
|
const response = await API.getFnFSettlementById(fnfId);
|
||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@ -191,9 +210,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
serviceCode: s.dealer?.dealerCode?.serviceCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.serviceCode || 'N/A',
|
serviceCode: s.dealer?.dealerCode?.serviceCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.serviceCode || 'N/A',
|
||||||
gearCode: s.dealer?.dealerCode?.gearCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gearCode || 'N/A',
|
gearCode: s.dealer?.dealerCode?.gearCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gearCode || 'N/A',
|
||||||
gmaCode: s.dealer?.dealerCode?.gmaCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gmaCode || 'N/A',
|
gmaCode: s.dealer?.dealerCode?.gmaCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gmaCode || 'N/A',
|
||||||
|
allLineItems: (s.lineItems || []).filter((li: any) => li.isActive !== false),
|
||||||
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
|
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
|
||||||
const c = (s.clearances || []).find((clearance: any) => normalizeDepartment(clearance.department) === deptName);
|
const c = (s.clearances || []).find((clearance: any) => normalizeDepartment(clearance.department) === deptName);
|
||||||
const relatedItems = (s.lineItems || []).filter((li: any) => normalizeDepartment(li.department) === deptName);
|
const relatedItems = (s.lineItems || []).filter(
|
||||||
|
(li: any) =>
|
||||||
|
normalizeDepartment(li.department) === deptName && isDepartmentClaimLine(li.description, li.sourceType),
|
||||||
|
);
|
||||||
|
|
||||||
// Calculate departmental net
|
// Calculate departmental net
|
||||||
let deptPayables = 0;
|
let deptPayables = 0;
|
||||||
@ -205,15 +228,24 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
});
|
});
|
||||||
|
|
||||||
const netAmount = deptPayables - deptRecoveries;
|
const netAmount = deptPayables - deptRecoveries;
|
||||||
|
const hasDuesAmount = Math.abs(netAmount) > 0;
|
||||||
|
const rawStatus = c?.status || 'Pending';
|
||||||
|
const normalizedStatus = hasDuesAmount
|
||||||
|
? 'Dues Pending'
|
||||||
|
: (rawStatus === 'Cleared' ? 'NOC Submitted' : rawStatus);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: c?.id || `dept-${deptName}`,
|
id: c?.id || `dept-${deptName}`,
|
||||||
departmentName: deptName,
|
departmentName: deptName,
|
||||||
status: c?.status || 'Pending',
|
status: normalizedStatus,
|
||||||
remarks: c?.remarks || '-',
|
remarks: c?.remarks || '-',
|
||||||
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : '-',
|
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : '-',
|
||||||
amount: Math.abs(netAmount),
|
amount: Math.abs(netAmount),
|
||||||
amountType: netAmount > 0 ? 'Payable Amount' : netAmount < 0 ? 'Recovery Amount' : null,
|
amountType: netAmount > 0
|
||||||
|
? 'Payable Amount'
|
||||||
|
: netAmount < 0
|
||||||
|
? 'Recovery Amount'
|
||||||
|
: null,
|
||||||
supportingDocument: c?.supportingDocument || null
|
supportingDocument: c?.supportingDocument || null
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@ -246,11 +278,17 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
const rItems: FinancialLineItem[] = [];
|
const rItems: FinancialLineItem[] = [];
|
||||||
const dItems: FinancialLineItem[] = [];
|
const dItems: FinancialLineItem[] = [];
|
||||||
|
|
||||||
(s.lineItems || []).forEach((li: any) => {
|
const allLineItems = (s.lineItems || []).filter((li: any) => li.isActive !== false);
|
||||||
|
const hasFinanceValidatedLines = allLineItems.some((li: any) => isFinanceValidatedLine(li.description, li.sourceType));
|
||||||
|
const calculationLineItems = hasFinanceValidatedLines
|
||||||
|
? allLineItems.filter((li: any) => isFinanceValidatedLine(li.description, li.sourceType))
|
||||||
|
: allLineItems.filter((li: any) => !isDepartmentClaimLine(li.description, li.sourceType));
|
||||||
|
|
||||||
|
calculationLineItems.forEach((li: any) => {
|
||||||
const item: FinancialLineItem = {
|
const item: FinancialLineItem = {
|
||||||
id: li.id,
|
id: li.id,
|
||||||
department: normalizeDepartment(li.department),
|
department: normalizeDepartment(li.department),
|
||||||
description: li.description || li.remarks || '',
|
description: cleanLineItemDescription(li.description || li.remarks || ''),
|
||||||
amount: Math.abs(li.amount)
|
amount: Math.abs(li.amount)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -282,7 +320,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
console.error('Fetch F&F error:', error);
|
console.error('Fetch F&F error:', error);
|
||||||
toast.error('Failed to fetch settlement details');
|
toast.error('Failed to fetch settlement details');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (showLoader) setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -350,6 +388,9 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
const [editingPayableId, setEditingPayableId] = useState<string | null>(null);
|
const [editingPayableId, setEditingPayableId] = useState<string | null>(null);
|
||||||
const [editingReceivableId, setEditingReceivableId] = useState<string | null>(null);
|
const [editingReceivableId, setEditingReceivableId] = useState<string | null>(null);
|
||||||
const [editingDeductionId, setEditingDeductionId] = useState<string | null>(null);
|
const [editingDeductionId, setEditingDeductionId] = useState<string | null>(null);
|
||||||
|
const [editingPayableDrafts, setEditingPayableDrafts] = useState<Record<string, FinancialLineItem>>({});
|
||||||
|
const [editingReceivableDrafts, setEditingReceivableDrafts] = useState<Record<string, FinancialLineItem>>({});
|
||||||
|
const [editingDeductionDrafts, setEditingDeductionDrafts] = useState<Record<string, FinancialLineItem>>({});
|
||||||
|
|
||||||
// Calculate dynamic settlement
|
// Calculate dynamic settlement
|
||||||
const calculateDynamicSettlement = () => {
|
const calculateDynamicSettlement = () => {
|
||||||
@ -369,6 +410,34 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const settlement = calculateDynamicSettlement();
|
const settlement = calculateDynamicSettlement();
|
||||||
|
const departmentReconciliation = ALL_DEPARTMENTS.map((dept) => {
|
||||||
|
const claim = (fnfCase?.departmentResponses || []).find((d: any) => d.departmentName === dept);
|
||||||
|
const claimAmount = Number(claim?.amount) || 0;
|
||||||
|
const claimType = claim?.amountType || '-';
|
||||||
|
|
||||||
|
const validatedPayable = payableItems
|
||||||
|
.filter((item) => normalizeDepartment(item.department) === dept)
|
||||||
|
.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
|
||||||
|
const validatedReceivable = receivableItems
|
||||||
|
.filter((item) => normalizeDepartment(item.department) === dept)
|
||||||
|
.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
|
||||||
|
const validatedDeduction = deductionItems
|
||||||
|
.filter((item) => normalizeDepartment(item.department) === dept)
|
||||||
|
.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
|
||||||
|
const validatedNet = validatedPayable - validatedReceivable - validatedDeduction;
|
||||||
|
const validatedAmount = Math.abs(validatedNet);
|
||||||
|
const validatedType = validatedNet > 0 ? 'Payable' : validatedNet < 0 ? 'Recovery' : '-';
|
||||||
|
const variance = validatedAmount - claimAmount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
department: dept,
|
||||||
|
claimAmount,
|
||||||
|
claimType,
|
||||||
|
validatedAmount,
|
||||||
|
validatedType,
|
||||||
|
variance
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const [settlementDetails, setSettlementDetails] = useState({
|
const [settlementDetails, setSettlementDetails] = useState({
|
||||||
verificationTransactionId: '',
|
verificationTransactionId: '',
|
||||||
@ -413,26 +482,44 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatePayable = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
|
const handleUpdatePayable = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
|
||||||
// Optimistic update
|
setEditingPayableDrafts((prev) => {
|
||||||
const updatedItems = payableItems.map(item =>
|
const base = prev[id] || payableItems.find((item) => item.id === id);
|
||||||
item.id === id ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) : value } : item
|
if (!base) return prev;
|
||||||
);
|
return {
|
||||||
setPayableItems(updatedItems);
|
...prev,
|
||||||
|
[id]: {
|
||||||
|
...base,
|
||||||
|
[field]: field === 'amount' ? Number(value) || 0 : value
|
||||||
|
} as FinancialLineItem
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// API update
|
const handleSavePayableEdit = async (id: string) => {
|
||||||
|
const draft = editingPayableDrafts[id];
|
||||||
|
if (!draft) {
|
||||||
|
setEditingPayableId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPayableItems((prev) => prev.map((item) => (item.id === id ? draft : item)));
|
||||||
try {
|
try {
|
||||||
const item = updatedItems.find(i => i.id === id);
|
await API.updateLineItem(id, {
|
||||||
if (item) {
|
department: draft.department,
|
||||||
await API.updateLineItem(id, {
|
description: draft.description,
|
||||||
department: item.department,
|
amount: -Math.abs(Number(draft.amount) || 0)
|
||||||
description: item.description,
|
});
|
||||||
amount: -Math.abs(item.amount)
|
setEditingPayableId(null);
|
||||||
});
|
setEditingPayableDrafts((prev) => {
|
||||||
fetchFnFDetails();
|
const next = { ...prev };
|
||||||
}
|
delete next[id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
toast.success('Changes saved');
|
||||||
|
fetchFnFDetails(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to update item');
|
toast.error('Failed to update item');
|
||||||
fetchFnFDetails(); // Rollback
|
fetchFnFDetails(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -466,7 +553,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
|
|
||||||
toast.success(`Clearance updated for ${selectedDept.departmentName}`);
|
toast.success(`Clearance updated for ${selectedDept.departmentName}`);
|
||||||
setShowClearanceDialog(false);
|
setShowClearanceDialog(false);
|
||||||
fetchFnFDetails();
|
setActiveTab('departments');
|
||||||
|
fetchFnFDetails(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Update clearance error:", error);
|
console.error("Update clearance error:", error);
|
||||||
toast.error("Failed to update department clearance");
|
toast.error("Failed to update department clearance");
|
||||||
@ -506,24 +594,44 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateReceivable = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
|
const handleUpdateReceivable = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
|
||||||
const updatedItems = receivableItems.map(item =>
|
setEditingReceivableDrafts((prev) => {
|
||||||
item.id === id ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) : value } : item
|
const base = prev[id] || receivableItems.find((item) => item.id === id);
|
||||||
);
|
if (!base) return prev;
|
||||||
setReceivableItems(updatedItems);
|
return {
|
||||||
|
...prev,
|
||||||
|
[id]: {
|
||||||
|
...base,
|
||||||
|
[field]: field === 'amount' ? Number(value) || 0 : value
|
||||||
|
} as FinancialLineItem
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveReceivableEdit = async (id: string) => {
|
||||||
|
const draft = editingReceivableDrafts[id];
|
||||||
|
if (!draft) {
|
||||||
|
setEditingReceivableId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReceivableItems((prev) => prev.map((item) => (item.id === id ? draft : item)));
|
||||||
try {
|
try {
|
||||||
const item = updatedItems.find(i => i.id === id);
|
await API.updateLineItem(id, {
|
||||||
if (item) {
|
department: draft.department,
|
||||||
await API.updateLineItem(id, {
|
description: draft.description,
|
||||||
department: item.department,
|
amount: Math.abs(Number(draft.amount) || 0)
|
||||||
description: item.description,
|
});
|
||||||
amount: Math.abs(item.amount)
|
setEditingReceivableId(null);
|
||||||
});
|
setEditingReceivableDrafts((prev) => {
|
||||||
fetchFnFDetails();
|
const next = { ...prev };
|
||||||
}
|
delete next[id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
toast.success('Changes saved');
|
||||||
|
fetchFnFDetails(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to update item');
|
toast.error('Failed to update item');
|
||||||
fetchFnFDetails();
|
fetchFnFDetails(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -569,24 +677,44 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateDeduction = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
|
const handleUpdateDeduction = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
|
||||||
const updatedItems = deductionItems.map(item =>
|
setEditingDeductionDrafts((prev) => {
|
||||||
item.id === id ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) : value } : item
|
const base = prev[id] || deductionItems.find((item) => item.id === id);
|
||||||
);
|
if (!base) return prev;
|
||||||
setDeductionItems(updatedItems);
|
return {
|
||||||
|
...prev,
|
||||||
|
[id]: {
|
||||||
|
...base,
|
||||||
|
[field]: field === 'amount' ? Number(value) || 0 : value
|
||||||
|
} as FinancialLineItem
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveDeductionEdit = async (id: string) => {
|
||||||
|
const draft = editingDeductionDrafts[id];
|
||||||
|
if (!draft) {
|
||||||
|
setEditingDeductionId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeductionItems((prev) => prev.map((item) => (item.id === id ? draft : item)));
|
||||||
try {
|
try {
|
||||||
const item = updatedItems.find(i => i.id === id);
|
await API.updateLineItem(id, {
|
||||||
if (item) {
|
department: draft.department,
|
||||||
await API.updateLineItem(id, {
|
description: draft.description,
|
||||||
department: item.department,
|
amount: Math.abs(Number(draft.amount) || 0)
|
||||||
description: item.description,
|
});
|
||||||
amount: Math.abs(item.amount)
|
setEditingDeductionId(null);
|
||||||
});
|
setEditingDeductionDrafts((prev) => {
|
||||||
fetchFnFDetails();
|
const next = { ...prev };
|
||||||
}
|
delete next[id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
toast.success('Changes saved');
|
||||||
|
fetchFnFDetails(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to update item');
|
toast.error('Failed to update item');
|
||||||
fetchFnFDetails();
|
fetchFnFDetails(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -773,7 +901,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Left Column - Case Details & Financial Info */}
|
{/* Left Column - Case Details & Financial Info */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
<Tabs defaultValue="overview" className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-5">
|
<TabsList className="grid w-full grid-cols-5">
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
<TabsTrigger value="financial">Financial</TabsTrigger>
|
<TabsTrigger value="financial">Financial</TabsTrigger>
|
||||||
@ -930,6 +1058,43 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="financial" className="space-y-4">
|
<TabsContent value="financial" className="space-y-4">
|
||||||
|
<Card className="border-blue-200 bg-blue-50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Department Claim vs Finance Validation</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Finance validated values are used for final settlement totals.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Department</TableHead>
|
||||||
|
<TableHead>Department Claim</TableHead>
|
||||||
|
<TableHead>Finance Validated</TableHead>
|
||||||
|
<TableHead>Variance</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{departmentReconciliation.map((row) => (
|
||||||
|
<TableRow key={row.department}>
|
||||||
|
<TableCell>{row.department}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{row.claimAmount > 0 ? `${row.claimType} ₹${row.claimAmount.toLocaleString('en-IN')}` : '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{row.validatedAmount > 0 ? `${row.validatedType} ₹${row.validatedAmount.toLocaleString('en-IN')}` : '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={row.variance === 0 ? 'text-slate-600' : row.variance > 0 ? 'text-red-600' : 'text-green-600'}>
|
||||||
|
{row.claimAmount === 0 && row.validatedAmount === 0 ? '-' : `₹${row.variance.toLocaleString('en-IN')}`}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Payables - Editable */}
|
{/* Payables - Editable */}
|
||||||
<Card className="border-green-200 bg-green-50">
|
<Card className="border-green-200 bg-green-50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -960,7 +1125,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{editingPayableId === item.id ? (
|
{editingPayableId === item.id ? (
|
||||||
<Select
|
<Select
|
||||||
value={item.department}
|
value={(editingPayableDrafts[item.id]?.department || item.department)}
|
||||||
onValueChange={(val) => handleUpdatePayable(item.id, 'department', val)}
|
onValueChange={(val) => handleUpdatePayable(item.id, 'department', val)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8">
|
<SelectTrigger className="h-8">
|
||||||
@ -979,7 +1144,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{editingPayableId === item.id ? (
|
{editingPayableId === item.id ? (
|
||||||
<Input
|
<Input
|
||||||
value={item.description}
|
value={(editingPayableDrafts[item.id]?.description || item.description)}
|
||||||
onChange={(e) => handleUpdatePayable(item.id, 'description', e.target.value)}
|
onChange={(e) => handleUpdatePayable(item.id, 'description', e.target.value)}
|
||||||
className="h-8"
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
@ -991,7 +1156,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
{editingPayableId === item.id ? (
|
{editingPayableId === item.id ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={item.amount}
|
value={(editingPayableDrafts[item.id]?.amount ?? item.amount)}
|
||||||
onChange={(e) => handleUpdatePayable(item.id, 'amount', e.target.value)}
|
onChange={(e) => handleUpdatePayable(item.id, 'amount', e.target.value)}
|
||||||
className="h-8 text-right"
|
className="h-8 text-right"
|
||||||
/>
|
/>
|
||||||
@ -1006,10 +1171,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => {
|
onClick={() => handleSavePayableEdit(item.id)}
|
||||||
setEditingPayableId(null);
|
|
||||||
toast.success('Changes saved');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -1018,7 +1180,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => setEditingPayableId(item.id)}
|
onClick={() => {
|
||||||
|
setEditingPayableId(item.id);
|
||||||
|
setEditingPayableDrafts((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[item.id]: { ...item }
|
||||||
|
}));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -1116,7 +1284,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{editingReceivableId === item.id ? (
|
{editingReceivableId === item.id ? (
|
||||||
<Select
|
<Select
|
||||||
value={item.department}
|
value={(editingReceivableDrafts[item.id]?.department || item.department)}
|
||||||
onValueChange={(val) => handleUpdateReceivable(item.id, 'department', val)}
|
onValueChange={(val) => handleUpdateReceivable(item.id, 'department', val)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8">
|
<SelectTrigger className="h-8">
|
||||||
@ -1135,7 +1303,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{editingReceivableId === item.id ? (
|
{editingReceivableId === item.id ? (
|
||||||
<Input
|
<Input
|
||||||
value={item.description}
|
value={(editingReceivableDrafts[item.id]?.description || item.description)}
|
||||||
onChange={(e) => handleUpdateReceivable(item.id, 'description', e.target.value)}
|
onChange={(e) => handleUpdateReceivable(item.id, 'description', e.target.value)}
|
||||||
className="h-8"
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
@ -1147,7 +1315,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
{editingReceivableId === item.id ? (
|
{editingReceivableId === item.id ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={item.amount}
|
value={(editingReceivableDrafts[item.id]?.amount ?? item.amount)}
|
||||||
onChange={(e) => handleUpdateReceivable(item.id, 'amount', e.target.value)}
|
onChange={(e) => handleUpdateReceivable(item.id, 'amount', e.target.value)}
|
||||||
className="h-8 text-right"
|
className="h-8 text-right"
|
||||||
/>
|
/>
|
||||||
@ -1162,10 +1330,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => {
|
onClick={() => handleSaveReceivableEdit(item.id)}
|
||||||
setEditingReceivableId(null);
|
|
||||||
toast.success('Changes saved');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -1174,7 +1339,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => setEditingReceivableId(item.id)}
|
onClick={() => {
|
||||||
|
setEditingReceivableId(item.id);
|
||||||
|
setEditingReceivableDrafts((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[item.id]: { ...item }
|
||||||
|
}));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -1272,7 +1443,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{editingDeductionId === item.id ? (
|
{editingDeductionId === item.id ? (
|
||||||
<Select
|
<Select
|
||||||
value={item.department}
|
value={(editingDeductionDrafts[item.id]?.department || item.department)}
|
||||||
onValueChange={(val) => handleUpdateDeduction(item.id, 'department', val)}
|
onValueChange={(val) => handleUpdateDeduction(item.id, 'department', val)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8">
|
<SelectTrigger className="h-8">
|
||||||
@ -1291,7 +1462,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{editingDeductionId === item.id ? (
|
{editingDeductionId === item.id ? (
|
||||||
<Input
|
<Input
|
||||||
value={item.description}
|
value={(editingDeductionDrafts[item.id]?.description || item.description)}
|
||||||
onChange={(e) => handleUpdateDeduction(item.id, 'description', e.target.value)}
|
onChange={(e) => handleUpdateDeduction(item.id, 'description', e.target.value)}
|
||||||
className="h-8"
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
@ -1303,7 +1474,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
{editingDeductionId === item.id ? (
|
{editingDeductionId === item.id ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={item.amount}
|
value={(editingDeductionDrafts[item.id]?.amount ?? item.amount)}
|
||||||
onChange={(e) => handleUpdateDeduction(item.id, 'amount', e.target.value)}
|
onChange={(e) => handleUpdateDeduction(item.id, 'amount', e.target.value)}
|
||||||
className="h-8 text-right"
|
className="h-8 text-right"
|
||||||
/>
|
/>
|
||||||
@ -1318,10 +1489,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => {
|
onClick={() => handleSaveDeductionEdit(item.id)}
|
||||||
setEditingDeductionId(null);
|
|
||||||
toast.success('Changes saved');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -1330,7 +1498,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => setEditingDeductionId(item.id)}
|
onClick={() => {
|
||||||
|
setEditingDeductionId(item.id);
|
||||||
|
setEditingDeductionDrafts((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[item.id]: { ...item }
|
||||||
|
}));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -46,28 +46,70 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRelevantPaymentStatus = (app: any) => {
|
const normalizeStatus = (status: any) => String(status || '').trim().toLowerCase();
|
||||||
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment';
|
const isVerifiedLikeStatus = (status: any) => {
|
||||||
const s = app.overallStatus || app.status;
|
const normalized = normalizeStatus(status);
|
||||||
const relevantType = (s.includes('LOI') || s === 'PAYMENT_VERIFICATION' || s === 'Security Details' || s === 'Payment Pending') ? 'SECURITY_DEPOSIT' : 'FIRST_FILL';
|
return normalized === 'verified' || normalized === 'paid';
|
||||||
const deposit = app.securityDeposits.find((d: any) => d.depositType === relevantType);
|
|
||||||
return deposit ? deposit.status : 'Awaiting Payment';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter for Payment Mode
|
const paymentRows = applications.flatMap((app: any) => {
|
||||||
const paymentApps = applications.filter((app: any) => {
|
|
||||||
const s = app.overallStatus || app.status;
|
const s = app.overallStatus || app.status;
|
||||||
return [
|
const isPaymentStage = [
|
||||||
'LOI In Progress', 'Security Details', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
|
'Payment Pending', 'Security Details', 'LOI In Progress', 'LOI Issued',
|
||||||
'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT', 'Payment Pending'
|
'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL'
|
||||||
].includes(s);
|
].includes(s);
|
||||||
|
const deposits = app.securityDeposits || [];
|
||||||
|
|
||||||
|
// Always include actual recorded deposits (including already verified rows)
|
||||||
|
if (deposits.length > 0) {
|
||||||
|
return deposits.map((d: any) => ({
|
||||||
|
id: d.id,
|
||||||
|
applicationId: app.applicationId || app.id,
|
||||||
|
application: app,
|
||||||
|
paymentStatus: d.status,
|
||||||
|
paymentType: d.depositType,
|
||||||
|
amount: d.amount,
|
||||||
|
createdAt: d.createdAt,
|
||||||
|
verificationDate: d.verifiedAt,
|
||||||
|
isVirtual: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep virtual pending rows for in-flight cases with no deposit record yet
|
||||||
|
if (isPaymentStage) {
|
||||||
|
if (['Payment Pending', 'Security Details', 'LOI In Progress'].includes(s)) {
|
||||||
|
return [{
|
||||||
|
id: `virtual-${app.id}-sd`,
|
||||||
|
applicationId: app.applicationId || app.id,
|
||||||
|
application: app,
|
||||||
|
paymentStatus: 'Pending',
|
||||||
|
paymentType: 'SECURITY_DEPOSIT',
|
||||||
|
amount: 500000,
|
||||||
|
createdAt: app.updatedAt,
|
||||||
|
verificationDate: null,
|
||||||
|
isVirtual: true
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [{
|
||||||
|
id: `virtual-${app.id}-ff`,
|
||||||
|
applicationId: app.applicationId || app.id,
|
||||||
|
application: app,
|
||||||
|
paymentStatus: 'Pending',
|
||||||
|
paymentType: 'FIRST_FILL',
|
||||||
|
amount: 1500000,
|
||||||
|
createdAt: app.updatedAt,
|
||||||
|
verificationDate: null,
|
||||||
|
isVirtual: true
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayApps = paymentApps.filter(app => {
|
const displayRows = paymentRows.filter((row: any) => {
|
||||||
const status = getRelevantPaymentStatus(app);
|
|
||||||
if (filterStatus === 'all') return true;
|
if (filterStatus === 'all') return true;
|
||||||
if (filterStatus === 'pending') return status !== 'Verified';
|
if (filterStatus === 'pending') return !isVerifiedLikeStatus(row.paymentStatus);
|
||||||
if (filterStatus === 'verified') return status === 'Verified';
|
if (filterStatus === 'verified') return isVerifiedLikeStatus(row.paymentStatus);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -107,7 +149,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
<div className="inline-flex p-1 bg-slate-100 rounded-xl">
|
<div className="inline-flex p-1 bg-slate-100 rounded-xl">
|
||||||
<div className="flex items-center px-4 py-2 bg-white rounded-lg text-slate-900 shadow-sm font-medium text-sm">
|
<div className="flex items-center px-4 py-2 bg-white rounded-lg text-slate-900 shadow-sm font-medium text-sm">
|
||||||
<IndianRupee className="w-4 h-4 mr-2 text-blue-600" />
|
<IndianRupee className="w-4 h-4 mr-2 text-blue-600" />
|
||||||
Pending Payments ({paymentApps.filter(a => getRelevantPaymentStatus(a) !== 'Verified').length})
|
Pending Payments ({paymentRows.filter((row: any) => !isVerifiedLikeStatus(row.paymentStatus)).length})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -126,7 +168,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
onClick={() => setFilterStatus('verified')}
|
onClick={() => setFilterStatus('verified')}
|
||||||
className={filterStatus === 'verified' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'}
|
className={filterStatus === 'verified' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'}
|
||||||
>
|
>
|
||||||
Completed
|
Verified
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={filterStatus === 'all' ? 'secondary' : 'ghost'}
|
variant={filterStatus === 'all' ? 'secondary' : 'ghost'}
|
||||||
@ -153,11 +195,12 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{displayApps.length > 0 ? (
|
{displayRows.length > 0 ? (
|
||||||
displayApps.map((app) => {
|
displayRows.map((row: any) => {
|
||||||
const statusLabel = getRelevantPaymentStatus(app);
|
const statusLabel = row.paymentStatus || 'Awaiting Payment';
|
||||||
|
const app = row.application || {};
|
||||||
return (
|
return (
|
||||||
<TableRow key={app.id} className="hover:bg-blue-50/20 group transition-all">
|
<TableRow key={row.id} className="hover:bg-blue-50/20 group transition-all">
|
||||||
<TableCell className="py-4 pl-6">
|
<TableCell className="py-4 pl-6">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-mono text-xs font-bold text-blue-600 mb-1">{app.applicationId || app.id}</span>
|
<span className="font-mono text-xs font-bold text-blue-600 mb-1">{app.applicationId || app.id}</span>
|
||||||
@ -176,7 +219,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CreditCard className="w-4 h-4 text-slate-400" />
|
<CreditCard className="w-4 h-4 text-slate-400" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{ (app.overallStatus === 'LOI Issued' || app.overallStatus === 'Security Details' || app.overallStatus === 'LOI In Progress') ? 'Security Deposit (₹5L)' : 'First Fill (₹15L)'}
|
{row.paymentType === 'SECURITY_DEPOSIT' ? 'Security Deposit (₹5L)' : 'First Fill (₹15L)'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -195,15 +238,15 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
<TableCell className="text-right pr-6">
|
<TableCell className="text-right pr-6">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={statusLabel === 'Verified' ? 'outline' : 'default'}
|
variant={isVerifiedLikeStatus(statusLabel) ? 'outline' : 'default'}
|
||||||
className={statusLabel !== 'Verified'
|
className={!isVerifiedLikeStatus(statusLabel)
|
||||||
? 'bg-blue-600 hover:bg-blue-700 shadow-md'
|
? 'bg-blue-600 hover:bg-blue-700 shadow-md'
|
||||||
: 'bg-white text-slate-600 border-slate-200'}
|
: 'bg-white text-slate-600 border-slate-200'}
|
||||||
onClick={() => handleAction(app.applicationId || app.id)}
|
onClick={() => handleAction(row.applicationId || app.id)}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<IndianRupee className="w-4 h-4 mr-2" />
|
<IndianRupee className="w-4 h-4 mr-2" />
|
||||||
{statusLabel === 'Verified' ? 'View Receipt' : 'Record Payment'}
|
{isVerifiedLikeStatus(statusLabel) ? 'View Receipt' : 'Record Payment'}
|
||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -217,7 +260,13 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center">
|
||||||
<CheckCircle className="w-6 h-6 text-slate-200" />
|
<CheckCircle className="w-6 h-6 text-slate-200" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm">No applications pending in the queue</p>
|
<p className="text-sm">
|
||||||
|
{filterStatus === 'verified'
|
||||||
|
? 'No verified payments found'
|
||||||
|
: filterStatus === 'pending'
|
||||||
|
? 'No pending payments in the queue'
|
||||||
|
: 'No onboarding payments found'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@ -62,10 +62,14 @@ const ALL_DEPARTMENTS = [
|
|||||||
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
|
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DEPARTMENT_CLAIM_PREFIX = "[DEPARTMENT_CLAIM]";
|
||||||
|
const FINANCE_VALIDATED_PREFIX = "[FINANCE_VALIDATED]";
|
||||||
|
|
||||||
export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [fnfCase, setFnfCase] = useState<any>(null);
|
const [fnfCase, setFnfCase] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState('details');
|
||||||
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
|
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
|
||||||
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||||
@ -128,6 +132,15 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
return name;
|
return name;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDepartmentClaimLine = (description?: string, sourceType?: string) =>
|
||||||
|
sourceType === "DepartmentClaim" ||
|
||||||
|
(typeof description === "string" &&
|
||||||
|
(description.startsWith(DEPARTMENT_CLAIM_PREFIX) || description.includes('Clearance:')));
|
||||||
|
|
||||||
|
const isFinanceValidatedLine = (description?: string, sourceType?: string) =>
|
||||||
|
sourceType === "FinanceValidated" ||
|
||||||
|
(typeof description === "string" && description.startsWith(FINANCE_VALIDATED_PREFIX));
|
||||||
|
|
||||||
const getFriendlyActionName = (action: string) => {
|
const getFriendlyActionName = (action: string) => {
|
||||||
if (!action) return 'Action';
|
if (!action) return 'Action';
|
||||||
const mapping: Record<string, string> = {
|
const mapping: Record<string, string> = {
|
||||||
@ -141,14 +154,20 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
return mapping[action] || action.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
|
return mapping[action] || action.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFnFDetails = async () => {
|
const fetchFnFDetails = async (showLoader: boolean = true) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
if (showLoader) setLoading(true);
|
||||||
const response = await API.getFnFSettlementById(fnfId);
|
const response = await API.getFnFSettlementById(fnfId);
|
||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const s = data.fnf;
|
const s = data.fnf;
|
||||||
// Map backend data to UI format
|
// Map backend data to UI format
|
||||||
|
const allLineItems = (s.lineItems || []).filter((li: any) => li.isActive !== false);
|
||||||
|
const hasFinanceValidatedLines = allLineItems.some((li: any) => isFinanceValidatedLine(li.description, li.sourceType));
|
||||||
|
const calculationLineItems = hasFinanceValidatedLines
|
||||||
|
? allLineItems.filter((li: any) => isFinanceValidatedLine(li.description, li.sourceType))
|
||||||
|
: allLineItems.filter((li: any) => !isDepartmentClaimLine(li.description, li.sourceType));
|
||||||
|
|
||||||
const mappedCase: any = {
|
const mappedCase: any = {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
caseNumber: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
|
caseNumber: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
|
||||||
@ -180,9 +199,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
: s.status === "Completed"
|
: s.status === "Completed"
|
||||||
? "Completed"
|
? "Completed"
|
||||||
: "Pending",
|
: "Pending",
|
||||||
totalPayableAmount: (s.lineItems || []).filter((li: any) => li.itemType === 'Payable').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0),
|
totalPayableAmount: calculationLineItems.filter((li: any) => li.itemType === 'Payable').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0),
|
||||||
totalRecoveryAmount: (s.lineItems || []).filter((li: any) => li.itemType === 'Receivable' || li.itemType === 'Recovery').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0),
|
totalRecoveryAmount: calculationLineItems.filter((li: any) => li.itemType === 'Receivable' || li.itemType === 'Recovery').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0),
|
||||||
totalDeductions: (s.lineItems || []).filter((li: any) => li.itemType === 'Deduction').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0),
|
totalDeductions: calculationLineItems.filter((li: any) => li.itemType === 'Deduction').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0),
|
||||||
|
allLineItems,
|
||||||
netAmount: 0,
|
netAmount: 0,
|
||||||
departmentResponses: [] as any[]
|
departmentResponses: [] as any[]
|
||||||
};
|
};
|
||||||
@ -198,8 +218,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
const c = (s.clearances || []).find(
|
const c = (s.clearances || []).find(
|
||||||
(clearance: any) => normalizeDepartment(clearance.department) === deptName,
|
(clearance: any) => normalizeDepartment(clearance.department) === deptName,
|
||||||
);
|
);
|
||||||
const relatedItems = (s.lineItems || []).filter(
|
const relatedItems = allLineItems.filter(
|
||||||
(li: any) => normalizeDepartment(li.department) === deptName,
|
(li: any) =>
|
||||||
|
normalizeDepartment(li.department) === deptName &&
|
||||||
|
isDepartmentClaimLine(li.description, li.sourceType),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate departmental net
|
// Calculate departmental net
|
||||||
@ -212,12 +234,17 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const netAmount = deptPayables - deptRecoveries;
|
const netAmount = deptPayables - deptRecoveries;
|
||||||
|
const hasDuesAmount = Math.abs(netAmount) > 0;
|
||||||
|
const rawStatus = c?.status || "Pending";
|
||||||
|
const normalizedStatus = hasDuesAmount
|
||||||
|
? "Dues Pending"
|
||||||
|
: (rawStatus === "Cleared" ? "NOC Submitted" : rawStatus);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: c?.id || `dept-${deptName}`,
|
id: c?.id || `dept-${deptName}`,
|
||||||
clearanceId: c?.id || null,
|
clearanceId: c?.id || null,
|
||||||
departmentName: deptName,
|
departmentName: deptName,
|
||||||
status: c?.status || "Pending",
|
status: normalizedStatus,
|
||||||
amountType: netAmount > 0 ? "Payable" : netAmount < 0 ? "Recovery" : null,
|
amountType: netAmount > 0 ? "Payable" : netAmount < 0 ? "Recovery" : null,
|
||||||
amount: Math.abs(netAmount),
|
amount: Math.abs(netAmount),
|
||||||
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : "-",
|
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : "-",
|
||||||
@ -272,7 +299,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
console.error("Fetch F&F details error:", error);
|
console.error("Fetch F&F details error:", error);
|
||||||
toast.error("Failed to fetch settlement details");
|
toast.error("Failed to fetch settlement details");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (showLoader) setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -423,7 +450,8 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
toast.success(`Clearance updated for ${selectedDept.departmentName}`);
|
toast.success(`Clearance updated for ${selectedDept.departmentName}`);
|
||||||
setShowClearanceDialog(false);
|
setShowClearanceDialog(false);
|
||||||
setClearanceFile(null);
|
setClearanceFile(null);
|
||||||
fetchFnFDetails();
|
setActiveTab('departments');
|
||||||
|
fetchFnFDetails(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Update clearance error:", error);
|
console.error("Update clearance error:", error);
|
||||||
toast.error("Failed to update department clearance");
|
toast.error("Failed to update department clearance");
|
||||||
@ -470,6 +498,32 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
).length;
|
).length;
|
||||||
const totalDepartments = fnfCase.departmentResponses.length;
|
const totalDepartments = fnfCase.departmentResponses.length;
|
||||||
const progressPercentage = (responsesReceived / totalDepartments) * 100;
|
const progressPercentage = (responsesReceived / totalDepartments) * 100;
|
||||||
|
const departmentReconciliation = ALL_DEPARTMENTS.map((deptName) => {
|
||||||
|
const claim = (fnfCase.departmentResponses || []).find((d: any) => d.departmentName === deptName);
|
||||||
|
const claimAmount = Number(claim?.amount) || 0;
|
||||||
|
const claimType = claim?.amountType || '-';
|
||||||
|
const deptLines = (fnfCase.allLineItems || []).filter((li: any) => normalizeDepartment(li.department) === deptName);
|
||||||
|
const payable = deptLines
|
||||||
|
.filter((li: any) => li.sourceType === 'FinanceValidated' && li.itemType === 'Payable')
|
||||||
|
.reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount) || 0), 0);
|
||||||
|
const receivable = deptLines
|
||||||
|
.filter((li: any) => li.sourceType === 'FinanceValidated' && (li.itemType === 'Receivable' || li.itemType === 'Recovery'))
|
||||||
|
.reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount) || 0), 0);
|
||||||
|
const deduction = deptLines
|
||||||
|
.filter((li: any) => li.sourceType === 'FinanceValidated' && li.itemType === 'Deduction')
|
||||||
|
.reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount) || 0), 0);
|
||||||
|
const validatedNet = payable - receivable - deduction;
|
||||||
|
const validatedAmount = Math.abs(validatedNet);
|
||||||
|
const validatedType = validatedNet > 0 ? 'Payable' : validatedNet < 0 ? 'Recovery' : '-';
|
||||||
|
return {
|
||||||
|
department: deptName,
|
||||||
|
claimAmount,
|
||||||
|
claimType,
|
||||||
|
validatedAmount,
|
||||||
|
validatedType,
|
||||||
|
variance: validatedAmount - claimAmount
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -607,7 +661,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<Tabs defaultValue="details" className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="progress">Progress</TabsTrigger>
|
<TabsTrigger value="progress">Progress</TabsTrigger>
|
||||||
<TabsTrigger value="details">Case Details</TabsTrigger>
|
<TabsTrigger value="details">Case Details</TabsTrigger>
|
||||||
@ -1383,6 +1437,39 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
{/* Financial Summary Tab */}
|
{/* Financial Summary Tab */}
|
||||||
<TabsContent value="financial">
|
<TabsContent value="financial">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<Card className="border-blue-200 bg-blue-50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Department Claim vs Finance Validation</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Final settlement totals are based on finance validated values.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Department</TableHead>
|
||||||
|
<TableHead>Department Claim</TableHead>
|
||||||
|
<TableHead>Finance Validated</TableHead>
|
||||||
|
<TableHead>Variance</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{departmentReconciliation.map((row) => (
|
||||||
|
<TableRow key={row.department}>
|
||||||
|
<TableCell>{row.department}</TableCell>
|
||||||
|
<TableCell>{row.claimAmount > 0 ? `${row.claimType} ₹${row.claimAmount.toLocaleString()}` : '-'}</TableCell>
|
||||||
|
<TableCell>{row.validatedAmount > 0 ? `${row.validatedType} ₹${row.validatedAmount.toLocaleString()}` : '-'}</TableCell>
|
||||||
|
<TableCell className={row.variance === 0 ? 'text-slate-600' : row.variance > 0 ? 'text-red-600' : 'text-green-600'}>
|
||||||
|
{row.claimAmount === 0 && row.validatedAmount === 0 ? '-' : `₹${row.variance.toLocaleString()}`}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Financial Summary</CardTitle>
|
<CardTitle>Financial Summary</CardTitle>
|
||||||
|
|||||||
@ -21,52 +21,6 @@ import { formatDateTime } from '../ui/utils';
|
|||||||
import { RESIGNATION_DOCUMENT_TYPES, RESIGNATION_STAGE_OPTIONS } from '../../lib/offboardingDocumentOptions';
|
import { RESIGNATION_DOCUMENT_TYPES, RESIGNATION_STAGE_OPTIONS } from '../../lib/offboardingDocumentOptions';
|
||||||
import { WIDE_DIALOG_CLASS } from '../../lib/dialogStyles';
|
import { WIDE_DIALOG_CLASS } from '../../lib/dialogStyles';
|
||||||
|
|
||||||
const ALL_DEPARTMENTS = [
|
|
||||||
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
|
|
||||||
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
|
|
||||||
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
|
|
||||||
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
|
|
||||||
];
|
|
||||||
|
|
||||||
const normalizeDepartment = (name: string) => {
|
|
||||||
if (!name) return name;
|
|
||||||
let inputName = name.trim();
|
|
||||||
|
|
||||||
// Exact match first
|
|
||||||
const exactMatch = ALL_DEPARTMENTS.find(d => d.toLowerCase() === inputName.toLowerCase());
|
|
||||||
if (exactMatch) return exactMatch;
|
|
||||||
|
|
||||||
// Smart mapping for shorthands
|
|
||||||
const mapping: Record<string, string> = {
|
|
||||||
'sales': 'Sales Department',
|
|
||||||
'service': 'Service Department',
|
|
||||||
'spares': 'Parts Department',
|
|
||||||
'parts': 'Parts Department',
|
|
||||||
'spares / parts': 'Parts Department',
|
|
||||||
'finance': 'Finance Department',
|
|
||||||
'accounts': 'Finance Department',
|
|
||||||
'warranty': 'Warranty Department',
|
|
||||||
'marketing': 'Marketing Department',
|
|
||||||
'hr': 'HR Department',
|
|
||||||
'it': 'IT Department',
|
|
||||||
'legal': 'Legal Department',
|
|
||||||
'logistics': 'Logistics Department',
|
|
||||||
'quality': 'Quality Department',
|
|
||||||
'fdd': 'Finance Department',
|
|
||||||
'apparel': 'Accessories Department',
|
|
||||||
'accessories': 'Accessories Department',
|
|
||||||
'dms': 'IT Department',
|
|
||||||
'rto': 'Admin Department',
|
|
||||||
'admin': 'Admin Department',
|
|
||||||
'admin / dd-admin': 'Admin Department'
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapped = mapping[inputName.toLowerCase().replace(' department', '')];
|
|
||||||
if (mapped) return mapped;
|
|
||||||
|
|
||||||
return name;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
interface ResignationDetailsProps {
|
interface ResignationDetailsProps {
|
||||||
resignationId: string;
|
resignationId: string;
|
||||||
@ -545,7 +499,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<TabsList className="bg-slate-100 p-1">
|
<TabsList className="bg-slate-100 p-1">
|
||||||
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
|
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
|
||||||
<TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger>
|
<TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger>
|
||||||
{currentUser?.role !== 'Dealer' && <TabsTrigger value="clearances" className="data-[state=active]:bg-white">Clearances</TabsTrigger>}
|
|
||||||
<TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger>
|
<TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger>
|
||||||
<TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
|
<TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@ -566,6 +519,10 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<Label className="text-slate-600">GST</Label>
|
<Label className="text-slate-600">GST</Label>
|
||||||
<p>{resignationData?.dealer?.dealerProfile?.gstNumber || resignationData?.outlet?.gstNumber || 'N/A'}</p>
|
<p>{resignationData?.dealer?.dealerProfile?.gstNumber || resignationData?.outlet?.gstNumber || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-600">Dealer Email</Label>
|
||||||
|
<p>{resignationData?.dealer?.email || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Sales Code</Label>
|
<Label className="text-slate-600">Sales Code</Label>
|
||||||
<p>{resignationData?.dealer?.dealerProfile?.dealerCode?.salesCode || 'N/A'}</p>
|
<p>{resignationData?.dealer?.dealerProfile?.dealerCode?.salesCode || 'N/A'}</p>
|
||||||
@ -756,90 +713,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Clearances Tab */}
|
|
||||||
{currentUser?.role !== 'Dealer' && (
|
|
||||||
<TabsContent value="clearances">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle>Departmental Clearances</CardTitle>
|
|
||||||
<CardDescription>Status of clearances from various departments</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Department</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Amount Type</TableHead>
|
|
||||||
<TableHead>Amount</TableHead>
|
|
||||||
<TableHead>Remarks</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{ALL_DEPARTMENTS.map((dept) => {
|
|
||||||
const settlement = resignationData?.settlement;
|
|
||||||
const fffClearance = (settlement?.clearances || []).find((c: any) => normalizeDepartment(c.department) === dept);
|
|
||||||
const relatedLineItems = (settlement?.lineItems || []).filter((li: any) => normalizeDepartment(li.department) === dept);
|
|
||||||
|
|
||||||
let deptPayables = 0;
|
|
||||||
let deptRecoveries = 0;
|
|
||||||
relatedLineItems.forEach((li: any) => {
|
|
||||||
const amt = Math.abs(parseFloat(li.amount) || 0);
|
|
||||||
if (li.itemType === 'Payable') deptPayables += amt;
|
|
||||||
else deptRecoveries += amt;
|
|
||||||
});
|
|
||||||
|
|
||||||
const netAmount = deptPayables - deptRecoveries;
|
|
||||||
const jsonClearance = (resignationData?.departmentalClearances || {})[dept] || { status: 'Pending', remarks: '', amount: 0, type: 'Recovery' };
|
|
||||||
const displayStatus = fffClearance
|
|
||||||
? (fffClearance.status === 'NOC Submitted' || fffClearance.status === 'Cleared' ? (netAmount < 0 ? 'Dues' : 'Cleared') : fffClearance.status)
|
|
||||||
: jsonClearance.status;
|
|
||||||
const displayRemarks = fffClearance ? fffClearance.remarks : jsonClearance.remarks;
|
|
||||||
const displayAmount = Math.abs(netAmount) || jsonClearance.amount || 0;
|
|
||||||
const displayType = netAmount > 0 ? 'Payable' : 'Recovery';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow key={dept}>
|
|
||||||
<TableCell>{dept}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge className={
|
|
||||||
displayStatus === 'Cleared' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
|
|
||||||
displayStatus === 'Dues' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
|
|
||||||
'bg-yellow-100 text-yellow-700 hover:bg-yellow-100'
|
|
||||||
}>
|
|
||||||
{displayStatus || 'Pending'}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={displayType === 'Recovery'
|
|
||||||
? 'bg-red-50 text-red-700 border-red-200'
|
|
||||||
: 'bg-green-50 text-green-700 border-green-200'}
|
|
||||||
>
|
|
||||||
{displayType}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span className={displayType === 'Recovery' ? 'text-red-600 font-bold' : 'text-green-600 font-bold'}>
|
|
||||||
₹{displayAmount.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="max-w-xs truncate">
|
|
||||||
{displayRemarks || 'Awaiting departmental verification.'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Documents Tab */}
|
{/* Documents Tab */}
|
||||||
<TabsContent value="documents">
|
<TabsContent value="documents">
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@ -153,7 +153,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if user can push to F&F (DD Lead and above)
|
// Check if user can push to F&F (DD Lead and above)
|
||||||
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role || currentUser.roleCode);
|
||||||
|
|
||||||
// Centralized Permissions Utility for Termination logic (Robust Validation)
|
// Centralized Permissions Utility for Termination logic (Robust Validation)
|
||||||
const getTerminationPermissions = () => {
|
const getTerminationPermissions = () => {
|
||||||
@ -163,7 +163,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
|
|
||||||
const currentStage = terminationData.currentStage;
|
const currentStage = terminationData.currentStage;
|
||||||
const status = terminationData.status;
|
const status = terminationData.status;
|
||||||
const userRole = currentUser.role;
|
const userRole = currentUser.role || currentUser.roleCode;
|
||||||
|
const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage);
|
||||||
|
|
||||||
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated';
|
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated';
|
||||||
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
|
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
|
||||||
@ -173,6 +174,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
(currentStage === 'ZBH Review' && userRole === 'ZBH') ||
|
(currentStage === 'ZBH Review' && userRole === 'ZBH') ||
|
||||||
(currentStage === 'DD Lead Review' && userRole === 'DD Lead') ||
|
(currentStage === 'DD Lead Review' && userRole === 'DD Lead') ||
|
||||||
(currentStage === 'Legal Verification' && userRole === 'Legal Admin') ||
|
(currentStage === 'Legal Verification' && userRole === 'Legal Admin') ||
|
||||||
|
(currentStage === 'DD Head Review' && userRole === 'DD Head') ||
|
||||||
(currentStage === 'NBH Evaluation' && userRole === 'NBH') ||
|
(currentStage === 'NBH Evaluation' && userRole === 'NBH') ||
|
||||||
(currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
|
(currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
|
||||||
(currentStage === 'CCO Approval' && userRole === 'CCO') ||
|
(currentStage === 'CCO Approval' && userRole === 'CCO') ||
|
||||||
@ -181,10 +183,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && !['Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage),
|
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && ![...['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'], 'Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage),
|
||||||
canIssueSCN: currentStage === 'NBH Evaluation' && (userRole === 'NBH' || userRole === 'Super Admin') && !isFinalState,
|
canIssueSCN: currentStage === 'NBH Evaluation' && (userRole === 'NBH' || userRole === 'Super Admin') && !isFinalState,
|
||||||
canUploadSCNResponse: currentStage === 'Show Cause Notice' && (['Legal Admin', 'DD Admin', 'Super Admin'].includes(userRole)) && !isFinalState,
|
canUploadSCNResponse: isScnStage && (['Legal Admin', 'DD Admin', 'Super Admin'].includes(userRole)) && !isFinalState,
|
||||||
canFinalize: ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage) && (userRole === currentStage.replace(' Approval', '') || userRole === 'Super Admin') && !isFinalState,
|
canFinalize: (
|
||||||
|
(currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
|
||||||
|
(currentStage === 'CCO Approval' && userRole === 'CCO') ||
|
||||||
|
(currentStage === 'CEO Final Approval' && userRole === 'CEO') ||
|
||||||
|
userRole === 'Super Admin'
|
||||||
|
) && ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage) && !isFinalState,
|
||||||
canPushToFnF: canPushToFnF && !isSettlementPhase && !isFinalState,
|
canPushToFnF: canPushToFnF && !isSettlementPhase && !isFinalState,
|
||||||
canWithdraw: userRole === 'ASM' && currentStage === 'Request Initiated' && !isFinalState,
|
canWithdraw: userRole === 'ASM' && currentStage === 'Request Initiated' && !isFinalState,
|
||||||
isFinalState,
|
isFinalState,
|
||||||
@ -196,6 +203,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
|
|
||||||
// Use actual data from backend
|
// Use actual data from backend
|
||||||
const request = terminationData || {};
|
const request = terminationData || {};
|
||||||
|
const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(request.currentStage);
|
||||||
|
|
||||||
const stageAliases: Record<string, string[]> = {
|
const stageAliases: Record<string, string[]> = {
|
||||||
'Submitted': ['Submitted', 'Request Initiated'],
|
'Submitted': ['Submitted', 'Request Initiated'],
|
||||||
@ -203,6 +211,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
'ZBH Review': ['ZBH Review'],
|
'ZBH Review': ['ZBH Review'],
|
||||||
'DD Lead Review': ['DD Lead Review'],
|
'DD Lead Review': ['DD Lead Review'],
|
||||||
'Legal Verification': ['Legal Verification'],
|
'Legal Verification': ['Legal Verification'],
|
||||||
|
'DD Head Review': ['DD Head Review'],
|
||||||
'NBH Evaluation': ['NBH Evaluation'],
|
'NBH Evaluation': ['NBH Evaluation'],
|
||||||
'Show Cause Notice (SCN)': ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'],
|
'Show Cause Notice (SCN)': ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'],
|
||||||
'Personal Hearing': ['Personal Hearing'],
|
'Personal Hearing': ['Personal Hearing'],
|
||||||
@ -213,6 +222,45 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
'Dealer Terminated': ['Terminated', 'Dealer Terminated']
|
'Dealer Terminated': ['Terminated', 'Dealer Terminated']
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stageSequence = [
|
||||||
|
'Submitted',
|
||||||
|
'RBM Review',
|
||||||
|
'ZBH Review',
|
||||||
|
'DD Lead Review',
|
||||||
|
'Legal Verification',
|
||||||
|
'DD Head Review',
|
||||||
|
'NBH Evaluation',
|
||||||
|
'Show Cause Notice (SCN)',
|
||||||
|
'Personal Hearing',
|
||||||
|
'NBH Final Approval',
|
||||||
|
'CCO Approval',
|
||||||
|
'CEO Final Approval',
|
||||||
|
'Legal - Termination Letter',
|
||||||
|
'Dealer Terminated'
|
||||||
|
];
|
||||||
|
|
||||||
|
const resolveCanonicalStage = (currentStage?: string) => {
|
||||||
|
if (!currentStage) return '';
|
||||||
|
const normalized = String(currentStage).trim();
|
||||||
|
const matched = stageSequence.find((stageName) =>
|
||||||
|
(stageAliases[stageName] || [stageName]).includes(normalized)
|
||||||
|
);
|
||||||
|
return matched || normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressStatus = (stageName: string) => {
|
||||||
|
const currentCanonical = resolveCanonicalStage(request.currentStage || request.status);
|
||||||
|
const currentIndex = stageSequence.indexOf(currentCanonical);
|
||||||
|
const stageIndex = stageSequence.indexOf(stageName);
|
||||||
|
|
||||||
|
if (stageIndex === -1) return 'pending';
|
||||||
|
if (currentIndex === -1) return stageName === 'Submitted' ? 'completed' : 'pending';
|
||||||
|
if (stageName === 'Dealer Terminated' && currentIndex >= stageIndex) return 'completed';
|
||||||
|
if (stageIndex < currentIndex) return 'completed';
|
||||||
|
if (stageIndex === currentIndex) return 'active';
|
||||||
|
return 'pending';
|
||||||
|
};
|
||||||
|
|
||||||
const allUploadedDocs = [
|
const allUploadedDocs = [
|
||||||
...(request.documents || []),
|
...(request.documents || []),
|
||||||
...(request.uploadedDocuments || [])
|
...(request.uploadedDocuments || [])
|
||||||
@ -236,17 +284,26 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
|
|
||||||
const getLatestStageTimelineEntry = (stageName: string) => {
|
const getLatestStageTimelineEntry = (stageName: string) => {
|
||||||
const aliases = stageAliases[stageName] || [stageName];
|
const aliases = stageAliases[stageName] || [stageName];
|
||||||
const entries = (request.timeline || []).filter((entry: any) =>
|
const entries = (request.timeline || []).filter((entry: any) => aliases.includes(entry.stage));
|
||||||
aliases.includes(entry.stage) || aliases.includes(entry.targetStage)
|
|
||||||
);
|
if (entries.length === 0) return null;
|
||||||
return entries.length > 0 ? entries[entries.length - 1] : null;
|
|
||||||
|
// Keep submitted row anchored to initiation details, not later stage-transition remarks.
|
||||||
|
if (stageName === 'Submitted') {
|
||||||
|
const initiatedEntry = entries.find((entry: any) =>
|
||||||
|
String(entry?.action || '').toLowerCase().includes('initiated')
|
||||||
|
);
|
||||||
|
return initiatedEntry || entries[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries[entries.length - 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
const progressStages = [
|
const progressStages = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Submitted',
|
name: 'Submitted',
|
||||||
status: 'completed',
|
status: getProgressStatus('Submitted'),
|
||||||
description: 'Termination request initiated',
|
description: 'Termination request initiated',
|
||||||
date: '',
|
date: '',
|
||||||
actionType: '',
|
actionType: '',
|
||||||
@ -257,73 +314,79 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'RBM Review',
|
name: 'RBM Review',
|
||||||
status: request.currentStage === 'RBM Review' ? 'active' : ['ZBH Review', 'DD Lead Review', 'Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('RBM Review'),
|
||||||
description: 'Regional Business Manager review'
|
description: 'Regional Business Manager review'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'ZBH Review',
|
name: 'ZBH Review',
|
||||||
status: request.currentStage === 'ZBH Review' ? 'active' : ['DD Lead Review', 'Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('ZBH Review'),
|
||||||
description: 'Zonal Business Head evaluation'
|
description: 'Zonal Business Head evaluation'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
name: 'DD Lead Review',
|
name: 'DD Lead Review',
|
||||||
status: request.currentStage === 'DD Lead Review' ? 'active' : ['Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('DD Lead Review'),
|
||||||
description: 'DD Lead validation'
|
description: 'DD Lead validation'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
name: 'Legal Verification',
|
name: 'Legal Verification',
|
||||||
status: request.currentStage === 'Legal Verification' ? 'active' : ['NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('Legal Verification'),
|
||||||
description: 'Legal team validates termination grounds'
|
description: 'Legal team validates termination grounds'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
name: 'NBH Evaluation',
|
name: 'DD Head Review',
|
||||||
status: request.currentStage === 'NBH Evaluation' ? 'active' : ['Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('DD Head Review'),
|
||||||
description: 'National Business Head decision'
|
description: 'DD Head strategic review'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
name: 'Show Cause Notice (SCN)',
|
name: 'NBH Evaluation',
|
||||||
status: request.currentStage === 'Show Cause Notice' ? 'active' : ['Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('NBH Evaluation'),
|
||||||
description: 'SCN sent to dealer, awaiting response'
|
description: 'National Business Head decision'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
name: 'Personal Hearing',
|
name: 'Show Cause Notice (SCN)',
|
||||||
status: request.currentStage === 'Personal Hearing' ? 'active' : ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('Show Cause Notice (SCN)'),
|
||||||
description: 'Evaluation of SCN response & Hearing'
|
description: 'SCN sent to dealer, awaiting response'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
name: 'NBH Final Approval',
|
name: 'Personal Hearing',
|
||||||
status: request.currentStage === 'NBH Final Approval' ? 'active' : ['CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('Personal Hearing'),
|
||||||
description: 'NBH final termination decision'
|
description: 'Evaluation of SCN response & Hearing'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
name: 'CCO Approval',
|
name: 'NBH Final Approval',
|
||||||
status: request.currentStage === 'CCO Approval' ? 'active' : ['CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('NBH Final Approval'),
|
||||||
description: 'Chief Commercial Officer approval'
|
description: 'NBH final termination decision'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
name: 'CEO Final Approval',
|
name: 'CCO Approval',
|
||||||
status: request.currentStage === 'CEO Final Approval' ? 'active' : ['Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: getProgressStatus('CCO Approval'),
|
||||||
description: 'CEO final authorization'
|
description: 'Chief Commercial Officer approval'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
name: 'Legal - Termination Letter',
|
name: 'CEO Final Approval',
|
||||||
status: request.currentStage === 'Legal - Termination Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
status: getProgressStatus('CEO Final Approval'),
|
||||||
description: 'Legal team issues final termination letter'
|
description: 'CEO final authorization'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 13,
|
id: 13,
|
||||||
|
name: 'Legal - Termination Letter',
|
||||||
|
status: getProgressStatus('Legal - Termination Letter'),
|
||||||
|
description: 'Legal team issues final termination letter'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
name: 'Dealer Terminated',
|
name: 'Dealer Terminated',
|
||||||
status: request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
status: getProgressStatus('Dealer Terminated'),
|
||||||
description: 'Dealership termination effective',
|
description: 'Dealership termination effective',
|
||||||
date: '',
|
date: '',
|
||||||
actionType: '',
|
actionType: '',
|
||||||
@ -590,6 +653,10 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<Label className="text-slate-600">GST</Label>
|
<Label className="text-slate-600">GST</Label>
|
||||||
<p>{request.dealer?.gstNumber || 'N/A'}</p>
|
<p>{request.dealer?.gstNumber || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-600">Dealer Email</Label>
|
||||||
|
<p>{request.dealer?.user?.email || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Label className="text-slate-600">Address</Label>
|
<Label className="text-slate-600">Address</Label>
|
||||||
<p>{request.dealer?.registeredAddress || request.dealer?.application?.address || 'N/A'}</p>
|
<p>{request.dealer?.registeredAddress || request.dealer?.application?.address || 'N/A'}</p>
|
||||||
@ -1101,16 +1168,16 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<DialogContent className="bg-white">
|
<DialogContent className="bg-white">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{request.currentStage === 'SCN' ? 'Upload SCN Response' : 'Issue Show Cause Notice (SCN)'}
|
{isScnStage ? 'Upload SCN Response' : 'Issue Show Cause Notice (SCN)'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{request.currentStage === 'SCN'
|
{isScnStage
|
||||||
? 'Upload the response received from the dealer regarding the SCN.'
|
? 'Upload the response received from the dealer regarding the SCN.'
|
||||||
: 'Confirm the issuance of a formal Show Cause Notice to the dealer.'}
|
: 'Confirm the issuance of a formal Show Cause Notice to the dealer.'}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 pt-4">
|
<div className="space-y-4 pt-4">
|
||||||
{request.currentStage === 'SCN' && (
|
{isScnStage && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>SCN Response File</Label>
|
<Label>SCN Response File</Label>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
@ -1144,11 +1211,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={request.currentStage === 'SCN' ? 'bg-amber-600 hover:bg-amber-700' : 'bg-purple-600 hover:bg-purple-700'}
|
className={isScnStage ? 'bg-amber-600 hover:bg-amber-700' : 'bg-purple-600 hover:bg-purple-700'}
|
||||||
onClick={request.currentStage === 'SCN' ? handleUploadSCNResponse : handleIssueSCN}
|
onClick={isScnStage ? handleUploadSCNResponse : handleIssueSCN}
|
||||||
disabled={isProcessing || (request.currentStage === 'SCN' && !scnFile)}
|
disabled={isProcessing || (isScnStage && !scnFile)}
|
||||||
>
|
>
|
||||||
{isProcessing ? 'Processing...' : request.currentStage === 'SCN' ? 'Upload Response' : 'Issue SCN'}
|
{isProcessing ? 'Processing...' : isScnStage ? 'Upload Response' : 'Issue SCN'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -86,10 +86,15 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
setDialogDataLoading(true);
|
setDialogDataLoading(true);
|
||||||
const response = await API.getDealers({ onboarded: 'true' });
|
const response = await API.getDealers({ onboarded: 'true', activeOnly: 'true' });
|
||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
if (!cancelled && data?.success) {
|
if (!cancelled && data?.success) {
|
||||||
setDealers(Array.isArray(data.data) ? data.data : []);
|
const activeDealers = (Array.isArray(data.data) ? data.data : []).filter((dealer: any) => {
|
||||||
|
const dealerStatus = String(dealer?.status || '').toLowerCase();
|
||||||
|
const userStatus = String(dealer?.user?.status || '').toLowerCase();
|
||||||
|
return dealerStatus === 'active' && dealer?.user?.isActive && userStatus === 'active';
|
||||||
|
});
|
||||||
|
setDealers(activeDealers);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
@ -211,21 +216,23 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
// Helper function to check if request is at current user's level
|
// Helper function to check if request is at current user's level
|
||||||
const isRequestAtMyLevel = (request: any) => {
|
const isRequestAtMyLevel = (request: any) => {
|
||||||
if (!currentUser) return false;
|
if (!currentUser) return false;
|
||||||
|
const userRole = currentUser.role || currentUser.roleCode;
|
||||||
|
|
||||||
const roleToStageMapping: Record<string, string[]> = {
|
const roleToStageMapping: Record<string, string[]> = {
|
||||||
'DD Lead': ['DD Lead Review'],
|
|
||||||
'RBM': ['RBM Review'],
|
'RBM': ['RBM Review'],
|
||||||
'ZBH': ['ZBH Review'],
|
'ZBH': ['ZBH Review'],
|
||||||
|
'DD Lead': ['DD Lead Review'],
|
||||||
|
'DD Head': ['DD Head Review'],
|
||||||
'NBH': ['NBH Evaluation', 'NBH Final Approval'],
|
'NBH': ['NBH Evaluation', 'NBH Final Approval'],
|
||||||
'Legal Admin': ['Legal Verification', 'Legal - Termination Letter'],
|
'Legal Admin': ['Legal Verification', 'Legal - Termination Letter'],
|
||||||
'Legal': ['Legal Verification'],
|
'Legal': ['Legal Verification'],
|
||||||
'DD Admin': ['Show Cause Notice', 'Terminated'],
|
'DD Admin': ['Show Cause Notice', 'Terminated'],
|
||||||
'CCO': ['CCO Approval'],
|
'CCO': ['CCO Approval'],
|
||||||
'CEO': ['CEO Final Approval'],
|
'CEO': ['CEO Final Approval'],
|
||||||
'Super Admin': ['DD Lead Review', 'RBM Review', 'ZBH Review', 'NBH Evaluation', 'Legal Verification', 'Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated']
|
'Super Admin': ['RBM Review', 'ZBH Review', 'DD Lead Review', 'DD Head Review', 'NBH Evaluation', 'Legal Verification', 'Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated']
|
||||||
};
|
};
|
||||||
|
|
||||||
const userStages = roleToStageMapping[currentUser.role] || [];
|
const userStages = roleToStageMapping[userRole] || [];
|
||||||
return userStages.some(stage =>
|
return userStages.some(stage =>
|
||||||
(request.currentStage && request.currentStage.includes(stage)) ||
|
(request.currentStage && request.currentStage.includes(stage)) ||
|
||||||
(request.status && request.status.includes(stage))
|
(request.status && request.status.includes(stage))
|
||||||
|
|||||||
@ -17,6 +17,9 @@ export const RoleGuard: React.FC<RoleGuardProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { user, isAuthenticated, loading } = useSelector((state: RootState) => state.auth);
|
const { user, isAuthenticated, loading } = useSelector((state: RootState) => state.auth);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const normalizedRole = String((user as any)?.role || (user as any)?.roleCode || '').trim().toLowerCase();
|
||||||
|
const normalizedAllowedRoles = (allowedRoles || []).map((r) => String(r).trim().toLowerCase());
|
||||||
|
const normalizedExcludedRoles = (excludeRoles || []).map((r) => String(r).trim().toLowerCase());
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="flex h-screen items-center justify-center">Loading...</div>;
|
return <div className="flex h-screen items-center justify-center">Loading...</div>;
|
||||||
@ -27,16 +30,16 @@ export const RoleGuard: React.FC<RoleGuardProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check excluded roles first (e.g. Block Prospective Dealer from main dashboard)
|
// Check excluded roles first (e.g. Block Prospective Dealer from main dashboard)
|
||||||
if (excludeRoles && user && excludeRoles.includes(user.role)) {
|
if (excludeRoles && user && normalizedExcludedRoles.includes(normalizedRole)) {
|
||||||
// If prospective dealer is excluded, redirect to their dashboard
|
// If prospective dealer is excluded, redirect to their dashboard
|
||||||
if (user.role === 'Prospective Dealer') {
|
if (normalizedRole === 'prospective dealer') {
|
||||||
return <Navigate to="/prospective-dashboard" replace />;
|
return <Navigate to="/prospective-dashboard" replace />;
|
||||||
}
|
}
|
||||||
return <Navigate to={redirectTo || '/dashboard'} replace />;
|
return <Navigate to={redirectTo || '/dashboard'} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check allowed roles (e.g. Only Prospective Dealer can see their dashboard)
|
// Check allowed roles (e.g. Only Prospective Dealer can see their dashboard)
|
||||||
if (allowedRoles && user && !allowedRoles.includes(user.role)) {
|
if (allowedRoles && user && !normalizedAllowedRoles.includes(normalizedRole)) {
|
||||||
// If regular dealer tries to access prospective dashboard
|
// If regular dealer tries to access prospective dashboard
|
||||||
return <Navigate to={redirectTo || '/dashboard'} replace />;
|
return <Navigate to={redirectTo || '/dashboard'} replace />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,24 +61,25 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
'Payment Pending', 'Security Details', 'LOI In Progress', 'LOI Issued',
|
'Payment Pending', 'Security Details', 'LOI In Progress', 'LOI Issued',
|
||||||
'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL'
|
'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL'
|
||||||
].includes(s);
|
].includes(s);
|
||||||
|
const deposits = app.securityDeposits || [];
|
||||||
|
|
||||||
if (isPaymentStage) {
|
// Always include real payment rows, even if the app has moved beyond payment stages.
|
||||||
const deposits = app.securityDeposits || [];
|
if (deposits.length > 0) {
|
||||||
if (deposits.length > 0) {
|
deposits.forEach((d: any) => {
|
||||||
deposits.forEach((d: any) => {
|
consolidatedPayments.push({
|
||||||
consolidatedPayments.push({
|
...d,
|
||||||
...d,
|
application: app,
|
||||||
application: app,
|
paymentStatus: d.status,
|
||||||
paymentStatus: d.status,
|
paymentType: d.depositType,
|
||||||
paymentType: d.depositType,
|
amount: d.amount,
|
||||||
amount: d.amount,
|
id: d.id,
|
||||||
id: d.id,
|
applicationId: app.applicationId || app.id,
|
||||||
applicationId: app.applicationId || app.id,
|
createdAt: d.createdAt,
|
||||||
createdAt: d.createdAt,
|
verificationDate: d.verifiedAt
|
||||||
verificationDate: d.verifiedAt
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else if (['Payment Pending', 'Security Details', 'LOI In Progress'].includes(s)) {
|
});
|
||||||
|
} else if (isPaymentStage) {
|
||||||
|
if (['Payment Pending', 'Security Details', 'LOI In Progress'].includes(s)) {
|
||||||
// Virtual pending record for Security Deposit (5L)
|
// Virtual pending record for Security Deposit (5L)
|
||||||
consolidatedPayments.push({
|
consolidatedPayments.push({
|
||||||
id: `virtual-${app.id}-sd`,
|
id: `virtual-${app.id}-sd`,
|
||||||
@ -175,8 +176,12 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pendingOnboarding = onboardingPayments.filter(p => p.paymentStatus !== 'Paid' && p.paymentStatus !== 'Verified');
|
const isVerifiedLikeStatus = (status: any) => {
|
||||||
const verifiedOnboarding = onboardingPayments.filter(p => p.paymentStatus === 'Paid' || p.paymentStatus === 'Verified');
|
const normalized = String(status || '').trim().toLowerCase();
|
||||||
|
return normalized === 'paid' || normalized === 'verified';
|
||||||
|
};
|
||||||
|
const pendingOnboarding = onboardingPayments.filter(p => !isVerifiedLikeStatus(p.paymentStatus));
|
||||||
|
const verifiedOnboarding = onboardingPayments.filter(p => isVerifiedLikeStatus(p.paymentStatus));
|
||||||
const pendingFnF = fnfSettlements.filter(f => f.status === 'Initiated' || f.status === 'Calculated');
|
const pendingFnF = fnfSettlements.filter(f => f.status === 'Initiated' || f.status === 'Calculated');
|
||||||
const completedFnF = fnfSettlements.filter(f => f.status === 'Completed' || f.status === 'Cleared');
|
const completedFnF = fnfSettlements.filter(f => f.status === 'Completed' || f.status === 'Cleared');
|
||||||
|
|
||||||
|
|||||||
384
src/components/dealer/DealerResignationDetailsPage.tsx
Normal file
384
src/components/dealer/DealerResignationDetailsPage.tsx
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
import { ArrowLeft, Calendar, CheckCircle2, Clock, FileText, MapPin, MessageSquare, Upload, User } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
|
import { Badge } from '../ui/badge';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||||
|
import { Label } from '../ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
|
import { resignationService } from '../../services/resignation.service';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
import { RESIGNATION_STAGE_OPTIONS, RESIGNATION_DOCUMENT_TYPES } from '../../lib/offboardingDocumentOptions';
|
||||||
|
|
||||||
|
interface DealerResignationDetailsPageProps {
|
||||||
|
resignationId: string;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
|
||||||
|
if (status === 'Rejected') return 'bg-red-100 text-red-700 border-red-300';
|
||||||
|
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
||||||
|
return 'bg-slate-100 text-slate-700 border-slate-300';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DealerResignationDetailsPage({ resignationId, onBack }: DealerResignationDetailsPageProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [details, setDetails] = useState<any>(null);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||||
|
const [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]);
|
||||||
|
const [uploadStage, setUploadStage] = useState<string>('');
|
||||||
|
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDetails = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [data, audits] = await Promise.all([
|
||||||
|
resignationService.getResignationById(resignationId),
|
||||||
|
fetchAuditLogs(resignationId)
|
||||||
|
]);
|
||||||
|
setDetails(data);
|
||||||
|
setAuditLogs(audits);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch resignation details:', error);
|
||||||
|
toast.error('Unable to load resignation details');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resignationId) {
|
||||||
|
fetchDetails();
|
||||||
|
}
|
||||||
|
}, [resignationId]);
|
||||||
|
|
||||||
|
const fetchAuditLogs = async (id: string) => {
|
||||||
|
try {
|
||||||
|
// Lazy import through existing API helper shape used in other modules.
|
||||||
|
const { API } = await import('../../api/API');
|
||||||
|
const response = await API.getAuditLogs('resignation', id) as any;
|
||||||
|
if (response?.data?.success) return response.data.data || [];
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDetails = async () => {
|
||||||
|
try {
|
||||||
|
const [data, audits] = await Promise.all([
|
||||||
|
resignationService.getResignationById(resignationId),
|
||||||
|
fetchAuditLogs(resignationId)
|
||||||
|
]);
|
||||||
|
setDetails(data);
|
||||||
|
setAuditLogs(audits);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Unable to refresh resignation details');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (!uploadFile) {
|
||||||
|
toast.error('Please choose a file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setUploading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', uploadFile);
|
||||||
|
formData.append('documentType', uploadDocType);
|
||||||
|
if (uploadStage) formData.append('stage', uploadStage);
|
||||||
|
await resignationService.uploadDocument(resignationId, formData);
|
||||||
|
toast.success('Document uploaded successfully');
|
||||||
|
setUploadFile(null);
|
||||||
|
await refreshDetails();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error?.response?.data?.message || 'Document upload failed');
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[320px] flex items-center justify-center">
|
||||||
|
<Clock className="w-8 h-8 animate-spin text-amber-600" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Button variant="outline" onClick={onBack}>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-8 text-center text-slate-600">
|
||||||
|
Resignation details not found.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = details.uploadedDocuments || [];
|
||||||
|
const timeline = Array.isArray(details.timeline) ? details.timeline : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button variant="outline" onClick={onBack}>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-slate-900">Resignation Request Details</h1>
|
||||||
|
<p className="text-slate-600 text-sm">
|
||||||
|
Track your request progress and uploaded documents
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-amber-600" />
|
||||||
|
Request Summary
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Current request status and key metadata</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Request ID</p>
|
||||||
|
<p className="text-slate-900">{details.resignationId || details.id}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Status</p>
|
||||||
|
<Badge className={`border ${getStatusColor(details.status || 'Pending')}`}>
|
||||||
|
{details.status || 'Pending'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Current Stage</p>
|
||||||
|
<p className="text-slate-900">{details.currentStage || 'Submitted'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Submitted On</p>
|
||||||
|
<p className="text-slate-900">{formatDateTime(details.submittedOn || details.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Resignation Type</p>
|
||||||
|
<p className="text-slate-900">{details.resignationType || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Progress</p>
|
||||||
|
<p className="text-slate-900">{details.progressPercentage || 0}%</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<User className="w-5 h-5 text-blue-600" />
|
||||||
|
Outlet and Dates
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Outlet</p>
|
||||||
|
<p className="text-slate-900">{details.outlet?.name || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Outlet Code</p>
|
||||||
|
<p className="text-slate-900">{details.outlet?.code || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Last Operational Date (Sales)</p>
|
||||||
|
<p className="text-slate-900">{details.lastOperationalDateSales ? formatDateTime(details.lastOperationalDateSales) : 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Last Operational Date (Services)</p>
|
||||||
|
<p className="text-slate-900">{details.lastOperationalDateServices ? formatDateTime(details.lastOperationalDateServices) : 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<p className="text-xs text-slate-500">Reason</p>
|
||||||
|
<p className="text-slate-900">{details.reason || '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<p className="text-xs text-slate-500 flex items-center gap-1">
|
||||||
|
<MapPin className="w-3 h-3" />
|
||||||
|
Outlet Address
|
||||||
|
</p>
|
||||||
|
<p className="text-slate-900">{details.outlet?.address || '-'}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-purple-600" />
|
||||||
|
Uploaded Documents
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Dealer can upload resignation-related documents for review</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4 p-3 border rounded-lg bg-slate-50">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Document Type</Label>
|
||||||
|
<Select value={uploadDocType} onValueChange={setUploadDocType}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{RESIGNATION_DOCUMENT_TYPES.map((docType) => (
|
||||||
|
<SelectItem key={docType} value={docType}>{docType}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Stage (Optional)</Label>
|
||||||
|
<Select value={uploadStage || 'none'} onValueChange={(v) => setUploadStage(v === 'none' ? '' : v)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select stage" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">No Stage</SelectItem>
|
||||||
|
{RESIGNATION_STAGE_OPTIONS.map((stage) => (
|
||||||
|
<SelectItem key={stage} value={stage}>{stage}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">File</Label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
|
||||||
|
onChange={(e) => setUploadFile(e.target.files?.[0] || null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button className="w-full" onClick={handleUpload} disabled={uploading}>
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
{uploading ? 'Uploading...' : 'Upload'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Document Type</TableHead>
|
||||||
|
<TableHead>File</TableHead>
|
||||||
|
<TableHead>Uploaded By</TableHead>
|
||||||
|
<TableHead>Uploaded On</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{docs.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center text-slate-500 py-6">
|
||||||
|
No documents uploaded yet.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
docs.map((doc: any) => (
|
||||||
|
<TableRow key={doc.id}>
|
||||||
|
<TableCell>{doc.documentType || '-'}</TableCell>
|
||||||
|
<TableCell>{doc.fileName || '-'}</TableCell>
|
||||||
|
<TableCell>{doc.uploader?.fullName || '-'}</TableCell>
|
||||||
|
<TableCell>{formatDateTime(doc.createdAt)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<MessageSquare className="w-5 h-5 text-blue-600" />
|
||||||
|
Work Notes Communication
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Official channel for internal-dealer clarifications</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/worknotes/resignation/${resignationId}`, {
|
||||||
|
state: {
|
||||||
|
applicationName: details?.dealer?.fullName || 'Resignation Request',
|
||||||
|
registrationNumber: details?.resignationId || resignationId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
|
Open Work Notes
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Calendar className="w-5 h-5 text-amber-600" />
|
||||||
|
Progress Timeline
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{timeline.length === 0 ? (
|
||||||
|
<p className="text-sm text-slate-500">No timeline events available yet.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{timeline.slice().reverse().map((entry: any, idx: number) => (
|
||||||
|
<div key={`${entry.timestamp || entry.createdAt}-${idx}`} className="p-3 border rounded-lg bg-slate-50">
|
||||||
|
<p className="text-sm text-slate-900">{entry.action || entry.stage || 'Stage Update'}</p>
|
||||||
|
<p className="text-xs text-slate-500">{formatDateTime(entry.timestamp || entry.createdAt)}</p>
|
||||||
|
<p className="text-xs text-slate-600">{entry.comments || entry.remarks || 'No remarks'}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Audit Trail</CardTitle>
|
||||||
|
<CardDescription>Traceability of status/actions on this request</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{auditLogs.length === 0 ? (
|
||||||
|
<p className="text-sm text-slate-500">No audit records found.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{auditLogs.map((log: any) => (
|
||||||
|
<div key={log.id} className="p-3 border rounded-lg">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm text-slate-900">{log.action || 'Action'}</p>
|
||||||
|
<p className="text-xs text-slate-500">{formatDateTime(log.createdAt || log.timestamp)}</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-600 mt-1">{log.remarks || log.description || 'No remarks'}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -36,14 +36,16 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [offboardingExpanded, setOffboardingExpanded] = useState(false);
|
const [offboardingExpanded, setOffboardingExpanded] = useState(false);
|
||||||
const [allRequestsExpanded, setAllRequestsExpanded] = useState(false);
|
const [allRequestsExpanded, setAllRequestsExpanded] = useState(false);
|
||||||
const currentRole = currentUser?.role || '';
|
const currentRole = currentUser?.role || currentUser?.roleCode || '';
|
||||||
|
const normalizedRole = String(currentRole).trim().toLowerCase();
|
||||||
|
const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole);
|
||||||
|
|
||||||
const resignationRoles = ['DD Admin', 'ASM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Super Admin'];
|
const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin'];
|
||||||
const terminationRoles = ['ASM', 'DD Lead', 'DD Admin', 'Super Admin'];
|
const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin'];
|
||||||
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin'];
|
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin'];
|
||||||
const canSeeResignation = resignationRoles.includes(currentRole);
|
const canSeeResignation = hasRole(resignationRoles);
|
||||||
const canSeeTermination = terminationRoles.includes(currentRole);
|
const canSeeTermination = hasRole(terminationRoles);
|
||||||
const canSeeFnF = fnfRoles.includes(currentRole);
|
const canSeeFnF = hasRole(fnfRoles);
|
||||||
const offboardingSubmenu = [
|
const offboardingSubmenu = [
|
||||||
canSeeResignation ? { id: 'resignation', label: 'Resignation' } : null,
|
canSeeResignation ? { id: 'resignation', label: 'Resignation' } : null,
|
||||||
canSeeTermination ? { id: 'termination', label: 'Termination' } : null,
|
canSeeTermination ? { id: 'termination', label: 'Termination' } : null,
|
||||||
@ -51,16 +53,16 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
].filter(Boolean) as { id: string; label: string }[];
|
].filter(Boolean) as { id: string; label: string }[];
|
||||||
|
|
||||||
// Finance role has only specific menu items
|
// Finance role has only specific menu items
|
||||||
const menuItems = currentRole === 'Finance' || currentRole === 'Finance Admin' ? [
|
const menuItems = hasRole(['Finance', 'Finance Admin']) ? [
|
||||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||||
{ id: 'finance-onboarding', label: 'Onboarding', icon: FileText },
|
{ id: 'finance-onboarding', label: 'Onboarding', icon: FileText },
|
||||||
{ id: 'finance-fnf', label: 'F&F', icon: UserMinus },
|
{ id: 'finance-fnf', label: 'F&F', icon: UserMinus },
|
||||||
] : currentRole === 'Dealer' ? [
|
] : hasRole(['Dealer']) ? [
|
||||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||||
{ id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus },
|
{ id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus },
|
||||||
{ id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw },
|
{ id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw },
|
||||||
{ id: 'dealer-relocation', label: 'Relocation Requests', icon: MapPin },
|
{ id: 'dealer-relocation', label: 'Relocation Requests', icon: MapPin },
|
||||||
] : currentRole === 'FDD' ? [
|
] : hasRole(['FDD']) ? [
|
||||||
{ id: 'fdd-dashboard', label: 'FDD Dashboard', icon: LayoutDashboard },
|
{ id: 'fdd-dashboard', label: 'FDD Dashboard', icon: LayoutDashboard },
|
||||||
] : [
|
] : [
|
||||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||||
@ -78,12 +80,12 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Add All Applications for DD role (before Dealership Requests)
|
// Add All Applications for DD role (before Dealership Requests)
|
||||||
if (currentRole === 'DD') {
|
if (hasRole(['DD'])) {
|
||||||
menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox });
|
menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add All Requests for DD Lead role (before Dealership Requests)
|
// Add All Requests for DD Lead role (before Dealership Requests)
|
||||||
if (currentRole === 'DD Lead' || currentRole === 'Super Admin') {
|
if (hasRole(['DD Lead', 'Super Admin'])) {
|
||||||
menuItems.splice(1, 0, {
|
menuItems.splice(1, 0, {
|
||||||
id: 'all-requests',
|
id: 'all-requests',
|
||||||
label: 'All Requests',
|
label: 'All Requests',
|
||||||
@ -98,11 +100,11 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add Master for Super Admin, DD Admin, and DD Lead
|
// Add Master for Super Admin, DD Admin, and DD Lead
|
||||||
if (currentRole === 'Super Admin' || currentRole === 'DD Admin' || currentRole === 'DD Lead') {
|
if (hasRole(['Super Admin', 'DD Admin', 'DD Lead'])) {
|
||||||
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
|
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentRole === 'Super Admin') {
|
if (hasRole(['Super Admin'])) {
|
||||||
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
||||||
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -220,55 +220,106 @@ export interface QuestionnaireResponse {
|
|||||||
|
|
||||||
// Mock test users for different roles
|
// Mock test users for different roles
|
||||||
export const mockUsers: User[] = [
|
export const mockUsers: User[] = [
|
||||||
{
|
{
|
||||||
id: '5',
|
id: '15',
|
||||||
name: 'Meera Iyer',
|
name: 'Super Admin',
|
||||||
email: 'ddlead@royalenfield.com',
|
email: 'admin@royalenfield.com',
|
||||||
password: 'Admin@123',
|
password: 'Admin@123',
|
||||||
role: 'DD Lead',
|
role: 'Super Admin',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '13',
|
id: '13',
|
||||||
name: 'Rahul Verma',
|
name: 'piyush',
|
||||||
|
email: 'piyush@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'DD-ZM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '14',
|
||||||
|
name: 'manish',
|
||||||
|
email: 'manish@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'RBM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '14',
|
||||||
|
name: 'manav',
|
||||||
|
email: 'manav@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'ZBH',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: 'Jaya',
|
||||||
|
email: 'jaya@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'DD Lead',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '14',
|
||||||
|
name: 'ganesh',
|
||||||
|
email: 'ganesh@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'DD Head',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '16',
|
||||||
|
name: 'Yashwin',
|
||||||
|
email: 'yashwin@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'NBH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '15',
|
||||||
|
name: 'FDD Team',
|
||||||
|
email: 'fdd@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'FDD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '13',
|
||||||
|
name: 'Finance Admin',
|
||||||
email: 'finance@royalenfield.com',
|
email: 'finance@royalenfield.com',
|
||||||
password: 'Admin@123',
|
password: 'Admin@123',
|
||||||
role: 'Finance',
|
role: 'Finance',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '14',
|
id: '13',
|
||||||
name: 'Amit Sharma',
|
name: 'abhishek',
|
||||||
email: 'dealer@royalenfield.com',
|
email: 'abhishek@royalenfield.com',
|
||||||
password: 'Admin@123',
|
password: 'Admin@123',
|
||||||
role: 'Dealer',
|
role: 'ASM',
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '15',
|
|
||||||
name: 'Laxman H',
|
|
||||||
email: 'admin@royalenfield.com',
|
|
||||||
password: 'Admin@123',
|
|
||||||
role: 'DD Lead',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '16',
|
|
||||||
name: 'Yashwin',
|
|
||||||
email: 'yashwin@gmail.com',
|
|
||||||
password: 'Admin@123',
|
|
||||||
role: 'ZBH',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '17',
|
|
||||||
name: 'Kenil',
|
|
||||||
email: 'kenil@gmail.com',
|
|
||||||
password: 'Admin@123',
|
|
||||||
role: 'DD Lead',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '18',
|
id: '18',
|
||||||
name: 'Lince',
|
name: 'Lince',
|
||||||
email: 'lince@gmail.com',
|
email: 'lince@royalenfield.com',
|
||||||
password: 'Admin@123',
|
password: 'Admin@123',
|
||||||
role: 'DD Admin',
|
role: 'DD Admin',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '18',
|
||||||
|
name: 'Legal Admin',
|
||||||
|
email: 'legal@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'Legal Admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '18',
|
||||||
|
name: 'CEO',
|
||||||
|
email: 'ceo@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'CEO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '18',
|
||||||
|
name: 'CCO',
|
||||||
|
email: 'cco@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'CCO',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Mock current user (default)
|
// Mock current user (default)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user