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 { RelocationRequestDetails } from './components/applications/RelocationRequestDetails';
|
||||
import { DealerResignationPage } from './components/dealer/DealerResignationPage';
|
||||
import { DealerResignationDetailsPage } from './components/dealer/DealerResignationDetailsPage';
|
||||
import { DealerConstitutionalChangePage } from './components/dealer/DealerConstitutionalChangePage';
|
||||
import { DealerRelocationPage } from './components/dealer/DealerRelocationPage';
|
||||
import QuestionnaireBuilder from './components/admin/QuestionnaireBuilder';
|
||||
@ -72,9 +73,11 @@ export default function App() {
|
||||
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const currentRole = currentUser?.role || '';
|
||||
const resignationRoles = ['DD Admin', 'ASM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Super Admin'];
|
||||
const terminationRoles = ['ASM', 'DD Lead', 'DD Admin', 'Super Admin'];
|
||||
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', '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 financeRoles = ['Finance', 'Finance Admin'];
|
||||
|
||||
@ -210,9 +213,9 @@ export default function App() {
|
||||
|
||||
{/* Dashboards */}
|
||||
<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}`)} /> :
|
||||
currentRole === 'Dealer' ?
|
||||
hasRole(['Dealer']) ?
|
||||
<DealerDashboard currentUser={currentUser} onNavigate={(path) => navigate(`/${path}`)} /> :
|
||||
<Dashboard onNavigate={(path) => navigate(`/${path}`)} />
|
||||
} />
|
||||
@ -229,7 +232,7 @@ export default function App() {
|
||||
} />
|
||||
|
||||
<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 */}
|
||||
@ -243,7 +246,7 @@ export default function App() {
|
||||
{/* Other Modules */}
|
||||
<Route path="/users" element={<UserManagementPage />} />
|
||||
<Route path="/approval-policies" element={
|
||||
(currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin')
|
||||
(hasRole(['Super Admin', 'DD Admin']))
|
||||
? <ApprovalPoliciesPage />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
@ -255,57 +258,57 @@ export default function App() {
|
||||
|
||||
{/* HR/Finance Modules (Simplified for brevity, following pattern) */}
|
||||
<Route path="/resignation" element={
|
||||
resignationRoles.includes(currentRole)
|
||||
hasRole(resignationRoles)
|
||||
? <ResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
<Route path="/resignation/:id" element={
|
||||
resignationRoles.includes(currentRole)
|
||||
hasRole(resignationRoles)
|
||||
? <ResignationDetails resignationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/resignation')} currentUser={currentUser} />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
|
||||
<Route path="/termination" element={
|
||||
terminationRoles.includes(currentRole)
|
||||
hasRole(terminationRoles)
|
||||
? <TerminationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/termination/${id}`)} />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
<Route path="/termination/:id" element={
|
||||
terminationRoles.includes(currentRole)
|
||||
hasRole(terminationRoles)
|
||||
? <TerminationDetails terminationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/termination')} currentUser={currentUser} />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
|
||||
<Route path="/fnf" element={
|
||||
fnfRoles.includes(currentRole)
|
||||
hasRole(fnfRoles)
|
||||
? <FnFPage currentUser={currentUser} onViewDetails={(id) => navigate(`/fnf/${id}`)} />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
<Route path="/fnf/:id" element={
|
||||
fnfRoles.includes(currentRole)
|
||||
hasRole(fnfRoles)
|
||||
? <FnFDetails fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/fnf')} currentUser={currentUser} />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
|
||||
<Route path="/finance-onboarding" element={
|
||||
financeRoles.includes(currentRole)
|
||||
hasRole(financeRoles)
|
||||
? <FinanceOnboardingPage onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
<Route path="/finance-onboarding/:id" element={
|
||||
financeRoles.includes(currentRole)
|
||||
hasRole(financeRoles)
|
||||
? <FinancePaymentDetailsPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
<Route path="/finance-audit/:id" element={<ApplicationDetails />} />
|
||||
|
||||
<Route path="/finance-fnf" element={
|
||||
financeRoles.includes(currentRole)
|
||||
hasRole(financeRoles)
|
||||
? <FinanceFnFPage onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
<Route path="/finance-fnf/:id" element={
|
||||
financeRoles.includes(currentRole)
|
||||
hasRole(financeRoles)
|
||||
? <FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />
|
||||
: <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} />} />
|
||||
|
||||
{/* 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-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}`),
|
||||
|
||||
// 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),
|
||||
getDealerById: (id: string) => client.get(`/dealer/${id}`),
|
||||
updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data),
|
||||
|
||||
@ -34,6 +34,12 @@ const workflowStages = [
|
||||
{ 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)
|
||||
const documentRequirements: Record<string, number[]> = {
|
||||
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
|
||||
@ -238,6 +244,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
'Submitted': 1,
|
||||
'ASM Review': 2,
|
||||
'ZM/RBM Review': 3,
|
||||
'ZM+RBM Review': 3,
|
||||
'ZBH Review': 4,
|
||||
'DD Lead Review': 5,
|
||||
'DD Head Review': 6,
|
||||
@ -263,7 +270,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
const aliases: Record<string, string[]> = {
|
||||
'Submitted': ['Submitted', 'Draft'],
|
||||
'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'],
|
||||
'DD Lead Review': ['DD Lead Review', 'Lead 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)
|
||||
const getConstitutionalPermissions = () => {
|
||||
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 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 || '')) ||
|
||||
['Rejected', 'Revoked', 'Completed'].includes(String(currentStage || ''));
|
||||
@ -306,7 +314,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
(!atSubmittedDbStage &&
|
||||
!!(
|
||||
(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 === 'DD Lead' && userRole === 'DD Lead') ||
|
||||
(stageDef?.role === 'DD Head' && userRole === 'DD Head') ||
|
||||
@ -323,12 +331,33 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
currentStage !== 'Legal Review' &&
|
||||
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 {
|
||||
canApprove: isCurrentlyAssigned && !isFinalState,
|
||||
canReject: isCurrentlyAssigned && !isFinalState,
|
||||
canApprove: isCurrentlyAssigned && !isFinalState && !hasCurrentUserApprovedZmRbm,
|
||||
canReject: isCurrentlyAssigned && !isFinalState && !hasCurrentUserApprovedZmRbm,
|
||||
canSendBack: canSendBackOrRevoke,
|
||||
canRevoke: canSendBackOrRevoke,
|
||||
isFinalState
|
||||
isFinalState,
|
||||
hasCurrentUserApprovedZmRbm
|
||||
};
|
||||
};
|
||||
|
||||
@ -384,7 +413,8 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
}
|
||||
} catch (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 {
|
||||
setIsActionLoading(false);
|
||||
}
|
||||
@ -406,7 +436,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
fileName: uploadFile.name,
|
||||
status: 'Pending Verification',
|
||||
uploadedOn: new Date().toISOString(),
|
||||
uploadedBy: currentUser?.fullName || 'Dealer'
|
||||
uploadedBy: currentUser?.name || 'Dealer'
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) existingDocs[existingIndex] = { ...existingDocs[existingIndex], ...payloadDoc };
|
||||
@ -437,7 +467,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
const isTargetByIndex = index === targetIndex;
|
||||
const isTargetByDocNumber = targetDoc.docNumber && doc.docNumber === targetDoc.docNumber;
|
||||
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;
|
||||
@ -636,7 +666,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
<ul className="list-disc space-y-1 pl-5 text-sm text-slate-600">
|
||||
{workflowStages.map((stage) => (
|
||||
<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>
|
||||
))}
|
||||
</ul>
|
||||
@ -656,6 +686,17 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
(atSubmittedGate ? index === 1 : index === currentStageIndex - 1);
|
||||
const timelineEntry = getLatestStageTimelineEntry(stage.name);
|
||||
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 (
|
||||
<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>
|
||||
<h4 className={`${isCurrent ? 'text-amber-900' : 'text-slate-900'}`}>
|
||||
{stage.name}
|
||||
{formatStageLabel(stage.name)}
|
||||
</h4>
|
||||
<p className={`text-sm ${isCurrent ? 'text-amber-700' : 'text-slate-600'}`}>
|
||||
{atSubmittedGate && index === 0
|
||||
? 'Dealer action: filing complete (no further step here).'
|
||||
: `Responsible: ${stage.role}`}
|
||||
: `Responsible: ${formatStageRole(stage.role)}`}
|
||||
{atSubmittedGate && index === 1 ? (
|
||||
<span className="block mt-0.5 text-amber-800/90">
|
||||
ASM approves to advance the request (first workflow action after submission).
|
||||
@ -722,6 +763,36 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
@ -771,7 +842,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
<option value="">Select document type</option>
|
||||
{requiredDocs.map((docNum) => (
|
||||
<option key={docNum} value={String(docNum)}>
|
||||
{isDocTypeUploaded(docNum) ? '✅ ' : ''}
|
||||
{isDocTypeUploaded(docNum) ? '✓ ' : ''}
|
||||
{documentNames[docNum]}
|
||||
</option>
|
||||
))}
|
||||
@ -1018,6 +1089,17 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
</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 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
|
||||
@ -62,6 +62,9 @@ const ALL_DEPARTMENTS = [
|
||||
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
|
||||
];
|
||||
|
||||
const DEPARTMENT_CLAIM_PREFIX = '[DEPARTMENT_CLAIM]';
|
||||
const FINANCE_VALIDATED_PREFIX = '[FINANCE_VALIDATED]';
|
||||
|
||||
interface FinanceFnFDetailsPageProps {
|
||||
fnfId: string;
|
||||
onBack: () => void;
|
||||
@ -88,6 +91,7 @@ interface FinancialLineItem {
|
||||
export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPageProps) {
|
||||
const [fnfCase, setFnfCase] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
|
||||
// Initialize editable line items
|
||||
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]);
|
||||
@ -167,9 +171,24 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
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 {
|
||||
setLoading(true);
|
||||
if (showLoader) setLoading(true);
|
||||
const response = await API.getFnFSettlementById(fnfId);
|
||||
const data = response.data as any;
|
||||
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',
|
||||
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',
|
||||
allLineItems: (s.lineItems || []).filter((li: any) => li.isActive !== false),
|
||||
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
|
||||
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
|
||||
let deptPayables = 0;
|
||||
@ -205,15 +228,24 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
});
|
||||
|
||||
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 {
|
||||
id: c?.id || `dept-${deptName}`,
|
||||
departmentName: deptName,
|
||||
status: c?.status || 'Pending',
|
||||
status: normalizedStatus,
|
||||
remarks: c?.remarks || '-',
|
||||
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : '-',
|
||||
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
|
||||
};
|
||||
}),
|
||||
@ -246,11 +278,17 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
const rItems: 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 = {
|
||||
id: li.id,
|
||||
department: normalizeDepartment(li.department),
|
||||
description: li.description || li.remarks || '',
|
||||
description: cleanLineItemDescription(li.description || li.remarks || ''),
|
||||
amount: Math.abs(li.amount)
|
||||
};
|
||||
|
||||
@ -282,7 +320,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
console.error('Fetch F&F error:', error);
|
||||
toast.error('Failed to fetch settlement details');
|
||||
} 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 [editingReceivableId, setEditingReceivableId] = 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
|
||||
const calculateDynamicSettlement = () => {
|
||||
@ -369,6 +410,34 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
};
|
||||
|
||||
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({
|
||||
verificationTransactionId: '',
|
||||
@ -413,26 +482,44 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
};
|
||||
|
||||
const handleUpdatePayable = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
|
||||
// Optimistic update
|
||||
const updatedItems = payableItems.map(item =>
|
||||
item.id === id ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) : value } : item
|
||||
);
|
||||
setPayableItems(updatedItems);
|
||||
setEditingPayableDrafts((prev) => {
|
||||
const base = prev[id] || payableItems.find((item) => item.id === id);
|
||||
if (!base) return prev;
|
||||
return {
|
||||
...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 {
|
||||
const item = updatedItems.find(i => i.id === id);
|
||||
if (item) {
|
||||
await API.updateLineItem(id, {
|
||||
department: item.department,
|
||||
description: item.description,
|
||||
amount: -Math.abs(item.amount)
|
||||
});
|
||||
fetchFnFDetails();
|
||||
}
|
||||
await API.updateLineItem(id, {
|
||||
department: draft.department,
|
||||
description: draft.description,
|
||||
amount: -Math.abs(Number(draft.amount) || 0)
|
||||
});
|
||||
setEditingPayableId(null);
|
||||
setEditingPayableDrafts((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
toast.success('Changes saved');
|
||||
fetchFnFDetails(false);
|
||||
} catch (error) {
|
||||
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}`);
|
||||
setShowClearanceDialog(false);
|
||||
fetchFnFDetails();
|
||||
setActiveTab('departments');
|
||||
fetchFnFDetails(false);
|
||||
} catch (error) {
|
||||
console.error("Update clearance error:", error);
|
||||
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 updatedItems = receivableItems.map(item =>
|
||||
item.id === id ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) : value } : item
|
||||
);
|
||||
setReceivableItems(updatedItems);
|
||||
setEditingReceivableDrafts((prev) => {
|
||||
const base = prev[id] || receivableItems.find((item) => item.id === id);
|
||||
if (!base) return prev;
|
||||
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 {
|
||||
const item = updatedItems.find(i => i.id === id);
|
||||
if (item) {
|
||||
await API.updateLineItem(id, {
|
||||
department: item.department,
|
||||
description: item.description,
|
||||
amount: Math.abs(item.amount)
|
||||
});
|
||||
fetchFnFDetails();
|
||||
}
|
||||
await API.updateLineItem(id, {
|
||||
department: draft.department,
|
||||
description: draft.description,
|
||||
amount: Math.abs(Number(draft.amount) || 0)
|
||||
});
|
||||
setEditingReceivableId(null);
|
||||
setEditingReceivableDrafts((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
toast.success('Changes saved');
|
||||
fetchFnFDetails(false);
|
||||
} catch (error) {
|
||||
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 updatedItems = deductionItems.map(item =>
|
||||
item.id === id ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) : value } : item
|
||||
);
|
||||
setDeductionItems(updatedItems);
|
||||
setEditingDeductionDrafts((prev) => {
|
||||
const base = prev[id] || deductionItems.find((item) => item.id === id);
|
||||
if (!base) return prev;
|
||||
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 {
|
||||
const item = updatedItems.find(i => i.id === id);
|
||||
if (item) {
|
||||
await API.updateLineItem(id, {
|
||||
department: item.department,
|
||||
description: item.description,
|
||||
amount: Math.abs(item.amount)
|
||||
});
|
||||
fetchFnFDetails();
|
||||
}
|
||||
await API.updateLineItem(id, {
|
||||
department: draft.department,
|
||||
description: draft.description,
|
||||
amount: Math.abs(Number(draft.amount) || 0)
|
||||
});
|
||||
setEditingDeductionId(null);
|
||||
setEditingDeductionDrafts((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
toast.success('Changes saved');
|
||||
fetchFnFDetails(false);
|
||||
} catch (error) {
|
||||
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">
|
||||
{/* Left Column - Case Details & Financial Info */}
|
||||
<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">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="financial">Financial</TabsTrigger>
|
||||
@ -930,6 +1058,43 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
</TabsContent>
|
||||
|
||||
<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 */}
|
||||
<Card className="border-green-200 bg-green-50">
|
||||
<CardHeader>
|
||||
@ -960,7 +1125,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
<TableCell>
|
||||
{editingPayableId === item.id ? (
|
||||
<Select
|
||||
value={item.department}
|
||||
value={(editingPayableDrafts[item.id]?.department || item.department)}
|
||||
onValueChange={(val) => handleUpdatePayable(item.id, 'department', val)}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
@ -979,7 +1144,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
<TableCell>
|
||||
{editingPayableId === item.id ? (
|
||||
<Input
|
||||
value={item.description}
|
||||
value={(editingPayableDrafts[item.id]?.description || item.description)}
|
||||
onChange={(e) => handleUpdatePayable(item.id, 'description', e.target.value)}
|
||||
className="h-8"
|
||||
/>
|
||||
@ -991,7 +1156,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
{editingPayableId === item.id ? (
|
||||
<Input
|
||||
type="number"
|
||||
value={item.amount}
|
||||
value={(editingPayableDrafts[item.id]?.amount ?? item.amount)}
|
||||
onChange={(e) => handleUpdatePayable(item.id, 'amount', e.target.value)}
|
||||
className="h-8 text-right"
|
||||
/>
|
||||
@ -1006,10 +1171,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8"
|
||||
onClick={() => {
|
||||
setEditingPayableId(null);
|
||||
toast.success('Changes saved');
|
||||
}}
|
||||
onClick={() => handleSavePayableEdit(item.id)}
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
</Button>
|
||||
@ -1018,7 +1180,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
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" />
|
||||
</Button>
|
||||
@ -1116,7 +1284,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
<TableCell>
|
||||
{editingReceivableId === item.id ? (
|
||||
<Select
|
||||
value={item.department}
|
||||
value={(editingReceivableDrafts[item.id]?.department || item.department)}
|
||||
onValueChange={(val) => handleUpdateReceivable(item.id, 'department', val)}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
@ -1135,7 +1303,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
<TableCell>
|
||||
{editingReceivableId === item.id ? (
|
||||
<Input
|
||||
value={item.description}
|
||||
value={(editingReceivableDrafts[item.id]?.description || item.description)}
|
||||
onChange={(e) => handleUpdateReceivable(item.id, 'description', e.target.value)}
|
||||
className="h-8"
|
||||
/>
|
||||
@ -1147,7 +1315,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
{editingReceivableId === item.id ? (
|
||||
<Input
|
||||
type="number"
|
||||
value={item.amount}
|
||||
value={(editingReceivableDrafts[item.id]?.amount ?? item.amount)}
|
||||
onChange={(e) => handleUpdateReceivable(item.id, 'amount', e.target.value)}
|
||||
className="h-8 text-right"
|
||||
/>
|
||||
@ -1162,10 +1330,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8"
|
||||
onClick={() => {
|
||||
setEditingReceivableId(null);
|
||||
toast.success('Changes saved');
|
||||
}}
|
||||
onClick={() => handleSaveReceivableEdit(item.id)}
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
</Button>
|
||||
@ -1174,7 +1339,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
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" />
|
||||
</Button>
|
||||
@ -1272,7 +1443,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
<TableCell>
|
||||
{editingDeductionId === item.id ? (
|
||||
<Select
|
||||
value={item.department}
|
||||
value={(editingDeductionDrafts[item.id]?.department || item.department)}
|
||||
onValueChange={(val) => handleUpdateDeduction(item.id, 'department', val)}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
@ -1291,7 +1462,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
<TableCell>
|
||||
{editingDeductionId === item.id ? (
|
||||
<Input
|
||||
value={item.description}
|
||||
value={(editingDeductionDrafts[item.id]?.description || item.description)}
|
||||
onChange={(e) => handleUpdateDeduction(item.id, 'description', e.target.value)}
|
||||
className="h-8"
|
||||
/>
|
||||
@ -1303,7 +1474,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
{editingDeductionId === item.id ? (
|
||||
<Input
|
||||
type="number"
|
||||
value={item.amount}
|
||||
value={(editingDeductionDrafts[item.id]?.amount ?? item.amount)}
|
||||
onChange={(e) => handleUpdateDeduction(item.id, 'amount', e.target.value)}
|
||||
className="h-8 text-right"
|
||||
/>
|
||||
@ -1318,10 +1489,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8"
|
||||
onClick={() => {
|
||||
setEditingDeductionId(null);
|
||||
toast.success('Changes saved');
|
||||
}}
|
||||
onClick={() => handleSaveDeductionEdit(item.id)}
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
</Button>
|
||||
@ -1330,7 +1498,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
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" />
|
||||
</Button>
|
||||
|
||||
@ -46,28 +46,70 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
||||
}
|
||||
};
|
||||
|
||||
const getRelevantPaymentStatus = (app: any) => {
|
||||
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment';
|
||||
const s = app.overallStatus || app.status;
|
||||
const relevantType = (s.includes('LOI') || s === 'PAYMENT_VERIFICATION' || s === 'Security Details' || s === 'Payment Pending') ? 'SECURITY_DEPOSIT' : 'FIRST_FILL';
|
||||
const deposit = app.securityDeposits.find((d: any) => d.depositType === relevantType);
|
||||
return deposit ? deposit.status : 'Awaiting Payment';
|
||||
const normalizeStatus = (status: any) => String(status || '').trim().toLowerCase();
|
||||
const isVerifiedLikeStatus = (status: any) => {
|
||||
const normalized = normalizeStatus(status);
|
||||
return normalized === 'verified' || normalized === 'paid';
|
||||
};
|
||||
|
||||
// Filter for Payment Mode
|
||||
const paymentApps = applications.filter((app: any) => {
|
||||
const paymentRows = applications.flatMap((app: any) => {
|
||||
const s = app.overallStatus || app.status;
|
||||
return [
|
||||
'LOI In Progress', 'Security Details', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
|
||||
'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT', 'Payment Pending'
|
||||
const isPaymentStage = [
|
||||
'Payment Pending', 'Security Details', 'LOI In Progress', 'LOI Issued',
|
||||
'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL'
|
||||
].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 status = getRelevantPaymentStatus(app);
|
||||
const displayRows = paymentRows.filter((row: any) => {
|
||||
if (filterStatus === 'all') return true;
|
||||
if (filterStatus === 'pending') return status !== 'Verified';
|
||||
if (filterStatus === 'verified') return status === 'Verified';
|
||||
if (filterStatus === 'pending') return !isVerifiedLikeStatus(row.paymentStatus);
|
||||
if (filterStatus === 'verified') return isVerifiedLikeStatus(row.paymentStatus);
|
||||
return true;
|
||||
});
|
||||
|
||||
@ -107,7 +149,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@ -126,7 +168,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
||||
onClick={() => setFilterStatus('verified')}
|
||||
className={filterStatus === 'verified' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'}
|
||||
>
|
||||
Completed
|
||||
Verified
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'all' ? 'secondary' : 'ghost'}
|
||||
@ -153,11 +195,12 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{displayApps.length > 0 ? (
|
||||
displayApps.map((app) => {
|
||||
const statusLabel = getRelevantPaymentStatus(app);
|
||||
{displayRows.length > 0 ? (
|
||||
displayRows.map((row: any) => {
|
||||
const statusLabel = row.paymentStatus || 'Awaiting Payment';
|
||||
const app = row.application || {};
|
||||
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">
|
||||
<div className="flex flex-col">
|
||||
<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">
|
||||
<CreditCard className="w-4 h-4 text-slate-400" />
|
||||
<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>
|
||||
</div>
|
||||
</TableCell>
|
||||
@ -195,15 +238,15 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
||||
<TableCell className="text-right pr-6">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={statusLabel === 'Verified' ? 'outline' : 'default'}
|
||||
className={statusLabel !== 'Verified'
|
||||
variant={isVerifiedLikeStatus(statusLabel) ? 'outline' : 'default'}
|
||||
className={!isVerifiedLikeStatus(statusLabel)
|
||||
? 'bg-blue-600 hover:bg-blue-700 shadow-md'
|
||||
: '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" />
|
||||
{statusLabel === 'Verified' ? 'View Receipt' : 'Record Payment'}
|
||||
{isVerifiedLikeStatus(statusLabel) ? 'View Receipt' : 'Record Payment'}
|
||||
</>
|
||||
</Button>
|
||||
</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">
|
||||
<CheckCircle className="w-6 h-6 text-slate-200" />
|
||||
</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>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@ -62,10 +62,14 @@ const ALL_DEPARTMENTS = [
|
||||
'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) {
|
||||
const navigate = useNavigate();
|
||||
const [fnfCase, setFnfCase] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('details');
|
||||
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
|
||||
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||
@ -128,6 +132,15 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
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) => {
|
||||
if (!action) return 'Action';
|
||||
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(' ');
|
||||
};
|
||||
|
||||
const fetchFnFDetails = async () => {
|
||||
const fetchFnFDetails = async (showLoader: boolean = true) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (showLoader) setLoading(true);
|
||||
const response = await API.getFnFSettlementById(fnfId);
|
||||
const data = response.data as any;
|
||||
if (data.success) {
|
||||
const s = data.fnf;
|
||||
// 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 = {
|
||||
id: s.id,
|
||||
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"
|
||||
? "Completed"
|
||||
: "Pending",
|
||||
totalPayableAmount: (s.lineItems || []).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),
|
||||
totalDeductions: (s.lineItems || []).filter((li: any) => li.itemType === 'Deduction').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: calculationLineItems.filter((li: any) => li.itemType === 'Receivable' || li.itemType === 'Recovery').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,
|
||||
departmentResponses: [] as any[]
|
||||
};
|
||||
@ -198,8 +218,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
const c = (s.clearances || []).find(
|
||||
(clearance: any) => normalizeDepartment(clearance.department) === deptName,
|
||||
);
|
||||
const relatedItems = (s.lineItems || []).filter(
|
||||
(li: any) => normalizeDepartment(li.department) === deptName,
|
||||
const relatedItems = allLineItems.filter(
|
||||
(li: any) =>
|
||||
normalizeDepartment(li.department) === deptName &&
|
||||
isDepartmentClaimLine(li.description, li.sourceType),
|
||||
);
|
||||
|
||||
// Calculate departmental net
|
||||
@ -212,12 +234,17 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
});
|
||||
|
||||
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 {
|
||||
id: c?.id || `dept-${deptName}`,
|
||||
clearanceId: c?.id || null,
|
||||
departmentName: deptName,
|
||||
status: c?.status || "Pending",
|
||||
status: normalizedStatus,
|
||||
amountType: netAmount > 0 ? "Payable" : netAmount < 0 ? "Recovery" : null,
|
||||
amount: Math.abs(netAmount),
|
||||
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);
|
||||
toast.error("Failed to fetch settlement details");
|
||||
} 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}`);
|
||||
setShowClearanceDialog(false);
|
||||
setClearanceFile(null);
|
||||
fetchFnFDetails();
|
||||
setActiveTab('departments');
|
||||
fetchFnFDetails(false);
|
||||
} catch (error) {
|
||||
console.error("Update clearance error:", error);
|
||||
toast.error("Failed to update department clearance");
|
||||
@ -470,6 +498,32 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
).length;
|
||||
const totalDepartments = fnfCase.departmentResponses.length;
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
@ -607,7 +661,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</Card>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs defaultValue="details" className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="progress">Progress</TabsTrigger>
|
||||
<TabsTrigger value="details">Case Details</TabsTrigger>
|
||||
@ -1383,6 +1437,39 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
{/* Financial Summary Tab */}
|
||||
<TabsContent value="financial">
|
||||
<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>
|
||||
<CardHeader>
|
||||
<CardTitle>Financial Summary</CardTitle>
|
||||
|
||||
@ -21,52 +21,6 @@ import { formatDateTime } from '../ui/utils';
|
||||
import { RESIGNATION_DOCUMENT_TYPES, RESIGNATION_STAGE_OPTIONS } from '../../lib/offboardingDocumentOptions';
|
||||
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 {
|
||||
resignationId: string;
|
||||
@ -545,7 +499,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<TabsList className="bg-slate-100 p-1">
|
||||
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</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="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
|
||||
</TabsList>
|
||||
@ -566,6 +519,10 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<Label className="text-slate-600">GST</Label>
|
||||
<p>{resignationData?.dealer?.dealerProfile?.gstNumber || resignationData?.outlet?.gstNumber || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Dealer Email</Label>
|
||||
<p>{resignationData?.dealer?.email || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Sales Code</Label>
|
||||
<p>{resignationData?.dealer?.dealerProfile?.dealerCode?.salesCode || 'N/A'}</p>
|
||||
@ -756,90 +713,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
</Card>
|
||||
</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 */}
|
||||
<TabsContent value="documents">
|
||||
<Card>
|
||||
|
||||
@ -153,7 +153,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
};
|
||||
|
||||
// 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)
|
||||
const getTerminationPermissions = () => {
|
||||
@ -163,7 +163,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
|
||||
const currentStage = terminationData.currentStage;
|
||||
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 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 === 'DD Lead Review' && userRole === 'DD Lead') ||
|
||||
(currentStage === 'Legal Verification' && userRole === 'Legal Admin') ||
|
||||
(currentStage === 'DD Head Review' && userRole === 'DD Head') ||
|
||||
(currentStage === 'NBH Evaluation' && userRole === 'NBH') ||
|
||||
(currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
|
||||
(currentStage === 'CCO Approval' && userRole === 'CCO') ||
|
||||
@ -181,10 +183,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
);
|
||||
|
||||
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,
|
||||
canUploadSCNResponse: currentStage === 'Show Cause Notice' && (['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,
|
||||
canUploadSCNResponse: isScnStage && (['Legal Admin', 'DD Admin', 'Super Admin'].includes(userRole)) && !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,
|
||||
canWithdraw: userRole === 'ASM' && currentStage === 'Request Initiated' && !isFinalState,
|
||||
isFinalState,
|
||||
@ -196,6 +203,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
|
||||
// Use actual data from backend
|
||||
const request = terminationData || {};
|
||||
const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(request.currentStage);
|
||||
|
||||
const stageAliases: Record<string, string[]> = {
|
||||
'Submitted': ['Submitted', 'Request Initiated'],
|
||||
@ -203,6 +211,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
'ZBH Review': ['ZBH Review'],
|
||||
'DD Lead Review': ['DD Lead Review'],
|
||||
'Legal Verification': ['Legal Verification'],
|
||||
'DD Head Review': ['DD Head Review'],
|
||||
'NBH Evaluation': ['NBH Evaluation'],
|
||||
'Show Cause Notice (SCN)': ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'],
|
||||
'Personal Hearing': ['Personal Hearing'],
|
||||
@ -213,6 +222,45 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
'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 = [
|
||||
...(request.documents || []),
|
||||
...(request.uploadedDocuments || [])
|
||||
@ -236,17 +284,26 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
|
||||
const getLatestStageTimelineEntry = (stageName: string) => {
|
||||
const aliases = stageAliases[stageName] || [stageName];
|
||||
const entries = (request.timeline || []).filter((entry: any) =>
|
||||
aliases.includes(entry.stage) || aliases.includes(entry.targetStage)
|
||||
);
|
||||
return entries.length > 0 ? entries[entries.length - 1] : null;
|
||||
const entries = (request.timeline || []).filter((entry: any) => aliases.includes(entry.stage));
|
||||
|
||||
if (entries.length === 0) return 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 = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Submitted',
|
||||
status: 'completed',
|
||||
status: getProgressStatus('Submitted'),
|
||||
description: 'Termination request initiated',
|
||||
date: '',
|
||||
actionType: '',
|
||||
@ -257,73 +314,79 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
{
|
||||
id: 2,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'NBH Evaluation',
|
||||
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',
|
||||
description: 'National Business Head decision'
|
||||
name: 'DD Head Review',
|
||||
status: getProgressStatus('DD Head Review'),
|
||||
description: 'DD Head strategic review'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Show Cause Notice (SCN)',
|
||||
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',
|
||||
description: 'SCN sent to dealer, awaiting response'
|
||||
name: 'NBH Evaluation',
|
||||
status: getProgressStatus('NBH Evaluation'),
|
||||
description: 'National Business Head decision'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Personal Hearing',
|
||||
status: request.currentStage === 'Personal Hearing' ? 'active' : ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Evaluation of SCN response & Hearing'
|
||||
name: 'Show Cause Notice (SCN)',
|
||||
status: getProgressStatus('Show Cause Notice (SCN)'),
|
||||
description: 'SCN sent to dealer, awaiting response'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'NBH Final Approval',
|
||||
status: request.currentStage === 'NBH Final Approval' ? 'active' : ['CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'NBH final termination decision'
|
||||
name: 'Personal Hearing',
|
||||
status: getProgressStatus('Personal Hearing'),
|
||||
description: 'Evaluation of SCN response & Hearing'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'CCO Approval',
|
||||
status: request.currentStage === 'CCO Approval' ? 'active' : ['CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Chief Commercial Officer approval'
|
||||
name: 'NBH Final Approval',
|
||||
status: getProgressStatus('NBH Final Approval'),
|
||||
description: 'NBH final termination decision'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'CEO Final Approval',
|
||||
status: request.currentStage === 'CEO Final Approval' ? 'active' : ['Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'CEO final authorization'
|
||||
name: 'CCO Approval',
|
||||
status: getProgressStatus('CCO Approval'),
|
||||
description: 'Chief Commercial Officer approval'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Legal - Termination Letter',
|
||||
status: request.currentStage === 'Legal - Termination Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
||||
description: 'Legal team issues final termination letter'
|
||||
name: 'CEO Final Approval',
|
||||
status: getProgressStatus('CEO Final Approval'),
|
||||
description: 'CEO final authorization'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Legal - Termination Letter',
|
||||
status: getProgressStatus('Legal - Termination Letter'),
|
||||
description: 'Legal team issues final termination letter'
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'Dealer Terminated',
|
||||
status: request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
||||
status: getProgressStatus('Dealer Terminated'),
|
||||
description: 'Dealership termination effective',
|
||||
date: '',
|
||||
actionType: '',
|
||||
@ -590,6 +653,10 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<Label className="text-slate-600">GST</Label>
|
||||
<p>{request.dealer?.gstNumber || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Dealer Email</Label>
|
||||
<p>{request.dealer?.user?.email || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-600">Address</Label>
|
||||
<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">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{request.currentStage === 'SCN' ? 'Upload SCN Response' : 'Issue Show Cause Notice (SCN)'}
|
||||
{isScnStage ? 'Upload SCN Response' : 'Issue Show Cause Notice (SCN)'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{request.currentStage === 'SCN'
|
||||
{isScnStage
|
||||
? 'Upload the response received from the dealer regarding the SCN.'
|
||||
: 'Confirm the issuance of a formal Show Cause Notice to the dealer.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 pt-4">
|
||||
{request.currentStage === 'SCN' && (
|
||||
{isScnStage && (
|
||||
<div className="space-y-2">
|
||||
<Label>SCN Response File</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
@ -1144,11 +1211,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={request.currentStage === 'SCN' ? 'bg-amber-600 hover:bg-amber-700' : 'bg-purple-600 hover:bg-purple-700'}
|
||||
onClick={request.currentStage === 'SCN' ? handleUploadSCNResponse : handleIssueSCN}
|
||||
disabled={isProcessing || (request.currentStage === 'SCN' && !scnFile)}
|
||||
className={isScnStage ? 'bg-amber-600 hover:bg-amber-700' : 'bg-purple-600 hover:bg-purple-700'}
|
||||
onClick={isScnStage ? handleUploadSCNResponse : handleIssueSCN}
|
||||
disabled={isProcessing || (isScnStage && !scnFile)}
|
||||
>
|
||||
{isProcessing ? 'Processing...' : request.currentStage === 'SCN' ? 'Upload Response' : 'Issue SCN'}
|
||||
{isProcessing ? 'Processing...' : isScnStage ? 'Upload Response' : 'Issue SCN'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
|
||||
@ -86,10 +86,15 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
(async () => {
|
||||
try {
|
||||
setDialogDataLoading(true);
|
||||
const response = await API.getDealers({ onboarded: 'true' });
|
||||
const response = await API.getDealers({ onboarded: 'true', activeOnly: 'true' });
|
||||
const data = response.data as any;
|
||||
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) {
|
||||
if (!cancelled) {
|
||||
@ -211,21 +216,23 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
// Helper function to check if request is at current user's level
|
||||
const isRequestAtMyLevel = (request: any) => {
|
||||
if (!currentUser) return false;
|
||||
const userRole = currentUser.role || currentUser.roleCode;
|
||||
|
||||
const roleToStageMapping: Record<string, string[]> = {
|
||||
'DD Lead': ['DD Lead Review'],
|
||||
'RBM': ['RBM Review'],
|
||||
'ZBH': ['ZBH Review'],
|
||||
'DD Lead': ['DD Lead Review'],
|
||||
'DD Head': ['DD Head Review'],
|
||||
'NBH': ['NBH Evaluation', 'NBH Final Approval'],
|
||||
'Legal Admin': ['Legal Verification', 'Legal - Termination Letter'],
|
||||
'Legal': ['Legal Verification'],
|
||||
'DD Admin': ['Show Cause Notice', 'Terminated'],
|
||||
'CCO': ['CCO 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 =>
|
||||
(request.currentStage && request.currentStage.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 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) {
|
||||
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)
|
||||
if (excludeRoles && user && excludeRoles.includes(user.role)) {
|
||||
if (excludeRoles && user && normalizedExcludedRoles.includes(normalizedRole)) {
|
||||
// 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={redirectTo || '/dashboard'} replace />;
|
||||
}
|
||||
|
||||
// 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
|
||||
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',
|
||||
'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL'
|
||||
].includes(s);
|
||||
const deposits = app.securityDeposits || [];
|
||||
|
||||
if (isPaymentStage) {
|
||||
const deposits = app.securityDeposits || [];
|
||||
if (deposits.length > 0) {
|
||||
deposits.forEach((d: any) => {
|
||||
consolidatedPayments.push({
|
||||
...d,
|
||||
application: app,
|
||||
paymentStatus: d.status,
|
||||
paymentType: d.depositType,
|
||||
amount: d.amount,
|
||||
id: d.id,
|
||||
applicationId: app.applicationId || app.id,
|
||||
createdAt: d.createdAt,
|
||||
verificationDate: d.verifiedAt
|
||||
});
|
||||
// Always include real payment rows, even if the app has moved beyond payment stages.
|
||||
if (deposits.length > 0) {
|
||||
deposits.forEach((d: any) => {
|
||||
consolidatedPayments.push({
|
||||
...d,
|
||||
application: app,
|
||||
paymentStatus: d.status,
|
||||
paymentType: d.depositType,
|
||||
amount: d.amount,
|
||||
id: d.id,
|
||||
applicationId: app.applicationId || app.id,
|
||||
createdAt: d.createdAt,
|
||||
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)
|
||||
consolidatedPayments.push({
|
||||
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 verifiedOnboarding = onboardingPayments.filter(p => p.paymentStatus === 'Paid' || p.paymentStatus === 'Verified');
|
||||
const isVerifiedLikeStatus = (status: any) => {
|
||||
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 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 [offboardingExpanded, setOffboardingExpanded] = 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 terminationRoles = ['ASM', 'DD Lead', 'DD Admin', 'Super Admin'];
|
||||
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 canSeeResignation = resignationRoles.includes(currentRole);
|
||||
const canSeeTermination = terminationRoles.includes(currentRole);
|
||||
const canSeeFnF = fnfRoles.includes(currentRole);
|
||||
const canSeeResignation = hasRole(resignationRoles);
|
||||
const canSeeTermination = hasRole(terminationRoles);
|
||||
const canSeeFnF = hasRole(fnfRoles);
|
||||
const offboardingSubmenu = [
|
||||
canSeeResignation ? { id: 'resignation', label: 'Resignation' } : null,
|
||||
canSeeTermination ? { id: 'termination', label: 'Termination' } : null,
|
||||
@ -51,16 +53,16 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
||||
].filter(Boolean) as { id: string; label: string }[];
|
||||
|
||||
// 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: 'finance-onboarding', label: 'Onboarding', icon: FileText },
|
||||
{ id: 'finance-fnf', label: 'F&F', icon: UserMinus },
|
||||
] : currentRole === 'Dealer' ? [
|
||||
] : hasRole(['Dealer']) ? [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus },
|
||||
{ id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw },
|
||||
{ id: 'dealer-relocation', label: 'Relocation Requests', icon: MapPin },
|
||||
] : currentRole === 'FDD' ? [
|
||||
] : hasRole(['FDD']) ? [
|
||||
{ id: 'fdd-dashboard', label: 'FDD 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)
|
||||
if (currentRole === 'DD') {
|
||||
if (hasRole(['DD'])) {
|
||||
menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox });
|
||||
}
|
||||
|
||||
// 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, {
|
||||
id: 'all-requests',
|
||||
label: 'All Requests',
|
||||
@ -98,11 +100,11 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
||||
}
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
if (currentRole === 'Super Admin') {
|
||||
if (hasRole(['Super Admin'])) {
|
||||
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
||||
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
||||
}
|
||||
|
||||
@ -220,55 +220,106 @@ export interface QuestionnaireResponse {
|
||||
|
||||
// Mock test users for different roles
|
||||
export const mockUsers: User[] = [
|
||||
{
|
||||
id: '5',
|
||||
name: 'Meera Iyer',
|
||||
email: 'ddlead@royalenfield.com',
|
||||
{
|
||||
id: '15',
|
||||
name: 'Super Admin',
|
||||
email: 'admin@royalenfield.com',
|
||||
password: 'Admin@123',
|
||||
role: 'DD Lead',
|
||||
role: 'Super Admin',
|
||||
},
|
||||
{
|
||||
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',
|
||||
password: 'Admin@123',
|
||||
role: 'Finance',
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
name: 'Amit Sharma',
|
||||
email: 'dealer@royalenfield.com',
|
||||
id: '13',
|
||||
name: 'abhishek',
|
||||
email: 'abhishek@royalenfield.com',
|
||||
password: 'Admin@123',
|
||||
role: 'Dealer',
|
||||
},
|
||||
{
|
||||
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',
|
||||
role: 'ASM',
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
name: 'Lince',
|
||||
email: 'lince@gmail.com',
|
||||
email: 'lince@royalenfield.com',
|
||||
password: 'Admin@123',
|
||||
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user