auditlogs enhanced end to end flow checked for the onboarding , cursor used for major file chnages
This commit is contained in:
parent
71e6c10c16
commit
d3bdea8318
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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">
|
||||||
@ -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 && (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
1
src/lib/dialogStyles.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const WIDE_DIALOG_CLASS = "!w-[80vw] !max-w-[80vw] sm:!max-w-[80vw]";
|
||||||
46
src/lib/offboardingDocumentOptions.ts
Normal file
46
src/lib/offboardingDocumentOptions.ts
Normal 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;
|
||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user