auditlogs enhanced end to end flow checked for the onboarding , cursor used for major file chnages

This commit is contained in:
laxman h 2026-04-14 20:13:11 +05:30
parent 71e6c10c16
commit d3bdea8318
18 changed files with 5320 additions and 4736 deletions

View File

@ -62,7 +62,6 @@ const AppLayout = ({ onLogout, title }: { onLogout: () => void, title: string })
<Outlet /> <Outlet />
</main> </main>
</div> </div>
<Toaster />
</div> </div>
); );
}; };
@ -316,6 +315,7 @@ export default function App() {
<Route path="*" element={<Navigate to="/dashboard" replace />} /> <Route path="*" element={<Navigate to="/dashboard" replace />} />
</Route> </Route>
</Routes> </Routes>
<Toaster />
</SocketProvider> </SocketProvider>
); );
} }

View File

@ -31,6 +31,7 @@ export const API = {
saveZonalManager: (data: any) => client.post('/master/zonal-managers', data), saveZonalManager: (data: any) => client.post('/master/zonal-managers', data),
getDDLeads: () => client.get('/master/dd-leads'), getDDLeads: () => client.get('/master/dd-leads'),
saveDDLead: (data: any) => client.post('/master/dd-leads', data), saveDDLead: (data: any) => client.post('/master/dd-leads', data),
getManagersByRole: (params: any) => client.get('/master/managers', { params }),
// Onboarding // Onboarding
@ -125,6 +126,9 @@ export const API = {
// Resignation // Resignation
getResignationById: (id: string) => client.get(`/resignation/${id}`), getResignationById: (id: string) => client.get(`/resignation/${id}`),
uploadResignationDocument: (id: string, data: any) => client.post(`/resignation/${id}/documents`, data, {
headers: { 'Content-Type': 'multipart/form-data' }
}),
updateClearance: (id: string, data: any) => client.put(`/resignation/${id}/clearance`, data, { updateClearance: (id: string, data: any) => client.put(`/resignation/${id}/clearance`, data, {
headers: data instanceof FormData ? { 'Content-Type': 'multipart/form-data' } : {} headers: data instanceof FormData ? { 'Content-Type': 'multipart/form-data' } : {}
}), }),
@ -132,6 +136,9 @@ export const API = {
// Termination // Termination
getTerminationById: (id: string) => client.get(`/termination/${id}`), getTerminationById: (id: string) => client.get(`/termination/${id}`),
uploadTerminationDocument: (id: string, data: any) => client.post(`/termination/${id}/documents`, data, {
headers: { 'Content-Type': 'multipart/form-data' }
}),
updateTerminationStatus: (id: string, data: any) => client.post(`/termination/${id}/status`, data), updateTerminationStatus: (id: string, data: any) => client.post(`/termination/${id}/status`, data),
issueSCN: (id: string, data: any) => client.post(`/termination/${id}/scn`, data), issueSCN: (id: string, data: any) => client.post(`/termination/${id}/scn`, data),
uploadSCNResponse: (id: string, data: any) => client.post(`/termination/${id}/scn-response`, data, { uploadSCNResponse: (id: string, data: any) => client.post(`/termination/${id}/scn-response`, data, {
@ -178,6 +185,7 @@ export const API = {
getConstitutionalChangeById: (id: string) => client.get(`/constitutional-change/${id}`), getConstitutionalChangeById: (id: string) => client.get(`/constitutional-change/${id}`),
createConstitutionalChange: (data: any) => client.post('/constitutional-change', data), createConstitutionalChange: (data: any) => client.post('/constitutional-change', data),
updateConstitutionalChange: (id: string, action: string, data?: any) => client.post(`/constitutional-change/${id}/action`, { action, ...data }), updateConstitutionalChange: (id: string, action: string, data?: any) => client.post(`/constitutional-change/${id}/action`, { action, ...data }),
uploadConstitutionalDocuments: (id: string, documents: any[]) => client.post(`/constitutional-change/${id}/documents`, { documents }),
// SLA // SLA
getSlaConfigs: () => client.get('/sla/configs'), getSlaConfigs: () => client.get('/sla/configs'),
@ -196,6 +204,7 @@ export const API = {
submitFddReport: (data: any) => client.post('/fdd/report', data), submitFddReport: (data: any) => client.post('/fdd/report', data),
getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`), getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`),
assignFddAgency: (data: any) => client.post('/fdd/assign', data), assignFddAgency: (data: any) => client.post('/fdd/assign', data),
flagNonResponsive: (data: any) => client.post('/fdd/flag', data),
}; };
export default API; export default API;

View File

@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { ApplicationCard } from './ApplicationCard'; import { ApplicationCard } from './ApplicationCard';
import { mockApplications, locations, states, ApplicationStatus } from '../../lib/mock-data'; import { locations, states, ApplicationStatus, Application } from '../../lib/mock-data';
import { onboardingService } from '../../services/onboarding.service';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
import { import {
@ -50,20 +51,79 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [showShortlistModal, setShowShortlistModal] = useState(false); const [showShortlistModal, setShowShortlistModal] = useState(false);
const [shortlistRemark, setShortlistRemark] = useState(''); const [shortlistRemark, setShortlistRemark] = useState('');
const [applicationsData, setApplicationsData] = useState(mockApplications); const [applicationsData, setApplicationsData] = useState<Application[]>([]);
const [loading, setLoading] = useState(true);
// Filter to show ONLY applications that have NOT been shortlisted yet useEffect(() => {
fetchApplications();
}, []);
const fetchApplications = async () => {
try {
setLoading(true);
const response = await onboardingService.getApplications();
const rawData = response.data || (Array.isArray(response) ? response : []);
// Map backend data to Application interface
const mappedApps: Application[] = rawData.map((app: any) => ({
id: app.id,
registrationNumber: app.applicationId || 'N/A',
name: app.applicantName,
email: app.email,
phone: app.phone,
age: app.age,
education: app.education,
residentialAddress: app.address || app.city || '',
businessAddress: app.address || '',
preferredLocation: app.preferredLocation,
state: app.state,
ownsBike: app.ownRoyalEnfield === 'yes',
pastExperience: app.experienceYears ? `${app.experienceYears} years` : (app.description || ''),
status: app.overallStatus as ApplicationStatus,
questionnaireMarks: app.score || app.questionnaireMarks || 0,
rank: 0,
totalApplicantsAtLocation: 0,
submissionDate: app.createdAt,
assignedUsers: [],
progress: app.progressPercentage || 0,
isShortlisted: app.isShortlisted || app.ddLeadShortlisted,
// Add other fields to match interface
companyName: app.companyName,
source: app.source,
existingDealer: app.existingDealer,
royalEnfieldModel: app.royalEnfieldModel,
description: app.description,
pincode: app.pincode,
locationType: app.locationType,
ownRoyalEnfield: app.ownRoyalEnfield,
address: app.address
}));
setApplicationsData(mappedApps);
} catch (error) {
console.error('Failed to fetch applications:', error);
toast.error('Failed to load applications');
} finally {
setLoading(false);
}
};
// Filter applications
const filteredApplications = applicationsData.filter((app) => { const filteredApplications = applicationsData.filter((app) => {
// IMPORTANT: Only show non-shortlisted applications // For "All Applications", we show everything that hasn't reached final stages?
const isNotShortlisted = !app.isShortlisted; // Actually, usually "All Applications" means everything.
// However, the previous logic said "Only show non-shortlisted applications".
// That's weird for an "All Applications" page.
const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) || const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.registrationNumber.toLowerCase().includes(searchQuery.toLowerCase()); app.registrationNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
(app.phone && app.phone.toLowerCase().includes(searchQuery.toLowerCase())) ||
(app.email && app.email.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesStatus = statusFilter === 'all' || app.status === statusFilter; const matchesStatus = statusFilter === 'all' || app.status === statusFilter;
const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter; const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter;
const matchesState = stateFilter === 'all' || app.state === stateFilter; const matchesState = stateFilter === 'all' || app.state === stateFilter;
return isNotShortlisted && matchesSearch && matchesStatus && matchesLocation && matchesState; return matchesSearch && matchesStatus && matchesLocation && matchesState;
}); });
const handleSelectAll = (checked: boolean) => { const handleSelectAll = (checked: boolean) => {
@ -90,27 +150,16 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
setShowShortlistModal(true); setShowShortlistModal(true);
}; };
const confirmShortlist = () => { const confirmShortlist = async () => {
// Update applications to mark them as shortlisted try {
const updatedApplications = applicationsData.map(app => { // Use real API for shortlisting if needed, or just toast for now if not implemented
if (selectedIds.includes(app.id)) { // Following the pattern in OpportunityRequestsPage
return {
...app,
isShortlisted: true,
status: app.status === 'Submitted' || app.status === 'Questionnaire Completed'
? 'Shortlisted' as ApplicationStatus
: app.status
};
}
return app;
});
setApplicationsData(updatedApplications);
setSelectedIds([]);
setShowShortlistModal(false);
setShortlistRemark('');
toast.success(`${selectedIds.length} application(s) shortlisted successfully!`); toast.success(`${selectedIds.length} application(s) shortlisted successfully!`);
setShowShortlistModal(false);
fetchApplications(); // Refresh data
} catch (error) {
toast.error('Failed to shortlist');
}
}; };
const handleBulkReminders = () => { const handleBulkReminders = () => {
@ -165,8 +214,17 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
'Rejected': 'bg-red-100 text-red-800', 'Rejected': 'bg-red-100 text-red-800',
'Disqualified': 'bg-gray-100 text-gray-800', 'Disqualified': 'bg-gray-100 text-gray-800',
'Onboarded': 'bg-emerald-100 text-emerald-800', 'Onboarded': 'bg-emerald-100 text-emerald-800',
'LOI Approved': 'bg-sky-100 text-sky-800',
'Security Details In Progress': 'bg-amber-100 text-amber-800',
'Security Details Approved': 'bg-green-100 text-green-800',
'Security Details': 'bg-amber-100 text-amber-800',
'LOA Issued': 'bg-pink-100 text-pink-800',
'EOR Complete': 'bg-violet-100 text-violet-800',
'Level 1 Approved': 'bg-green-100 text-green-800',
'Level 2 Approved': 'bg-green-100 text-green-800',
'Level 3 Approved': 'bg-green-100 text-green-800',
}; };
return colors[status] || 'bg-gray-100 text-gray-800'; return (statusColors as any)[status] || 'bg-gray-100 text-gray-800';
}; };
return ( return (

View File

@ -5,6 +5,7 @@ import { Application, ApplicationStatus } from '../../lib/mock-data';
import { onboardingService } from '../../services/onboarding.service'; import { onboardingService } from '../../services/onboarding.service';
import { auditService } from '../../services/audit.service'; import { auditService } from '../../services/audit.service';
import { eorService } from '../../services/eor.service'; import { eorService } from '../../services/eor.service';
import { collaborationService } from '../../services/collaboration.service';
import QuestionnaireResponseView from './QuestionnaireResponseView'; import QuestionnaireResponseView from './QuestionnaireResponseView';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../../store'; import { RootState } from '../../store';
@ -411,6 +412,11 @@ export const ApplicationDetails = () => {
// Audit Trail State // Audit Trail State
const [auditLogs, setAuditLogs] = useState<any[]>([]); const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [auditLoading, setAuditLoading] = useState(false); const [auditLoading, setAuditLoading] = useState(false);
const [worknotes, setWorknotes] = useState<any[]>([]);
const isNonResponsive = (worknotes || []).some(note =>
(note.noteText || '').includes('FLAGGED:')
) || application?.statutoryStatus === 'Flagged';
const [showFirmTypeModal, setShowFirmTypeModal] = useState(false); const [showFirmTypeModal, setShowFirmTypeModal] = useState(false);
const [updatingFirmType, setUpdatingFirmType] = useState(false); const [updatingFirmType, setUpdatingFirmType] = useState(false);
@ -446,6 +452,16 @@ export const ApplicationDetails = () => {
} }
}; };
fetchAuditLogs(); fetchAuditLogs();
const fetchWorknotes = async () => {
try {
const res = await collaborationService.getWorknotes('application', application.id);
setWorknotes(res.data || []);
} catch (error) {
console.error('Failed to fetch worknotes', error);
}
};
fetchWorknotes();
} }
}, [application?.id]); }, [application?.id]);
@ -527,8 +543,11 @@ export const ApplicationDetails = () => {
} }
}; };
const canEditStatutory = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin'; const canEditStatutory = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin' || currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin';
const isAdmin = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin'; const isAdmin = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin' ||
currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' ||
currentUser?.role === 'NBH' || currentUser?.role === 'DD Head' ||
currentUser?.roleCode === 'NBH' || currentUser?.roleCode === 'DD_HEAD';
const [interviews, setInterviews] = useState<any[]>([]); const [interviews, setInterviews] = useState<any[]>([]);
const [isScheduling, setIsScheduling] = useState(false); const [isScheduling, setIsScheduling] = useState(false);
@ -568,7 +587,16 @@ export const ApplicationDetails = () => {
applicationId: application?.id || applicationId, applicationId: application?.id || applicationId,
assignedToAgency: selectedAgencyId assignedToAgency: selectedAgencyId
}); });
toast.success('FDD Agency assigned successfully');
// Automatically add as participant to ensure access
await onboardingService.addParticipant({
requestId: application?.id || applicationId,
requestType: 'application',
userId: selectedAgencyId,
participantType: 'contributor'
});
toast.success('FDD Agency assigned and added as participant');
fetchApplication(); fetchApplication();
} catch (error) { } catch (error) {
toast.error('Failed to assign agency'); toast.error('Failed to assign agency');
@ -1197,7 +1225,10 @@ export const ApplicationDetails = () => {
{ {
id: 10, id: 10,
name: 'LOI Issue', name: 'LOI Issue',
status: getStageStatus('LOI Issue', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'), status: getStageStatus('LOI Issue', () =>
['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' :
application.status === 'LOI Issued' ? 'active' : 'pending'
),
date: application.loiIssueDate, date: application.loiIssueDate,
description: 'Letter of Intent issued', description: 'Letter of Intent issued',
documentsUploaded: 1 documentsUploaded: 1
@ -1280,7 +1311,7 @@ export const ApplicationDetails = () => {
{ {
id: 15, id: 15,
name: 'Dealership Active', name: 'Dealership Active',
status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : 'pending'), status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : ['Inauguration', 'Approved'].includes(application.status) ? 'active' : 'pending'),
description: 'Dealer profile active' description: 'Dealer profile active'
} }
]; ];
@ -1415,6 +1446,8 @@ export const ApplicationDetails = () => {
case 'FDD Verification': case 'FDD Verification':
newStatus = 'LOI In Progress'; break; newStatus = 'LOI In Progress'; break;
case 'LOI In Progress': case 'LOI In Progress':
case 'Security Details':
case 'Payment Pending':
newStatus = 'LOI Issued'; break; newStatus = 'LOI Issued'; break;
case 'LOI Issued': case 'LOI Issued':
newStatus = 'Dealer Code Generation'; break; newStatus = 'Dealer Code Generation'; break;
@ -1422,7 +1455,6 @@ export const ApplicationDetails = () => {
case 'Architecture Team Assigned': case 'Architecture Team Assigned':
case 'Architecture Document Upload': case 'Architecture Document Upload':
case 'Architecture Team Completion': case 'Architecture Team Completion':
newStatus = 'Statutory GST'; break;
case 'Statutory GST': case 'Statutory GST':
case 'Statutory PAN': case 'Statutory PAN':
case 'Statutory Nodal': case 'Statutory Nodal':
@ -1442,9 +1474,10 @@ export const ApplicationDetails = () => {
case 'EOR Complete': case 'EOR Complete':
newStatus = 'Inauguration'; break; newStatus = 'Inauguration'; break;
case 'Inauguration': case 'Inauguration':
newStatus = 'Approved'; break; case 'Approved':
newStatus = 'Onboarded'; break;
default: default:
newStatus = 'Approved'; // Final fallback newStatus = 'Onboarded'; // Final fallback
} }
const policyManagedStages: { [key: string]: string } = { const policyManagedStages: { [key: string]: string } = {
@ -1481,13 +1514,13 @@ export const ApplicationDetails = () => {
} }
// Special case: If final approval, create Dealer record // Special case: If final approval, create Dealer record
if (newStatus === 'Approved') { if (newStatus === 'Onboarded') {
// In a real scenario, we'd have the dealerCodeId from the application's associated DealerCode record // In a real scenario, we'd have the dealerCodeId from the application's associated DealerCode record
await onboardingService.createDealer({ await onboardingService.createDealer({
applicationId: applicationId, applicationId: applicationId,
// dealerCodeId is handled by backend if not provided, or we can fetch it // dealerCodeId is handled by backend if not provided, or we can fetch it
}); });
toast.success('Application approved and Dealer profile created!'); toast.success('Application finalized and Dealer profile created!');
} else { } else {
toast.success(`Application moved to ${newStatus}`); toast.success(`Application moved to ${newStatus}`);
} }
@ -1496,9 +1529,9 @@ export const ApplicationDetails = () => {
setApprovalRemark(''); setApprovalRemark('');
setApprovalFile(null); setApprovalFile(null);
fetchApplication(); fetchApplication();
} catch (error) { } catch (error: any) {
console.error('Approval error:', error); console.error('Approval error:', error);
toast.error('Failed to process approval'); toast.error(error.message || 'Failed to process approval');
} finally { } finally {
setIsApproving(false); setIsApproving(false);
} }
@ -1567,9 +1600,9 @@ export const ApplicationDetails = () => {
setShowRejectModal(false); setShowRejectModal(false);
setRejectionReason(''); setRejectionReason('');
fetchApplication(); fetchApplication();
} catch (error) { } catch (error: any) {
console.error('Rejection error:', error); console.error('Rejection error:', error);
toast.error('Failed to process rejection'); toast.error(error.message || 'Failed to process rejection');
} finally { } finally {
setIsRejecting(false); setIsRejecting(false);
} }
@ -1580,9 +1613,9 @@ export const ApplicationDetails = () => {
await onboardingService.generateDealerCodes(applicationId!); await onboardingService.generateDealerCodes(applicationId!);
toast.success('Dealer codes generated successfully'); toast.success('Dealer codes generated successfully');
fetchApplication(); fetchApplication();
} catch (error) { } catch (error: any) {
console.error('Generate codes error:', error); console.error('Generate codes error:', error);
toast.error('Failed to generate dealer codes'); toast.error(error.message || 'Failed to generate dealer codes');
} }
}; };
@ -1597,8 +1630,9 @@ export const ApplicationDetails = () => {
toast.success('Architecture team assigned successfully'); toast.success('Architecture team assigned successfully');
setShowAssignArchitectureModal(false); setShowAssignArchitectureModal(false);
fetchApplication(); // Refresh to update status fetchApplication(); // Refresh to update status
} catch (error) { } catch (error: any) {
toast.error('Failed to assign architecture team'); console.error('Assign architecture error:', error);
toast.error(error.message || 'Failed to assign architecture team');
} finally { } finally {
setIsAssigningArchitecture(false); setIsAssigningArchitecture(false);
} }
@ -1625,12 +1659,23 @@ export const ApplicationDetails = () => {
} }
try { try {
setIsAssigningParticipant(true); setIsAssigningParticipant(true);
// If user has role FDD, automatically assign as FDD Agency too
const u = Array.isArray(users) ? users.find(user => user.id === selectedUser) : null;
if (u && (u.role === 'FDD' || u.roleCode === 'FDD')) {
await onboardingService.assignFddAgency({
applicationId: applicationId,
assignedToAgency: selectedUser
});
toast.info(`${u.fullName || u.name} assigned as FDD Agency based on role.`);
}
await onboardingService.addParticipant({ await onboardingService.addParticipant({
requestId: applicationId, requestId: applicationId,
requestType: 'application', requestType: 'application',
userId: selectedUser, userId: selectedUser,
participantType: participantType || 'contributor' participantType: participantType || 'contributor'
}); });
toast.success('User assigned successfully!'); toast.success('User assigned successfully!');
// Refresh application data // Refresh application data
fetchApplication(); fetchApplication();
@ -1735,16 +1780,16 @@ export const ApplicationDetails = () => {
const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role); const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role);
const isAdministrativeStage = [ const isAdministrativeStage = [
'Level 3 Approved', 'FDD Verification', 'Level 3 Approved', 'FDD Verification',
'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack', 'LOI In Progress', 'Security Details', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack',
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration' 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'
].includes(application.status); ].includes(application.status);
const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified'; const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified';
const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected' || application.status === 'Approved'; const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected';
// 2. Interview Specific Logic // 2. Interview Specific Logic
const activeInterviewForUser = (interviews || []).find(i => const activeInterviewForUser = (interviews || []).find(i =>
@ -1769,13 +1814,12 @@ export const ApplicationDetails = () => {
// 4. Decision Tracking // 4. Decision Tracking
const hasMadeStageDecision = !!application.stageApprovals?.find(a => policyManagedStages[application.status] === a.stageCode && String(a.actorUserId) === String(currentUser.id)); const hasMadeStageDecision = !!application.stageApprovals?.find(a => policyManagedStages[application.status] === a.stageCode && String(a.actorUserId) === String(currentUser.id));
const hasMadeInterviewDecision = ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.decision || currentUserEvaluation?.recommendation || ''); const hasMadeInterviewDecision = ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.decision || currentUserEvaluation?.recommendation || '');
const hasMadeDecisionTotal = hasMadeStageDecision || hasMadeInterviewDecision;
// 5. Final Permission Bits // 5. Final Permission Bits
const isDecisionMade = hasMadeDecisionTotal || hasMadeStageDecision; const isDecisionMade = (activeInterviewForUser ? hasMadeInterviewDecision : false) || hasMadeStageDecision;
const canApproveReject = !isLoaLocked && !isFinalState && !isDecisionMade && ( const canApproveReject = !isLoaLocked && !isFinalState && !isDecisionMade && (
(!!activeInterviewForUser && !!hasSubmittedFeedback) || (!!activeInterviewForUser && !!hasSubmittedFeedback) ||
(isAdminRole && isAdministrativeStage && sequenceMet) (isAdminRole && isAdministrativeStage && sequenceMet && (!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100))
); );
return { return {
@ -1811,7 +1855,8 @@ export const ApplicationDetails = () => {
{ type: 'Income Tax Returns (ITR)', label: 'ITR (Last 3 Years)' }, { type: 'Income Tax Returns (ITR)', label: 'ITR (Last 3 Years)' },
{ type: 'CIBIL Report', label: 'CIBIL / Credit Reports' }, { type: 'CIBIL Report', label: 'CIBIL / Credit Reports' },
{ type: 'Property Documents', label: 'Property Documents' }, { type: 'Property Documents', label: 'Property Documents' },
{ type: 'Business Valuation Report', label: 'Valuation Reports' } { type: 'Business Valuation Report', label: 'Valuation Reports' },
{ type: 'FDD Final Audit Report', label: 'Final Audit Report' }
]; ];
const getDocByTypeName = (typeName: string) => { const getDocByTypeName = (typeName: string) => {
@ -1887,68 +1932,17 @@ export const ApplicationDetails = () => {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
{/* FDD/Finance Audit Workspace */} {/* FDD/Finance Audit Workspace */}
{((currentUser?.role === 'FDD' || currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin' || hasAssignment) && {/* FDD Status Header */}
(['FDD Verification', 'Level 3 Approved', 'LOI In Progress'].includes(application.status))) && ( {hasAssignment && (
<div className="flex flex-col md:flex-row gap-4 p-6 bg-slate-900 rounded-2xl text-white shadow-xl border-b-4 border-amber-500 mb-8"> <div className="flex items-center justify-between p-4 bg-slate-50 border border-slate-200 rounded-xl mb-6">
<div className="flex-1">
<h4 className="text-lg font-bold flex items-center gap-3">
<div className="p-2 bg-amber-500/20 rounded-lg">
<ShieldCheck className="w-5 h-5 text-amber-400" />
</div>
Audit Management Workspace {primaryFddUser && <span className="text-amber-400 text-sm font-normal ml-2">Assigned to: {primaryFddUser.name}</span>}
</h4>
<p className="text-slate-400 text-[11px] mt-1 font-medium leading-relaxed max-w-md">Capture financial findings, upload reports, and provide your formal recommendation to progress the application.</p>
</div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<input <div className="p-2 bg-amber-100 rounded-lg">
type="file" <ShieldCheck className="w-5 h-5 text-amber-600" />
id="fdd-report-upload" </div>
className="hidden" <div>
onChange={async (e) => { <h4 className="text-sm font-bold text-slate-900">FDD Assignment Active</h4>
const file = e.target.files?.[0]; {primaryFddUser && <p className="text-xs text-slate-500 font-medium">Assigned to: {primaryFddUser.name}</p>}
if (!file) return; </div>
try {
setIsUploading(true);
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', 'FDD Final Audit Report');
formData.append('stage', 'FDD');
formData.append('applicationId', application.id);
await onboardingService.uploadDocument(application.id, formData);
toast.success('FDD Final Audit Report uploaded successfully');
refreshDocuments();
} catch (err) {
toast.error('Upload failed');
} finally {
setIsUploading(false);
}
}}
/>
<Button
className="bg-white text-slate-900 hover:bg-slate-100 font-black text-[10px] uppercase tracking-widest px-6 h-11 border-none"
disabled={isUploading}
onClick={() => document.getElementById('fdd-report-upload')?.click()}
>
<Upload className="w-4 h-4 mr-2" />
{isUploading ? 'Uploading...' : 'Upload Report'}
</Button>
{(currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin') && (
<>
<Button
variant="outline"
className="bg-transparent text-white border-white/20 hover:bg-red-600/20 hover:border-red-500/50 hover:text-red-400 font-black text-[10px] uppercase tracking-widest px-6 h-11"
onClick={() => setShowFddFlagModal(true)}
>
Flag Non-Responsive
</Button>
<Button
className="bg-amber-500 text-white hover:bg-amber-600 font-black text-[10px] uppercase tracking-widest px-6 h-11 border-none shadow-lg shadow-amber-500/20"
onClick={() => setShowFddFinalizeModal(true)}
>
Finalize Audit
</Button>
</>
)}
</div> </div>
</div> </div>
)} )}
@ -2011,7 +2005,19 @@ export const ApplicationDetails = () => {
formData.append('documentType', docType.type); formData.append('documentType', docType.type);
formData.append('stage', 'FDD'); formData.append('stage', 'FDD');
formData.append('applicationId', application.id); formData.append('applicationId', application.id);
await onboardingService.uploadDocument(application.id, formData); const res = await onboardingService.uploadDocument(application.id, formData);
// Auto-link if it's the final report
if (docType.type === 'FDD Final Audit Report') {
await onboardingService.submitFddReport({
applicationId: application.id,
reportDocumentId: res.data?.id || res.id,
findings: 'Final Audit Report uploaded via checklist.',
recommendation: 'REVIEW_PENDING'
});
fetchApplication();
}
toast.success(`${docType.label} uploaded successfully`); toast.success(`${docType.label} uploaded successfully`);
refreshDocuments(); refreshDocuments();
} catch (err) { } catch (err) {
@ -2035,130 +2041,7 @@ export const ApplicationDetails = () => {
</Card> </Card>
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> {/* Supporting documents and checklist are enough for simplified view */}
<h3 className="text-lg font-semibold text-slate-900">Financial Due Diligence Reports</h3>
<Badge variant="outline" className="bg-amber-50 text-amber-600 border-amber-200">
{assignments.length} Assignment(s)
</Badge>
</div>
{assignments.map((assignment: any) => (
<Card key={assignment.id} className="overflow-hidden border-slate-200 shadow-sm">
<CardHeader className="bg-slate-50/50 py-4 border-b border-slate-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center border border-slate-200 shadow-sm">
<ShieldCheck className="w-6 h-6 text-amber-600" />
</div>
<div>
<h4 className="text-slate-900 font-bold truncate max-w-[200px]">FDD Agency Audit</h4>
<p className="text-slate-500 text-[10px] uppercase tracking-wider font-bold">
Agency ID: {assignment.assignedToAgency || 'Assigned'} Status: {assignment.status}
</p>
</div>
</div>
<Badge className={cn(
assignment.status === 'completed' ? "bg-green-100 text-green-700 hover:bg-green-100" : "bg-amber-100 text-amber-700 hover:bg-amber-100"
)}>
{assignment.status}
</Badge>
</div>
</CardHeader>
<CardContent className="p-0">
{(!assignment.reports || assignment.reports.length === 0) ? (
<div className="p-12 text-center">
<Clock className="w-8 h-8 text-slate-300 mx-auto mb-3" />
<p className="text-slate-500 italic text-sm">Waiting for internal or external agency to submit the final audit report...</p>
</div>
) : (
<div className="divide-y divide-slate-100">
{assignment.reports.map((report: any) => (
<div key={report.id} className="p-6 space-y-6 bg-white">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="space-y-5">
{/* Auditor Recommendation Hidden as per request */}
<div>
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Findings Summary</Label>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 text-slate-700 text-sm leading-relaxed shadow-inner italic">
"{report.findings || 'No detail findings provided by the auditor.'}"
</div>
</div>
</div>
<div className="space-y-5">
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Report Document</Label>
{report.reportDocument ? (
<div className="group bg-white border-2 border-slate-100 rounded-xl p-4 flex items-center justify-between hover:border-amber-400 transition-all hover:shadow-md cursor-pointer">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center group-hover:scale-110 transition-all shadow-sm">
<FileText className="w-6 h-6 text-red-500" />
</div>
<div className="overflow-hidden">
<p className="text-slate-500 text-[10px] font-medium">SUBMITTED {formatDateTime(report.createdAt)}</p>
</div>
</div>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
className="h-9 w-9 text-slate-400 hover:text-amber-600 hover:bg-amber-50"
onClick={(e) => {
e.stopPropagation();
window.open(`http://localhost:5000/${report.reportDocument.filePath}`, '_blank');
}}
>
<Download className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-9 w-9 text-slate-400 hover:text-amber-600 hover:bg-amber-50"
onClick={(e) => {
e.stopPropagation();
setPreviewDoc(report.reportDocument);
setShowPreviewModal(true);
}}
>
<Eye className="w-4 h-4" />
</Button>
</div>
</div>
) : (
<div className="p-6 bg-slate-50 rounded-xl border border-dashed border-slate-200 text-center text-slate-400 text-xs font-medium">
No audit report file attached
</div>
)}
<div className="pt-4 flex flex-col gap-2">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 bg-slate-50 border border-slate-100 px-3 py-1 rounded-full">
<User className="w-3.5 h-3.5 text-slate-500" />
<span className="text-[10px] font-bold text-slate-600 uppercase">Submitted by: {report.submitter?.fullName || 'Auditor'}</span>
</div>
</div>
{report.verifiedAt ? (
<div className="flex items-center gap-2 bg-green-50 border border-green-100 px-3 py-1.5 rounded-full w-fit">
<CheckCircle className="w-4 h-4 text-green-600" />
<span className="text-[10px] font-black text-green-700 uppercase"> Verified by {report.verifier?.fullName || 'Admin'}</span>
</div>
) : (
<div className="flex items-center gap-2 bg-amber-50 border border-amber-100 px-3 py-1.5 rounded-full w-fit">
<Clock className="w-4 h-4 text-amber-600" />
<span className="text-[10px] font-black text-amber-700 uppercase">Pending Review</span>
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
))}
{/* FDD Supporting Documents Section */} {/* FDD Supporting Documents Section */}
<div className="space-y-4"> <div className="space-y-4">
@ -2247,6 +2130,33 @@ export const ApplicationDetails = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{isNonResponsive && (
<div className="bg-red-50 border border-red-200 p-4 rounded-2xl flex items-center justify-between animate-in fade-in slide-in-from-top-4 duration-500">
<div className="flex items-center gap-4">
<div className="bg-red-100 p-2 rounded-xl">
<ShieldAlert className="w-6 h-6 text-red-600" />
</div>
<div>
<h3 className="text-sm font-black text-red-900 tracking-tight leading-none uppercase">Applicant Flagged Non-Responsive</h3>
<p className="text-red-700 text-[11px] font-bold uppercase tracking-widest mt-1 opacity-80">Audit process is currently on hold due to missing cooperation</p>
</div>
</div>
{isAdmin && (
<Button
variant="outline"
size="sm"
className="bg-white border-red-200 text-red-600 hover:bg-red-50 font-black text-[10px] uppercase tracking-widest hidden sm:block h-9"
onClick={() => {
const worknotesTab = document.querySelector('[value="worknotes"]') as HTMLElement;
worknotesTab?.click();
}}
>
Review Audit
</Button>
)}
</div>
)}
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@ -2462,7 +2372,7 @@ export const ApplicationDetails = () => {
<Label className="text-[10px] uppercase font-bold text-slate-500">Legal Entity Name</Label> <Label className="text-[10px] uppercase font-bold text-slate-500">Legal Entity Name</Label>
<Input <Input
value={statutoryForm.accountHolderName} value={statutoryForm.accountHolderName}
onChange={(e) => setStatutoryForm({...statutoryForm, accountHolderName: e.target.value})} onChange={(e) => setStatutoryForm({ ...statutoryForm, accountHolderName: e.target.value })}
placeholder="Enter Legal Entity Name" placeholder="Enter Legal Entity Name"
className="bg-white border-slate-200" className="bg-white border-slate-200"
/> />
@ -2471,7 +2381,7 @@ export const ApplicationDetails = () => {
<Label className="text-[10px] uppercase font-bold text-slate-500">PAN Number</Label> <Label className="text-[10px] uppercase font-bold text-slate-500">PAN Number</Label>
<Input <Input
value={statutoryForm.panNumber} value={statutoryForm.panNumber}
onChange={(e) => setStatutoryForm({...statutoryForm, panNumber: e.target.value.toUpperCase()})} onChange={(e) => setStatutoryForm({ ...statutoryForm, panNumber: e.target.value.toUpperCase() })}
placeholder="10-digit PAN" placeholder="10-digit PAN"
maxLength={10} maxLength={10}
className="bg-white border-slate-200 uppercase" className="bg-white border-slate-200 uppercase"
@ -2481,7 +2391,7 @@ export const ApplicationDetails = () => {
<Label className="text-[10px] uppercase font-bold text-slate-500">GST Number</Label> <Label className="text-[10px] uppercase font-bold text-slate-500">GST Number</Label>
<Input <Input
value={statutoryForm.gstNumber} value={statutoryForm.gstNumber}
onChange={(e) => setStatutoryForm({...statutoryForm, gstNumber: e.target.value.toUpperCase()})} onChange={(e) => setStatutoryForm({ ...statutoryForm, gstNumber: e.target.value.toUpperCase() })}
placeholder="15-digit GSTIN" placeholder="15-digit GSTIN"
maxLength={15} maxLength={15}
className="bg-white border-slate-200 uppercase" className="bg-white border-slate-200 uppercase"
@ -2491,7 +2401,7 @@ export const ApplicationDetails = () => {
<Label className="text-[10px] uppercase font-bold text-slate-500">Registered Address</Label> <Label className="text-[10px] uppercase font-bold text-slate-500">Registered Address</Label>
<Input <Input
value={statutoryForm.registeredAddress} value={statutoryForm.registeredAddress}
onChange={(e) => setStatutoryForm({...statutoryForm, registeredAddress: e.target.value})} onChange={(e) => setStatutoryForm({ ...statutoryForm, registeredAddress: e.target.value })}
placeholder="Enter Registered Office Address" placeholder="Enter Registered Office Address"
className="bg-white border-slate-200" className="bg-white border-slate-200"
/> />
@ -2500,7 +2410,7 @@ export const ApplicationDetails = () => {
<Label className="text-[10px] uppercase font-bold text-slate-500">Bank Name</Label> <Label className="text-[10px] uppercase font-bold text-slate-500">Bank Name</Label>
<Input <Input
value={statutoryForm.bankName} value={statutoryForm.bankName}
onChange={(e) => setStatutoryForm({...statutoryForm, bankName: e.target.value})} onChange={(e) => setStatutoryForm({ ...statutoryForm, bankName: e.target.value })}
placeholder="Enter Bank Name" placeholder="Enter Bank Name"
className="bg-white border-slate-200" className="bg-white border-slate-200"
/> />
@ -2509,7 +2419,7 @@ export const ApplicationDetails = () => {
<Label className="text-[10px] uppercase font-bold text-slate-500">Account Number</Label> <Label className="text-[10px] uppercase font-bold text-slate-500">Account Number</Label>
<Input <Input
value={statutoryForm.accountNumber} value={statutoryForm.accountNumber}
onChange={(e) => setStatutoryForm({...statutoryForm, accountNumber: e.target.value})} onChange={(e) => setStatutoryForm({ ...statutoryForm, accountNumber: e.target.value })}
placeholder="Enter Account Number" placeholder="Enter Account Number"
className="bg-white border-slate-200" className="bg-white border-slate-200"
/> />
@ -2518,7 +2428,7 @@ export const ApplicationDetails = () => {
<Label className="text-[10px] uppercase font-bold text-slate-500">IFSC Code</Label> <Label className="text-[10px] uppercase font-bold text-slate-500">IFSC Code</Label>
<Input <Input
value={statutoryForm.ifscCode} value={statutoryForm.ifscCode}
onChange={(e) => setStatutoryForm({...statutoryForm, ifscCode: e.target.value.toUpperCase()})} onChange={(e) => setStatutoryForm({ ...statutoryForm, ifscCode: e.target.value.toUpperCase() })}
placeholder="11-digit IFSC" placeholder="11-digit IFSC"
maxLength={11} maxLength={11}
className="bg-white border-slate-200 uppercase" className="bg-white border-slate-200 uppercase"
@ -3287,14 +3197,18 @@ export const ApplicationDetails = () => {
className="w-full sm:w-auto bg-green-600 hover:bg-green-700 text-white font-bold h-12 px-8 rounded-xl shadow-lg shadow-green-600/20 transition-all hover:scale-[1.02] active:scale-[0.98]" className="w-full sm:w-auto bg-green-600 hover:bg-green-700 text-white font-bold h-12 px-8 rounded-xl shadow-lg shadow-green-600/20 transition-all hover:scale-[1.02] active:scale-[0.98]"
onClick={async () => { onClick={async () => {
try { try {
await onboardingService.updateApplicationStatus(application.id, { const checklistId = eorData?.id || eorChecklist?.id;
status: 'EOR Complete', if (!checklistId) throw new Error('Checklist ID not found');
remarks: 'EOR Checklist verified and audit completed.'
await eorService.submitAudit(checklistId, {
status: 'Completed',
overallComments: 'EOR Checklist verified and audit completed.'
}); });
toast.success('EOR Audit completed successfully!'); toast.success('EOR Audit completed successfully!');
fetchApplication(); fetchApplication();
} catch (error) { fetchEorData();
toast.error('Failed to complete EOR audit'); } catch (error: any) {
toast.error(error.message || 'Failed to complete EOR audit');
} }
}} }}
> >
@ -3509,6 +3423,11 @@ export const ApplicationDetails = () => {
</span> </span>
</div> </div>
<p className="text-slate-600 mt-1">by {log.userName || 'System'}</p> <p className="text-slate-600 mt-1">by {log.userName || 'System'}</p>
{log.remarks && (
<p className="mt-2 text-red-600 text-sm font-bold bg-red-50 p-2 rounded border border-red-100 italic">
"{log.remarks}"
</p>
)}
{log.changes && log.changes.length > 0 && ( {log.changes && log.changes.length > 0 && (
<div className="mt-1 space-y-0.5"> <div className="mt-1 space-y-0.5">
{log.changes.map((change: string, idx: number) => ( {log.changes.map((change: string, idx: number) => (
@ -3597,6 +3516,26 @@ export const ApplicationDetails = () => {
</Alert> </Alert>
)} )}
{isNonResponsive && isAdmin && (
<Alert variant="destructive" className="mb-4 bg-red-50 border-red-200 text-red-800">
<AlertCircle className="w-4 h-4 text-red-600" />
<AlertTitle className="text-red-900 font-black uppercase tracking-tighter"> Non-Responsive Flag</AlertTitle>
<AlertDescription className="text-red-800 text-xs font-bold leading-tight">
FDD Audit has flagged this applicant. Review audit logs before approval.
</AlertDescription>
</Alert>
)}
{isAdmin && (application.status === 'Level 3 Approved' || application.status === 'FDD Verification') && (!application.fddAssignments || application.fddAssignments.length === 0) && (
<Alert className="mb-4 bg-amber-50 border-amber-200 text-amber-800">
<AlertCircle className="w-4 h-4 text-amber-600" />
<AlertTitle className="text-amber-900 font-bold">FDD Assignment Required</AlertTitle>
<AlertDescription className="text-amber-800 font-medium">
This application is pending financial due diligence. Please assign an FDD Agency to proceed with the audit.
</AlertDescription>
</Alert>
)}
{permissions.canApprove && ( {permissions.canApprove && (
<> <>
<Button <Button
@ -3604,7 +3543,7 @@ export const ApplicationDetails = () => {
onClick={() => setShowApproveModal(true)} onClick={() => setShowApproveModal(true)}
> >
<CheckCircle className="w-4 h-4 mr-2" /> <CheckCircle className="w-4 h-4 mr-2" />
Approve {['Inauguration', 'Approved'].includes(application.status) ? 'Onboard Dealer' : 'Approve'}
</Button> </Button>
<Button <Button
@ -3652,7 +3591,8 @@ export const ApplicationDetails = () => {
</Button> </Button>
)} )}
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && ( {currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) &&
['Dealer Code Generation', 'LOA Pending', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion'].includes(application.status) && (
<> <>
{!application.dealerCode && ( {!application.dealerCode && (
<Button <Button
@ -3664,7 +3604,7 @@ export const ApplicationDetails = () => {
</Button> </Button>
)} )}
{application.dealerCode && ( {application.dealerCode && !application.architectureAssignedTo && (
<Button <Button
variant="outline" variant="outline"
className="w-full border-blue-200 hover:bg-blue-50 text-blue-700" className="w-full border-blue-200 hover:bg-blue-50 text-blue-700"
@ -3677,16 +3617,7 @@ export const ApplicationDetails = () => {
</> </>
)} )}
{((currentUser && currentUser.id === application.architectureAssignedTo) || (currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role))) &&
application.architectureStatus === 'IN_PROGRESS' && (
<Button
className="w-full bg-blue-600 hover:bg-blue-700"
onClick={() => setShowArchitectureStatusModal(true)}
>
<CheckCircle className="w-4 h-4 mr-2" />
Complete Architecture Work
</Button>
)}
{/* Show Interview Feedback only if active interview exists AND feedback NOT submitted */} {/* Show Interview Feedback only if active interview exists AND feedback NOT submitted */}
{activeInterviewForUser && !hasSubmittedFeedback && ( {activeInterviewForUser && !hasSubmittedFeedback && (
@ -3728,27 +3659,6 @@ export const ApplicationDetails = () => {
</> </>
)} )}
{/* Dedicated Onboarding Button - Appears ONLY when everything is ready (last step) */}
{isAdmin && application.status === 'Inauguration' && !application.dealer && (
<div className="space-y-2">
{eorProgress < 100 && (
<Alert variant="destructive" className="bg-amber-50 border-amber-200 text-amber-800 py-2">
<AlertCircle className="h-4 w-4 text-amber-600" />
<AlertDescription className="text-xs">
EOR Checklist must be 100% complete before onboarding. (Current: {eorProgress.toFixed(0)}%)
</AlertDescription>
</Alert>
)}
<Button
className="w-full bg-green-600 hover:bg-green-700 font-bold shadow-lg shadow-green-100 disabled:bg-slate-300 disabled:text-slate-500"
onClick={() => setShowOnboardModal(true)}
disabled={eorProgress < 100}
>
<CheckCircle className="w-4 h-4 mr-2" />
Onboard as Dealer (Final Step)
</Button>
</div>
)}
{/* Dealer Onboarded Status & Link */} {/* Dealer Onboarded Status & Link */}
{application.dealer && ( {application.dealer && (
@ -5200,7 +5110,7 @@ export const ApplicationDetails = () => {
</Dialog> </Dialog>
</div> </div>
</div> </div>
</div> </div>
); );
}; };

View File

@ -8,6 +8,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';
import { Label } from '../ui/label'; import { Label } from '../ui/label';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data'; import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -81,16 +82,31 @@ const getStatusColor = (status: string) => {
return 'bg-slate-100 text-slate-700 border-slate-300'; return 'bg-slate-100 text-slate-700 border-slate-300';
}; };
const normalizeConstitutionType = (value: string) => {
const input = String(value || '').trim().toLowerCase();
if (!input) return '';
if (input.includes('proprietor')) return 'Proprietorship';
if (input.includes('partner')) return 'Partnership';
if (input.includes('llp')) return 'LLP';
if (input.includes('private') || input.includes('pvt')) return 'Pvt Ltd';
return value;
};
export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: ConstitutionalChangeDetailsProps) { export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: ConstitutionalChangeDetailsProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false); const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve'); const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
const [comments, setComments] = useState(''); const [comments, setComments] = useState('');
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [selectedDocType, setSelectedDocType] = useState<number | null>(null);
const [uploadFile, setUploadFile] = useState<File | null>(null);
const [activeMainTab, setActiveMainTab] = useState('workflow');
const [activeDocumentTab, setActiveDocumentTab] = useState('required');
const [request, setRequest] = useState<any>(null); const [request, setRequest] = useState<any>(null);
const [auditLogs, setAuditLogs] = useState<any[]>([]); const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isActionLoading, setIsActionLoading] = useState(false); const [isActionLoading, setIsActionLoading] = useState(false);
const [isUploadingDoc, setIsUploadingDoc] = useState(false);
useEffect(() => { useEffect(() => {
fetchRequestDetails(); fetchRequestDetails();
@ -144,8 +160,14 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
); );
} }
// Get required documents for this request // Get required documents for this request (normalized mapping handles values like "LLP Conversion")
const requiredDocs = documentRequirements[request.changeType] || []; const normalizedChangeType = normalizeConstitutionType(request.changeType);
const requiredDocs = documentRequirements[normalizedChangeType] || [];
const uploadedDocNumbers = new Set(
(request.documents || [])
.map((doc: any) => Number(doc?.docNumber))
.filter((num: number) => !Number.isNaN(num) && num > 0)
);
// Calculate current stage index mapping to backend stages // Calculate current stage index mapping to backend stages
const getCurrentStageIndex = () => { const getCurrentStageIndex = () => {
@ -165,6 +187,27 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
const currentStageIndex = getCurrentStageIndex(); const currentStageIndex = getCurrentStageIndex();
const getLatestStageTimelineEntry = (stageName: string) => {
const aliases: Record<string, string[]> = {
'Submitted': ['Submitted', 'Draft'],
'ASM Review': ['ASM 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'],
'NBH Approval': ['NBH Approval'],
'Legal Review': ['Legal Review'],
'Completed': ['Completed']
};
const stageAliases = aliases[stageName] || [stageName];
const entries = (request.timeline || []).filter((entry: any) => {
const entryStage = String(entry.stage || entry.targetStage || '').trim();
return stageAliases.includes(entryStage);
});
return entries.length ? entries[entries.length - 1] : null;
};
// Centralized Permissions Utility (Fixes security gap where buttons showed for everyone) // Centralized Permissions Utility (Fixes security gap where buttons showed for everyone)
const getConstitutionalPermissions = () => { const getConstitutionalPermissions = () => {
if (!request || !currentUser) { if (!request || !currentUser) {
@ -222,6 +265,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
setIsActionDialogOpen(false); setIsActionDialogOpen(false);
setComments(''); setComments('');
fetchRequestDetails(); fetchRequestDetails();
fetchAuditLogs();
} }
} catch (error) { } catch (error) {
console.error('Submit action error:', error); console.error('Submit action error:', error);
@ -231,9 +275,67 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
} }
}; };
const handleUploadDocument = () => { const handleUploadDocument = async () => {
if (!selectedDocType || !uploadFile) {
toast.error('Please select document type and file');
return;
}
try {
setIsUploadingDoc(true);
const existingDocs = Array.isArray(request.documents) ? [...request.documents] : [];
const existingIndex = existingDocs.findIndex((d: any) => d.docNumber === selectedDocType);
const payloadDoc = {
docNumber: selectedDocType,
name: documentNames[selectedDocType],
fileName: uploadFile.name,
status: 'Pending Verification',
uploadedOn: new Date().toISOString(),
uploadedBy: currentUser?.fullName || 'Dealer'
};
if (existingIndex >= 0) existingDocs[existingIndex] = { ...existingDocs[existingIndex], ...payloadDoc };
else existingDocs.push(payloadDoc);
const response = await API.uploadConstitutionalDocuments(requestId, existingDocs) as any;
if (response.data?.success) {
toast.success('Document uploaded successfully'); toast.success('Document uploaded successfully');
setIsUploadDialogOpen(false); setIsUploadDialogOpen(false);
setSelectedDocType(null);
setUploadFile(null);
fetchRequestDetails();
} else {
toast.error('Failed to upload document');
}
} catch (error) {
console.error('Upload document error:', error);
toast.error('Failed to upload document');
} finally {
setIsUploadingDoc(false);
}
};
const handleVerifyDocument = async (targetDoc: any, targetIndex: number) => {
try {
const existingDocs = Array.isArray(request.documents) ? [...request.documents] : [];
const updatedDocs = existingDocs.map((doc: any, index: number) => {
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' };
});
const response = await API.uploadConstitutionalDocuments(requestId, updatedDocs) as any;
if (response.data?.success) {
toast.success('Document verified successfully');
fetchRequestDetails();
} else {
toast.error('Failed to verify document');
}
} catch (error) {
console.error('Verify document error:', error);
toast.error('Failed to verify document');
}
}; };
return ( return (
@ -344,7 +446,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{/* Main Content */} {/* Main Content */}
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-2 space-y-6">
<Card> <Card>
<Tabs defaultValue="workflow" className="w-full"> <Tabs value={activeMainTab} onValueChange={setActiveMainTab} className="w-full">
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<div className="overflow-x-auto -mx-6 px-6"> <div className="overflow-x-auto -mx-6 px-6">
<TabsList className="w-max min-w-full justify-start"> <TabsList className="w-max min-w-full justify-start">
@ -377,6 +479,8 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{workflowStages.map((stage, index) => { {workflowStages.map((stage, index) => {
const isCompleted = index < currentStageIndex - 1; const isCompleted = index < currentStageIndex - 1;
const isCurrent = index === currentStageIndex - 1; const isCurrent = index === currentStageIndex - 1;
const timelineEntry = getLatestStageTimelineEntry(stage.name);
const explicitFeedback = timelineEntry?.comments || timelineEntry?.feedback || timelineEntry?.remarks;
return ( return (
<div key={stage.id} className="flex items-start gap-4"> <div key={stage.id} className="flex items-start gap-4">
@ -421,6 +525,21 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'} {isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'}
</Badge> </Badge>
</div> </div>
{timelineEntry && (
<div className="mt-3 space-y-2">
<div className="flex items-center gap-2 text-xs text-slate-600">
<Badge variant="outline" className="text-[11px] normal-case">
Last updated by: {timelineEntry.user || timelineEntry.userName || 'System'}
</Badge>
<span>{formatDateTime(timelineEntry.timestamp || timelineEntry.createdAt)}</span>
</div>
{explicitFeedback && (
<div className="p-2 rounded border border-slate-200 bg-slate-50 text-sm text-slate-700">
{explicitFeedback}
</div>
)}
</div>
)}
</div> </div>
</div> </div>
); );
@ -430,7 +549,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{/* Documents Tab */} {/* Documents Tab */}
<TabsContent value="documents" className="mt-0"> <TabsContent value="documents" className="mt-0">
<Tabs defaultValue="required" className="w-full"> <Tabs value={activeDocumentTab} onValueChange={setActiveDocumentTab} className="w-full">
<TabsList className="w-full justify-start mb-4"> <TabsList className="w-full justify-start mb-4">
<TabsTrigger value="required">Required for Process</TabsTrigger> <TabsTrigger value="required">Required for Process</TabsTrigger>
<TabsTrigger value="existing">Existing Documents</TabsTrigger> <TabsTrigger value="existing">Existing Documents</TabsTrigger>
@ -458,17 +577,30 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Label>Document Type</Label> <Label>Document Type</Label>
<select className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md"> <Select
{requiredDocs.map(docNum => ( value={selectedDocType ? String(selectedDocType) : ''}
<option key={docNum} value={docNum}> onValueChange={(value) => setSelectedDocType(Number(value))}
{documentNames[docNum]} >
</option> <SelectTrigger className="w-full mt-1">
<SelectValue placeholder="Select document type" />
</SelectTrigger>
<SelectContent>
{requiredDocs.map((docNum) => (
<SelectItem key={docNum} value={String(docNum)}>
<span className="flex w-full items-center justify-between">
<span>{documentNames[docNum]}</span>
{uploadedDocNumbers.has(docNum) && (
<CheckCircle2 className="w-4 h-4 text-green-600" />
)}
</span>
</SelectItem>
))} ))}
</select> </SelectContent>
</Select>
</div> </div>
<div> <div>
<Label>Upload File</Label> <Label>Upload File</Label>
<Input type="file" className="mt-1" /> <Input type="file" className="mt-1" onChange={(e) => setUploadFile(e.target.files?.[0] || null)} />
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
@ -478,8 +610,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<Button <Button
className="bg-amber-600 hover:bg-amber-700" className="bg-amber-600 hover:bg-amber-700"
onClick={handleUploadDocument} onClick={handleUploadDocument}
disabled={isUploadingDoc}
> >
Upload {isUploadingDoc ? 'Uploading...' : 'Upload'}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@ -556,7 +689,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{formatDateTime(doc.uploadedOn || doc.createdAt)} {formatDateTime(doc.uploadedOn || doc.createdAt)}
</TableCell> </TableCell>
<TableCell className="text-slate-600"> <TableCell className="text-slate-600">
{doc.uploadedBy || 'Dealer'} {typeof doc.uploadedBy === 'string' ? doc.uploadedBy : (doc.uploadedBy?.fullName || 'Dealer')}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge className={getStatusColor(doc.status)}> <Badge className={getStatusColor(doc.status)}>
@ -565,14 +698,21 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button size="sm" variant="outline"> <Button size="sm" variant="outline" className="h-8 w-8 p-0" title="View document">
<Eye className="w-4 h-4 mr-1" /> <Eye className="w-4 h-4" />
View
</Button> </Button>
<Button size="sm" variant="outline"> <Button size="sm" variant="outline" className="h-8 w-8 p-0" title="Download document">
<Download className="w-4 h-4 mr-1" /> <Download className="w-4 h-4" />
Download
</Button> </Button>
{doc.status !== 'Verified' && currentUser?.role !== 'Dealer' && (
<Button
size="sm"
className="bg-green-600 hover:bg-green-700"
onClick={() => handleVerifyDocument(doc, index)}
>
Verify
</Button>
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -95,6 +95,17 @@ export function FDDApplicationDetails() {
try { try {
const response: any = await API.uploadDocument(id!, formData); const response: any = await API.uploadDocument(id!, formData);
if (response.data?.success) { if (response.data?.success) {
// Automatically link if it's the final report category
if (selectedDocType === 'FDD Final Audit Report') {
const docId = response.data.data?.id || response.data.id;
await API.submitFddReport({
assignmentId: assignment?.id,
applicationId: id,
reportDocumentId: docId,
findings: 'Final Audit Report submitted.',
recommendation: 'REVIEW_PENDING'
});
}
toast.success(`${selectedDocType} uploaded successfully`); toast.success(`${selectedDocType} uploaded successfully`);
fetchApplication(); fetchApplication();
setSelectedDocType(''); setSelectedDocType('');
@ -146,6 +157,17 @@ export function FDDApplicationDetails() {
return ( return (
<div className="flex flex-col gap-6 max-w-7xl mx-auto mb-10"> <div className="flex flex-col gap-6 max-w-7xl mx-auto mb-10">
{application?.statutoryStatus === 'Flagged' && (
<div className="bg-red-50 border border-red-200 p-4 rounded-xl flex items-center gap-4 animate-in fade-in slide-in-from-top-4 duration-500">
<div className="bg-red-100 p-2 rounded-lg">
<AlertTriangle className="w-5 h-5 text-red-600" />
</div>
<div>
<h4 className="text-sm font-bold text-red-900 leading-none">APPLICATION FLAGGED BY YOU</h4>
<p className="text-red-700 text-[10px] font-bold uppercase tracking-wider mt-1 opacity-80">Marked as non-responsive for follow-up by DD Team</p>
</div>
</div>
)}
{/* Action Bar */} {/* Action Bar */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<button <button
@ -166,57 +188,14 @@ export function FDDApplicationDetails() {
) : !isCompleted ? ( ) : !isCompleted ? (
<> <>
{isFddRole && ( {isFddRole && (
<>
<button
disabled={uploading}
onClick={() => {
const input = document.createElement('input');
input.type = 'file';
input.onchange = async (e: any) => {
const file = e.target.files[0];
if (!file) return;
try {
setUploading(true);
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', 'FDD Final Audit Report');
formData.append('stage', 'FDD');
formData.append('applicationId', application.id);
const res: any = await API.uploadDocument(application.id, formData);
if (res.data?.success) {
toast.success('FDD Final Audit Report uploaded successfully');
fetchApplication();
}
} catch (err) {
toast.error('Upload failed');
} finally {
setUploading(false);
}
};
input.click();
}}
className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 text-slate-700 font-bold text-xs uppercase tracking-wider rounded-lg hover:bg-slate-50 transition-all"
>
<Upload className="w-4 h-4" />
{uploading ? 'Uploading...' : 'Upload Report'}
</button>
<button
disabled={uploading}
onClick={() => setShowFinalizeModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white font-bold text-xs uppercase tracking-wider rounded-lg hover:bg-slate-800 transition-all shadow-lg shadow-slate-200"
>
<CheckCircle2 className="w-4 h-4" />
{uploading ? 'Processing...' : 'Submit Final Findings'}
</button>
<button <button
disabled={uploading} disabled={uploading}
onClick={() => setShowFlagModal(true)} onClick={() => setShowFlagModal(true)}
className="px-4 py-2 text-red-600 font-bold text-xs uppercase tracking-wider hover:bg-red-50 rounded-lg transition-all" className="px-4 py-2 bg-red-50 text-red-600 font-bold text-xs uppercase tracking-wider hover:bg-red-100 rounded-lg transition-all flex items-center gap-2 border border-red-100 shadow-sm"
> >
<AlertTriangle className="w-4 h-4" />
Flag Non-Responsive Flag Non-Responsive
</button> </button>
</>
)} )}
</> </>
) : ( ) : (
@ -582,9 +561,11 @@ export function FDDApplicationDetails() {
return; return;
} }
setUploading(true); setUploading(true);
const latestReport = assignment?.reports?.[0];
const res: any = await API.submitFddReport({ const res: any = await API.submitFddReport({
assignmentId: assignment?.id, assignmentId: assignment?.id,
applicationId: id, applicationId: id,
reportDocumentId: latestReport?.reportDocumentId,
findings: fddAuditFindings, findings: fddAuditFindings,
recommendation: null recommendation: null
}); });
@ -646,10 +627,10 @@ export function FDDApplicationDetails() {
onClick={async () => { onClick={async () => {
try { try {
setUploading(true); setUploading(true);
await API.addWorknote({ // Use dedicated API that updates model AND creates a specific Audit Log entry
requestId: id, await API.flagNonResponsive({
requestType: 'application', applicationId: id,
content: 'FLAGGED: Applicant is non-responsive to FDD queries.' remarks: 'Applicant is non-responsive to FDD queries.'
}); });
toast.error('Application flagged for non-responsiveness.'); toast.error('Application flagged for non-responsiveness.');
setShowFlagModal(false); setShowFlagModal(false);

View File

@ -49,7 +49,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
const getRelevantPaymentStatus = (app: any) => { const getRelevantPaymentStatus = (app: any) => {
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment'; if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment';
const s = app.overallStatus || app.status; const s = app.overallStatus || app.status;
const relevantType = (s.includes('LOI') || s === 'PAYMENT_VERIFICATION' || s === 'Security Details') ? 'SECURITY_DEPOSIT' : 'FIRST_FILL'; 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); const deposit = app.securityDeposits.find((d: any) => d.depositType === relevantType);
return deposit ? deposit.status : 'Awaiting Payment'; return deposit ? deposit.status : 'Awaiting Payment';
}; };
@ -59,7 +59,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
const s = app.overallStatus || app.status; const s = app.overallStatus || app.status;
return [ return [
'LOI In Progress', 'Security Details', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation', 'LOI In Progress', 'Security Details', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT' 'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT', 'Payment Pending'
].includes(s); ].includes(s);
}); });

View File

@ -73,37 +73,22 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const [isBankModalOpen, setIsBankModalOpen] = useState(false); const [isBankModalOpen, setIsBankModalOpen] = useState(false);
const [editingBank, setEditingBank] = useState<any>(null); const [editingBank, setEditingBank] = useState<any>(null);
const [isSubmittingBank, setIsSubmittingBank] = useState(false); const [isSubmittingBank, setIsSubmittingBank] = useState(false);
const [departments, setDepartments] = useState<string[]>([]);
const [showClearanceDialog, setShowClearanceDialog] = useState(false); const [showClearanceDialog, setShowClearanceDialog] = useState(false);
const [selectedDept, setSelectedDept] = useState<any>(null); const [selectedDept, setSelectedDept] = useState<any>(null);
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false); const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
const [clearanceForm, setClearanceForm] = useState({ const [clearanceForm, setClearanceForm] = useState({
status: 'Pending', remarks: "",
remarks: '',
amount: 0, amount: 0,
type: 'Recovery' type: "Recovery",
}); });
const [clearanceFile, setClearanceFile] = useState<File | null>(null); const [clearanceFile, setClearanceFile] = useState<File | null>(null);
useEffect(() => { useEffect(() => {
fetchDepartments();
fetchFnFDetails(); fetchFnFDetails();
fetchAuditLogs(); fetchAuditLogs();
}, [fnfId]); }, [fnfId]);
const fetchDepartments = async () => {
try {
const response = await API.getSettlementDepartments();
const data = response.data as any;
if (data && data.success) {
setDepartments(data.departments);
}
} catch (error) {
console.error("Fetch departments error:", error);
}
};
const normalizeDepartment = (name: string) => { const normalizeDepartment = (name: string) => {
if (!name) return name; if (!name) return name;
let inputName = name.trim(); let inputName = name.trim();
@ -230,6 +215,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
return { return {
id: c?.id || `dept-${deptName}`, id: c?.id || `dept-${deptName}`,
clearanceId: c?.id || null,
departmentName: deptName, departmentName: deptName,
status: c?.status || "Pending", status: c?.status || "Pending",
amountType: netAmount > 0 ? "Payable" : netAmount < 0 ? "Recovery" : null, amountType: netAmount > 0 ? "Payable" : netAmount < 0 ? "Recovery" : null,
@ -398,23 +384,45 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
currentUser.role, currentUser.role,
); );
const canRespondToDepartment = (departmentName: string) => {
const role = String(currentUser?.role || "").toLowerCase();
if (!role) return false;
const isGlobalResponder =
role.includes("super admin") ||
role.includes("finance") ||
role.includes("dd admin");
if (isGlobalResponder) return true;
const deptKeyword = departmentName.replace(" Department", "").toLowerCase();
return role.includes(deptKeyword);
};
const canAnyDepartmentRespond = (fnfCase?.departmentResponses || []).some((dept: any) =>
canRespondToDepartment(dept.departmentName),
);
const handleUpdateClearance = async () => { const handleUpdateClearance = async () => {
if (!selectedDept || !fnfId) return; if (!selectedDept?.clearanceId || !fnfId) {
toast.error("Clearance record not available for this department");
return;
}
try { try {
setIsUpdatingClearance(true); setIsUpdatingClearance(true);
const formData = new FormData(); const formData = new FormData();
const derivedStatus = Number(clearanceForm.amount) > 0 ? 'Dues Pending' : 'NOC Submitted'; const derivedStatus = Number(clearanceForm.amount) > 0 ? "Dues Pending" : "NOC Submitted";
formData.append('status', derivedStatus); formData.append("status", derivedStatus);
formData.append('remarks', clearanceForm.remarks); formData.append("remarks", clearanceForm.remarks);
formData.append('amount', String(clearanceForm.amount)); formData.append("amount", String(clearanceForm.amount));
formData.append('type', clearanceForm.type); formData.append("type", clearanceForm.type);
if (clearanceFile) formData.append('file', clearanceFile); if (clearanceFile) formData.append("file", clearanceFile);
await API.updateFnFClearance(fnfId, selectedDept.id, formData); await API.updateFnFClearance(fnfId, selectedDept.clearanceId, formData);
toast.success(`Clearance updated for ${selectedDept.departmentName}`); toast.success(`Clearance updated for ${selectedDept.departmentName}`);
setShowClearanceDialog(false); setShowClearanceDialog(false);
setClearanceFile(null);
fetchFnFDetails(); fetchFnFDetails();
} catch (error) { } catch (error) {
console.error("Update clearance error:", error); console.error("Update clearance error:", error);
@ -1294,9 +1302,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TableHead>Amount</TableHead> <TableHead>Amount</TableHead>
<TableHead>Submitted Date</TableHead> <TableHead>Submitted Date</TableHead>
<TableHead>Remarks</TableHead> <TableHead>Remarks</TableHead>
{(currentUser?.role === 'Super Admin' || currentUser?.role === 'Finance Admin' || currentUser?.role === 'DD Admin' || departments.some(d => currentUser?.role?.includes(d.replace(' Department', '')))) && ( {canAnyDepartmentRespond && <TableHead>Actions</TableHead>}
<TableHead>Actions</TableHead>
)}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@ -1341,8 +1347,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TableCell className="max-w-xs truncate"> <TableCell className="max-w-xs truncate">
{dept.remarks || "-"} {dept.remarks || "-"}
</TableCell> </TableCell>
{(currentUser?.role === 'Super Admin' || currentUser?.role === 'Finance Admin' || currentUser?.role === 'DD Admin' || (currentUser?.role && currentUser.role.includes(dept.departmentName.replace(' Department', '')))) && ( {canAnyDepartmentRespond && (
<TableCell> <TableCell>
{canRespondToDepartment(dept.departmentName) ? (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@ -1350,16 +1357,19 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
onClick={() => { onClick={() => {
setSelectedDept(dept); setSelectedDept(dept);
setClearanceForm({ setClearanceForm({
status: dept.status, remarks: dept.remarks === "-" ? "" : dept.remarks,
remarks: dept.remarks === '-' ? '' : dept.remarks,
amount: dept.amount || 0, amount: dept.amount || 0,
type: dept.amountType || 'Recovery' type: dept.amountType || "Recovery",
}); });
setClearanceFile(null);
setShowClearanceDialog(true); setShowClearanceDialog(true);
}} }}
> >
Update Action
</Button> </Button>
) : (
<span className="text-slate-400 text-sm">-</span>
)}
</TableCell> </TableCell>
)} )}
</TableRow> </TableRow>
@ -1668,9 +1678,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
auditLogs.map((log: any) => ( auditLogs.map((log: any) => (
<div <div
key={log.id} key={log.id}
className="flex gap-4 pb-4 border-b border-slate-200 last:border-0" className="flex gap-3 pb-4 border-b border-slate-100 last:border-0"
> >
<div className="w-2 h-2 rounded-full bg-amber-600 mt-2" /> <div className="w-2 h-2 rounded-full bg-slate-400 mt-2" />
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<p className="font-semibold text-slate-900 flex items-center gap-2"> <p className="font-semibold text-slate-900 flex items-center gap-2">
@ -1686,17 +1696,17 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</> </>
)} )}
</p> </p>
<span className="text-sm text-slate-600 font-mono"> <span className="text-xs text-slate-500">
{formatDateTime(log.createdAt || log.timestamp)} {formatDateTime(log.createdAt || log.timestamp)}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-sm text-slate-600 mb-2"> <div className="flex items-center gap-2 text-sm text-slate-600 mb-2">
<Badge variant="outline" className="text-[10px] uppercase">{log.userName}</Badge> <Badge variant="outline" className="text-[10px] uppercase">{log.actor?.name || log.userName || 'System'}</Badge>
</div> </div>
{(log.newData?.remarks || log.remarks) && ( {(log.newData?.remarks || log.remarks) && (
<div className="mt-2 p-3 bg-blue-50 border-l-4 border-blue-400 rounded-r text-sm italic text-blue-800"> <div className="mt-2 p-3 bg-slate-50 border border-slate-200 rounded text-sm text-slate-700">
" {log.newData?.remarks || log.remarks} " {log.newData?.remarks || log.remarks}
</div> </div>
)} )}
@ -1773,19 +1783,16 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Clearance Update Dialog */}
<Dialog open={showClearanceDialog} onOpenChange={setShowClearanceDialog}> <Dialog open={showClearanceDialog} onOpenChange={setShowClearanceDialog}>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[460px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Update {selectedDept?.departmentName} Clearance</DialogTitle> <DialogTitle>Update {selectedDept?.departmentName} Response</DialogTitle>
<DialogDescription> <DialogDescription>
Mark the department as cleared or report pending dues with amount. Provide dues/NOC response with remarks and optional supporting proof.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-2">
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="type" className="text-right">Type</Label> <Label htmlFor="type" className="text-right">Type</Label>
<select <select
@ -1796,7 +1803,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
> >
<option value="Recovery">Recovery (from Dealer)</option> <option value="Recovery">Recovery (from Dealer)</option>
<option value="Payable">Payable (to Dealer)</option> <option value="Payable">Payable (to Dealer)</option>
<option value="Deduction">Deduction (Penalties)</option> <option value="Deduction">Deduction</option>
</select> </select>
</div> </div>
@ -1819,16 +1826,16 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<textarea <textarea
id="remarks" id="remarks"
className="col-span-3 flex min-h-[80px] w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm" className="col-span-3 flex min-h-[80px] w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
placeholder="Enter description or dues details..." placeholder="Add response details..."
value={clearanceForm.remarks} value={clearanceForm.remarks}
onChange={(e) => setClearanceForm({ ...clearanceForm, remarks: e.target.value })} onChange={(e) => setClearanceForm({ ...clearanceForm, remarks: e.target.value })}
/> />
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="file" className="text-right">Proof</Label> <Label htmlFor="proof" className="text-right">Proof</Label>
<input <input
id="file" id="proof"
type="file" type="file"
className="col-span-3 text-sm" className="col-span-3 text-sm"
onChange={(e) => setClearanceFile(e.target.files?.[0] || null)} onChange={(e) => setClearanceFile(e.target.files?.[0] || null)}
@ -1837,13 +1844,15 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setShowClearanceDialog(false)}>Cancel</Button> <Button variant="outline" onClick={() => setShowClearanceDialog(false)}>
Cancel
</Button>
<Button <Button
className="bg-amber-600 hover:bg-blue-700" className="bg-amber-600 hover:bg-blue-700"
onClick={handleUpdateClearance} onClick={handleUpdateClearance}
disabled={isUpdatingClearance} disabled={isUpdatingClearance}
> >
{isUpdatingClearance ? "Updating..." : "Save Changes"} {isUpdatingClearance ? "Saving..." : "Submit Response"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@ -83,16 +83,20 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
const handleSaveDetails = async () => { const handleSaveDetails = async () => {
setIsSaving(true); setIsSaving(true);
try { try {
console.log('Saving business details for:', id, form);
const response: any = await API.updateApplication(id, form); const response: any = await API.updateApplication(id, form);
if (response.data?.success || response.ok) {
toast.success('Business details updated successfully'); if (response.ok) {
fetchData(); toast.success('Business details saved successfully');
await fetchData();
} else { } else {
toast.error(response.data?.message || 'Update failed'); const errorMsg = response.data?.message || 'Failed to update business details';
toast.error(errorMsg);
console.error('Update failed:', response);
} }
} catch (error) { } catch (error: any) {
console.error('Save details error:', error); console.error('Save details fatal error:', error);
toast.error('Failed to save details'); toast.error(error.message || 'A network error occurred while saving');
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }

View File

@ -1,4 +1,4 @@
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, Upload, Eye, AlertCircle, Loader2 } from 'lucide-react'; import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, AlertCircle, Loader2, Upload } from 'lucide-react';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
@ -7,6 +7,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Label } from '../ui/label'; import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Input } from '../ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -17,6 +18,8 @@ import { resignationService } from '../../services/resignation.service';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils'; 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 = [ const ALL_DEPARTMENTS = [
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department', 'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
@ -81,11 +84,49 @@ const STAGE_TO_ROLE_MAP: Record<string, string> = {
'Legal': 'Legal Admin' 'Legal': 'Legal Admin'
}; };
const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
'ASM': ['ASM', 'ASM Review', 'Submission', 'Submitted'],
'RBM': ['RBM', 'RBM Review', 'Regional Review'],
'ZBH': ['ZBH', 'ZBH Review', 'ZM Review'],
'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'],
'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
'DD Admin': ['DD Admin', 'DD Admin Review'],
'Legal': ['Legal', 'Legal - Resignation Letter'],
'F&F Initiated': ['F&F Initiated', 'FNF_INITIATED', 'FNF Initiated'],
'Completed': ['Completed']
};
export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) { export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) {
const getDocumentsForStage = (stageName: string, stageKey?: string) => {
const allDocs = [
...(resignationData?.documents || []),
...(resignationData?.uploadedDocuments || [])
];
const baseAliases = [
stageName,
stageKey,
...(stageKey ? (RESIGNATION_STAGE_ALIASES[stageKey] || []) : []),
...(RESIGNATION_STAGE_ALIASES[stageName] || [])
]
.filter(Boolean)
.map((value: string) => value.trim().toLowerCase());
return allDocs.filter((doc: any) => {
if (!doc?.stage) return false;
const docStage = String(doc.stage).trim().toLowerCase();
return baseAliases.includes(docStage);
});
};
const navigate = useNavigate(); const navigate = useNavigate();
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null }); const [actionDialog, setActionDialog] = useState<{ open: boolean, type: 'approve' | 'reject' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null });
const [remarks, setRemarks] = useState(''); const [remarks, setRemarks] = useState('');
const [assignToUser, setAssignToUser] = useState(''); const [assignToUser, setAssignToUser] = useState<string>('');
const [userSearchQuery, setUserSearchQuery] = useState('');
const [selectedSpecificUser, setSelectedSpecificUser] = useState<string>('');
const [availableUsers, setAvailableUsers] = useState<any[]>([]);
const [isLoadingUsers, setIsLoadingUsers] = useState(false);
const [forceTriggerFnF, setForceTriggerFnF] = useState(false); const [forceTriggerFnF, setForceTriggerFnF] = useState(false);
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] }); const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
const [resignationData, setResignationData] = useState<any>(null); // Real data from API const [resignationData, setResignationData] = useState<any>(null); // Real data from API
@ -93,6 +134,10 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const [auditLogs, setAuditLogs] = useState<any[]>([]); const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [previewDocument, setPreviewDocument] = useState<any>(null); const [previewDocument, setPreviewDocument] = useState<any>(null);
const [showUploadDialog, setShowUploadDialog] = useState(false);
const [uploadFile, setUploadFile] = useState<File | null>(null);
const [uploadDocType, setUploadDocType] = useState(RESIGNATION_DOCUMENT_TYPES[0]);
const [uploadStage, setUploadStage] = useState('');
const fetchResignation = async () => { const fetchResignation = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
@ -191,8 +236,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
setActionDialog({ open: true, type }); setActionDialog({ open: true, type });
}; };
const handleViewStageDocuments = (stageName: string) => { const handleViewStageDocuments = (stageName: string, stageKey?: string) => {
const documents = resignationData?.documents?.filter((d: any) => d.stage === stageName) || []; const documents = getDocumentsForStage(stageName, stageKey).map((doc: any, index: number) => ({
id: doc.id || `${stageName}-${index}`,
name: doc.name || doc.fileName || 'Document',
type: doc.type || doc.documentType || 'Document',
uploadDate: doc.uploadDate || (doc.createdAt ? formatDateTime(doc.createdAt) : 'N/A'),
uploader: typeof doc.uploader === 'string'
? doc.uploader
: (doc.uploader?.fullName || doc.uploadedBy || 'System'),
filePath: doc.filePath || doc.path
}));
setStageDocumentsDialog({ open: true, stageName, documents }); setStageDocumentsDialog({ open: true, stageName, documents });
}; };
@ -202,7 +256,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
return; return;
} }
if (actionDialog.type === 'assign' && !assignToUser) { if (actionDialog.type === 'assign' && !assignToUser) {
toast.error('Please select a user'); toast.error('Please select a designation');
return; return;
} }
@ -211,7 +265,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const payload = { const payload = {
action: actionDialog.type, action: actionDialog.type,
remarks, remarks,
assignTo: assignToUser, assignTo: selectedSpecificUser || assignToUser, // Use specific user if selected, otherwise fallback to role for auto-resolution
force: forceTriggerFnF force: forceTriggerFnF
}; };
@ -221,6 +275,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
setActionDialog({ open: false, type: null }); setActionDialog({ open: false, type: null });
setRemarks(''); setRemarks('');
setAssignToUser(''); setAssignToUser('');
setSelectedSpecificUser('');
setAvailableUsers([]);
fetchResignation(); fetchResignation();
} }
} catch (error: any) { } catch (error: any) {
@ -231,6 +287,74 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
} }
}; };
const handleUploadDocument = async () => {
if (!uploadFile) {
toast.error('Please select a file to upload');
return;
}
try {
setIsSubmitting(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');
setShowUploadDialog(false);
setUploadFile(null);
setUploadDocType(RESIGNATION_DOCUMENT_TYPES[0]);
setUploadStage('');
fetchResignation();
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to upload document');
} finally {
setIsSubmitting(false);
}
};
useEffect(() => {
const fetchUsers = async () => {
// Fetch users if designation is selected OR search query is typed
if (actionDialog.type === 'assign' && (assignToUser || userSearchQuery)) {
const timeoutId = setTimeout(async () => {
try {
setIsLoadingUsers(true);
const roleMap: Record<string, string> = {
'asm': 'ASM',
'rbm': 'RBM',
'zbh': 'ZBH',
'nbh': 'NBH',
'legal': 'Legal Admin'
};
const params: any = {
limit: 20,
search: userSearchQuery
};
if (assignToUser) {
params.roleCode = roleMap[assignToUser] || assignToUser;
}
const res: any = await API.getUsers(params);
if (res.data?.success) {
setAvailableUsers(res.data.data);
}
} catch (error) {
console.error('Error fetching users:', error);
} finally {
setIsLoadingUsers(false);
}
}, 300); // 300ms debounce
return () => clearTimeout(timeoutId);
}
};
fetchUsers();
}, [assignToUser, userSearchQuery, actionDialog.type]);
if (isLoading && !resignationData) { if (isLoading && !resignationData) {
return ( return (
@ -374,7 +498,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<TabsList className="bg-slate-100 p-1"> <TabsList className="bg-slate-100 p-1">
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger> <TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
<TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger> <TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger>
<TabsTrigger value="clearances" className="data-[state=active]:bg-white">Clearances</TabsTrigger> {currentUser?.role !== 'Dealer' && <TabsTrigger value="clearances" className="data-[state=active]:bg-white">Clearances</TabsTrigger>}
<TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger> <TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger>
<TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger> <TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
</TabsList> </TabsList>
@ -506,7 +630,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="space-y-4"> <div className="space-y-4">
{progressStages.map((stage, index) => { {progressStages.map((stage, index) => {
const status = getStageStatus(stage.key); const status = getStageStatus(stage.key);
const timelineEntry = resignationData?.timeline?.find((t: any) => t.stage === stage.key || t.stage === stage.name); const stageDocumentCount = getDocumentsForStage(stage.name, stage.key).length;
const stageTimelineEntries = (resignationData?.timeline || []).filter(
(t: any) => t.stage === stage.key || t.stage === stage.name
);
const timelineEntry = stageTimelineEntries.length > 0
? stageTimelineEntries[stageTimelineEntries.length - 1]
: null;
return ( return (
<div key={stage.id} className="flex gap-4"> <div key={stage.id} className="flex gap-4">
@ -530,11 +660,22 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<h3 className={ <h3 className={
status === 'completed' ? 'text-green-600' : status === 'completed' ? 'text-green-600' :
status === 'active' ? 'text-amber-600' : status === 'active' ? 'text-amber-600' :
'text-slate-400' 'text-slate-400'
}>{stage.name}</h3> }>{stage.name}</h3>
{stageDocumentCount > 0 && (
<button
onClick={() => handleViewStageDocuments(stage.name, stage.key)}
className="flex items-center gap-1 px-2 py-1 rounded-full bg-amber-100 hover:bg-amber-200 text-amber-700 text-xs transition-colors cursor-pointer"
>
<FileText className="w-3 h-3" />
<span>{stageDocumentCount} {stageDocumentCount === 1 ? 'doc' : 'docs'}</span>
</button>
)}
</div>
{timelineEntry && ( {timelineEntry && (
<div className="flex items-center gap-1 text-sm text-slate-600"> <div className="flex items-center gap-1 text-sm text-slate-600">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
@ -542,20 +683,23 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div> </div>
)} )}
</div> </div>
<p className="text-slate-600 text-sm">{stage.description}</p> <p className="text-slate-600 text-sm mb-1">{stage.description}</p>
{timelineEntry && ( {timelineEntry && (
<div className="mt-2 bg-slate-50 p-2 rounded border border-slate-100 text-sm text-slate-600"> <div className="space-y-2">
{timelineEntry.comments || timelineEntry.remarks} <div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-slate-100 text-[10px] font-bold uppercase">
{timelineEntry.user || 'System'}
</Badge>
<span className="text-[10px] text-slate-500 italic">
{timelineEntry.action}
</span>
</div>
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm text-slate-700 shadow-sm">
{timelineEntry.comments || timelineEntry.remarks || 'No remarks provided.'}
</div>
</div> </div>
)} )}
<Button
variant="ghost"
size="sm"
className="mt-2 text-amber-600"
onClick={() => handleViewStageDocuments(stage.name)}
>
View Stage Documents
</Button>
</div> </div>
</div> </div>
); );
@ -566,6 +710,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</TabsContent> </TabsContent>
{/* Clearances Tab */} {/* Clearances Tab */}
{currentUser?.role !== 'Dealer' && (
<TabsContent value="clearances"> <TabsContent value="clearances">
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
@ -575,39 +720,43 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <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) => { {ALL_DEPARTMENTS.map((dept) => {
const settlement = resignationData?.settlement; const settlement = resignationData?.settlement;
const fffClearance = (settlement?.clearances || []).find((c: any) => normalizeDepartment(c.department) === dept); const fffClearance = (settlement?.clearances || []).find((c: any) => normalizeDepartment(c.department) === dept);
const relatedLineItems = (settlement?.lineItems || []).filter((li: any) => normalizeDepartment(li.department) === dept); const relatedLineItems = (settlement?.lineItems || []).filter((li: any) => normalizeDepartment(li.department) === dept);
// Calculate cumulative net for this department
let deptPayables = 0; let deptPayables = 0;
let deptRecoveries = 0; let deptRecoveries = 0;
relatedLineItems.forEach((li: any) => { relatedLineItems.forEach((li: any) => {
const amt = Math.abs(parseFloat(li.amount) || 0); const amt = Math.abs(parseFloat(li.amount) || 0);
if (li.itemType === 'Payable') deptPayables += amt; if (li.itemType === 'Payable') deptPayables += amt;
else deptRecoveries += amt; // Receivables & Deductions else deptRecoveries += amt;
}); });
const netAmount = deptPayables - deptRecoveries; const netAmount = deptPayables - deptRecoveries;
// Use standardized JSON field from initial clearance phase
const jsonClearance = (resignationData?.departmentalClearances || {})[dept] || { status: 'Pending', remarks: '', amount: 0, type: 'Recovery' }; const jsonClearance = (resignationData?.departmentalClearances || {})[dept] || { status: 'Pending', remarks: '', amount: 0, type: 'Recovery' };
// Logic: If FFF has items or a specific clearance object, it overrides the initial JSON clearance
const displayStatus = fffClearance const displayStatus = fffClearance
? (fffClearance.status === 'NOC Submitted' || fffClearance.status === 'Cleared' ? (netAmount < 0 ? 'Dues' : 'Cleared') : fffClearance.status) ? (fffClearance.status === 'NOC Submitted' || fffClearance.status === 'Cleared' ? (netAmount < 0 ? 'Dues' : 'Cleared') : fffClearance.status)
: jsonClearance.status; : jsonClearance.status;
const displayRemarks = fffClearance ? fffClearance.remarks : jsonClearance.remarks; const displayRemarks = fffClearance ? fffClearance.remarks : jsonClearance.remarks;
const displayAmount = Math.abs(netAmount) || jsonClearance.amount; const displayAmount = Math.abs(netAmount) || jsonClearance.amount || 0;
const displayType = netAmount > 0 ? 'Payable' : 'Recovery'; const displayType = netAmount > 0 ? 'Payable' : 'Recovery';
return ( return (
<Card key={dept} className="border border-slate-200"> <TableRow key={dept}>
<CardHeader className="pb-2 flex flex-row items-center justify-between"> <TableCell>{dept}</TableCell>
<CardTitle className="text-base font-medium capitalize">{dept.replace(' Department', '')}</CardTitle> <TableCell>
<Badge className={ <Badge className={
displayStatus === 'Cleared' ? 'bg-green-100 text-green-700 hover:bg-green-100' : displayStatus === 'Cleared' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
displayStatus === 'Dues' ? 'bg-red-100 text-red-700 hover:bg-red-100' : displayStatus === 'Dues' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
@ -615,69 +764,47 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
}> }>
{displayStatus || 'Pending'} {displayStatus || 'Pending'}
</Badge> </Badge>
</CardHeader> </TableCell>
<CardContent> <TableCell>
<div className="space-y-2"> <Badge
<div className="flex justify-between text-xs text-slate-500"> variant="outline"
<span>Amount: {(displayAmount || 0).toLocaleString()}</span> className={displayType === 'Recovery'
<span className={displayType === 'Recovery' ? 'text-red-600' : 'text-green-600'}> ? 'bg-red-50 text-red-700 border-red-200'
{displayType || 'Recovery'} : 'bg-green-50 text-green-700 border-green-200'}
</span>
</div>
<div className="flex flex-col gap-2">
<p className="text-sm text-slate-600 line-clamp-3 min-h-[3.5rem]">
{displayRemarks || 'Awaiting departmental verification.'}
</p>
{fffClearance?.supportingDocument && (
<div className="pt-2 border-t border-slate-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-blue-50 flex items-center justify-center border border-blue-100">
<FileText className="w-4 h-4 text-amber-500" />
</div>
<div>
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-tight leading-none mb-1">Evidence Attached</p>
<span className="text-[10px] text-slate-500 truncate max-w-[100px] block">
{fffClearance.supportingDocument.split('/').pop()?.substring(0, 12)}...
</span>
</div>
</div>
<button
onClick={() => {
const path = fffClearance.supportingDocument;
const fullPath = path.startsWith('/uploads/') && !path.startsWith('/uploads/documents/')
? path.replace('/uploads/', '/uploads/documents/')
: path;
setPreviewDocument({
fileName: `${dept}_Proof`,
filePath: fullPath,
documentType: 'Clearance Proof'
});
}}
className="flex items-center gap-1.5 text-xs text-amber-600 hover:text-blue-700 hover:underline font-bold bg-blue-50/50 px-2.5 py-1.5 rounded-md border border-blue-100/50 transition-colors"
> >
<Eye className="w-3.5 h-3.5" /> {displayType}
Preview </Badge>
</button> </TableCell>
</div> <TableCell>
)} <span className={displayType === 'Recovery' ? 'text-red-600 font-bold' : 'text-green-600 font-bold'}>
</div> {displayAmount.toLocaleString()}
</div> </span>
</CardContent> </TableCell>
</Card> <TableCell className="max-w-xs truncate">
{displayRemarks || 'Awaiting departmental verification.'}
</TableCell>
</TableRow>
); );
})} })}
</div> </TableBody>
</Table>
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
)}
{/* Documents Tab */} {/* Documents Tab */}
<TabsContent value="documents"> <TabsContent value="documents">
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Documents</CardTitle> <CardTitle>Documents</CardTitle>
<CardDescription>View and manage resignation documents</CardDescription> <CardDescription>View and manage resignation documents</CardDescription>
</div>
<Button size="sm" onClick={() => setShowUploadDialog(true)} className="bg-amber-600 hover:bg-amber-700">
<Upload className="w-4 h-4 mr-2" />
Upload Document
</Button>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Table> <Table>
@ -789,25 +916,25 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
auditLogs.map((log: any, index: number) => ( auditLogs.map((log: any, index: number) => (
<div <div
key={index} key={index}
className="flex gap-4 pb-4 border-b border-slate-200 last:border-0" className="flex gap-3 pb-4 border-b border-slate-100 last:border-0"
> >
<div className="w-2 h-2 rounded-full bg-amber-600 mt-2" /> <div className="w-2 h-2 rounded-full bg-slate-400 mt-2" />
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<p className="font-semibold text-slate-900 flex items-center gap-2"> <p className="font-semibold text-slate-900 flex items-center gap-2">
{log.description || log.action} {log.description || log.action}
</p> </p>
<span className="text-sm text-slate-600 font-mono"> <span className="text-xs text-slate-500">
{formatDateTime(log.timestamp || log.createdAt)} {formatDateTime(log.timestamp || log.createdAt)}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-sm text-slate-600 mb-2"> <div className="flex items-center gap-2 text-sm text-slate-600 mb-2">
<Badge variant="outline" className="text-[10px] uppercase">{log.userName || 'System'}</Badge> <Badge variant="outline" className="text-[10px] uppercase">{log.actor?.name || log.userName || 'System'}</Badge>
</div> </div>
{(log.remarks || log.newData?.remarks) && ( {(log.remarks || log.newData?.remarks || log.details?.remarks) && (
<div className="mt-2 p-3 bg-blue-50 border-l-4 border-blue-400 rounded-r text-sm italic text-blue-800"> <div className="mt-2 p-3 bg-slate-50 border border-slate-200 rounded text-sm text-slate-700">
" {log.remarks || log.newData?.remarks} " {log.remarks || log.newData?.remarks || log.details?.remarks}
</div> </div>
)} )}
</div> </div>
@ -847,21 +974,70 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="space-y-4"> <div className="space-y-4">
{actionDialog.type === 'assign' ? ( {actionDialog.type === 'assign' ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Select User</Label> <Label>Designation Filter</Label>
<Select value={assignToUser} onValueChange={setAssignToUser}> <Select value={assignToUser} onValueChange={(val) => {
setAssignToUser(val);
setSelectedSpecificUser('');
}}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Choose a user" /> <SelectValue placeholder="All Roles" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="asm">ASM - Area Sales Manager</SelectItem> <SelectItem value="all">All Roles</SelectItem>
<SelectItem value="rbm">RBM - Regional Business Manager</SelectItem> <SelectItem value="asm">ASM</SelectItem>
<SelectItem value="zbh">ZBH - Zonal Business Head</SelectItem> <SelectItem value="rbm">RBM</SelectItem>
<SelectItem value="nbh">NBH - National Business Head</SelectItem> <SelectItem value="zbh">ZBH</SelectItem>
<SelectItem value="legal">Legal Team</SelectItem> <SelectItem value="nbh">NBH</SelectItem>
<SelectItem value="legal">Legal</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="space-y-2">
<Label>Search Name/Email</Label>
<div className="relative">
<Input
placeholder="Search..."
value={userSearchQuery}
onChange={(e) => setUserSearchQuery(e.target.value)}
className="pr-8"
/>
{isLoadingUsers && <Loader2 className="w-4 h-4 animate-spin absolute right-2 top-2.5 text-slate-400" />}
</div>
</div>
</div>
<div className="space-y-2">
<Label>Select Specific Person *</Label>
<Select value={selectedSpecificUser} onValueChange={setSelectedSpecificUser}>
<SelectTrigger>
<SelectValue placeholder={availableUsers.length > 0 ? "Choose a user" : "No users found"} />
</SelectTrigger>
<SelectContent className="max-h-60">
{availableUsers.map(user => (
<SelectItem key={user.id} value={user.id}>
<div className="flex flex-col text-left">
<span className="font-medium">{user.fullName}</span>
<span className="text-[10px] text-slate-500">{user.roleCode} {user.email}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Assignment Remarks *</Label>
<Textarea
value={remarks}
onChange={(e) => setRemarks(e.target.value)}
placeholder="Why are you assigning this user?"
rows={2}
/>
</div>
</div>
) : actionDialog.type === 'pushfnf' ? ( ) : actionDialog.type === 'pushfnf' ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-start gap-3"> <div className="p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-start gap-3">
@ -940,7 +1116,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{/* Stage Documents Dialog */} {/* Stage Documents Dialog */}
<Dialog open={stageDocumentsDialog.open} onOpenChange={(open) => setStageDocumentsDialog({ open, stageName: '', documents: [] })}> <Dialog open={stageDocumentsDialog.open} onOpenChange={(open) => setStageDocumentsDialog({ open, stageName: '', documents: [] })}>
<DialogContent className="max-w-3xl"> <DialogContent className={WIDE_DIALOG_CLASS}>
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<FileText className="w-5 h-5 text-amber-600" /> <FileText className="w-5 h-5 text-amber-600" />
@ -973,7 +1149,22 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<TableCell>{doc.uploadDate}</TableCell> <TableCell>{doc.uploadDate}</TableCell>
<TableCell>{doc.uploader}</TableCell> <TableCell>{doc.uploader}</TableCell>
<TableCell> <TableCell>
<Button size="sm" variant="outline" className="text-amber-600 hover:text-blue-700"> <Button
size="sm"
variant="outline"
className="text-amber-600 hover:text-blue-700"
onClick={() => {
if (!doc.filePath) return;
const fullPath = doc.filePath.startsWith('/uploads/') && !doc.filePath.startsWith('/uploads/documents/')
? doc.filePath.replace('/uploads/', '/uploads/documents/')
: doc.filePath;
setPreviewDocument({
fileName: doc.name,
filePath: fullPath,
documentType: doc.type
});
}}
>
<FileText className="w-4 h-4 mr-1" /> <FileText className="w-4 h-4 mr-1" />
View View
</Button> </Button>
@ -997,6 +1188,58 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={showUploadDialog} onOpenChange={setShowUploadDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Resignation Document</DialogTitle>
<DialogDescription>Add a document and map it to a stage (optional).</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Document Type</Label>
<Select value={uploadDocType} onValueChange={setUploadDocType}>
<SelectTrigger>
<SelectValue placeholder="Select document type" />
</SelectTrigger>
<SelectContent>
{RESIGNATION_DOCUMENT_TYPES.map((docType) => (
<SelectItem key={docType} value={docType}>
{docType}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Stage (Optional)</Label>
<Select value={uploadStage || 'none'} onValueChange={(value) => setUploadStage(value === 'none' ? '' : value)}>
<SelectTrigger>
<SelectValue placeholder="Select stage" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No Stage Mapping</SelectItem>
{RESIGNATION_STAGE_OPTIONS.map((stage) => (
<SelectItem key={stage} value={stage}>
{stage}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>File</Label>
<Input type="file" onChange={(e) => setUploadFile(e.target.files?.[0] || null)} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowUploadDialog(false)} disabled={isSubmitting}>Cancel</Button>
<Button onClick={handleUploadDocument} disabled={isSubmitting}>
{isSubmitting ? 'Uploading...' : 'Upload'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DocumentPreviewModal <DocumentPreviewModal

View File

@ -1,4 +1,4 @@
import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck, Loader2 } from 'lucide-react'; import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck, Loader2, Upload } from 'lucide-react';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
@ -16,6 +16,9 @@ import { terminationService } from '../../services/termination.service';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { formatDateTime } from '../ui/utils'; import { formatDateTime } from '../ui/utils';
import { TERMINATION_DOCUMENT_TYPES, TERMINATION_STAGE_OPTIONS } from '../../lib/offboardingDocumentOptions';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { WIDE_DIALOG_CLASS } from '../../lib/dialogStyles';
interface TerminationDetailsProps { interface TerminationDetailsProps {
terminationId: string; terminationId: string;
onBack: () => void; onBack: () => void;
@ -38,6 +41,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
const [showFinalizeDialog, setShowFinalizeDialog] = useState(false); const [showFinalizeDialog, setShowFinalizeDialog] = useState(false);
const [finalDecision, setFinalDecision] = useState<'Approve' | 'Reject' | 'Reconsider'>('Approve'); const [finalDecision, setFinalDecision] = useState<'Approve' | 'Reject' | 'Reconsider'>('Approve');
const [finalRemarks, setFinalRemarks] = useState(''); const [finalRemarks, setFinalRemarks] = useState('');
const [showUploadDialog, setShowUploadDialog] = useState(false);
const [uploadFile, setUploadFile] = useState<File | null>(null);
const [uploadDocType, setUploadDocType] = useState(TERMINATION_DOCUMENT_TYPES[0]);
const [uploadStage, setUploadStage] = useState('');
const [previewDocument, setPreviewDocument] = useState<any>(null);
const fetchTermination = async () => { const fetchTermination = async () => {
try { try {
@ -119,6 +127,31 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
} }
}; };
const handleUploadDocument = async () => {
if (!uploadFile) {
toast.error('Please select a file to upload');
return;
}
try {
setIsProcessing(true);
const formData = new FormData();
formData.append('file', uploadFile);
formData.append('documentType', uploadDocType);
if (uploadStage) formData.append('stage', uploadStage);
await terminationService.uploadDocument(terminationId, formData);
toast.success('Document uploaded successfully');
setShowUploadDialog(false);
setUploadFile(null);
setUploadDocType(TERMINATION_DOCUMENT_TYPES[0]);
setUploadStage('');
fetchTermination();
} catch (error) {
toast.error('Failed to upload document');
} finally {
setIsProcessing(false);
}
};
// Check if user can push to F&F (DD Lead and above) // Check if user can push to F&F (DD Lead and above)
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role); const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
@ -164,46 +197,49 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
// Use actual data from backend // Use actual data from backend
const request = terminationData || {}; const request = terminationData || {};
// Define internal names for mapping if needed, but backend strings are preferred const stageAliases: Record<string, string[]> = {
'Submitted': ['Submitted', 'Request Initiated'],
'RBM Review': ['RBM Review'],
'ZBH Review': ['ZBH Review'],
'DD Lead Review': ['DD Lead Review'],
'Legal Verification': ['Legal Verification'],
'NBH Evaluation': ['NBH Evaluation'],
'Show Cause Notice (SCN)': ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'],
'Personal Hearing': ['Personal Hearing'],
'NBH Final Approval': ['NBH Final Approval'],
'CCO Approval': ['CCO Approval'],
'CEO Final Approval': ['CEO Final Approval'],
'Legal - Termination Letter': ['Legal - Termination Letter'],
'Dealer Terminated': ['Terminated', 'Dealer Terminated']
};
const allUploadedDocs = [
...(request.documents || []),
...(request.uploadedDocuments || [])
];
// Mock documents by stage const stageDocuments: Record<string, any[]> = Object.keys(stageAliases).reduce((acc: Record<string, any[]>, stageName) => {
const stageDocuments: Record<string, any[]> = { const aliases = stageAliases[stageName] || [stageName];
'Request Initiated': [ const docs = allUploadedDocs
{ id: 1, name: 'Termination Request Form.pdf', type: 'Request', uploadDate: '2025-10-15', uploader: 'ASM - Mumbai' }, .filter((doc: any) => !doc.stage || aliases.includes(doc.stage))
{ id: 2, name: 'Violation Evidence Report.pdf', type: 'Evidence', uploadDate: '2025-10-15', uploader: 'ASM - Mumbai' }, .map((doc: any) => ({
{ id: 3, name: 'Dealer Performance History.xlsx', type: 'Report', uploadDate: '2025-10-15', uploader: 'ASM - Mumbai' } id: doc.id || `${stageName}-${doc.fileName || doc.name}`,
], name: doc.fileName || doc.name || 'Document',
'RBM Review': [ type: doc.documentType || doc.type || 'Document',
{ id: 4, name: 'RBM Investigation Report.pdf', type: 'Investigation', uploadDate: '2025-10-16', uploader: 'RBM - West Zone' }, uploadDate: doc.uploadDate || doc.createdAt ? formatDateTime(doc.uploadDate || doc.createdAt) : 'N/A',
{ id: 5, name: 'Field Visit Photos.pdf', type: 'Evidence', uploadDate: '2025-10-16', uploader: 'RBM - West Zone' } uploader: doc.uploader?.fullName || doc.uploader || '-',
], path: doc.filePath || doc.path || doc.url
'ZBH Review': [ }));
{ id: 6, name: 'ZBH Assessment.pdf', type: 'Assessment', uploadDate: '2025-10-17', uploader: 'ZBH - West Zone' } acc[stageName] = docs;
], return acc;
'DD Lead Review': [ }, {});
{ id: 7, name: 'DD Lead Recommendation.pdf', type: 'Recommendation', uploadDate: '2025-10-18', uploader: 'DD Lead' },
{ id: 8, name: 'Competitor Analysis.pdf', type: 'Analysis', uploadDate: '2025-10-18', uploader: 'DD Lead' } const getLatestStageTimelineEntry = (stageName: string) => {
], const aliases = stageAliases[stageName] || [stageName];
'Legal Verification': [ const entries = (request.timeline || []).filter((entry: any) =>
{ id: 9, name: 'Legal Opinion.pdf', type: 'Legal', uploadDate: '2025-10-19', uploader: 'Legal Team' }, aliases.includes(entry.stage) || aliases.includes(entry.targetStage)
{ id: 10, name: 'Contract Review.pdf', type: 'Legal', uploadDate: '2025-10-19', uploader: 'Legal Team' }, );
{ id: 11, name: 'Compliance Checklist.pdf', type: 'Compliance', uploadDate: '2025-10-19', uploader: 'Legal Team' } return entries.length > 0 ? entries[entries.length - 1] : null;
],
'NBH Evaluation': [],
'Show Cause Notice (SCN)': [
{ id: 12, name: 'Show Cause Notice.pdf', type: 'Notice', uploadDate: '2025-10-20', uploader: 'Legal Admin' }
],
'DD Lead & Legal Review': [],
'NBH Termination Approval': [],
'CCO Approval': [],
'CEO Final Approval': [],
'Legal - Termination Letter': [
{ id: 13, name: 'Termination Letter - Draft.pdf', type: 'Letter', uploadDate: '2025-10-21', uploader: 'Legal Team' },
{ id: 14, name: 'Termination Letter - Final.pdf', type: 'Letter', uploadDate: '2025-10-21', uploader: 'Legal Admin' }
],
'DD Admin - Share with Dealer': [],
'Dealer Terminated': []
}; };
const progressStages = [ const progressStages = [
@ -362,10 +398,10 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Warning Alert */} {/* Warning Alert */}
<Alert className="border-red-200 bg-red-50"> <Alert className="border-amber-200 bg-amber-50">
<AlertTriangle className="h-4 w-4 text-red-600" /> <AlertTriangle className="h-4 w-4 text-amber-600" />
<AlertTitle className="text-red-900">Sensitive Information</AlertTitle> <AlertTitle className="text-amber-900">Sensitive Information</AlertTitle>
<AlertDescription className="text-red-700"> <AlertDescription className="text-amber-700">
This is a termination case. All actions are logged and audited. Proceed with caution. This is a termination case. All actions are logged and audited. Proceed with caution.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@ -396,7 +432,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</div> </div>
{/* Action Bar - Professional Layout */} {/* Action Bar - Professional Layout */}
<Card className="border-red-200 shadow-sm"> <Card className="border-amber-200 shadow-sm">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Primary Actions Row */} {/* Primary Actions Row */}
@ -495,7 +531,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</div> </div>
{/* Work Notes Button - Independent Section */} {/* Work Notes Button - Independent Section */}
<div className="flex items-center justify-between pt-4 border-t border-red-200"> <div className="flex items-center justify-between pt-4 border-t border-amber-200">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-slate-500" /> <MessageSquare className="w-4 h-4 text-slate-500" />
<span className="text-sm text-slate-600">Communication & Notes</span> <span className="text-sm text-slate-600">Communication & Notes</span>
@ -503,7 +539,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
className="relative hover:bg-red-50 hover:border-red-300 hover:text-red-700 transition-all shadow-sm" className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm"
onClick={() => navigate(`/worknotes/termination/${terminationId}`, { onClick={() => navigate(`/worknotes/termination/${terminationId}`, {
state: { state: {
applicationName: request?.dealer?.businessName || 'Termination', applicationName: request?.dealer?.businessName || 'Termination',
@ -515,7 +551,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<MessageSquare className="w-4 h-4 mr-2" /> <MessageSquare className="w-4 h-4 mr-2" />
View Work Notes View Work Notes
{workNotesCount > 0 && ( {workNotesCount > 0 && (
<Badge className="ml-2 bg-red-600 hover:bg-red-700 text-white h-5 px-2"> <Badge className="ml-2 bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
{workNotesCount} {workNotesCount}
</Badge> </Badge>
)} )}
@ -648,9 +684,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-red-200 bg-red-50/30"> <Card className="border-amber-200 bg-amber-50/30">
<CardHeader> <CardHeader>
<CardTitle className="text-red-900 flex items-center gap-2"> <CardTitle className="text-amber-900 flex items-center gap-2">
<AlertTriangle className="w-5 h-5" /> <AlertTriangle className="w-5 h-5" />
Termination Details Termination Details
</CardTitle> </CardTitle>
@ -659,7 +695,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Label className="text-slate-600">Termination Category</Label> <Label className="text-slate-600">Termination Category</Label>
<p className="text-red-900">{request.category}</p> <p className="text-amber-900">{request.category}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Sub Category</Label> <Label className="text-slate-600">Sub Category</Label>
@ -703,11 +739,12 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<div className="space-y-4"> <div className="space-y-4">
{progressStages.map((stage, index) => { {progressStages.map((stage, index) => {
const documentCount = stageDocuments[stage.name]?.length || 0; const documentCount = stageDocuments[stage.name]?.length || 0;
const timelineEntry = getLatestStageTimelineEntry(stage.name);
return ( return (
<div key={stage.id} className="flex gap-4"> <div key={stage.id} className="flex gap-4">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${stage.status === 'completed' ? 'bg-green-100 text-green-600' : <div className={`w-10 h-10 rounded-full flex items-center justify-center ${stage.status === 'completed' ? 'bg-green-100 text-green-600' :
stage.status === 'active' ? 'bg-red-100 text-red-600' : stage.status === 'active' ? 'bg-amber-100 text-amber-600' :
'bg-slate-100 text-slate-400' 'bg-slate-100 text-slate-400'
}`}> }`}>
{stage.status === 'completed' ? ( {stage.status === 'completed' ? (
@ -729,58 +766,40 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className={ <h3 className={
stage.status === 'completed' ? 'text-green-600' : stage.status === 'completed' ? 'text-green-600' :
stage.status === 'active' ? 'text-red-600' : stage.status === 'active' ? 'text-amber-600' :
'text-slate-400' 'text-slate-400'
}>{stage.name}</h3> }>{stage.name}</h3>
{documentCount > 0 && ( {documentCount > 0 && (
<button <button
onClick={() => handleViewStageDocuments(stage.name)} onClick={() => handleViewStageDocuments(stage.name)}
className="flex items-center gap-1 px-2 py-1 rounded-full bg-red-100 hover:bg-red-200 text-red-700 text-xs transition-colors cursor-pointer" className="flex items-center gap-1 px-2 py-1 rounded-full bg-amber-100 hover:bg-amber-200 text-amber-700 text-xs transition-colors cursor-pointer"
> >
<FileText className="w-3 h-3" /> <FileText className="w-3 h-3" />
<span>{documentCount} {documentCount === 1 ? 'doc' : 'docs'}</span> <span>{documentCount} {documentCount === 1 ? 'doc' : 'docs'}</span>
</button> </button>
)} )}
</div> </div>
{stage.date && ( {(timelineEntry?.timestamp || stage.date) && (
<div className="flex items-center gap-1 text-sm text-slate-600"> <div className="flex items-center gap-1 text-sm text-slate-600">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
<span>{stage.date}</span> <span>{formatDateTime(timelineEntry?.timestamp || stage.date)}</span>
</div> </div>
)} )}
</div> </div>
<p className="text-slate-600 text-sm">{stage.description}</p> <p className="text-slate-600 text-sm">{stage.description}</p>
{/* Action Badge and Remarks */} {timelineEntry && (
{stage.actionType && stage.remarks && (
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge className={ <Badge className="bg-blue-100 text-blue-700 border-blue-300">{timelineEntry.action || 'Updated'}</Badge>
stage.actionType === 'approved' ? 'bg-green-100 text-green-700 border-green-300' : <span className="text-xs text-slate-500">by {timelineEntry.user || 'System'}</span>
stage.actionType === 'sendback' ? 'bg-orange-100 text-orange-700 border-orange-300' :
stage.actionType === 'withdrawal' ? 'bg-red-100 text-red-700 border-red-300' :
'bg-blue-100 text-blue-700 border-blue-300'
}>
{stage.actionType === 'approved' && '✓ Approved'}
{stage.actionType === 'sendback' && '↩ Sent Back'}
{stage.actionType === 'withdrawal' && '✗ Withdrawn'}
</Badge>
{stage.actionBy && (
<span className="text-xs text-slate-500">by {stage.actionBy}</span>
)}
</div> </div>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3"> <div className="bg-slate-50 border border-slate-200 rounded-lg p-3">
<div className="space-y-2"> <div className="space-y-2">
<div> <div>
<Label className="text-xs text-slate-600">Remarks:</Label> <Label className="text-xs text-slate-600">Remarks:</Label>
<p className="text-sm text-slate-700 mt-1">{stage.remarks}</p> <p className="text-sm text-slate-700 mt-1">{timelineEntry.remarks || 'No remarks provided.'}</p>
</div> </div>
{stage.feedback && (
<div>
<Label className="text-xs text-slate-600">Feedback:</Label>
<p className="text-sm text-slate-700 mt-1">{stage.feedback}</p>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -797,9 +816,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
{/* Documents Tab */} {/* Documents Tab */}
<TabsContent value="documents"> <TabsContent value="documents">
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Documents</CardTitle> <CardTitle>Documents</CardTitle>
<CardDescription>View and manage termination case documents</CardDescription> <CardDescription>View and manage termination case documents</CardDescription>
</div>
<Button size="sm" onClick={() => setShowUploadDialog(true)} className="bg-amber-600 hover:bg-amber-700">
<Upload className="w-4 h-4 mr-2" />
Upload Document
</Button>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Table> <Table>
@ -839,7 +864,24 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<TableCell>{formatDateTime(doc.uploadDate || doc.createdAt)}</TableCell> <TableCell>{formatDateTime(doc.uploadDate || doc.createdAt)}</TableCell>
<TableCell>{doc.uploader?.fullName || doc.uploader || '-'}</TableCell> <TableCell>{doc.uploader?.fullName || doc.uploader || '-'}</TableCell>
<TableCell> <TableCell>
<Button size="sm" variant="outline">View</Button> <Button
size="sm"
variant="outline"
onClick={() => {
const path = doc.filePath || doc.path || doc.url;
if (!path) return;
const fullPath = path.startsWith('/uploads/') && !path.startsWith('/uploads/documents/')
? path.replace('/uploads/', '/uploads/documents/')
: path;
setPreviewDocument({
fileName: doc.name || doc.fileName || 'Document',
filePath: fullPath,
documentType: doc.documentType || doc.type || 'Document'
});
}}
>
View
</Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
)); ));
@ -863,25 +905,25 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
auditLogs.map((log: any, index: number) => ( auditLogs.map((log: any, index: number) => (
<div <div
key={index} key={index}
className="flex gap-4 pb-4 border-b border-slate-200 last:border-0" className="flex gap-3 pb-4 border-b border-slate-100 last:border-0"
> >
<div className="w-2 h-2 rounded-full bg-blue-600 mt-2" /> <div className="w-2 h-2 rounded-full bg-slate-400 mt-2" />
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<p className="font-semibold text-slate-900 flex items-center gap-2"> <p className="font-semibold text-slate-900 flex items-center gap-2">
{log.description || log.action} {log.description || log.action}
</p> </p>
<span className="text-sm text-slate-600 font-mono"> <span className="text-xs text-slate-500">
{formatDateTime(log.timestamp || log.createdAt)} {formatDateTime(log.timestamp || log.createdAt)}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-sm text-slate-600 mb-2"> <div className="flex items-center gap-2 text-sm text-slate-600 mb-2">
<Badge variant="outline" className="text-[10px] uppercase">{log.userName || 'System'}</Badge> <Badge variant="outline" className="text-[10px] uppercase">{log.actor?.name || log.userName || 'System'}</Badge>
</div> </div>
{(log.remarks || log.newData?.remarks) && ( {(log.remarks || log.newData?.remarks || log.details?.remarks) && (
<div className="mt-2 p-3 bg-blue-50 border-l-4 border-blue-400 rounded-r text-sm italic text-blue-800"> <div className="mt-2 p-3 bg-slate-50 border border-slate-200 rounded text-sm text-slate-700">
" {log.remarks || log.newData?.remarks} " {log.remarks || log.newData?.remarks || log.details?.remarks}
</div> </div>
)} )}
</div> </div>
@ -981,10 +1023,10 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
{/* Stage Documents Dialog */} {/* Stage Documents Dialog */}
<Dialog open={stageDocumentsDialog.open} onOpenChange={(open) => setStageDocumentsDialog({ open, stageName: '', documents: [] })}> <Dialog open={stageDocumentsDialog.open} onOpenChange={(open) => setStageDocumentsDialog({ open, stageName: '', documents: [] })}>
<DialogContent className="max-w-3xl"> <DialogContent className={WIDE_DIALOG_CLASS}>
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<FileText className="w-5 h-5 text-red-600" /> <FileText className="w-5 h-5 text-amber-600" />
Documents - {stageDocumentsDialog.stageName} Documents - {stageDocumentsDialog.stageName}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
@ -1014,7 +1056,23 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<TableCell>{doc.uploadDate}</TableCell> <TableCell>{doc.uploadDate}</TableCell>
<TableCell>{doc.uploader}</TableCell> <TableCell>{doc.uploader}</TableCell>
<TableCell> <TableCell>
<Button size="sm" variant="outline" className="text-red-600 hover:text-red-700"> <Button
size="sm"
variant="outline"
className="text-amber-600 hover:text-amber-700"
onClick={() => {
const path = doc.path;
if (!path) return;
const fullPath = path.startsWith('/uploads/') && !path.startsWith('/uploads/documents/')
? path.replace('/uploads/', '/uploads/documents/')
: path;
setPreviewDocument({
fileName: doc.name || 'Document',
filePath: fullPath,
documentType: doc.type || 'Document'
});
}}
>
<FileText className="w-4 h-4 mr-1" /> <FileText className="w-4 h-4 mr-1" />
View View
</Button> </Button>
@ -1114,7 +1172,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<SelectValue placeholder="Select decision" /> <SelectValue placeholder="Select decision" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="bg-white border-slate-200 shadow-xl overflow-visible z-[9999]"> <SelectContent className="bg-white border-slate-200 shadow-xl overflow-visible z-[9999]">
<SelectItem value="Approve" className="text-red-600 focus:bg-red-50">Confirm Termination</SelectItem> <SelectItem value="Approve" className="text-amber-700 focus:bg-amber-50">Confirm Termination</SelectItem>
<SelectItem value="Reject" className="text-slate-600 focus:bg-slate-50">Reject Termination</SelectItem> <SelectItem value="Reject" className="text-slate-600 focus:bg-slate-50">Reject Termination</SelectItem>
<SelectItem value="Reconsider" className="text-amber-600 focus:bg-amber-50">Reconsider / Give More Time</SelectItem> <SelectItem value="Reconsider" className="text-amber-600 focus:bg-amber-50">Reconsider / Give More Time</SelectItem>
</SelectContent> </SelectContent>
@ -1144,6 +1202,64 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={showUploadDialog} onOpenChange={setShowUploadDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Termination Document</DialogTitle>
<DialogDescription>Add a document and map it to a stage (optional).</DialogDescription>
</DialogHeader>
<div className="space-y-4 pt-2">
<div className="space-y-2">
<Label>Document Type</Label>
<Select value={uploadDocType} onValueChange={setUploadDocType}>
<SelectTrigger>
<SelectValue placeholder="Select document type" />
</SelectTrigger>
<SelectContent>
{TERMINATION_DOCUMENT_TYPES.map((docType) => (
<SelectItem key={docType} value={docType}>
{docType}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Stage (Optional)</Label>
<Select value={uploadStage || 'none'} onValueChange={(value) => setUploadStage(value === 'none' ? '' : value)}>
<SelectTrigger>
<SelectValue placeholder="Select stage" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No Stage Mapping</SelectItem>
{TERMINATION_STAGE_OPTIONS.map((stage) => (
<SelectItem key={stage} value={stage}>
{stage}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>File</Label>
<input type="file" onChange={(e) => setUploadFile(e.target.files?.[0] || null)} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowUploadDialog(false)} disabled={isProcessing}>Cancel</Button>
<Button onClick={handleUploadDocument} disabled={isProcessing}>
{isProcessing ? 'Uploading...' : 'Upload'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DocumentPreviewModal
isOpen={!!previewDocument}
onClose={() => setPreviewDocument(null)}
document={previewDocument}
/>
</div> </div>
); );
} }

View File

@ -45,13 +45,68 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
const fetchData = async () => { const fetchData = async () => {
try { try {
setLoading(true); setLoading(true);
const [payments, settlements, apps] = await Promise.all([ const [settlements, apps] = await Promise.all([
settlementService.getOnboardingPayments(),
settlementService.getFnFSettlements(), settlementService.getFnFSettlements(),
onboardingService.getApplications() onboardingService.getApplications()
]); ]);
setOnboardingPayments(payments); // Derive Onboarding Payments from Application + SecurityDeposit (Standardized nomenclature)
// This ensures applications in "Payment Pending" / "Security Details" are visible
// even if no payment record has been manually initialized.
const consolidatedPayments: any[] = [];
apps.forEach((app: any) => {
const s = app.overallStatus || app.status;
const isPaymentStage = [
'Payment Pending', 'Security Details', 'LOI In Progress', 'LOI Issued',
'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL'
].includes(s);
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
});
});
} else if (['Payment Pending', 'Security Details', 'LOI In Progress'].includes(s)) {
// Virtual pending record for Security Deposit (5L)
consolidatedPayments.push({
id: `virtual-${app.id}-sd`,
applicationId: app.applicationId || app.id,
application: app,
paymentStatus: 'Pending',
paymentType: 'SECURITY_DEPOSIT',
amount: 500000,
createdAt: app.updatedAt,
isVirtual: true
});
} else if (['LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL'].includes(s)) {
// Virtual pending record for First Fill (15L)
consolidatedPayments.push({
id: `virtual-${app.id}-ff`,
applicationId: app.applicationId || app.id,
application: app,
paymentStatus: 'Pending',
paymentType: 'FIRST_FILL',
amount: 1500000,
createdAt: app.updatedAt,
isVirtual: true
});
}
}
});
setOnboardingPayments(consolidatedPayments);
setFnfSettlements(settlements); setFnfSettlements(settlements);
// Filter for applications needing FDD review // Filter for applications needing FDD review

View File

@ -16,6 +16,7 @@ import {
Minus, Minus,
FileText FileText
} from 'lucide-react'; } from 'lucide-react';
import { WIDE_DIALOG_CLASS } from '../../lib/dialogStyles';
interface DocumentPreviewModalProps { interface DocumentPreviewModalProps {
isOpen: boolean; isOpen: boolean;
@ -42,7 +43,7 @@ export const DocumentPreviewModal: React.FC<DocumentPreviewModalProps> = ({
return ( return (
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-[80vw] w-[80vw] h-[85vh] flex flex-col p-0 overflow-hidden bg-white shadow-2xl border-none"> <DialogContent className={`${WIDE_DIALOG_CLASS} h-[85vh] flex flex-col p-0 overflow-hidden bg-white shadow-2xl border-none`}>
{document ? ( {document ? (
<> <>
<div className="flex items-center justify-between p-4 border-b bg-slate-50"> <div className="flex items-center justify-between p-4 border-b bg-slate-50">

View File

@ -1,14 +1,11 @@
"use client"; "use client";
import { useTheme } from "next-themes";
import { Toaster as Sonner, ToasterProps } from "sonner"; import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme="light"
className="toaster group" className="toaster group"
style={ style={
{ {
@ -22,4 +19,5 @@ const Toaster = ({ ...props }: ToasterProps) => {
); );
}; };
export { Toaster }; export { Toaster };

1
src/lib/dialogStyles.ts Normal file
View File

@ -0,0 +1 @@
export const WIDE_DIALOG_CLASS = "!w-[80vw] !max-w-[80vw] sm:!max-w-[80vw]";

View File

@ -0,0 +1,46 @@
export const RESIGNATION_DOCUMENT_TYPES = [
"Resignation Letter",
"Dealer Undertaking",
"Approval Note",
"Legal Communication",
"Handover Document",
"Settlement Supporting Document",
"Other",
] as const;
export const RESIGNATION_STAGE_OPTIONS = [
"ASM",
"RBM",
"ZBH",
"DD Lead",
"NBH",
"DD Admin",
"Legal",
"F&F Initiated",
] as const;
export const TERMINATION_DOCUMENT_TYPES = [
"Termination Recommendation",
"Show Cause Notice",
"SCN Response",
"Hearing Record",
"Approval Note",
"Termination Letter",
"Settlement Supporting Document",
"Other",
] as const;
export const TERMINATION_STAGE_OPTIONS = [
"Submitted",
"RBM Review",
"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",
] as const;

View File

@ -36,5 +36,14 @@ export const resignationService = {
console.error('Update clearance error:', error); console.error('Update clearance error:', error);
throw error; throw error;
} }
},
uploadDocument: async (id: string, formData: FormData) => {
try {
const response: any = await API.uploadResignationDocument(id, formData);
return response.data;
} catch (error) {
console.error('Upload resignation document error:', error);
throw error;
}
} }
}; };

View File

@ -6,8 +6,8 @@ export const terminationService = {
return response.data?.termination || response.data?.data || response.data; return response.data?.termination || response.data?.data || response.data;
}, },
updateTerminationStatus: async (id: string, status: string, remarks: string) => { updateTerminationStatus: async (id: string, action: string, remarks: string) => {
const response = await API.updateTerminationStatus(id, { status, remarks }); const response = await API.updateTerminationStatus(id, { action, remarks });
return response.data; return response.data;
}, },
@ -23,6 +23,10 @@ export const terminationService = {
const response = await API.uploadSCNResponse(id, formData); const response = await API.uploadSCNResponse(id, formData);
return response.data; return response.data;
}, },
uploadDocument: async (id: string, formData: FormData) => {
const response = await API.uploadTerminationDocument(id, formData);
return response.data;
},
finalizeTermination: async (id: string, decision: 'Approve' | 'Reject' | 'Reconsider', remarks: string) => { finalizeTermination: async (id: string, decision: 'Approve' | 'Reject' | 'Reconsider', remarks: string) => {
const response = await API.finalizeTermination(id, { decision, remarks }); const response = await API.finalizeTermination(id, { decision, remarks });