From 06967561600390563b78894a3216140142318f92 Mon Sep 17 00:00:00 2001
From: laxman h
Date: Fri, 3 Apr 2026 20:32:40 +0530
Subject: [PATCH] caht enhanced and made centralized componet separate FDD ui
creted for document upload
---
src/App.tsx | 22 +-
.../applications/ApplicationDetails.tsx | 108 +---
.../ConstitutionalChangeDetails.tsx | 122 +----
.../applications/FDDApplicationDetails.tsx | 478 ++++++++++++++++++
.../applications/FinanceOnboardingPage.tsx | 3 +-
src/components/applications/FnFDetails.tsx | 43 +-
.../ProspectiveApplicationDetails.tsx | 295 +++++++++++
.../applications/RelocationRequestDetails.tsx | 170 ++-----
.../applications/RelocationRequestPage.tsx | 451 +----------------
.../applications/ResignationDetails.tsx | 57 +--
.../applications/ResignationPage.tsx | 226 +--------
.../applications/TerminationDetails.tsx | 74 ++-
src/components/applications/WorkNotesPage.tsx | 266 +++++++---
src/components/applications/WorknotePage.tsx | 398 ---------------
src/components/dashboard/FDDDashboardPage.tsx | 207 ++++++++
src/components/dashboard/FinanceDashboard.tsx | 3 +-
.../dashboard/ProspectiveDashboardPage.tsx | 352 +++++--------
src/components/layout/Sidebar.tsx | 2 +
src/components/ui/DocumentPreviewModal.tsx | 199 ++------
src/services/collaboration.service.ts | 33 ++
src/services/eor.service.ts | 9 +-
21 files changed, 1571 insertions(+), 1947 deletions(-)
create mode 100644 src/components/applications/FDDApplicationDetails.tsx
create mode 100644 src/components/applications/ProspectiveApplicationDetails.tsx
delete mode 100644 src/components/applications/WorknotePage.tsx
create mode 100644 src/components/dashboard/FDDDashboardPage.tsx
create mode 100644 src/services/collaboration.service.ts
diff --git a/src/App.tsx b/src/App.tsx
index 36c199c..b45e4d0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -14,6 +14,8 @@ import { Dashboard } from './components/dashboard/Dashboard';
import { FinanceDashboard } from './components/dashboard/FinanceDashboard';
import { DealerDashboard } from './components/dashboard/DealerDashboard';
import { ProspectiveDashboardPage } from './components/dashboard/ProspectiveDashboardPage';
+import { FDDDashboardPage } from './components/dashboard/FDDDashboardPage';
+import { FDDApplicationDetails } from './components/applications/FDDApplicationDetails';
import { ApplicationsPage } from './components/applications/ApplicationsPage';
import { AllApplicationsPage } from './components/applications/AllApplicationsPage';
import { OpportunityRequestsPage } from './components/applications/OpportunityRequestsPage';
@@ -146,6 +148,7 @@ export default function App() {
'/dealer-relocation': 'Dealer Relocation Requests',
'/questionnaire-builder': 'Questionnaire Builder',
'/approval-policies': 'Approval Policies',
+ '/fdd-dashboard': 'FDD Dashboard',
};
return titles[pathname] || 'Dashboard';
};
@@ -181,7 +184,7 @@ export default function App() {
{/* Prospective Dealer Route - STRICTLY ISOLATED */}
@@ -189,6 +192,8 @@ export default function App() {
}
/>
+
+
{/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
@@ -209,11 +214,10 @@ export default function App() {
{/* Applications */}
navigate(`/applications/${id}`)} initialFilter="all" />} />
} />
- window.history.back()}
/>
} />
@@ -222,6 +226,10 @@ export default function App() {
currentUser?.role === 'DD' ? navigate(`/applications/${id}`)} initialFilter="all" /> :
} />
+ {/* FDD Routes - Integrated into Layout */}
+ } />
+ } />
+
{/* Admin/Lead Routes */}
navigate(`/applications/${id}`)} />} />
navigate(`/applications/${id}`)} />} />
@@ -256,10 +264,10 @@ export default function App() {
navigate('/finance-fnf')} />} />
navigate(`/constitutional-change/${id}`)} />} />
- navigate('/constitutional-change')} currentUser={currentUser} onOpenWorknote={() => { }} />} />
+ navigate('/constitutional-change')} currentUser={currentUser} />} />
navigate(`/relocation-requests/${id}`)} />} />
- navigate('/relocation-requests')} currentUser={currentUser} onOpenWorknote={() => { }} />} />
+ navigate('/relocation-requests')} currentUser={currentUser} />} />
{/* Dealer Routes */}
navigate(`/resignation/${id}`)} />} />
diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx
index 6124361..b26aff3 100644
--- a/src/components/applications/ApplicationDetails.tsx
+++ b/src/components/applications/ApplicationDetails.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
-import { mockWorkNotes, Application, ApplicationStatus } from '../../lib/mock-data';
+import { Application, ApplicationStatus } from '../../lib/mock-data';
import { onboardingService } from '../../services/onboarding.service';
import { auditService } from '../../services/audit.service';
import { eorService } from '../../services/eor.service';
@@ -395,7 +395,6 @@ export function ApplicationDetails() {
const [rejectionReason, setRejectionReason] = useState('');
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState([]);
- const [showWorkNoteModal, setShowWorkNoteModal] = useState(false);
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [showKTMatrixModal, setShowKTMatrixModal] = useState(false);
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
@@ -405,7 +404,6 @@ export function ApplicationDetails() {
const [selectedStage, setSelectedStage] = useState(null);
const [interviewMode, setInterviewMode] = useState('virtual');
const [approvalRemark, setApprovalRemark] = useState('');
- const [workNote, setWorkNote] = useState('');
const [expandedBranches, setExpandedBranches] = useState<{ [key: string]: boolean }>({
'architectural-work': true,
'statutory-documents': true
@@ -1514,16 +1512,6 @@ export function ApplicationDetails() {
}
};
- const handleWorkNote = () => {
- if (!workNote.trim()) {
- toast.warning('Please enter a note');
- return;
- }
- toast.info(`Work note added: ${workNote}`);
- setShowWorkNoteModal(false);
- setWorkNote('');
- };
-
const handleAddParticipant = async () => {
if (!selectedUser) {
toast.warning('Please select a user');
@@ -1663,7 +1651,20 @@ export function ApplicationDetails() {
- {/* Actions can be added here in the future */}
+
@@ -1898,7 +1899,7 @@ export function ApplicationDetails() {
"w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white",
approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-slate-300"
)}>
- {approver.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
+ {approver.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase()}
{approver.name}
@@ -2826,7 +2827,7 @@ export function ApplicationDetails() {
@@ -3218,48 +3186,6 @@ export function ApplicationDetails() {
- {/* Work Note Modal */}
- < Dialog open={showWorkNoteModal} onOpenChange={setShowWorkNoteModal} >
-
-
- Add Work Note
-
- Add a note to track progress and communicate with team members.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/applications/ConstitutionalChangeDetails.tsx b/src/components/applications/ConstitutionalChangeDetails.tsx
index 0df566c..d0befe4 100644
--- a/src/components/applications/ConstitutionalChangeDetails.tsx
+++ b/src/components/applications/ConstitutionalChangeDetails.tsx
@@ -12,12 +12,12 @@ import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner';
import { API } from '../../api/API';
+import { useNavigate } from 'react-router-dom';
interface ConstitutionalChangeDetailsProps {
requestId: string;
onBack: () => void;
currentUser: UserType | null;
- onOpenWorknote?: (requestId: string, requestType: 'relocation' | 'constitutional-change' | 'fnf' | 'resignation' | 'termination', requestTitle: string) => void;
}
// Workflow stages as per the process flow
@@ -83,16 +83,15 @@ const getStatusColor = (status: string) => {
return 'bg-slate-100 text-slate-700 border-slate-300';
};
-export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, onOpenWorknote }: ConstitutionalChangeDetailsProps) {
+export function ConstitutionalChangeDetails({ requestId, onBack }: ConstitutionalChangeDetailsProps) {
+ const navigate = useNavigate();
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
const [comments, setComments] = useState('');
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
- const [isWorknoteDialogOpen, setIsWorknoteDialogOpen] = useState(false);
const [request, setRequest] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isActionLoading, setIsActionLoading] = useState(false);
- const [newWorknote, setNewWorknote] = useState('');
useEffect(() => {
fetchRequestDetails();
@@ -192,26 +191,6 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
setIsUploadDialogOpen(false);
};
- const handleAddWorknote = async () => {
- if (newWorknote.trim()) {
- try {
- const response = await API.addWorknote({
- requestId,
- requestType: 'constitutional-change',
- message: newWorknote
- }) as any;
-
- if (response.data.success) {
- setNewWorknote('');
- toast.success('Worknote added successfully');
- fetchRequestDetails();
- }
- } catch (error) {
- toast.error('Failed to add worknote');
- }
- }
- };
-
return (
{/* Header */}
@@ -622,16 +601,16 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
@@ -699,87 +678,6 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
- {/* Worknotes Dialog */}
-
);
}
diff --git a/src/components/applications/FDDApplicationDetails.tsx b/src/components/applications/FDDApplicationDetails.tsx
new file mode 100644
index 0000000..4bf15de
--- /dev/null
+++ b/src/components/applications/FDDApplicationDetails.tsx
@@ -0,0 +1,478 @@
+import { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { API } from '../../api/API';
+import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
+import { Badge } from '../ui/badge';
+import {
+ ArrowLeft,
+ FileText,
+ Upload,
+ Loader2,
+ Eye
+} from 'lucide-react';
+import { WorkNotesPage } from './WorkNotesPage';
+import { toast } from 'sonner';
+import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
+
+export function FDDApplicationDetails() {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const [application, setApplication] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [uploading, setUploading] = useState(false);
+ const [selectedDocType, setSelectedDocType] = useState('');
+ const [activeTab, setActiveTab] = useState<'details' | 'worknotes'>('details');
+ const [isPreviewOpen, setIsPreviewOpen] = useState(false);
+ const [selectedPreviewDoc, setSelectedPreviewDoc] = useState(null);
+
+ useEffect(() => {
+ if (id) fetchApplication();
+ }, [id]);
+
+ const fetchApplication = async () => {
+ setLoading(true);
+ try {
+ const response: any = await API.getApplicationById(id!);
+ if (response.data?.success) {
+ setApplication(response.data.data);
+ } else {
+ toast.error(response.data?.message || 'Failed to authorize access');
+ navigate('/fdd-dashboard');
+ }
+ } catch (error) {
+ console.error('Error fetching application:', error);
+ const errorMsg = (error as any).response?.data?.message || 'Access Denied: Not authorized for FDD access';
+ toast.error(errorMsg);
+ navigate('/fdd-dashboard');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleFileUpload = async (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file || !selectedDocType) {
+ if (!selectedDocType) toast.error('Please select a document type first');
+ return;
+ }
+
+ setUploading(true);
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('documentType', selectedDocType);
+ formData.append('requestId', id!);
+ formData.append('requestType', 'application');
+
+ try {
+ const response: any = await API.uploadDocument(id!, formData);
+ if (response.data?.success) {
+ toast.success(`${selectedDocType} uploaded successfully`);
+ fetchApplication();
+ setSelectedDocType('');
+ }
+ } catch (error) {
+ toast.error('Failed to upload document');
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ const handlePreview = (doc: any) => {
+ if (!doc || !doc.filePath) {
+ toast.error('Document source file not found');
+ return;
+ }
+
+ setSelectedPreviewDoc({
+ fileName: doc.originalName || doc.fileName || 'Document',
+ filePath: doc.filePath,
+ documentType: doc.documentType,
+ createdAt: doc.createdAt,
+ mimeType: doc.mimeType
+ });
+ setIsPreviewOpen(true);
+ };
+
+ if (loading) {
+ return (
+
+
+
Authenticating and loading secure data...
+
+ );
+ }
+
+ if (!application) return null;
+
+ return (
+
+ {/* Action Bar */}
+
+
+
+
+
+
+
+
+
+ {/* Header Card */}
+
+
+
+
+
+ {application.applicantName.charAt(0)}
+
+
+
+
{application.applicantName}
+
+ {application.applicationId}
+
+
+
+ {application.city}, {application.state}
+ •
+ {application.businessType || 'Dealership'}
+
+
+
+
+
+
Status
+
Financial Due Diligence
+
+
+
+
+
+
+ {/* Navigation Tabs */}
+
+
+
+
+
+ {activeTab === 'details' ? (
+
+ {/* Left Column: Financial Data & Uploads */}
+
+
+
+
+
+ Financial Report Submission
+
+
+
+
+
+
+
+
Select and upload the due diligence report
+
PDF or JPG formats accepted (Max 10MB)
+
+
+
+
+
+ {uploading ? (
+
+
+ Uploading...
+
+ ) : (
+ <>
+
+
+ Browse & Upload
+
+ >
+ )}
+
+
+
+
+ {/* List of Uploaded Documents */}
+
+
Submitted Documentation
+
+ {/* SECTION 1: APPLICANT DOCUMENTS */}
+
+
+
+ Applicant's KYC & Financials
+
+
+ {application.uploadedDocuments?.filter((d: any) => !d.uploader || d.uploader.roleCode !== 'FDD').map((doc: any, i: number) => (
+
+
+
+
+
+
+
+
{doc.originalName || doc.fileName}
+
APPLICANT
+
+
+ {doc.documentType} • {new Date(doc.createdAt).toLocaleDateString()}
+ {doc.uploader?.fullName && ` • by ${doc.uploader.fullName}`}
+
+
+
+
+
+
+
+
+
+
+ ))}
+ {application.uploadedDocuments?.filter((d: any) => !d.uploader || d.uploader.roleCode !== 'FDD').length === 0 && (
+
No documents from applicant yet.
+ )}
+
+
+
+ {/* SECTION 2: MY SUBMISSIONS */}
+
+
+
+ My Uploaded Reports
+
+
+ {application.uploadedDocuments?.filter((d: any) => d.uploader?.roleCode === 'FDD').map((doc: any, i: number) => (
+
+
+
+
+
+
+
+
{doc.originalName || doc.fileName}
+
YOUR AUDIT REPORT
+
+
+ {doc.documentType} • {new Date(doc.createdAt).toLocaleDateString()}
+ {doc.uploader?.fullName && ` • by ${doc.uploader.fullName}`}
+
+
+
+
+
+
+
+
+
+
+ ))}
+ {application.uploadedDocuments?.filter((d: any) => d.uploader?.roleCode === 'FDD').length === 0 && (
+
+
No audit reports uploaded yet.
+
+ )}
+
+
+
+
+
+
+
+
+ {/* Right Column: Applicant Meta & Guidelines */}
+
+
+
+ Applicant Profile
+
+
+
+
Target Location
+
{application.city}, {application.state}
+
+
+
+
Education
+
{application.education || 'N/A'}
+
+
+
Experience
+
{application.experienceYears || '0'} Years
+
+
+
Investment Cap
+
{application.investmentCapacity || 'N/A'}
+
+
+
Age
+
{application.age || 'N/A'}
+
+
+
+
+
Communication
+
{application.email}
+
{application.phone}
+
+
+
FDD Due Date
+
April 25, 2026
+
+
+
+
+
+
Instructions
+
+ - Bank statements must cover 12 months.
+ - GST discrepancies must be noted.
+ - Verify property papers with originals.
+
+
+
+
+ ) : (
+
+ setActiveTab('details')} requestId={id} requestType="application" />
+
+ )}
+
+
setIsPreviewOpen(false)}
+ document={selectedPreviewDoc}
+ />
+
+ );
+}
diff --git a/src/components/applications/FinanceOnboardingPage.tsx b/src/components/applications/FinanceOnboardingPage.tsx
index c8a3246..197abe6 100644
--- a/src/components/applications/FinanceOnboardingPage.tsx
+++ b/src/components/applications/FinanceOnboardingPage.tsx
@@ -46,7 +46,8 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
const stage = app.currentStage;
return [
'LOI In Progress', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
- 'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT'
+ 'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT',
+ 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
].includes(s) || stage === 'Finance';
});
setApplications(financeApps);
diff --git a/src/components/applications/FnFDetails.tsx b/src/components/applications/FnFDetails.tsx
index 67a61e4..5798ade 100644
--- a/src/components/applications/FnFDetails.tsx
+++ b/src/components/applications/FnFDetails.tsx
@@ -10,7 +10,7 @@ import { Progress } from '../ui/progress';
import { useState, useEffect } from 'react';
import { User, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
import { API } from '../../api/API';
-import { WorkNotesPage } from './WorkNotesPage';
+import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
interface FnFDetailsProps {
@@ -20,6 +20,7 @@ interface FnFDetailsProps {
}
export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
+ const navigate = useNavigate();
const [fnfCase, setFnfCase] = useState(null);
const [loading, setLoading] = useState(true);
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
@@ -161,17 +162,32 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
{fnfCase.requestType}
-
- {/* Action Button */}
- {canSendToStakeholders && fnfCase.status === 'New' && (
-
-
- {request.status}
-
+
+ navigate(`/worknotes/relocation/${requestId}`, {
+ state: {
+ applicationName: request?.outlet?.name || 'Relocation',
+ registrationNumber: request?.requestId || '',
+ participants: request?.participants || []
+ }
+ })}
+ >
+
+ View Work Notes
+ {request?.worknotes?.length > 0 && (
+
+ {request.worknotes.length}
+
+ )}
+
+
+
+ {request.status}
+
+
{/* Request Overview */}
@@ -938,16 +929,16 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{
- if (onOpenWorknote) {
- onOpenWorknote(requestId, 'relocation', `${request.outlet?.name} (${request.outlet?.code}) - Relocation Request`);
- } else {
- setIsWorknoteDialogOpen(true);
+ onClick={() => navigate(`/worknotes/relocation/${requestId}`, {
+ state: {
+ applicationName: request?.outlet?.name || 'Relocation',
+ registrationNumber: request?.requestId || '',
+ participants: request?.participants || []
}
- }}
+ })}
>
- Worknotes ({worknotes.length})
+ Worknotes ({request?.worknotes?.length || 0})
@@ -1044,90 +1035,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{/* Worknotes Dialog */}
-
+ {/* Worknotes Dialog - handled in Header */}
{
};
export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) {
- const [isDialogOpen, setIsDialogOpen] = useState(false);
const [requests, setRequests] = useState([]);
const [isLoading, setIsLoading] = useState(true);
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [dealerCode, setDealerCode] = useState('');
- const [dealerData, setDealerData] = useState(null);
- const [proposedAddress, setProposedAddress] = useState('');
- const [proposedCity, setProposedCity] = useState('');
- const [proposedState, setProposedState] = useState('');
- const [proposedPincode, setProposedPincode] = useState('');
- const [distance, setDistance] = useState('');
- const [reason, setReason] = useState('');
- const [propertyType, setPropertyType] = useState('');
- const [expectedDate, setExpectedDate] = useState('');
- const [locationMode, setLocationMode] = useState<'manual' | 'map'>('manual');
- const [mapCoordinates] = useState({ lat: 19.0760, lng: 72.8777 }); // Default to Mumbai
- const [selectedLocation, setSelectedLocation] = useState<{ lat: number; lng: number } | null>(null);
useEffect(() => {
fetchRequests();
@@ -65,122 +45,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
}
};
- const handleDealerCodeChange = async (code: string) => {
- setDealerCode(code);
- if (code.length >= 4) {
- try {
- const response = await API.getOutletByCode(code) as any;
- if (response.data.success && response.data.outlet) {
- const outlet = response.data.outlet;
- setDealerData({
- dealerName: outlet.name,
- dealerCode: outlet.code,
- currentAddress: outlet.address || 'N/A',
- city: outlet.city || 'N/A',
- state: outlet.state || 'N/A',
- pincode: outlet.pincode || 'N/A',
- dealershipName: outlet.name,
- gst: outlet.gst || 'N/A',
- region: outlet.region?.name || 'N/A',
- zone: outlet.zone?.name || 'N/A'
- });
- toast.success('Dealer details loaded successfully');
- } else {
- setDealerData(null);
- }
- } catch (error) {
- setDealerData(null);
- }
- } else {
- setDealerData(null);
- }
- };
- const handleMapClick = (e: React.MouseEvent) => {
- const rect = e.currentTarget.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- // Convert click position to approximate lat/lng (mock calculation)
- const lat = mapCoordinates.lat + (y - rect.height / 2) / 1000;
- const lng = mapCoordinates.lng + (x - rect.width / 2) / 1000;
-
- setSelectedLocation({ lat, lng });
-
- // Mock reverse geocoding - auto-fill address fields
- const mockLocations = [
- { city: 'Mumbai', state: 'Maharashtra', pincode: '400001', address: 'Nariman Point, South Mumbai' },
- { city: 'Mumbai', state: 'Maharashtra', pincode: '400051', address: 'Andheri East, Mumbai' },
- { city: 'Mumbai', state: 'Maharashtra', pincode: '400070', address: 'Powai, Mumbai' },
- { city: 'Bangalore', state: 'Karnataka', pincode: '560001', address: 'MG Road, Bangalore' },
- { city: 'Chennai', state: 'Tamil Nadu', pincode: '600001', address: 'Anna Salai, Chennai' },
- ];
-
- const randomLocation = mockLocations[Math.floor(Math.random() * mockLocations.length)];
- setProposedAddress(randomLocation.address);
- setProposedCity(randomLocation.city);
- setProposedState(randomLocation.state);
- setProposedPincode(randomLocation.pincode);
-
- toast.success('Location selected from map');
- };
-
- const handleResetForm = () => {
- setDealerCode('');
- setDealerData(null);
- setProposedAddress('');
- setProposedCity('');
- setProposedState('');
- setProposedPincode('');
- setDistance('');
- setReason('');
- setPropertyType('');
- setExpectedDate('');
- setLocationMode('manual');
- setSelectedLocation(null);
- };
-
- const handleSubmitRequest = async (e: React.FormEvent) => {
- e.preventDefault();
-
- if (!dealerData) {
- toast.error('Please enter a valid dealer code');
- return;
- }
-
- if (!proposedAddress.trim() || !proposedCity.trim() || !proposedState.trim() || !proposedPincode.trim()) {
- toast.error('Please enter complete proposed location details');
- return;
- }
-
- try {
- setIsSubmitting(true);
- const payload = {
- dealerCode,
- currentLocation: dealerData.currentAddress,
- proposedLocation: `${proposedAddress}, ${proposedCity}, ${proposedState} - ${proposedPincode}`,
- distance,
- reason,
- propertyType,
- expectedDate: expectedDate || null,
- coordinates: selectedLocation ? `${selectedLocation.lat},${selectedLocation.lng}` : null
- };
-
- const response = await API.createRelocationRequest(payload) as any;
-
- if (response.data.success) {
- toast.success('Relocation request submitted successfully');
- setIsDialogOpen(false);
- handleResetForm();
- fetchRequests();
- }
- } catch (error) {
- console.error('Submit relocation request error:', error);
- toast.error('Failed to submit relocation request');
- } finally {
- setIsSubmitting(false);
- }
- };
// Statistics
const stats = [
@@ -219,318 +84,10 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
Manage dealer relocation requests - Moving dealership to a new location
+
+ • Note: Relocation requests are initiated by the dealer.
+
-
-
{/* Statistics Cards */}
diff --git a/src/components/applications/ResignationDetails.tsx b/src/components/applications/ResignationDetails.tsx
index 9eb59d3..ab9a8fd 100644
--- a/src/components/applications/ResignationDetails.tsx
+++ b/src/components/applications/ResignationDetails.tsx
@@ -3,14 +3,14 @@ import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { Badge } from '../ui/badge';
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
import { User as UserType } from '../../lib/mock-data';
-import { WorkNotesPage } from './WorkNotesPage';
import { toast } from 'sonner';
import { resignationService } from '../../services/resignation.service';
import { Loader2 } from 'lucide-react';
@@ -23,8 +23,8 @@ interface ResignationDetailsProps {
}
export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) {
+ const navigate = useNavigate();
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null });
- const [workNotesOpen, setWorkNotesOpen] = useState(false);
const [remarks, setRemarks] = useState('');
const [assignToUser, setAssignToUser] = useState('');
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
@@ -248,37 +248,26 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
Communication & Notes
-
+ navigate(`/worknotes/resignation/${resignationId}`, {
+ state: {
+ applicationName: resignationData?.outlet?.name || 'Resignation',
+ registrationNumber: resignationData?.resignationId || '',
+ participants: resignationData?.participants || []
+ }
+ })}
+ >
+
+ View Work Notes
+ {resignationData?.worknotes?.length > 0 && (
+
+ {resignationData.worknotes.length}
+
+ )}
+
diff --git a/src/components/applications/ResignationPage.tsx b/src/components/applications/ResignationPage.tsx
index 7c4ccc7..5493dab 100644
--- a/src/components/applications/ResignationPage.tsx
+++ b/src/components/applications/ResignationPage.tsx
@@ -1,13 +1,8 @@
-import { FileText, Calendar, Plus, Eye } from 'lucide-react';
+import { FileText, Calendar, Eye } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
-import { Input } from '../ui/input';
-import { Label } from '../ui/label';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
-import { Textarea } from '../ui/textarea';
import { useState, useEffect } from 'react';
import { API } from '../../api/API';
import { toast } from 'sonner';
@@ -26,19 +21,8 @@ const getStatusColor = (status: string) => {
};
export function ResignationPage({ currentUser, onViewDetails }: ResignationPageProps) {
- const [isDialogOpen, setIsDialogOpen] = useState(false);
- const [dealerCode, setDealerCode] = useState('');
- const [autoFilledData, setAutoFilledData] = useState(null);
const [resignations, setResignations] = useState([]);
const [loading, setLoading] = useState(true);
- const [formData, setFormData] = useState({
- resignationType: 'Voluntary',
- lastOperationalDateSales: '',
- lastOperationalDateServices: '',
- resignationReason: '',
- customerDescription: '',
- document: null as File | null
- });
const fetchResignations = async () => {
setLoading(true);
@@ -60,68 +44,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
fetchResignations();
}, []);
- const handleDealerCodeChange = async (code: string) => {
- setDealerCode(code);
- if (code.length >= 5) {
- try {
- const response = await API.getOutletByCode(code);
- const data = response.data as any;
- if (data?.success) {
- setAutoFilledData(data.outlet);
- toast.success('Dealer details loaded');
- } else {
- setAutoFilledData(null);
- }
- } catch (error) {
- setAutoFilledData(null);
- }
- } else {
- setAutoFilledData(null);
- }
- };
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!autoFilledData) {
- toast.error('Please enter a valid dealer code');
- return;
- }
-
- try {
- const payload = {
- outletId: autoFilledData.id,
- resignationType: formData.resignationType,
- lastOperationalDateSales: formData.lastOperationalDateSales,
- lastOperationalDateServices: formData.lastOperationalDateServices,
- reason: formData.resignationReason,
- additionalInfo: formData.customerDescription
- };
-
- const response = await API.createResignation(payload);
- const data = response.data as any;
- if (data?.success) {
- toast.success('Resignation request submitted successfully');
- setIsDialogOpen(false);
- fetchResignations();
- // Reset form
- setDealerCode('');
- setAutoFilledData(null);
- setFormData({
- resignationType: 'Voluntary',
- lastOperationalDateSales: '',
- lastOperationalDateServices: '',
- resignationReason: '',
- customerDescription: '',
- document: null
- });
- }
- } catch (error: any) {
- console.error('Error submitting resignation:', error);
- toast.error(error.response?.data?.message || 'Failed to submit resignation request');
- }
- };
-
- const isDDLead = currentUser?.role === 'DD Lead';
// Helper function to check if request is at current user's level
const isRequestAtMyLevel = (request: any) => {
@@ -201,153 +124,12 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
Resignation Requests
Track and manage dealer resignation requests
- {!isDDLead && (
-
- • Note: Only DD Lead can create resignation requests. Current role: {currentUser?.role || 'Not logged in'}
-
- )}
+
+ • Note: Resignation requests are initiated by the dealer or via ASM.
+
- {isDDLead && (
-
- )}
diff --git a/src/components/applications/TerminationDetails.tsx b/src/components/applications/TerminationDetails.tsx
index 6711a4a..2730271 100644
--- a/src/components/applications/TerminationDetails.tsx
+++ b/src/components/applications/TerminationDetails.tsx
@@ -1,19 +1,19 @@
-import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck } from 'lucide-react';
+import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck, Loader2 } from 'lucide-react';
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { Badge } from '../ui/badge';
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
-import { toast } from 'sonner';
-import { terminationService } from '../../services/termination.service';
import { useState, useEffect } from 'react';
import { User, mockWorkNotes, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
-import { WorkNotesPage } from './WorkNotesPage';
+import { toast } from 'sonner';
+import { terminationService } from '../../services/termination.service';
+import { useNavigate } from 'react-router-dom';
interface TerminationDetailsProps {
terminationId: string;
@@ -22,13 +22,13 @@ interface TerminationDetailsProps {
}
export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) {
+ const navigate = useNavigate();
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null });
const [remarks, setRemarks] = useState('');
const [assignToUser, setAssignToUser] = useState('');
- const [workNotesOpen, setWorkNotesOpen] = useState(false);
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
+ const [isLoading, setIsLoading] = useState(true);
const [terminationData, setTerminationData] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
const [showSCNDialog, setShowSCNDialog] = useState(false);
const [scnFile, setScnFile] = useState(null);
const [scnRemarks, setScnRemarks] = useState('');
@@ -53,6 +53,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
fetchTermination();
}, [terminationId]);
+ if (isLoading) {
+ return (
+
+
+
Loading termination details...
+
+ );
+ }
+
const handleIssueSCN = async () => {
try {
setIsProcessing(true);
@@ -443,37 +452,26 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
Communication & Notes
-
+ navigate(`/worknotes/termination/${terminationId}`, {
+ state: {
+ applicationName: terminationData?.dealerName || 'Termination',
+ registrationNumber: terminationId || '',
+ participants: terminationData?.participants || []
+ }
+ })}
+ >
+
+ View Work Notes
+ {workNotesCount > 0 && (
+
+ {workNotesCount}
+
+ )}
+
diff --git a/src/components/applications/WorkNotesPage.tsx b/src/components/applications/WorkNotesPage.tsx
index 5f7a357..837ec03 100644
--- a/src/components/applications/WorkNotesPage.tsx
+++ b/src/components/applications/WorkNotesPage.tsx
@@ -4,7 +4,6 @@ import { RootState } from '../../store';
import { useParams, useLocation, useNavigate } from 'react-router-dom';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
-import { ScrollArea } from '../ui/scroll-area';
import { Avatar, AvatarFallback } from '../ui/avatar';
import { toast } from 'sonner';
import {
@@ -16,8 +15,15 @@ import {
MessageSquare,
FileText,
File as FileIcon,
- X
+ X,
+ RefreshCcw,
+ Users,
+ Search,
+ ChevronRight,
+ Info,
+ Clock as ClockIcon
} from 'lucide-react';
+import { Badge } from '../ui/badge';
import {
Dialog,
DialogContent,
@@ -52,7 +58,9 @@ interface WorkNote {
// Participant interface for mentions
interface WorkNotesPageProps {
- applicationId: string;
+ requestId: string;
+ requestType?: 'application' | 'relocation' | 'constitutional' | 'resignation' | 'termination' | 'fnf';
+ mode?: 'page' | 'modal';
applicationName: string;
registrationNumber: string;
onBack: () => void;
@@ -66,18 +74,22 @@ interface ParticipantUI {
email: string;
initials: string;
color: string;
+ role?: string;
+ isOnline?: boolean;
}
const BACKEND_URL = (import.meta as any).env?.VITE_API_URL?.replace('/api', '') || 'http://localhost:5000';
export function WorkNotesPage(props: Partial) {
const { user: currentUser } = useSelector((state: RootState) => state.auth);
- const { id } = useParams<{ id: string }>();
+ const { id, type } = useParams<{ id: string, type: string }>();
const location = useLocation();
const navigate = useNavigate();
// Use props if provided (modal mode), otherwise use URL and state
- const applicationId = props.applicationId || id || '';
+ const requestId = props.requestId || id || '';
+ const requestType = props.requestType || type || (location.state?.requestType as any) || 'application';
+ const mode = props.mode || (location.state?.mode as any) || 'page';
const [appName, setAppName] = useState(props.applicationName || location.state?.applicationName || 'Application');
const [regNumber, setRegNumber] = useState(props.registrationNumber || location.state?.registrationNumber || '');
const onBack = props.onBack || (() => navigate(-1));
@@ -92,6 +104,8 @@ export function WorkNotesPage(props: Partial) {
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
const [attachedFiles, setAttachedFiles] = useState([]);
const [isUploading, setIsUploading] = useState(false);
+ const [isSidebarOpen, setIsSidebarOpen] = useState(true);
+ const [participantSearch, setParticipantSearch] = useState('');
const { socket } = useSocket();
const inputRef = useRef(null);
@@ -162,46 +176,50 @@ export function WorkNotesPage(props: Partial) {
seenIds.add(id);
const name = p.user?.fullName || p.user?.name || p.fullName || p.name || 'Unknown User';
const email = p.user?.email || p.email || '';
+ const role = p.user?.roleCode || p.roleCode || p.user?.role || p.role || 'Participant';
participantsList.push({
id,
name,
email,
initials: getInitials(name),
- color: getAvatarColor(name)
+ color: getAvatarColor(name),
+ role,
+ isOnline: false // Could be linked to socket later
});
}
});
console.log('Participants list for mentions:', participantsList.map(p => ({ id: p.id, name: p.name })));
+ const fetchNotes = async () => {
+ try {
+ setIsLoading(true);
+ const res: any = await worknoteService.getWorknotes(requestId, requestType);
+ if (res.success) {
+ setNotes(res.data.map((n: any) => ({
+ id: n.id,
+ noteText: n.noteText,
+ noteType: n.noteType,
+ createdAt: n.createdAt,
+ userId: n.userId,
+ author: n.author || { name: 'System', email: '', role: 'system' },
+ attachments: n.attachments || []
+ })));
+ }
+ } catch (error) {
+ console.error('Fetch notes error:', error);
+ toast.error('Failed to load work notes');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
// Fetch Notes on load and join socket room
useEffect(() => {
- const fetchNotes = async () => {
- try {
- const res: any = await worknoteService.getWorknotes(applicationId, 'application');
- if (res.success) {
- setNotes(res.data.map((n: any) => ({
- id: n.id,
- noteText: n.noteText,
- noteType: n.noteType,
- createdAt: n.createdAt,
- userId: n.userId,
- author: n.author || { name: 'System', email: '', role: 'system' },
- attachments: n.attachments || []
- })));
- }
- } catch (error) {
- console.error('Fetch notes error:', error);
- toast.error('Failed to load work notes');
- } finally {
- setIsLoading(false);
- }
- };
-
fetchNotes();
if (socket) {
- socket.emit('join_room', applicationId);
+ socket.emit('join_room', requestId);
socket.on('new_worknote', (newNote: any) => {
setNotes(prev => {
@@ -229,18 +247,18 @@ export function WorkNotesPage(props: Partial) {
});
return () => {
- socket.emit('leave_room', applicationId);
+ socket.emit('leave_room', requestId);
socket.off('new_worknote');
};
}
- }, [applicationId, socket]);
+ }, [requestId, requestType, socket]);
// Fetch application details if metadata or participants are missing (e.g. on refresh)
useEffect(() => {
- if (applicationId) {
+ if (requestId && requestType === 'application') {
const fetchApplicationDetails = async () => {
try {
- const appData = await onboardingService.getApplicationById(applicationId);
+ const appData = await onboardingService.getApplicationById(requestId);
if (appData) {
// Update participants if not provided
if (externalParticipants.length === 0 && appData.participants) {
@@ -260,7 +278,7 @@ export function WorkNotesPage(props: Partial) {
};
fetchApplicationDetails();
}
- }, [applicationId, externalParticipants.length, props.applicationName, props.registrationNumber, location.state]);
+ }, [requestId, requestType, externalParticipants.length, props.applicationName, props.registrationNumber, location.state]);
// Auto-scroll logic
const messagesEndRef = useRef(null);
@@ -330,7 +348,7 @@ export function WorkNotesPage(props: Partial) {
setIsUploading(true);
try {
for (const file of Array.from(files)) {
- const res: any = await worknoteService.uploadAttachment(file, applicationId, 'application');
+ const res: any = await worknoteService.uploadAttachment(file, requestId, requestType);
if (res.success) {
setAttachedFiles(prev => [...prev, res.data]);
}
@@ -403,8 +421,8 @@ export function WorkNotesPage(props: Partial) {
setNotes(prev => [tempNote, ...prev]);
const res: any = await worknoteService.addWorknote({
- requestId: applicationId,
- requestType: 'application',
+ requestId: requestId,
+ requestType: requestType,
noteText: processedMessage,
noteType: 'General',
tags: mentionedUserIds,
@@ -491,30 +509,56 @@ export function WorkNotesPage(props: Partial) {
- {/* Participant Avatars */}
-
- {participantsList.slice(0, 3).map((participant, index) => (
-
-
- {participant.initials}
-
-
- ))}
- {participantsList.length > 3 && (
-
- +{participantsList.length - 3}
-
- )}
+ {/* Participant Avatars & Refresh & Toggle Sidebar */}
+
+
+ {participantsList.slice(0, 3).map((participant, index) => (
+
+
+ {participant.initials}
+
+
+ ))}
+ {participantsList.length > 3 && (
+
+ +{participantsList.length - 3}
+
+ )}
+
+
+
+
+ Sync
+
+
+
setIsSidebarOpen(!isSidebarOpen)}
+ className={`flex items-center gap-1.5 px-2 h-9 rounded-lg transition-all ${isSidebarOpen ? 'bg-blue-50 text-blue-600 hover:bg-blue-100' : 'text-slate-500 hover:bg-slate-50'}`}
+ >
+
+ Participants
+
-
-
- {[...notes].reverse().map((note) => {
+
+ {/* Main Chat Engine */}
+
+
+
+ {[...notes].reverse().map((note) => {
const isMe = (note?.author?.email && currentUser?.email && note.author.email.toLowerCase() === currentUser.email.toLowerCase()) ||
(note?.userId && currentUser?.id && note.userId === currentUser.id) ||
note.id.startsWith('temp-');
@@ -609,12 +653,12 @@ export function WorkNotesPage(props: Partial) {
)}
-
-
+
+
- {/* Input Area - Stays fixed because it's a sibling of ScrollArea */}
-
-
+ {/* Input Area */}
+
+
{/* Attachment Previews */}
{attachedFiles.length > 0 && (
@@ -757,11 +801,103 @@ export function WorkNotesPage(props: Partial) {
-
- Press Enter to send • Use @ to mention someone • {isUploading ? 'Uploading files...' : 'Files attached appear above'}
+
+
+ Press Enter to send • Use @ to mention • {isUploading ? 'Uploading files...' : 'Files attached appear above'}
+
+
+ {/* Right Sidebar - Participants */}
+ {isSidebarOpen && (
+
+
+
+
+
+ Participants
+
+ {participantsList.length}
+
+
+ setIsSidebarOpen(false)}>
+
+
+
+
+
+
+ setParticipantSearch(e.target.value)}
+ className="pl-9 bg-slate-50 border-slate-200 h-9 text-sm rounded-lg focus-visible:bg-white transition-all"
+ />
+
+
+
+
+ {participantsList
+ .filter(p => p.name.toLowerCase().includes(participantSearch.toLowerCase()) || p.role?.toLowerCase().includes(participantSearch.toLowerCase()))
+ .map((participant) => (
+
+
+
+
+ {participant.initials}
+
+
+ {participant.isOnline && (
+
+ )}
+
+
+
+
{participant.name}
+ {participant.id === currentUser?.id && (
+
You
+ )}
+
+
+ {participant.role}
+
+
+ {participant.email}
+
+
+
+
+ ))}
+
+ {participantsList.length === 0 && (
+
+
+
No participants found
+
+ )}
+
+
+
+
+
+
+
+
Last Activity
+
Just now
+
+
+
+
+ )}
+
{/* Preview Modal */}