caht enhanced and made centralized componet separate FDD ui creted for document upload

This commit is contained in:
laxman h 2026-04-03 20:32:40 +05:30
parent c9de800c47
commit 0696756160
21 changed files with 1571 additions and 1947 deletions

View File

@ -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() {
<Routes>
{/* Prospective Dealer Route - STRICTLY ISOLATED */}
<Route
path="/prospective-dashboard"
path="/prospective-dashboard/*"
element={
<RoleGuard allowedRoles={['Prospective Dealer']}>
<ProspectiveDashboardPage />
@ -189,6 +192,8 @@ export default function App() {
}
/>
{/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
<Route element={
<RoleGuard excludeRoles={['Prospective Dealer']} redirectTo="/prospective-dashboard">
@ -209,11 +214,10 @@ export default function App() {
{/* Applications */}
<Route path="/applications" element={<ApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" />} />
<Route path="/applications/:id" element={<ApplicationDetails />} />
<Route path="/applications/:id/worknotes" element={
{/* Centralized Work Notes Route */}
<Route path="/worknotes/:type/:id" element={
<WorkNotesPage
applicationId={window.location.pathname.split('/')[2]}
applicationName=""
registrationNumber=""
onBack={() => window.history.back()}
/>
} />
@ -222,6 +226,10 @@ export default function App() {
currentUser?.role === 'DD' ? <AllApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" /> : <Navigate to="/dashboard" />
} />
{/* FDD Routes - Integrated into Layout */}
<Route path="/fdd-dashboard" element={<FDDDashboardPage />} />
<Route path="/fdd-dashboard/application/:id" element={<FDDApplicationDetails />} />
{/* Admin/Lead Routes */}
<Route path="/opportunity-requests" element={<OpportunityRequestsPage onViewDetails={(id) => navigate(`/applications/${id}`)} />} />
<Route path="/non-opportunities" element={<NonOpportunitiesPage onViewDetails={(id) => navigate(`/applications/${id}`)} />} />
@ -256,10 +264,10 @@ export default function App() {
<Route path="/finance-fnf/:id" element={<FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />} />
<Route path="/constitutional-change" element={<ConstitutionalChangePage currentUser={currentUser} onViewDetails={(id) => navigate(`/constitutional-change/${id}`)} />} />
<Route path="/constitutional-change/:id" element={<ConstitutionalChangeDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/constitutional-change')} currentUser={currentUser} onOpenWorknote={() => { }} />} />
<Route path="/constitutional-change/:id" element={<ConstitutionalChangeDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/constitutional-change')} currentUser={currentUser} />} />
<Route path="/relocation-requests" element={<RelocationRequestPage currentUser={currentUser} onViewDetails={(id) => navigate(`/relocation-requests/${id}`)} />} />
<Route path="/relocation-requests/:id" element={<RelocationRequestDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/relocation-requests')} currentUser={currentUser} onOpenWorknote={() => { }} />} />
<Route path="/relocation-requests/:id" element={<RelocationRequestDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/relocation-requests')} currentUser={currentUser} />} />
{/* Dealer Routes */}
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />} />

View File

@ -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<any[]>([]);
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<string | null>(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() {
</div>
</div>
<div className="flex flex-wrap gap-2">
{/* Actions can be added here in the future */}
<Button
variant="outline"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm"
onClick={() => navigate(`/worknotes/application/${application.id}`, {
state: {
applicationName: application.name,
registrationNumber: application.registrationNumber,
participants: application.participants
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
</Button>
</div>
</div>
@ -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()}
</div>
<div className="flex flex-col">
<span className="text-[10px] font-medium text-slate-700 leading-none">{approver.name}</span>
@ -2826,7 +2827,7 @@ export function ApplicationDetails() {
<Button
variant="outline"
className="w-full"
onClick={() => navigate(`/applications/${application.id}/worknotes`, {
onClick={() => navigate(`/worknotes/application/${application.id}`, {
state: {
applicationName: application.name,
registrationNumber: application.registrationNumber,
@ -3028,39 +3029,6 @@ export function ApplicationDetails() {
</CardContent>
</Card>
)}
{/* Work Notes Chat */}
{/* Only show Work Notes card for shortlisted applications (opportunity requests and regular dealership requests) */}
{/* Hide Work Notes for non-opportunity requests (lead generation) - no workflow tracking needed */}
{/* {
application.isShortlisted !== false && (
<Card>
<CardHeader>
<CardTitle>Work Notes</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-64">
<div className="space-y-4">
{mockWorkNotes.map((note) => (
<div key={note.id} className="flex gap-3">
<div className="w-8 h-8 bg-amber-600 rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-white">{note.user.charAt(0)}</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<p className="text-slate-900">{note.user}</p>
<span className="text-slate-500">{note.timestamp}</span>
</div>
<p className="text-slate-600 mt-1">{note.message}</p>
</div>
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
)
} */}
</div >
</div >
@ -3218,48 +3186,6 @@ export function ApplicationDetails() {
</DialogContent>
</Dialog >
{/* Work Note Modal */}
< Dialog open={showWorkNoteModal} onOpenChange={setShowWorkNoteModal} >
<DialogContent>
<DialogHeader>
<DialogTitle>Add Work Note</DialogTitle>
<DialogDescription>
Add a note to track progress and communicate with team members.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Note</Label>
<Textarea
placeholder="Enter your note... Use @username to mention someone"
value={workNote}
onChange={(e) => setWorkNote(e.target.value)}
className="mt-2"
rows={4}
/>
</div>
<div>
<Label>Attachments (Optional)</Label>
<Input type="file" className="mt-2" multiple />
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1"
onClick={() => setShowWorkNoteModal(false)}
>
Cancel
</Button>
<Button
className="flex-1 bg-amber-600 hover:bg-amber-700"
onClick={handleWorkNote}
>
Add Note
</Button>
</div>
</div>
</DialogContent>
</Dialog >

View File

@ -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<any>(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 (
<div className="space-y-6">
{/* Header */}
@ -622,16 +601,16 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
<Button
variant="outline"
className="w-full border-blue- blue-700 hover:bg-blue-50"
onClick={() => {
if (onOpenWorknote) {
onOpenWorknote(requestId, 'constitutional-change', `${request.outlet?.name || 'N/A'} (${request.outlet?.code || 'N/A'}) - Constitutional Change Request`);
} else {
setIsWorknoteDialogOpen(true);
onClick={() => navigate(`/worknotes/constitutional-change/${requestId}`, {
state: {
applicationName: request?.outlet?.name || 'Constitutional Change',
registrationNumber: requestId || '',
participants: request?.participants || []
}
}}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
Worknotes ({(request.worknotes || []).length})
Worknotes ({(request?.worknotes || []).length})
</Button>
</div>
</CardContent>
@ -699,87 +678,6 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
</DialogContent>
</Dialog>
{/* Worknotes Dialog */}
<Dialog open={isWorknoteDialogOpen} onOpenChange={setIsWorknoteDialogOpen}>
<DialogContent className="max-w-3xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Worknotes - Discussion Platform</DialogTitle>
<DialogDescription>
Collaborate with team members on this constitutional change request. All discussions are logged and timestamped.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Discussion Thread */}
<div className="space-y-2">
<Label>Discussion History ({(request.worknotes || []).length} messages)</Label>
<div className="border border-slate-200 rounded-lg p-4 max-h-96 overflow-y-auto bg-slate-50">
<div className="space-y-4">
{(request.worknotes || []).map((note: any) => (
<div key={note.id} className="flex items-start gap-3">
{/* Avatar */}
<div className="w-10 h-10 rounded-full bg-amber-600 flex items-center justify-center text-white flex-shrink-0">
{note.author?.fullName?.slice(0, 2).toUpperCase() || note.user?.fullName?.slice(0, 2).toUpperCase() || 'UN'}
</div>
{/* Message Content */}
<div className="flex-1 bg-white rounded-lg p-3 border border-slate-200">
<div className="flex items-start justify-between mb-1">
<div>
<h5 className="text-slate-900">{note.author?.fullName || note.user?.fullName || 'Unknown User'}</h5>
<Badge variant="outline" className="border-slate-300 text-xs">
{note.author?.role || note.user?.role?.name || 'User'}
</Badge>
</div>
<span className="text-slate-500 text-xs">{new Date(note.createdAt).toLocaleString()}</span>
</div>
<p className="text-slate-700 text-sm mt-2">{note.noteText || note.message}</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* Add New Worknote */}
<div className="space-y-2">
<Label htmlFor="newWorknote">Add New Worknote</Label>
<Textarea
id="newWorknote"
value={newWorknote}
onChange={(e) => setNewWorknote(e.target.value)}
placeholder="Type your message here... Share updates, ask questions, or provide feedback."
rows={3}
className="resize-none"
/>
<p className="text-slate-500 text-xs">
Posting as: {currentUser?.name || 'Anonymous'} ({currentUser?.role || 'User'})
</p>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setIsWorknoteDialogOpen(false);
setNewWorknote('');
}}
>
Close
</Button>
<Button
type="button"
className="bg-amber-600 hover:bg-amber-700"
onClick={handleAddWorknote}
disabled={!newWorknote.trim()}
>
<MessageSquare className="w-4 h-4 mr-2" />
Post Worknote
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -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<any>(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<any>(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<HTMLInputElement>) => {
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 (
<div className="flex flex-col items-center justify-center h-[70vh] bg-slate-50/50 rounded-2xl border border-slate-200 border-dashed">
<Loader2 className="w-12 h-12 animate-spin text-blue-600 mb-4" />
<p className="text-slate-500 font-medium">Authenticating and loading secure data...</p>
</div>
);
}
if (!application) return null;
return (
<div className="flex flex-col gap-6 max-w-7xl mx-auto mb-10">
{/* Action Bar */}
<div className="flex items-center justify-between">
<button
onClick={() => navigate('/fdd-dashboard')}
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 font-medium transition-all group"
>
<div className="p-2 rounded-full group-hover:bg-slate-100 transition-colors">
<ArrowLeft className="w-5 h-5" />
</div>
Back to Dashboard
</button>
<div className="flex items-center gap-3">
<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 Audit Report');
formData.append('applicationId', application.id);
await API.uploadDocument(application.id, formData);
toast.success('FDD Audit Report uploaded successfully');
fetchApplication(); // Refresh to show the new doc
} 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={async () => {
if (!window.confirm('Are you sure you want to flag this application as non-responsive?')) return;
try {
setUploading(true);
await API.submitStageDecision({
applicationId: application.id,
stageCode: 'FDD_VERIFICATION',
decision: 'Rejected',
remarks: 'Applicant is non-responsive to FDD queries.'
});
toast.error('Application flagged and returned to admin.');
navigate('/fdd-dashboard');
} catch (e) { toast.error('Action failed'); } finally { setUploading(false); }
}}
className="px-4 py-2 text-red-600 font-bold text-xs uppercase tracking-wider hover:bg-red-50 rounded-lg transition-all"
>
Flag Non-Responsive
</button>
<button
disabled={uploading}
onClick={async () => {
if (!window.confirm('Finalizing the report will submit your findings and lock this case. Proceed?')) return;
try {
setUploading(true);
const res: any = await API.submitStageDecision({
applicationId: application.id,
stageCode: 'FDD_VERIFICATION',
decision: 'Approved',
remarks: 'FDD Verification completed and report uploaded.',
nextStatus: 'Security Details',
nextProgress: 75
});
if (res.data?.success) {
toast.success('FDD Report submitted successfully.');
navigate('/fdd-dashboard');
}
} catch (e) { toast.error('Failed to submit report'); } finally { setUploading(false); }
}}
className="px-6 py-2 bg-blue-600 text-white font-bold text-xs uppercase tracking-wider rounded-lg shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all hover:scale-[1.02] disabled:opacity-50"
>
{uploading ? 'Processing...' : 'Finalize & Submit Report'}
</button>
</div>
</div>
{/* Header Card */}
<Card className="border border-slate-200 shadow-sm bg-white">
<CardContent className="p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-slate-900 text-white rounded-lg flex items-center justify-center font-bold text-xl">
{application.applicantName.charAt(0)}
</div>
<div>
<div className="flex items-center gap-2 mb-0.5">
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">{application.applicantName}</h1>
<Badge variant="outline" className="text-slate-500 font-medium px-2 py-0">
{application.applicationId}
</Badge>
</div>
<div className="flex items-center gap-3 text-sm text-slate-500">
<span>{application.city}, {application.state}</span>
<span className="text-slate-300"></span>
<span>{application.businessType || 'Dealership'}</span>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="text-right hidden md:block">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Status</p>
<p className="text-sm font-bold text-slate-700">Financial Due Diligence</p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Navigation Tabs */}
<div className="flex items-center gap-8 border-b border-slate-200">
<button
onClick={() => setActiveTab('details')}
className={`pb-3 text-sm font-semibold transition-all relative ${
activeTab === 'details' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
}`}
>
Workspace
{activeTab === 'details' && <div className="absolute bottom-[-1px] left-0 right-0 h-0.5 bg-blue-600" />}
</button>
<button
onClick={() => setActiveTab('worknotes')}
className={`pb-3 text-sm font-semibold transition-all relative ${
activeTab === 'worknotes' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
}`}
>
<div className="flex items-center gap-2">
Work Notes
<span className="bg-slate-100 text-slate-500 px-1.5 py-0.5 rounded text-[10px]">0</span>
</div>
{activeTab === 'worknotes' && <div className="absolute bottom-[-1px] left-0 right-0 h-0.5 bg-blue-600" />}
</button>
</div>
{activeTab === 'details' ? (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column: Financial Data & Uploads */}
<div className="lg:col-span-2 space-y-6">
<Card className="border border-slate-200 shadow-sm bg-white">
<CardHeader className="border-b border-slate-100 px-6 py-4">
<CardTitle className="text-base font-bold flex items-center gap-2 text-slate-800">
<Upload className="w-4 h-4 text-slate-500" />
Financial Report Submission
</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="p-10 border-2 border-dashed border-slate-200 rounded-lg flex flex-col items-center justify-center text-center">
<div className="w-12 h-12 bg-slate-50 text-slate-400 rounded-full flex items-center justify-center mb-4">
<FileText className="w-6 h-6" />
</div>
<p className="text-slate-600 font-medium mb-1">Select and upload the due diligence report</p>
<p className="text-slate-400 text-xs mb-6">PDF or JPG formats accepted (Max 10MB)</p>
<div className="w-full max-w-sm space-y-4">
<select
value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)}
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded text-sm font-medium text-slate-700 outline-none focus:ring-1 focus:ring-blue-500 transition-all"
>
<option value="">Select Document Category...</option>
<option value="Final FDD Audit Report">Final FDD Audit Report</option>
<option value="Bank Statement Analysis">Bank Statement Analysis</option>
<option value="Credit Compliance Report">Credit Compliance Report</option>
<option value="Business Valuation Report">Business Valuation Report</option>
<option value="Property Verification Report">Property Verification Report</option>
</select>
<div className="relative">
{uploading ? (
<div className="w-full py-2.5 bg-slate-100 rounded flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-slate-400" />
<span className="text-slate-500 text-xs font-bold uppercase tracking-wider">Uploading...</span>
</div>
) : (
<>
<input
type="file"
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={handleFileUpload}
disabled={!selectedDocType}
/>
<div className={`w-full py-2.5 text-center font-bold uppercase tracking-wider text-xs rounded transition-all ${
!selectedDocType ? 'bg-slate-100 text-slate-300' : 'bg-slate-900 text-white hover:bg-slate-800'
}`}>
Browse & Upload
</div>
</>
)}
</div>
</div>
</div>
{/* List of Uploaded Documents */}
<div className="mt-8 border-t border-slate-100 pt-8">
<h3 className="text-sm font-bold text-slate-800 mb-4 px-1">Submitted Documentation</h3>
<div className="space-y-6">
{/* SECTION 1: APPLICANT DOCUMENTS */}
<div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
Applicant's KYC & Financials
</p>
<div className="space-y-2">
{application.uploadedDocuments?.filter((d: any) => !d.uploader || d.uploader.roleCode !== 'FDD').map((doc: any, i: number) => (
<div key={i} className="p-3 border border-slate-100 rounded flex items-center justify-between hover:bg-slate-50 transition-all group">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-slate-400 group-hover:bg-white transition-colors">
<FileText className="w-4 h-4" />
</div>
<div>
<div className="flex items-center gap-2">
<p className="text-xs font-bold text-slate-900">{doc.originalName || doc.fileName}</p>
<span className="text-[8px] bg-slate-100 text-slate-500 px-1 py-0.5 rounded uppercase font-bold tracking-tighter">APPLICANT</span>
</div>
<p className="text-[10px] text-slate-400 font-medium">
{doc.documentType} {new Date(doc.createdAt).toLocaleDateString()}
{doc.uploader?.fullName && ` • by ${doc.uploader.fullName}`}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => handlePreview(doc)}
className="p-1.5 hover:bg-white rounded text-slate-400 hover:text-blue-600 transition-all"
>
<Eye className="w-4 h-4" />
</button>
<a href={`http://localhost:5000${doc.filePath?.startsWith('/') ? '' : '/'}${doc.filePath}`} target="_blank" className="p-1.5 hover:bg-white rounded text-blue-600 transition-all">
<Upload className="w-3.5 h-3.5 rotate-180" />
</a>
</div>
</div>
))}
{application.uploadedDocuments?.filter((d: any) => !d.uploader || d.uploader.roleCode !== 'FDD').length === 0 && (
<p className="text-[10px] text-slate-400 italic px-1">No documents from applicant yet.</p>
)}
</div>
</div>
{/* SECTION 2: MY SUBMISSIONS */}
<div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div>
My Uploaded Reports
</p>
<div className="space-y-2">
{application.uploadedDocuments?.filter((d: any) => d.uploader?.roleCode === 'FDD').map((doc: any, i: number) => (
<div key={i} className="p-3 border border-amber-100 bg-amber-50/30 rounded flex items-center justify-between hover:bg-amber-50 transition-all group">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-amber-100 flex items-center justify-center text-amber-500">
<FileText className="w-4 h-4" />
</div>
<div>
<div className="flex items-center gap-2">
<p className="text-xs font-bold text-slate-900">{doc.originalName || doc.fileName}</p>
<span className="text-[8px] bg-amber-500 text-white px-1 py-0.5 rounded uppercase font-bold tracking-tighter">YOUR AUDIT REPORT</span>
</div>
<p className="text-[10px] text-slate-400 font-medium">
{doc.documentType} {new Date(doc.createdAt).toLocaleDateString()}
{doc.uploader?.fullName && ` • by ${doc.uploader.fullName}`}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => handlePreview(doc)}
className="p-1.5 hover:bg-white rounded text-slate-400 hover:text-amber-600 transition-all"
>
<Eye className="w-4 h-4" />
</button>
<a href={`http://localhost:5000${doc.filePath?.startsWith('/') ? '' : '/'}${doc.filePath}`} target="_blank" className="p-1.5 hover:bg-white rounded text-blue-600 transition-all">
<Upload className="w-3.5 h-3.5 rotate-180" />
</a>
</div>
</div>
))}
{application.uploadedDocuments?.filter((d: any) => d.uploader?.roleCode === 'FDD').length === 0 && (
<div className="text-center py-4 bg-slate-50 border border-dashed border-slate-200 rounded-lg">
<p className="text-slate-400 text-[10px]">No audit reports uploaded yet.</p>
</div>
)}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Right Column: Applicant Meta & Guidelines */}
<div className="space-y-6">
<Card className="border border-slate-200 shadow-sm bg-white">
<CardHeader className="border-b border-slate-100 px-6 py-4">
<CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-500">Applicant Profile</CardTitle>
</CardHeader>
<CardContent className="p-6 space-y-4">
<div className="space-y-1 pb-4 border-b border-slate-50">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Target Location</p>
<p className="text-sm font-extrabold text-slate-900">{application.city}, {application.state}</p>
</div>
<div className="grid grid-cols-2 gap-4 text-xs">
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Education</p>
<p className="font-bold text-slate-800">{application.education || 'N/A'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Experience</p>
<p className="font-bold text-slate-800">{application.experienceYears || '0'} Years</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Investment Cap</p>
<p className="font-bold text-slate-800">{application.investmentCapacity || 'N/A'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Age</p>
<p className="font-bold text-slate-800">{application.age || 'N/A'}</p>
</div>
</div>
<div className="space-y-1 pt-4 border-t border-slate-50 text-xs">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Communication</p>
<p className="font-bold text-slate-800">{application.email}</p>
<p className="text-slate-500 font-medium">{application.phone}</p>
</div>
<div className="space-y-1 pt-4 border-t border-slate-50 text-xs">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">FDD Due Date</p>
<p className="font-bold text-slate-800">April 25, 2026</p>
</div>
</CardContent>
</Card>
<div className="p-6 bg-slate-900 rounded-lg text-white font-medium">
<h4 className="text-sm font-bold mb-2">Instructions</h4>
<ul className="text-xs text-slate-300 space-y-2 list-disc pl-4">
<li>Bank statements must cover 12 months.</li>
<li>GST discrepancies must be noted.</li>
<li>Verify property papers with originals.</li>
</ul>
</div>
</div>
</div>
) : (
<div className="bg-white rounded-lg border border-slate-200 min-h-[600px] overflow-hidden">
<WorkNotesPage onBack={() => setActiveTab('details')} requestId={id} requestType="application" />
</div>
)}
<DocumentPreviewModal
isOpen={isPreviewOpen}
onClose={() => setIsPreviewOpen(false)}
document={selectedPreviewDoc}
/>
</div>
);
}

View File

@ -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);

View File

@ -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<any>(null);
const [loading, setLoading] = useState(true);
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
@ -161,8 +162,22 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
{fnfCase.requestType}
</Badge>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-3">
<Button
variant="outline"
onClick={() => navigate(`/worknotes/fnf/${fnfId}`, {
state: {
applicationName: fnfCase.dealerName || 'F&F Settlement',
registrationNumber: fnfId || '',
participants: fnfCase.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
</Button>
{/* Action Button */}
{canSendToStakeholders && fnfCase.status === 'New' && (
<Button
className="bg-blue-600 hover:bg-blue-700"
@ -173,6 +188,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Button>
)}
</div>
</div>
{/* Progress Summary */}
<Card>
@ -217,7 +233,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TabsTrigger value="departments">Department Responses</TabsTrigger>
<TabsTrigger value="financial">Financial Summary</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger>
<TabsTrigger value="worknotes">Work Notes</TabsTrigger>
<TabsTrigger value="audit">Audit Trail</TabsTrigger>
</TabsList>
@ -900,11 +916,6 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Card>
</TabsContent>
{/* Work Notes Tab */}
<TabsContent value="worknotes">
<WorkNotesPage />
</TabsContent>
{/* Audit Trail Tab */}
<TabsContent value="audit">
<Card>

View File

@ -0,0 +1,295 @@
import { useState, useEffect } from 'react';
import {
ChevronLeft,
Upload,
Clock,
RefreshCw,
File
} from 'lucide-react';
import { toast } from 'sonner';
import { API } from '../../api/API';
interface Props {
id: string;
onBack: () => void;
}
export function ProspectiveApplicationDetails({ id, onBack }: Props) {
const [details, setDetails] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [documents, setDocuments] = useState<any[]>([]);
const [selectedDocType, setSelectedDocType] = useState('');
const [file, setFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
useEffect(() => {
fetchData();
}, [id]);
const fetchData = async () => {
setLoading(true);
try {
const [detailsRes, docsRes]: [any, any] = await Promise.all([
API.getApplicationById(id),
API.getDocuments(id)
]);
if (detailsRes.data?.success) {
setDetails(detailsRes.data.data);
}
if (docsRes.data?.success || docsRes.ok) {
setDocuments(docsRes.data.data || []);
}
} catch (error) {
console.error('Failed to fetch details:', error);
toast.error('Failed to load application details');
} finally {
setLoading(false);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
}
};
const handleUpload = async () => {
if (!file || !selectedDocType) {
toast.error('Please select a document type and file');
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', selectedDocType);
setIsUploading(true);
try {
const response: any = await API.uploadDocument(id, formData);
if (response.data?.success || response.ok) {
toast.success('Document uploaded successfully');
setFile(null);
setSelectedDocType('');
const fileInput = document.getElementById('file-upload') as HTMLInputElement;
if (fileInput) fileInput.value = '';
// Refresh documents
const docsRes: any = await API.getDocuments(id);
if (docsRes.data?.success || docsRes.ok) {
setDocuments(docsRes.data.data || []);
}
} else {
toast.error(response.data?.message || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
toast.error('Upload failed');
} finally {
setIsUploading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<RefreshCw className="w-8 h-8 animate-spin text-amber-600" />
</div>
);
}
if (!details) {
return (
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
<p className="text-slate-600 mb-4">Application details not found.</p>
<button onClick={onBack} className="bg-amber-600 text-white px-4 py-2 rounded-md hover:bg-amber-700">Go Back</button>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center mb-4">
<button
onClick={onBack}
className="mr-3 p-1.5 rounded-full hover:bg-slate-200 text-slate-600 transition-colors"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div>
<h1 className="text-slate-900 text-2xl font-bold mb-1">Application Details</h1>
<div className="flex items-center gap-2">
<p className="text-slate-600 font-medium">
{details.applicationId || 'Loading...'}
</p>
{details.districtId ? (
<span className="text-[10px] bg-green-100 text-green-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Opportunity</span>
) : (
<span className="text-[10px] bg-blue-100 text-blue-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Future Reference</span>
)}
</div>
</div>
</div>
<div className="animate-in fade-in duration-500">
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6 mb-6">
<h4 className="text-lg font-semibold text-slate-900 mb-4 border-b pb-2">Status & Tracking</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-slate-500 mb-1">Overall Status</p>
<p className="font-medium text-slate-900">{details.overallStatus || '-'}</p>
</div>
<div>
<p className="text-sm text-slate-500 mb-1">Current Stage</p>
<p className="font-medium text-slate-900">{details.currentStage || '-'}</p>
</div>
<div>
<p className="text-sm text-slate-500 mb-1">Location</p>
<p className="font-medium text-slate-900">{details.city}, {details.state}</p>
</div>
<div>
<p className="text-sm text-slate-500 mb-1">Applied Date</p>
<p className="font-medium text-slate-900">
{details.createdAt ? new Date(details.createdAt).toLocaleDateString() : '-'}
</p>
</div>
</div>
{details.statusHistory?.[0]?.changeReason && (
<div className="mt-4 p-3 bg-amber-50 border border-amber-100 rounded-lg">
<p className="text-xs font-semibold text-amber-800 uppercase tracking-wider mb-1">Latest Feedback</p>
<p className="text-sm text-amber-900 italic">"{details.statusHistory[0].changeReason}"</p>
</div>
)}
<div className="mt-6">
<div className="flex justify-between items-center mb-1">
<p className="text-sm font-medium text-slate-700">Application Progress</p>
<p className="text-sm font-medium text-amber-600">{details.progressPercentage || 0}%</p>
</div>
<div className="w-full bg-slate-100 rounded-full h-2.5">
<div className="bg-amber-500 h-2.5 rounded-full transition-all" style={{ width: `${details.progressPercentage || 0}%` }}></div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="p-6 border-b border-slate-200">
<h4 className="flex items-center gap-2 text-lg font-semibold text-slate-900">
<Upload className="w-5 h-5 text-blue-600" /> Document Upload
</h4>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg border border-slate-200">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">Document Type</label>
<select
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm"
value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)}
disabled={isUploading}
>
<option value="">Select type...</option>
<option value="PAN Card">PAN Card</option>
<option value="GST Certificate">GST Certificate</option>
<option value="Aadhaar Card">Aadhaar Card</option>
<option value="Initial Security Deposit Receipt">Initial Security Deposit Receipt</option>
<option value="Final Security Deposit Receipt">Final Security Deposit Receipt</option>
<option value="Partnership Deed">Partnership Deed</option>
<option value="LLP Agreement">LLP Agreement</option>
<option value="Certificate of Incorporation">Certificate of Incorporation</option>
<option value="MOA">MOA (Memorandum of Association)</option>
<option value="AOA">AOA (Articles of Association)</option>
<option value="Firm Registration">Firm Registration</option>
<option value="Rental Agreement">Rental Agreement</option>
<option value="Property Documents">Property Documents</option>
<option value="Nodal Agreement">Nodal Agreement</option>
<option value="Cancelled Check">Cancelled Check</option>
<option value="LOI Acknowledgement">LOI Acknowledgement</option>
<option value="Architecture Blueprint">Architecture Blueprint</option>
<option value="Site Plan">Site Plan</option>
<option value="Other">Other</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">File</label>
<input
type="file"
id="file-upload"
className="w-full text-sm"
onChange={handleFileChange}
disabled={isUploading}
/>
</div>
<div className="md:col-span-2 flex justify-end mt-2">
<button
onClick={handleUpload}
disabled={!file || !selectedDocType || isUploading}
className="bg-amber-600 text-white px-4 py-2 rounded-md hover:bg-amber-700 disabled:opacity-50 flex items-center gap-2"
>
{isUploading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />}
Upload
</button>
</div>
</div>
<div className="space-y-3">
<h3 className="font-medium text-slate-900">Uploaded Documents ({documents.length})</h3>
<div className="space-y-2">
{documents.length > 0 ? documents.map((doc) => (
<div key={doc.id} className="flex justify-between items-center p-3 border border-slate-200 rounded-lg bg-white">
<div className="flex items-center gap-3">
<File className="w-5 h-5 text-blue-600" />
<div>
<p className="text-sm font-medium">{doc.documentType}</p>
<p className="text-xs text-slate-500">{doc.fileName}</p>
</div>
</div>
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold uppercase ${doc.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
{doc.status || 'Pending'}
</span>
</div>
)) : (
<p className="text-sm text-slate-500 italic text-center py-4 bg-slate-50 rounded-lg">No documents uploaded yet.</p>
)}
</div>
</div>
</div>
</div>
</div>
<div className="lg:col-span-1">
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-4 bg-slate-50 border-b border-slate-200">
<h3 className="text-sm font-bold text-slate-900 flex items-center gap-2 uppercase tracking-wide">
<Clock className="w-4 h-4 text-amber-600" /> Timeline
</h3>
</div>
<div className="p-6">
{details.statusHistory?.length > 0 ? (
<div className="relative space-y-6">
<div className="absolute left-[11px] top-2 bottom-4 w-0.5 bg-slate-200"></div>
{[...details.statusHistory].reverse().map((item: any) => (
<div key={item.id} className="relative pl-8">
<div className="absolute left-0 top-1 w-[24px] h-[24px] rounded-full border-2 bg-white flex items-center justify-center border-green-500">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
</div>
<div>
<p className="text-sm font-semibold text-slate-900">{item.newStatus}</p>
<p className="text-[11px] text-slate-500">{new Date(item.createdAt).toLocaleString()}</p>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-slate-500 italic text-center">No history available</p>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { ArrowLeft, CheckCircle2, Clock, AlertCircle, Upload, Download, Eye, Navigation, MapPin, GitBranch, MessageSquare, Loader2 } from 'lucide-react';
import { ArrowLeft, CheckCircle2, Clock, AlertCircle, Upload, Download, Eye, Navigation, MapPin, MessageSquare, Loader2 } from 'lucide-react';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils';
import { Button } from '../ui/button';
@ -11,6 +11,7 @@ import { Textarea } from '../ui/textarea';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner';
import { API } from '../../api/API';
@ -19,7 +20,6 @@ interface RelocationRequestDetailsProps {
requestId: string;
onBack: () => void;
currentUser: UserType | null;
onOpenWorknote?: (requestId: string, requestType: 'relocation' | 'constitutional-change' | 'fnf' | 'resignation' | 'termination', requestTitle: string) => void;
}
// Workflow stages configuration
@ -60,7 +60,8 @@ const getStatusColor = (status: string) => {
return 'bg-slate-100 text-slate-700 border-slate-300';
};
export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpenWorknote }: RelocationRequestDetailsProps) {
export function RelocationRequestDetails({ requestId, onBack, currentUser }: RelocationRequestDetailsProps) {
const navigate = useNavigate();
const [request, setRequest] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
@ -68,9 +69,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
const [comments, setComments] = useState('');
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [isWorknoteDialogOpen, setIsWorknoteDialogOpen] = useState(false);
const [worknotes, setWorknotes] = useState<any[]>([]);
const [newWorknote, setNewWorknote] = useState('');
const [eorChecklist, setEorChecklist] = useState<any>(null);
const [isEorLoading, setIsEorLoading] = useState(false);
const [isSubmittingEor, setIsSubmittingEor] = useState(false);
@ -145,7 +143,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
const response = await API.getRelocationRequestById(requestId) as any;
if (response.data.success) {
setRequest(response.data.request);
setWorknotes(response.data.request.worknotes || []);
// Auto-fetch EOR checklist if in the correct stage
const currentStage = response.data.request.currentStage;
@ -194,7 +191,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
if (!request || !currentUser) return false;
// Check for Super Admin bypass
const isAdmin = currentUser.role === 'Super Admin' || currentUser.role === 'Super Admin';
const isAdmin = (currentUser?.role as any) === 'Super Admin' || currentUser.role === 'Super Admin';
if (isAdmin) return true;
// Check if user's role matches the role required for the current stage
@ -203,16 +200,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
const showActions = canUserAction() && request.status !== 'Completed' && request.status !== 'Rejected';
if (!request) {
return (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Request Not Found</h2>
<p className="text-slate-600 mb-4">The relocation request you're looking for doesn't exist.</p>
<Button onClick={onBack}>Go Back</Button>
</div>
);
}
const handleAction = (type: 'approve' | 'reject' | 'hold') => {
setActionType(type);
setIsActionDialogOpen(true);
@ -244,25 +231,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
}
};
const handleAddWorknote = async () => {
if (newWorknote.trim()) {
try {
const response = await API.addWorknote({
requestId,
requestType: 'relocation',
message: newWorknote
}) as any;
if (response.data.success) {
setNewWorknote('');
fetchRequestDetails();
toast.success('Worknote added successfully');
}
} catch (error) {
console.error('Add worknote error:', error);
toast.error('Failed to add worknote');
}
}
};
const handleUploadDocument = async () => {
if (!selectedFile || !selectedDocType) {
@ -356,10 +325,32 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm"
onClick={() => navigate(`/worknotes/relocation/${requestId}`, {
state: {
applicationName: request?.outlet?.name || 'Relocation',
registrationNumber: request?.requestId || '',
participants: request?.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
{request?.worknotes?.length > 0 && (
<Badge className="ml-2 bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
{request.worknotes.length}
</Badge>
)}
</Button>
<Badge className={getStatusColor(request.status)}>
{request.status}
</Badge>
</div>
</div>
{/* Request Overview */}
<Card>
@ -938,16 +929,16 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<Button
variant="outline"
className="w-full border-blue-300 text-blue-700 hover:bg-blue-50"
onClick={() => {
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 || []
}
}}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
Worknotes ({worknotes.length})
Worknotes ({request?.worknotes?.length || 0})
</Button>
</CardContent>
</Card>
@ -1044,90 +1035,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{/* Worknotes Dialog */}
<Dialog open={isWorknoteDialogOpen} onOpenChange={setIsWorknoteDialogOpen}>
<DialogContent className="max-w-3xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Worknotes - Discussion Platform</DialogTitle>
<DialogDescription>
Collaborate with team members on this relocation request. All discussions are logged and timestamped.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Discussion Thread */}
<div className="space-y-2">
<Label>Discussion History ({worknotes.length} messages)</Label>
<div className="border border-slate-200 rounded-lg p-4 max-h-96 overflow-y-auto bg-slate-50">
<div className="space-y-4">
{worknotes.length > 0 ? worknotes.map((note: any) => (
<div key={note.id} className="flex items-start gap-3">
{/* Avatar */}
<div className="w-10 h-10 rounded-full bg-amber-600 flex items-center justify-center text-white flex-shrink-0">
{note.author?.fullName?.slice(0, 2).toUpperCase() || 'AN'}
</div>
{/* Message Content */}
<div className="flex-1 bg-white rounded-lg p-3 border border-slate-200">
<div className="flex items-start justify-between mb-1">
<div>
<h5 className="text-slate-900">{note.author?.fullName}</h5>
<Badge variant="outline" className="border-slate-300 text-xs">
{note.author?.role || 'User'}
</Badge>
</div>
<span className="text-slate-500 text-xs">{formatDateTime(note.createdAt)}</span>
</div>
<p className="text-slate-700 text-sm mt-2">{note.noteText}</p>
</div>
</div>
)) : (
<div className="text-center py-4 text-slate-500">
No worknotes yet
</div>
)}
</div>
</div>
</div>
{/* Add New Worknote */}
<div className="space-y-2">
<Label htmlFor="newWorknote">Add New Worknote</Label>
<Textarea
id="newWorknote"
value={newWorknote}
onChange={(e) => setNewWorknote(e.target.value)}
placeholder="Type your message here... Share updates, ask questions, or provide feedback."
rows={3}
className="resize-none"
/>
<p className="text-slate-500 text-xs">
Posting as: {currentUser?.name || 'Anonymous'} ({currentUser?.role || 'User'})
</p>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setIsWorknoteDialogOpen(false);
setNewWorknote('');
}}
>
Close
</Button>
<Button
type="button"
className="bg-amber-600 hover:bg-amber-700"
onClick={handleAddWorknote}
disabled={!newWorknote.trim()}
>
<MessageSquare className="w-4 h-4 mr-2" />
Post Worknote
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Worknotes Dialog - handled in Header */}
<DocumentPreviewModal
isOpen={isPreviewOpen}

View File

@ -1,14 +1,9 @@
import { FileText, Calendar, Building, Plus, Eye, MapPin, Navigation, Loader2 } from 'lucide-react';
import { FileText, Calendar, Building, Eye, MapPin, Navigation, Loader2 } 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
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 { User } from '../../lib/mock-data';
import { toast } from 'sonner';
@ -28,23 +23,8 @@ const getStatusColor = (status: string) => {
};
export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [requests, setRequests] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [dealerCode, setDealerCode] = useState('');
const [dealerData, setDealerData] = useState<any>(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<HTMLDivElement>) => {
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
<p className="text-slate-600">
Manage dealer relocation requests - Moving dealership to a new location
</p>
<span className="block mt-1 text-slate-500 text-sm">
Note: Relocation requests are initiated by the dealer.
</span>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" />
New Relocation Request
</Button>
</DialogTrigger>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create Relocation Request</DialogTitle>
<DialogDescription>
Submit a request for dealership relocation. All fields are mandatory.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmitRequest} className="space-y-4">
{/* Dealer Code */}
<div className="space-y-2">
<Label htmlFor="dealerCode">Dealer Code *</Label>
<Input
id="dealerCode"
placeholder="Enter dealer code (e.g., DL-MH-001)"
value={dealerCode}
onChange={(e) => handleDealerCodeChange(e.target.value)}
required
/>
</div>
{/* Auto-populated Dealer Details */}
{dealerData && (
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-3">
<h3 className="text-slate-900">Current Dealership Details</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-slate-600">Dealer Name:</span>
<p className="text-slate-900">{dealerData.dealerName}</p>
</div>
<div>
<span className="text-slate-600">Dealership Name:</span>
<p className="text-slate-900">{dealerData.dealershipName}</p>
</div>
<div className="col-span-2">
<span className="text-slate-600">Current Location:</span>
<p className="text-slate-900">{dealerData.currentAddress}</p>
</div>
<div>
<span className="text-slate-600">GST:</span>
<p className="text-slate-900">{dealerData.gst}</p>
</div>
<div>
<span className="text-slate-600">Region/Zone:</span>
<p className="text-slate-900">{dealerData.region} / {dealerData.zone}</p>
</div>
</div>
</div>
)}
{/* Proposed New Location */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-slate-900">Proposed New Location *</h3>
{/* Location Mode Toggle */}
<div className="flex items-center gap-2 bg-slate-100 rounded-lg p-1">
<button
type="button"
onClick={() => setLocationMode('manual')}
className={`px-3 py-1 rounded text-sm transition-colors ${
locationMode === 'manual'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
Manual Entry
</button>
<button
type="button"
onClick={() => setLocationMode('map')}
className={`px-3 py-1 rounded text-sm transition-colors flex items-center gap-1 ${
locationMode === 'map'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<MapPin className="w-3 h-3" />
Map Location
</button>
</div>
</div>
{/* Map Mode */}
{locationMode === 'map' && (
<div className="space-y-3">
{/* Map Picker */}
<div className="border-2 border-amber-300 rounded-lg overflow-hidden">
<div
onClick={handleMapClick}
className="relative h-64 bg-gradient-to-br from-green-100 via-blue-50 to-amber-50 cursor-crosshair"
style={{
backgroundImage: `
linear-gradient(to right, rgba(148, 163, 184, 0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(148, 163, 184, 0.1) 1px, transparent 1px)
`,
backgroundSize: '20px 20px'
}}
>
{/* Map Roads/Features */}
<div className="absolute inset-0">
<div className="absolute top-1/4 left-0 right-0 h-1 bg-slate-300 opacity-30" />
<div className="absolute top-1/2 left-0 right-0 h-2 bg-slate-400 opacity-40" />
<div className="absolute top-3/4 left-0 right-0 h-1 bg-slate-300 opacity-30" />
<div className="absolute left-1/4 top-0 bottom-0 w-1 bg-slate-300 opacity-30" />
<div className="absolute left-1/2 top-0 bottom-0 w-2 bg-slate-400 opacity-40" />
<div className="absolute left-3/4 top-0 bottom-0 w-1 bg-slate-300 opacity-30" />
</div>
{/* Center Marker (current location) */}
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className="flex flex-col items-center">
<Building className="w-6 h-6 text-blue-600" />
<div className="text-xs text-blue-900 bg-white px-2 py-1 rounded shadow-sm mt-1">
Current Location
</div>
</div>
</div>
{/* Selected Location Marker */}
{selectedLocation && (
<div className="absolute top-1/3 left-2/3 transform -translate-x-1/2 -translate-y-full">
<div className="flex flex-col items-center animate-bounce">
<MapPin className="w-8 h-8 text-amber-600 drop-shadow-lg" />
<div className="text-xs text-amber-900 bg-amber-100 px-2 py-1 rounded shadow-md border border-amber-300">
New Location
</div>
</div>
</div>
)}
{/* Instructions */}
<div className="absolute bottom-2 left-2 bg-white/90 px-3 py-2 rounded shadow-sm border border-slate-200">
<p className="text-xs text-slate-700">
<MapPin className="w-3 h-3 inline mr-1" />
Click anywhere on the map to select new location
</p>
</div>
{/* Coordinates Display */}
{selectedLocation && (
<div className="absolute top-2 right-2 bg-amber-600 text-white px-3 py-2 rounded shadow-md text-xs">
Lat: {selectedLocation.lat.toFixed(4)}, Lng: {selectedLocation.lng.toFixed(4)}
</div>
)}
</div>
</div>
{selectedLocation && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3 text-sm text-green-800">
Location selected! Address details auto-filled below.
</div>
)}
</div>
)}
{/* Manual Entry Mode - Address Fields */}
<div className="space-y-2">
<Label htmlFor="proposedAddress">Complete Address *</Label>
<Input
id="proposedAddress"
placeholder="Building/Shop number, Street, Locality"
value={proposedAddress}
onChange={(e) => setProposedAddress(e.target.value)}
required
readOnly={locationMode === 'map' && !!selectedLocation}
className={locationMode === 'map' && selectedLocation ? 'bg-green-50' : ''}
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-2">
<Label htmlFor="proposedCity">City *</Label>
<Input
id="proposedCity"
placeholder="City"
value={proposedCity}
onChange={(e) => setProposedCity(e.target.value)}
required
readOnly={locationMode === 'map' && !!selectedLocation}
className={locationMode === 'map' && selectedLocation ? 'bg-green-50' : ''}
/>
</div>
<div className="space-y-2">
<Label htmlFor="proposedState">State *</Label>
<Input
id="proposedState"
placeholder="State"
value={proposedState}
onChange={(e) => setProposedState(e.target.value)}
required
readOnly={locationMode === 'map' && !!selectedLocation}
className={locationMode === 'map' && selectedLocation ? 'bg-green-50' : ''}
/>
</div>
<div className="space-y-2">
<Label htmlFor="proposedPincode">Pincode *</Label>
<Input
id="proposedPincode"
placeholder="Pincode"
value={proposedPincode}
onChange={(e) => setProposedPincode(e.target.value)}
required
readOnly={locationMode === 'map' && !!selectedLocation}
className={locationMode === 'map' && selectedLocation ? 'bg-green-50' : ''}
/>
</div>
</div>
</div>
{/* Distance & Property Details */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="distance">Distance from Current Location *</Label>
<Input
id="distance"
placeholder="e.g., 12 km"
value={distance}
onChange={(e) => setDistance(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="propertyType">Property Type *</Label>
<Select value={propertyType} onValueChange={setPropertyType} required>
<SelectTrigger>
<SelectValue placeholder="Select property type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Owned">Owned</SelectItem>
<SelectItem value="Leased">Leased</SelectItem>
<SelectItem value="Rented">Rented</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Expected Relocation Date */}
<div className="space-y-2">
<Label htmlFor="expectedDate">Expected Relocation Date</Label>
<Input
id="expectedDate"
type="date"
value={expectedDate}
onChange={(e) => setExpectedDate(e.target.value)}
/>
</div>
{/* Reason */}
<div className="space-y-2">
<Label htmlFor="reason">Reason for Relocation *</Label>
<Textarea
id="reason"
placeholder="Provide detailed reason for relocation request..."
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={4}
required
/>
</div>
{/* Required Documents Info */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-blue-900 mb-2">Documents Required (to be uploaded later)</h4>
<ul className="text-blue-800 text-sm space-y-1">
<li> Property documents for new location</li>
<li> Lease/Rental agreement for new location</li>
<li> NOC from current landlord</li>
<li> Municipal approvals</li>
<li> Fire safety certificate</li>
<li> Pollution clearance</li>
<li> Layout/Floor plan of new location</li>
<li> Photos of new location</li>
<li> Locality map</li>
</ul>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setIsDialogOpen(false)}
>
Cancel
</Button>
<Button
type="submit"
className="bg-amber-600 hover:bg-amber-700"
disabled={!dealerData || isSubmitting}
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
'Submit Request'
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
{/* Statistics Cards */}

View File

@ -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,12 +248,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<MessageSquare className="w-4 h-4 text-slate-500" />
<span className="text-sm text-slate-600">Communication & Notes</span>
</div>
<Dialog open={workNotesOpen} onOpenChange={setWorkNotesOpen}>
<DialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm"
onClick={() => navigate(`/worknotes/resignation/${resignationId}`, {
state: {
applicationName: resignationData?.outlet?.name || 'Resignation',
registrationNumber: resignationData?.resignationId || '',
participants: resignationData?.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
@ -263,22 +268,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</Badge>
)}
</Button>
</DialogTrigger>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-amber-600" />
Work Notes - {resignationId}
</DialogTitle>
<DialogDescription>
View all communications and internal notes for this resignation request
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto">
<WorkNotesPage />
</div>
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>

View File

@ -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<any>(null);
const [resignations, setResignations] = useState<any[]>([]);
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
<CardTitle>Resignation Requests</CardTitle>
<CardDescription>
Track and manage dealer resignation requests
{!isDDLead && (
<span className="block mt-1 text-amber-600">
Note: Only DD Lead can create resignation requests. Current role: {currentUser?.role || 'Not logged in'}
<span className="block mt-1 text-slate-500">
Note: Resignation requests are initiated by the dealer or via ASM.
</span>
)}
</CardDescription>
</div>
{isDDLead && (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" />
Create Resignation Request
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create Resignation Request</DialogTitle>
<DialogDescription>
Fill in the details to create a new resignation request
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Dealer Code - Auto-fetch trigger */}
<div className="space-y-2">
<Label htmlFor="dealerCode">Dealer Code *</Label>
<Input
id="dealerCode"
value={dealerCode}
onChange={(e) => handleDealerCodeChange(e.target.value)}
placeholder="e.g., DL-MH-001"
required
/>
</div>
{/* Auto-filled data */}
{autoFilledData && (
<div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg">
<div>
<Label className="text-slate-600">Dealership Name</Label>
<p>{autoFilledData.name}</p>
</div>
<div>
<Label className="text-slate-600">Code</Label>
<p>{autoFilledData.code}</p>
</div>
<div>
<Label className="text-slate-600">Address</Label>
<p>{autoFilledData.address}</p>
</div>
<div>
<Label className="text-slate-600">City</Label>
<p>{autoFilledData.city}</p>
</div>
<div>
<Label className="text-slate-600">State</Label>
<p>{autoFilledData.state}</p>
</div>
<div>
<Label className="text-slate-600">Type</Label>
<p>{autoFilledData.type}</p>
</div>
</div>
)}
{/* Date fields */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Resignation Type *</Label>
<Select value={formData.resignationType} onValueChange={(value) => setFormData({...formData, resignationType: value})}>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Voluntary">Voluntary</SelectItem>
<SelectItem value="Retirement">Retirement</SelectItem>
<SelectItem value="Health Issues">Health Issues</SelectItem>
<SelectItem value="Business Closure">Business Closure</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>LWD Sales *</Label>
<Input
type="date"
value={formData.lastOperationalDateSales}
onChange={(e) => setFormData({...formData, lastOperationalDateSales: e.target.value})}
required
/>
</div>
<div className="space-y-2">
<Label>LWD Services *</Label>
<Input
type="date"
value={formData.lastOperationalDateServices}
onChange={(e) => setFormData({...formData, lastOperationalDateServices: e.target.value})}
required
/>
</div>
</div>
{/* Text fields */}
<div className="space-y-2">
<Label htmlFor="reason">Resignation Reason *</Label>
<Input
id="reason"
value={formData.resignationReason}
onChange={(e) => setFormData({...formData, resignationReason: e.target.value})}
placeholder="Brief reason for resignation"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Dealer Voice *</Label>
<Textarea
id="description"
value={formData.customerDescription}
onChange={(e) => setFormData({...formData, customerDescription: e.target.value})}
placeholder="Detailed description provided by customer"
rows={4}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="document">Upload Document</Label>
<Input
id="document"
type="file"
onChange={(e) => setFormData({...formData, document: e.target.files?.[0] || null})}
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button type="submit" className="bg-amber-600 hover:bg-amber-700">
Submit Request
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)}
</div>
</CardHeader>
<CardContent>

View File

@ -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<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [showSCNDialog, setShowSCNDialog] = useState(false);
const [scnFile, setScnFile] = useState<File | null>(null);
const [scnRemarks, setScnRemarks] = useState('');
@ -53,6 +53,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
fetchTermination();
}, [terminationId]);
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] space-y-4">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
<p className="text-slate-600">Loading termination details...</p>
</div>
);
}
const handleIssueSCN = async () => {
try {
setIsProcessing(true);
@ -443,12 +452,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<MessageSquare className="w-4 h-4 text-slate-500" />
<span className="text-sm text-slate-600">Communication & Notes</span>
</div>
<Dialog open={workNotesOpen} onOpenChange={setWorkNotesOpen}>
<DialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="relative hover:bg-red-50 hover:border-red-300 hover:text-red-700 transition-all"
className="relative hover:bg-red-50 hover:border-red-300 hover:text-red-700 transition-all shadow-sm"
onClick={() => navigate(`/worknotes/termination/${terminationId}`, {
state: {
applicationName: terminationData?.dealerName || 'Termination',
registrationNumber: terminationId || '',
participants: terminationData?.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
@ -458,22 +472,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</Badge>
)}
</Button>
</DialogTrigger>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-red-600" />
Work Notes - {terminationId}
</DialogTitle>
<DialogDescription>
View all communications and internal notes for this termination case
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto">
<WorkNotesPage />
</div>
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>

View File

@ -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<WorkNotesPageProps>) {
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<WorkNotesPageProps>) {
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
const [attachedFiles, setAttachedFiles] = useState<Attachment[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [participantSearch, setParticipantSearch] = useState('');
const { socket } = useSocket();
const inputRef = useRef<HTMLInputElement>(null);
@ -162,23 +176,25 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
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 })));
// Fetch Notes on load and join socket room
useEffect(() => {
const fetchNotes = async () => {
try {
const res: any = await worknoteService.getWorknotes(applicationId, 'application');
setIsLoading(true);
const res: any = await worknoteService.getWorknotes(requestId, requestType);
if (res.success) {
setNotes(res.data.map((n: any) => ({
id: n.id,
@ -198,10 +214,12 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
}
};
// Fetch Notes on load and join socket room
useEffect(() => {
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<WorkNotesPageProps>) {
});
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<WorkNotesPageProps>) {
};
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<HTMLDivElement>(null);
@ -330,7 +348,7 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
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<WorkNotesPageProps>) {
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,29 +509,55 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
</div>
</div>
{/* Participant Avatars */}
<div className="flex items-center -space-x-2">
{/* Participant Avatars & Refresh & Toggle Sidebar */}
<div className="flex items-center gap-4">
<div className="hidden sm:flex items-center -space-x-2 mr-2">
{participantsList.slice(0, 3).map((participant, index) => (
<Avatar
key={index}
className="w-8 h-8 border-2 border-white"
className="w-8 h-8 border-2 border-white ring-1 ring-slate-100"
>
<AvatarFallback className={`${participant.color} text-white text-xs`}>
<AvatarFallback className={`${participant.color} text-white text-[10px]`}>
{participant.initials}
</AvatarFallback>
</Avatar>
))}
{participantsList.length > 3 && (
<div className="w-8 h-8 rounded-full bg-slate-200 border-2 border-white flex items-center justify-center">
<span className="text-slate-600 text-xs">+{participantsList.length - 3}</span>
<div className="w-8 h-8 rounded-full bg-slate-100 border-2 border-white flex items-center justify-center ring-1 ring-slate-100">
<span className="text-slate-600 text-[10px] font-bold">+{participantsList.length - 3}</span>
</div>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={fetchNotes}
className="text-slate-500 hover:text-blue-600 flex items-center gap-1.5 px-2 h-9 rounded-lg hover:bg-slate-50 transition-colors"
disabled={isLoading}
>
<RefreshCcw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
<span className="text-xs font-medium hidden md:inline">Sync</span>
</Button>
<Button
variant={isSidebarOpen ? 'secondary' : 'ghost'}
size="sm"
onClick={() => 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'}`}
>
<Users className="w-4 h-4" />
<span className="text-xs font-medium hidden md:inline">Participants</span>
</Button>
</div>
</div>
</div>
<ScrollArea className="flex-1 px-6 py-4 min-h-0">
<div className="max-w-4xl mx-auto space-y-6 flex flex-col">
<div className="flex-1 flex overflow-hidden">
{/* Main Chat Engine */}
<div className="flex-1 flex flex-col min-w-0 bg-white min-h-0 relative">
<div className="flex-1 overflow-y-auto px-6 py-4 custom-scrollbar bg-slate-50 relative z-0">
<div className={`max-w-4xl mx-auto space-y-6 flex flex-col py-4 ${mode === 'modal' ? '' : 'px-4'}`}>
{[...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) ||
@ -610,10 +654,10 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
)}
<div ref={messagesEndRef} />
</div>
</ScrollArea>
</div>
{/* Input Area - Stays fixed because it's a sibling of ScrollArea */}
<div className="bg-white border-t border-slate-200 px-6 py-4 z-10">
{/* Input Area */}
<div className="bg-white border-t border-slate-100 px-6 py-4 shadow-[0_-4px_10px_-5px_rgba(0,0,0,0.05)]">
<div className="max-w-4xl mx-auto space-y-4">
{/* Attachment Previews */}
@ -757,10 +801,102 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
</Button>
</div>
<p className="text-slate-400 text-[10px] px-1">
Press Enter to send Use @ to mention someone {isUploading ? 'Uploading files...' : 'Files attached appear above'}
<p className="text-slate-400 text-[10px] px-1 flex items-center gap-1">
<Info className="w-3 h-3" />
<span>Press Enter to send Use @ to mention {isUploading ? 'Uploading files...' : 'Files attached appear above'}</span>
</p>
</div>
</div>
</div>
{/* Right Sidebar - Participants */}
{isSidebarOpen && (
<div className={`w-[280px] lg:w-[320px] bg-slate-50 border-l border-slate-200 flex flex-col transition-all animate-in slide-in-from-right-full ${mode === 'modal' ? 'hidden lg:flex' : 'flex'}`}>
<div className="p-4 border-b border-slate-200 bg-white">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-900 flex items-center gap-2">
<Users className="w-4 h-4 text-blue-600" />
Participants
<Badge variant="secondary" className="bg-slate-100 text-slate-600 ml-1">
{participantsList.length}
</Badge>
</h3>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-600" onClick={() => setIsSidebarOpen(false)}>
<X className="w-4 h-4" />
</Button>
</div>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
type="text"
placeholder="Search people..."
value={participantSearch}
onChange={(e) => 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"
/>
</div>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1 custom-scrollbar">
{participantsList
.filter(p => p.name.toLowerCase().includes(participantSearch.toLowerCase()) || p.role?.toLowerCase().includes(participantSearch.toLowerCase()))
.map((participant) => (
<div
key={participant.id}
className="group flex items-start gap-3 p-3 rounded-xl hover:bg-white hover:shadow-sm border border-transparent hover:border-slate-100 transition-all cursor-default"
>
<div className="relative">
<Avatar className="w-10 h-10 ring-2 ring-transparent group-hover:ring-blue-100 transition-all">
<AvatarFallback className={`${participant.color} text-white text-xs font-bold`}>
{participant.initials}
</AvatarFallback>
</Avatar>
{participant.isOnline && (
<span className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-slate-50 rounded-full shadow-sm"></span>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-0.5">
<p className="text-sm font-semibold text-slate-900 truncate">{participant.name}</p>
{participant.id === currentUser?.id && (
<Badge variant="outline" className="text-[9px] h-4 px-1 border-blue-200 text-blue-600 bg-blue-50">You</Badge>
)}
</div>
<p className="text-[11px] text-slate-500 font-medium uppercase tracking-wider mb-1">
{participant.role}
</p>
<p className="text-[11px] text-slate-400 truncate italic">
{participant.email}
</p>
</div>
<ChevronRight className="w-4 h-4 text-slate-300 group-hover:text-slate-400 opacity-0 group-hover:opacity-100 transition-all self-center" />
</div>
))}
{participantsList.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-center opacity-50">
<Users className="w-8 h-8 text-slate-300 mb-2" />
<p className="text-xs text-slate-500">No participants found</p>
</div>
)}
</div>
<div className="p-4 bg-white border-t border-slate-200">
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Active Session</span>
</div>
<div className="flex items-center gap-3 p-2 rounded-lg bg-slate-50 border border-slate-100">
<ClockIcon className="w-4 h-4 text-slate-400" />
<div>
<p className="text-[10px] text-slate-500 font-medium uppercase">Last Activity</p>
<p className="text-xs text-slate-900 font-semibold">Just now</p>
</div>
</div>
</div>
</div>
)}
</div>
{/* Preview Modal */}
@ -805,3 +941,5 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
</div>
);
}
export default WorkNotesPage;

View File

@ -1,398 +0,0 @@
import { ArrowLeft, MessageSquare, Send, Clock, User as UserIcon } from 'lucide-react';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Textarea } from '../ui/textarea';
import { Label } from '../ui/label';
import { useState } from 'react';
import { User } from '../../lib/mock-data';
import { toast } from 'sonner';
interface WorknotePageProps {
requestId: string;
requestType: 'relocation' | 'constitutional-change' | 'fnf' | 'resignation' | 'termination';
requestTitle: string;
onBack: () => void;
currentUser: User | null;
}
// Mock worknotes - Discussion platform for requests
const initialWorknotes = [
{
id: 1,
user: 'Rajesh Kumar',
role: 'ASM',
message: 'I have visited the proposed location. The area has good visibility and footfall. However, parking might be a concern during peak hours.',
timestamp: '2025-12-21 10:30 AM',
avatar: 'RK'
},
{
id: 2,
user: 'Priya Sharma',
role: 'RBM',
message: 'Thanks for the site visit update. Can we get clarity on the parking arrangements from the dealer?',
timestamp: '2025-12-21 03:45 PM',
avatar: 'PS'
},
{
id: 3,
user: 'Amit Sharma',
role: 'Dealer',
message: 'We have secured dedicated parking for 15 bikes in the basement. Additionally, there\'s street parking available during non-peak hours.',
timestamp: '2025-12-22 09:15 AM',
avatar: 'AS'
},
{
id: 4,
user: 'Suresh Patel',
role: 'DD-ZM',
message: 'Good to know about parking. What about the competition analysis in the new area? Any other Royal Enfield dealers nearby?',
timestamp: '2025-12-23 11:00 AM',
avatar: 'SP'
},
{
id: 5,
user: 'Amit Sharma',
role: 'Dealer',
message: 'Nearest RE dealer is 8km away in Powai. This location will help us tap into the Andheri East market which is currently underserved.',
timestamp: '2025-12-23 02:20 PM',
avatar: 'AS'
},
{
id: 6,
user: 'Vikram Singh',
role: 'DD Lead',
message: 'The market analysis looks promising. @Amit Sharma, please also share the projected sales figures for the first year at the new location.',
timestamp: '2025-12-24 09:00 AM',
avatar: 'VS'
},
{
id: 7,
user: 'Amit Sharma',
role: 'Dealer',
message: 'Based on the catchment area analysis, we are projecting 180-200 units in Year 1, with a growth rate of 15-20% YoY. Detailed projection sheet will be uploaded in documents section.',
timestamp: '2025-12-24 02:30 PM',
avatar: 'AS'
},
{
id: 8,
user: 'Neha Kapoor',
role: 'DD Head',
message: 'Excellent! The projections align with our regional targets. Once the financial documents are verified, we can move forward with approval.',
timestamp: '2025-12-25 10:15 AM',
avatar: 'NK'
}
];
// Generate avatar color based on role
const getAvatarColor = (role: string) => {
const colorMap: Record<string, string> = {
'Dealer': 'bg-blue-600',
'ASM': 'bg-green-600',
'RBM': 'bg-purple-600',
'DD-ZM': 'bg-amber-600',
'ZBH': 'bg-red-600',
'DD Lead': 'bg-indigo-600',
'DD Head': 'bg-pink-600',
'NBH': 'bg-teal-600',
'DD Admin': 'bg-orange-600',
'Super Admin': 'bg-slate-700',
'Finance': 'bg-emerald-600'
};
return colorMap[role] || 'bg-slate-600';
};
export function WorknotePage({ requestId, requestType, requestTitle, onBack, currentUser }: WorknotePageProps) {
const [worknotes, setWorknotes] = useState(initialWorknotes);
const [newWorknote, setNewWorknote] = useState('');
const handleAddWorknote = () => {
if (newWorknote.trim()) {
const now = new Date();
const timestamp = now.toLocaleString('en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
const newNote = {
id: worknotes.length + 1,
user: currentUser?.name || 'Anonymous',
role: currentUser?.role || 'User',
message: newWorknote,
timestamp: timestamp,
avatar: currentUser?.name?.split(' ').map(n => n[0]).join('').toUpperCase() || 'AN'
};
setWorknotes([...worknotes, newNote]);
setNewWorknote('');
toast.success('Worknote posted successfully');
// Auto-scroll to bottom after adding new note
setTimeout(() => {
const container = document.getElementById('worknotes-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 100);
}
};
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
handleAddWorknote();
}
};
return (
<div className="flex flex-col min-h-screen">
{/* Header */}
<div className="bg-white border-b border-slate-200 px-4 sm:px-6 py-4 sticky top-0 z-10">
<div className="flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-3 sm:gap-4">
<Button
variant="outline"
onClick={onBack}
className="flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Back to Request
</Button>
<div>
<div className="flex items-center gap-3">
<MessageSquare className="w-5 h-5 sm:w-6 sm:h-6 text-amber-600" />
<h1 className="text-slate-900 text-lg sm:text-xl">Worknotes Discussion</h1>
</div>
<p className="text-slate-600 text-xs sm:text-sm mt-1">
{requestId} - {requestTitle}
</p>
</div>
</div>
<Badge variant="outline" className="border-blue-300 text-blue-700">
{worknotes.length} Messages
</Badge>
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 overflow-y-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6">
{/* Discussion Thread - Takes 2 columns */}
<div className="lg:col-span-2 flex flex-col min-h-0">
<Card className="flex flex-col">
<CardHeader className="border-b border-slate-200 py-3">
<CardTitle className="flex items-center gap-2 text-base">
<MessageSquare className="w-5 h-5 text-amber-600" />
Discussion Thread
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{/* Messages Container */}
<div
id="worknotes-container"
className="h-[300px] sm:h-[350px] lg:h-[400px] overflow-y-auto p-3 sm:p-4 space-y-2 bg-slate-50"
>
{worknotes.map((note) => {
const isCurrentUser = note.user === currentUser?.name;
return (
<div
key={note.id}
className={`flex items-start gap-2 ${isCurrentUser ? 'flex-row-reverse' : ''}`}
>
{/* Avatar */}
<div className={`w-7 h-7 rounded-full ${getAvatarColor(note.role)} flex items-center justify-center text-white flex-shrink-0 text-xs`}>
{note.avatar}
</div>
{/* Message Content */}
<div className={`flex-1 max-w-2xl ${isCurrentUser ? 'items-end' : ''}`}>
<div className={`bg-white rounded-lg p-2.5 border border-slate-200 shadow-sm ${isCurrentUser ? 'bg-amber-50 border-amber-200' : ''}`}>
<div className={`flex items-start justify-between mb-1 gap-2 ${isCurrentUser ? 'flex-row-reverse' : ''}`}>
<div className={isCurrentUser ? 'text-right' : ''}>
<h5 className="text-slate-900 text-xs font-medium">{note.user}</h5>
<Badge variant="outline" className="border-slate-300 text-[10px] h-4 px-1.5 mt-0.5">
{note.role}
</Badge>
</div>
<div className={`flex items-center gap-1 text-slate-500 text-[10px] ${isCurrentUser ? 'flex-row-reverse' : ''}`}>
<Clock className="w-2.5 h-2.5" />
<span className="whitespace-nowrap">{note.timestamp}</span>
</div>
</div>
<p className="text-slate-700 whitespace-pre-wrap text-xs leading-relaxed">{note.message}</p>
</div>
</div>
</div>
);
})}
{worknotes.length === 0 && (
<div className="flex flex-col items-center justify-center h-full text-center py-12">
<MessageSquare className="w-16 h-16 text-slate-300 mb-4" />
<h3 className="text-slate-900 mb-2">No worknotes yet</h3>
<p className="text-slate-600 text-sm">Start the discussion by posting the first worknote</p>
</div>
)}
</div>
</CardContent>
</Card>
{/* Input Area - Fixed at bottom */}
<Card className="mt-3 lg:mt-4">
<CardContent className="p-3 sm:p-4">
<div className="space-y-2 sm:space-y-3">
<div className="flex items-center justify-between flex-wrap gap-2">
<Label htmlFor="newWorknote" className="text-slate-900 text-sm">
Add New Worknote
</Label>
<span className="text-slate-500 text-xs hidden sm:inline">
Press Ctrl + Enter to send
</span>
</div>
<Textarea
id="newWorknote"
value={newWorknote}
onChange={(e) => setNewWorknote(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="Type your message here..."
rows={2}
className="resize-none text-sm"
/>
<div className="flex items-center justify-between flex-wrap gap-2">
<p className="text-slate-500 text-xs">
Posting as: <span className="text-slate-900">{currentUser?.name || 'Anonymous'}</span>
</p>
<Button
onClick={handleAddWorknote}
disabled={!newWorknote.trim()}
className="bg-amber-600 hover:bg-amber-700 text-sm h-9"
>
<Send className="w-3.5 h-3.5 mr-2" />
Post
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Sidebar - Guidelines and Info */}
<div className="space-y-3 lg:space-y-4 max-h-[600px] lg:max-h-[700px] overflow-y-auto pr-1">
{/* Request Info */}
<Card>
<CardHeader className="py-3">
<CardTitle className="text-sm">Request Information</CardTitle>
</CardHeader>
<CardContent className="space-y-2.5">
<div>
<p className="text-slate-600 text-xs mb-1">Request ID</p>
<p className="text-slate-900 text-xs break-all">{requestId}</p>
</div>
<div>
<p className="text-slate-600 text-xs mb-1">Request Type</p>
<Badge variant="outline" className="capitalize text-xs">
{requestType.replace('-', ' ')}
</Badge>
</div>
<div>
<p className="text-slate-600 text-xs mb-1">Title</p>
<p className="text-slate-900 text-xs break-words">{requestTitle}</p>
</div>
</CardContent>
</Card>
{/* Statistics */}
<Card>
<CardHeader className="py-3">
<CardTitle className="text-sm">Discussion Statistics</CardTitle>
</CardHeader>
<CardContent className="space-y-2.5">
<div className="flex items-center justify-between">
<span className="text-slate-600 text-xs">Total Messages</span>
<span className="text-slate-900 text-sm">{worknotes.length}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-600 text-xs">Participants</span>
<span className="text-slate-900 text-sm">
{new Set(worknotes.map(n => n.user)).size}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-600 text-xs">Last Activity</span>
<span className="text-slate-900 text-xs">
{worknotes.length > 0 ? worknotes[worknotes.length - 1].timestamp.split(' ')[0] : 'N/A'}
</span>
</div>
</CardContent>
</Card>
{/* Guidelines */}
<Card>
<CardHeader className="py-3">
<CardTitle className="text-sm">Worknote Guidelines</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-1.5 text-slate-600 text-xs">
<li className="flex items-start gap-2">
<span className="text-amber-600 mt-0.5"></span>
<span>Be clear and concise in your messages</span>
</li>
<li className="flex items-start gap-2">
<span className="text-amber-600 mt-0.5"></span>
<span>Use @ mentions to tag specific users</span>
</li>
<li className="flex items-start gap-2">
<span className="text-amber-600 mt-0.5"></span>
<span>All worknotes are permanently logged</span>
</li>
<li className="flex items-start gap-2">
<span className="text-amber-600 mt-0.5"></span>
<span>Stay professional and on-topic</span>
</li>
<li className="flex items-start gap-2">
<span className="text-amber-600 mt-0.5"></span>
<span>Include relevant details and context</span>
</li>
<li className="flex items-start gap-2">
<span className="text-amber-600 mt-0.5"></span>
<span>Respond to queries in a timely manner</span>
</li>
</ul>
</CardContent>
</Card>
{/* Participants */}
<Card>
<CardHeader className="py-3">
<CardTitle className="text-sm">Active Participants</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{Array.from(new Set(worknotes.map(n => JSON.stringify({ user: n.user, role: n.role, avatar: n.avatar }))))
.map(str => JSON.parse(str))
.map((participant, index) => (
<div key={index} className="flex items-center gap-2">
<div className={`w-6 h-6 rounded-full ${getAvatarColor(participant.role)} flex items-center justify-center text-white text-xs`}>
{participant.avatar}
</div>
<div className="flex-1 min-w-0">
<p className="text-slate-900 text-xs truncate">{participant.user}</p>
<p className="text-slate-600 text-[10px]">{participant.role}</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,207 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { API } from '../../api/API';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import {
FileText,
Clock,
ArrowRight,
RefreshCw,
Search,
Filter,
AlertCircle,
CheckCircle2
} from 'lucide-react';
import { toast } from 'sonner';
export function FDDDashboardPage() {
const navigate = useNavigate();
const [applications, setApplications] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
fetchApplications();
}, []);
const fetchApplications = async () => {
setLoading(true);
try {
const response: any = await API.getApplications();
if (response.data?.success) {
setApplications(response.data.data);
}
} catch (error) {
console.error('Failed to fetch FDD applications:', error);
toast.error('Failed to load assigned applications');
} finally {
setLoading(false);
}
};
const filteredApplications = applications.filter(app =>
app.applicationId.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.applicantName.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.city?.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="space-y-6 max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">FDD Dashboard</h1>
<p className="text-slate-500">Manage financial due diligence for assigned dealer applications</p>
</div>
<button
onClick={fetchApplications}
className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors text-sm font-medium"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="bg-white border-slate-200 shadow-sm">
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Assigned</p>
<h3 className="text-2xl font-bold text-slate-900">{applications.length}</h3>
</div>
<div className="p-2 bg-slate-100 rounded-lg text-slate-600">
<FileText className="w-5 h-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white border-slate-200 shadow-sm">
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Pending Reports</p>
<h3 className="text-2xl font-bold text-slate-900">
{applications.filter(a => a.currentStage === 'FDD' || a.overallStatus === 'Active').length}
</h3>
</div>
<div className="p-2 bg-slate-100 rounded-lg text-slate-600">
<Clock className="w-5 h-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white border-slate-200 shadow-sm">
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed Reports</p>
<h3 className="text-2xl font-bold text-slate-900">0</h3>
</div>
<div className="p-2 bg-slate-100 rounded-lg text-slate-600">
<CheckCircle2 className="w-5 h-5" />
</div>
</div>
</CardContent>
</Card>
</div>
<Card className="shadow-sm border-slate-200 overflow-hidden">
<CardHeader className="bg-white border-b border-slate-100 px-6 py-4 flex flex-row items-center justify-between">
<CardTitle className="text-lg font-semibold flex items-center gap-2">
<FileText className="w-5 h-5 text-blue-600" />
My Assigned Cases
</CardTitle>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search by ID or Name..."
className="pl-9 pr-4 py-2 bg-slate-50 border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 transition-all outline-none"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<button className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors">
<Filter className="w-4 h-4 text-slate-600" />
</button>
</div>
</CardHeader>
<CardContent className="p-0">
{loading ? (
<div className="flex flex-col items-center justify-center py-20 px-6">
<RefreshCw className="w-10 h-10 animate-spin text-blue-600 mb-4" />
<p className="text-slate-500 font-medium">Synchronizing application data...</p>
</div>
) : filteredApplications.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 px-6 text-center">
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mb-6">
<AlertCircle className="w-10 h-10 text-slate-200" />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-2">No Applications Found</h3>
<p className="text-slate-500 max-w-md mx-auto">
You don't have any applications assigned for Financial Due Diligence at this time.
</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Application Details</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Location</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Current Stage</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredApplications.map((app) => (
<tr
key={app.id}
className="hover:bg-slate-50 transition-colors cursor-pointer group"
onClick={() => navigate(`/fdd-dashboard/application/${app.id}`)}
>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center font-bold">
{app.applicantName.charAt(0)}
</div>
<div>
<p className="text-sm font-bold text-slate-900 group-hover:text-blue-600 transition-colors">{app.applicationId}</p>
<p className="text-xs text-slate-500">{app.applicantName}</p>
</div>
</div>
</td>
<td className="px-6 py-4">
<div>
<p className="text-sm font-medium text-slate-700">{app.city}, {app.state}</p>
<p className="text-xs text-slate-400 capitalize">{app.locationType || 'New Market'}</p>
</div>
</td>
<td className="px-6 py-4">
<Badge className={`px-3 py-1 rounded-full text-[10px] uppercase font-bold tracking-wider ${
app.overallStatus === 'Completed' ? 'bg-green-100 text-green-700' :
'bg-amber-100 text-amber-700'
}`}>
{app.overallStatus === 'Active' ? 'FDD Pending' : app.overallStatus}
</Badge>
</td>
<td className="px-6 py-4 text-right">
<button className="p-2 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all">
<ArrowRight className="w-5 h-5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -77,7 +77,8 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
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);

View File

@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import {
LayoutDashboard,
Calendar,
FileText,
UserMinus,
RefreshCcw,
@ -13,18 +14,16 @@ import {
User,
RefreshCw,
HelpCircle,
Upload,
Clock,
CheckCircle,
File,
X
} from 'lucide-react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { useNavigate, Routes, Route, useParams } from 'react-router-dom';
import { RootState } from '../../store';
import { logout } from '../../store/slices/authSlice';
import { toast } from 'sonner';
import { API } from '../../api/API';
import { Badge } from '../ui/badge';
import { ProspectiveApplicationDetails } from '../applications/ProspectiveApplicationDetails';
export function ProspectiveDashboardPage() {
const dispatch = useDispatch();
@ -33,69 +32,6 @@ export function ProspectiveDashboardPage() {
const [collapsed, setCollapsed] = useState(false);
const [activeTab, setActiveTab] = useState('applicant');
// Document State
const [documents, setDocuments] = useState<any[]>([]);
const [selectedDocType, setSelectedDocType] = useState('');
const [file, setFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
useEffect(() => {
if (user?.id) {
fetchDocuments();
}
}, [user?.id]);
const fetchDocuments = async () => {
try {
const response: any = await API.getDocuments(user.id);
if (response.ok && response.data) {
setDocuments(response.data.data);
}
} catch (error) {
console.error('Failed to fetch documents', error);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
}
};
const handleUpload = async () => {
if (!file || !selectedDocType) {
toast.error('Please select a document type and file');
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', selectedDocType);
setIsUploading(true);
try {
// Using user.id as it corresponds to Application ID (UUID) from login response
const response: any = await API.uploadDocument(user.id, formData);
if (response.ok) {
toast.success('Document uploaded successfully');
setFile(null);
setSelectedDocType('');
// Reset file input manually if needed, or rely on state
const fileInput = document.getElementById('file-upload') as HTMLInputElement;
if (fileInput) fileInput.value = '';
fetchDocuments();
} else {
toast.error(response.data?.message || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
toast.error('Upload failed');
} finally {
setIsUploading(false);
}
};
const handleLogout = () => {
dispatch(logout());
toast.info('Logged out successfully');
@ -125,20 +61,17 @@ export function ProspectiveDashboardPage() {
</div>
</div>
{!collapsed && (
<div className="p-4 border-b border-slate-800">
{/* Search Removed/hidden */}
</div>
)}
<nav className="flex-1 p-4 space-y-2">
<div>
<button
onClick={() => setActiveTab('applicant')}
onClick={() => {
setActiveTab('applicant');
navigate('/prospective-dashboard');
}}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${activeTab === 'applicant' ? 'bg-amber-600 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`}
>
<FileText className="w-5 h-5 flex-shrink-0" />
{!collapsed && <span className="flex-1 text-left">My Application</span>}
{!collapsed && <span className="flex-1 text-left">My Applications</span>}
</button>
</div>
</nav>
@ -147,12 +80,12 @@ export function ProspectiveDashboardPage() {
{!collapsed && (
<div className="px-4 py-2 bg-slate-800 rounded-lg mb-2">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-600 rounded-full flex items-center justify-center">
<span>{user?.name?.charAt(0) || 'A'}</span>
<div className="w-10 h-10 bg-amber-600 rounded-full flex items-center justify-center text-white">
<span className="font-bold">{user?.name?.charAt(0) || 'A'}</span>
</div>
<div className="flex-1 min-w-0">
<p className="truncate">{user?.name || 'Amit Sharma'}</p>
<p className="text-slate-400 truncate">{user?.role || 'Prospective'}</p>
<p className="truncate text-sm font-medium">{user?.name || 'Applicant'}</p>
<p className="text-slate-400 truncate text-xs">{user?.role || 'Prospective'}</p>
</div>
</div>
</div>
@ -185,134 +118,105 @@ export function ProspectiveDashboardPage() {
<p className="text-slate-600 text-xs">{user?.role || 'User'}</p>
</div>
</div>
<button className="p-2 rounded-md hover:bg-slate-100" title="Refresh">
<button className="p-2 rounded-md hover:bg-slate-100" title="Refresh" onClick={() => window.location.reload()}>
<RefreshCw className="w-4 h-4 text-slate-600" />
</button>
<button className="p-2 rounded-md hover:bg-slate-100" title="Help">
<HelpCircle className="w-4 h-4 text-slate-600" />
</button>
<button className="relative p-2 rounded-md hover:bg-slate-100">
<Bell className="w-4 h-4 text-slate-600" />
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
</button>
</div>
</div>
</header>
<main className="flex-1 overflow-y-auto p-6">
{activeTab === 'applicant' ? (
<Routes>
<Route path="/" element={<ProspectiveApplicationList />} />
<Route path="/application/:id" element={<ProspectiveApplicationDetailsWrapper />} />
</Routes>
</main>
</div>
</div>
);
}
function ProspectiveApplicationList() {
const navigate = useNavigate();
const { user } = useSelector((state: RootState) => state.auth);
const [applications, setApplications] = useState<any[]>([]);
useEffect(() => {
if (user?.id) {
fetchApplications();
}
}, [user?.id]);
const fetchApplications = async () => {
try {
const response: any = await API.getApplications();
if (response.data?.success) {
setApplications(response.data.data);
}
} catch (error) {
console.error('Failed to fetch applications', error);
}
};
return (
<div className="max-w-7xl mx-auto">
<div className="space-y-6">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-slate-900 text-2xl font-bold mb-2">Applicant Portal</h1>
<p className="text-slate-600">Upload required documents for verification</p>
</div>
{/* Document Upload Card */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="p-6 border-b border-slate-200">
<h4 className="flex items-center gap-2 text-lg font-semibold text-slate-900">
<Upload className="w-5 h-5 text-blue-600" />
Document Upload
</h4>
<p className="text-slate-500 mt-1">Upload all required documents for your dealership application</p>
</div>
<div className="p-6 space-y-6">
<div className="space-y-4 p-4 bg-slate-50 rounded-lg border border-slate-200">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">Select Document Type</label>
<select
className="flex h-10 w-full items-center justify-between rounded-md border border-slate-300 bg-white px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 disabled:cursor-not-allowed disabled:opacity-50"
value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)}
disabled={isUploading}
>
<option value="">Choose document type...</option>
<option value="PAN Card">PAN Card</option>
<option value="GST Certificate">GST Certificate</option>
<option value="Aadhaar Card">Aadhaar Card</option>
<option value="Partnership Deed">Partnership Deed</option>
<option value="LLP Agreement">LLP Agreement</option>
<option value="Certificate of Incorporation">Certificate of Incorporation</option>
<option value="MOA">MOA</option>
<option value="AOA">AOA</option>
<option value="Board Resolution">Board Resolution</option>
<option value="Initial Security Deposit Receipt">Initial Security Deposit Receipt</option>
<option value="Final Security Deposit Receipt">Final Security Deposit Receipt</option>
<option value="Trade License/Firm Registration">Trade License/Firm Registration</option>
<option value="Bank Statement">Bank Statement</option>
<option value="Cancelled Check">Cancelled Check</option>
<option value="Rental Agreement">Rental Agreement</option>
<option value="Property Document">Property Document</option>
<option value="Other">Other</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">Upload File</label>
<div className="flex items-center gap-3">
<input
id="file-upload"
type="file"
className="block w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-amber-600 file:text-white hover:file:bg-amber-700 disabled:opacity-50 disabled:cursor-not-allowed"
onChange={handleFileChange}
disabled={isUploading}
/>
</div>
<h1 className="text-slate-900 text-3xl font-bold mb-2">My Applications</h1>
<p className="text-slate-500 font-medium">Track and manage your dealership applications</p>
</div>
</div>
<div className="flex justify-end">
<button
onClick={handleUpload}
disabled={!file || !selectedDocType || isUploading}
className="px-4 py-2 bg-amber-600 text-white rounded-md hover:bg-amber-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{isUploading ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Uploading...
</>
) : (
<>
<Upload className="w-4 h-4" />
Upload Document
</>
)}
</button>
{applications.length === 0 ? (
<div className="bg-white rounded-2xl border border-slate-200 border-dashed p-12 text-center">
<div className="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4">
<FileText className="w-8 h-8 text-slate-400" />
</div>
<p className="text-slate-500 text-sm">Accepted formats: PDF, JPG, PNG (Max size: 10MB)</p>
</div>
<div className="space-y-3">
<h3 className="font-medium text-slate-900">Uploaded Documents ({documents.length})</h3>
{documents.length === 0 ? (
<div className="text-center py-8 text-slate-500 bg-slate-50 rounded-lg border border-dashed border-slate-300">
<FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No documents uploaded yet</p>
<h3 className="text-lg font-semibold text-slate-900 mb-1">No applications found</h3>
<p className="text-slate-500 max-w-sm mx-auto mb-6">
You haven't submitted any dealership applications yet.
</p>
</div>
) : (
<div className="space-y-2">
{documents.map((doc) => (
<div key={doc.id} className="flex items-center justify-between p-4 bg-white rounded-lg border border-slate-200 hover:border-amber-300 transition-colors">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<File className="w-5 h-5 text-blue-600" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{applications.map((app) => (
<div
key={app.id}
className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm hover:shadow-md hover:border-amber-500 cursor-pointer transition-all group"
onClick={() => navigate(`/prospective-dashboard/application/${app.id}`)}
>
<div className="flex justify-between items-start mb-4">
<div className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center group-hover:bg-amber-600 transition-colors">
<FileText className="w-6 h-6 text-amber-600 group-hover:text-white" />
</div>
<div>
<p className="font-medium text-slate-900">{doc.documentType}</p>
<p className="text-slate-500 text-sm">{doc.fileName}</p>
<p className="text-slate-400 text-xs mt-1">Uploaded on {new Date(doc.createdAt).toLocaleDateString()}</p>
<Badge className={`px-4 py-1.5 rounded-xl text-[10px] uppercase font-bold ${app.overallStatus === 'Completed' ? 'bg-green-100 text-green-700' :
app.overallStatus === 'Rejected' ? 'bg-red-100 text-red-700' :
'bg-amber-100 text-amber-700'}`}>
{app.overallStatus || 'Active'}
</Badge>
</div>
<h3 className="text-xl font-bold text-slate-900 mb-1 truncate">{app.applicationId}</h3>
<p className="text-slate-500 text-sm mb-4 font-medium">{app.city}, {app.state}</p>
<div className="space-y-4 pt-6 border-t border-slate-100">
<div className="flex justify-between items-center">
<span className="text-xs text-slate-500 font-medium">Current Stage</span>
<span className="text-xs font-bold text-slate-900 bg-slate-100 px-3 py-1 rounded-lg">{app.currentStage || 'Initial'}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-slate-500 font-medium">Applied</span>
<span className="text-xs font-bold text-slate-600">{new Date(app.createdAt).toLocaleDateString()}</span>
</div>
<div className="mt-6">
<div className="flex justify-between items-center mb-1">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Progress</span>
<span className="text-xs font-bold text-amber-600">{app.progressPercentage || 0}%</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div className="bg-amber-500 h-2 rounded-full transition-all duration-1000" style={{ width: `${app.progressPercentage || 0}%` }}></div>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium ${doc.status === 'Approved' ? 'bg-green-100 text-green-700 border-green-200' :
doc.status === 'Rejected' ? 'bg-red-100 text-red-700 border-red-200' :
'bg-yellow-100 text-yellow-700 border-yellow-200'
}`}>
{doc.status === 'Approved' ? <CheckCircle className="w-3 h-3 mr-1" /> : <Clock className="w-3 h-3 mr-1" />}
{doc.status || 'Pending'}
</span>
</div>
</div>
))}
@ -320,23 +224,21 @@ export function ProspectiveDashboardPage() {
)}
</div>
</div>
</div>
</div>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<h2 className="text-2xl font-semibold text-slate-900 mb-2">
{activeTab === 'dashboard' ? 'Dashboard' :
activeTab === 'resignations' ? 'My Resignations' :
activeTab === 'constitutional' ? 'Constitutional Change' :
'Relocation Requests'}
</h2>
<p className="text-slate-500">Coming soon...</p>
</div>
</div>
)}
</main>
</div >
</div >
);
}
function ProspectiveApplicationDetailsWrapper() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
if (!id) return null;
return (
<div className="max-w-7xl mx-auto">
<ProspectiveApplicationDetails
id={id}
onBack={() => navigate('/prospective-dashboard')}
/>
</div>
);
}

View File

@ -47,6 +47,8 @@ export function Sidebar({ onLogout }: SidebarProps) {
{ id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus },
{ id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw },
{ id: 'dealer-relocation', label: 'Relocation Requests', icon: MapPin },
] : currentUser?.role === 'FDD' ? [
{ id: 'fdd-dashboard', label: 'FDD Dashboard', icon: LayoutDashboard },
] : [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'applications', label: 'Dealership Requests', icon: FileText },

View File

@ -4,7 +4,7 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription
DialogDescription,
} from './dialog';
import { Button } from './button';
import {
@ -37,166 +37,53 @@ export const DocumentPreviewModal: React.FC<DocumentPreviewModalProps> = ({
const [zoomScale, setZoomScale] = useState(1);
const [rotation, setRotation] = useState(0);
if (!document) return null;
const handleReset = () => {
setZoomScale(1);
setRotation(0);
};
const baseUrl = 'http://localhost:5000';
const fileUrl = `${baseUrl}/${document.filePath}`;
const fileUrl = document ? `${baseUrl}${document.filePath.startsWith('/') ? '' : '/'}${document.filePath}` : '';
return (
<Dialog open={isOpen} onOpenChange={(open) => {
if (!open) {
handleReset();
onClose();
}
}}>
{/* Overriding sm:max-w-lg with sm:max-w-[95vw] and sm:h-[95vh] */}
{/* Also hiding the default close button injected by DialogContent */}
<DialogContent className="fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-[80vw] max-w-[80vw] h-[80vh] sm:max-w-[80vw] sm:h-[80vh] overflow-hidden flex flex-col p-0 bg-white border-slate-200 shadow-2xl [&>button]:hidden z-[100]">
{/* Simple Standard Header */}
<div className="flex items-center justify-between px-6 py-3 border-b bg-slate-50 flex-shrink-0">
<div className="flex items-center gap-3 min-w-0 pr-4">
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center border border-amber-200 flex-shrink-0">
<Eye className="w-4 h-4 text-amber-600" />
</div>
<div className="min-w-0 truncate">
<DialogTitle className="text-slate-900 text-sm font-semibold truncate">
{document.fileName || 'Document Preview'}
</DialogTitle>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[10px] text-slate-500 font-medium truncate">{document.documentType || 'Document'}</span>
{document.createdAt && (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl h-[85vh] flex flex-col p-0 overflow-hidden bg-white shadow-2xl border-none">
{document ? (
<>
<span className="text-slate-300 text-[10px] flex-shrink-0"></span>
<span className="text-[10px] text-slate-400 truncate">
{new Date(document.createdAt).toLocaleDateString()}
</span>
</>
)}
<div className="flex items-center justify-between p-4 border-b bg-slate-50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center border border-amber-200">
<Eye className="w-5 h-5 text-amber-600" />
</div>
<div>
<DialogTitle className="text-sm font-bold text-slate-900 leading-none mb-1">
{document.fileName}
</DialogTitle>
<p className="text-[10px] text-slate-500 font-medium uppercase tracking-wider">{document.documentType}</p>
</div>
</div>
</div>
<div className="flex items-center gap-4 flex-shrink-0 pl-2 pr-10"> {/* pr-10 to leave space for the X button we'll reveal */}
{/* Standard Control Group */}
<div className="flex items-center gap-1 bg-white border border-slate-200 p-1 rounded-lg shadow-sm">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-500 hover:text-slate-900 hover:bg-slate-100"
onClick={() => setZoomScale(s => Math.max(0.25, s - 0.25))}
title="Zoom Out"
>
<Minus className="w-3.5 h-3.5" />
</Button>
<div className="px-2 min-w-[50px] text-center">
<span className="text-[11px] font-medium text-slate-600">
{Math.round(zoomScale * 100)}%
</span>
</div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-500 hover:text-slate-900 hover:bg-slate-100"
onClick={() => setZoomScale(s => Math.min(4, s + 0.25))}
title="Zoom In"
>
<Plus className="w-3.5 h-3.5" />
</Button>
<div className="w-px h-4 bg-slate-200 mx-1"></div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-500 hover:text-slate-900 hover:bg-slate-100"
onClick={() => setRotation(r => (r + 90) % 360)}
title="Rotate"
>
<RotateCw className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-500 hover:text-slate-900 hover:bg-slate-100"
onClick={handleReset}
title="Reset"
>
<RefreshCw className="w-3.5 h-3.5" />
<div className="flex items-center gap-2 pr-10">
<Button variant="outline" size="sm" className="h-8 gap-2" onClick={() => window.open(fileUrl, '_blank')}>
<Download className="w-4 h-4" />
<span className="hidden sm:inline">Download</span>
</Button>
</div>
<Button
variant="outline"
size="sm"
className="h-8 gap-2 text-xs font-medium border-slate-200 hover:bg-slate-50 hidden sm:flex"
onClick={() => window.open(fileUrl, '_blank')}
>
<Download className="w-3.5 h-3.5 text-slate-500" />
Download
</Button>
</div>
{/* Manually absolute-positioned standard close for the Header */}
<Button
variant="ghost"
size="icon"
className="absolute top-2.5 right-3 h-8 w-8 text-slate-400 hover:text-slate-600 rounded-lg z-50 transition-colors"
onClick={onClose}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
<span className="sr-only">Close</span>
</Button>
</div>
{/* Clean Standard Viewport Area */}
<div className="flex-1 relative overflow-hidden flex items-center justify-center bg-slate-100">
<div className="w-full h-full flex items-center justify-center overflow-auto p-4 sm:p-8 scrollbar-thin">
{document.fileName?.match(/\.(jpg|jpeg|png|gif|webp)$/i) || document.mimeType?.includes('image') ? (
<div
className="transition-transform duration-200 ease-out"
style={{ transform: `scale(${zoomScale}) rotate(${rotation}deg)` }}
>
<img
src={fileUrl}
alt={document.fileName}
className="max-h-[90vh] max-w-full object-contain shadow-xl rounded-sm border border-white"
/>
</div>
) : document.fileName?.match(/\.pdf$/i) || document.mimeType?.includes('pdf') ? (
<div className="w-full h-full flex items-center justify-center">
<div className="flex-1 bg-slate-100 relative overflow-hidden flex items-center justify-center p-4">
{document.fileName?.toLowerCase().endsWith('.pdf') ? (
<iframe
src={`${fileUrl}#toolbar=0`}
className="w-full h-full max-w-7xl bg-white shadow-lg border border-slate-200"
style={{ transform: `scale(${zoomScale}) rotate(${rotation}deg)` }}
title={document.fileName}
className="w-full h-full bg-white shadow-inner rounded-sm"
title="Preview"
/>
</div>
) : (
<div className="bg-white p-12 rounded-xl shadow-sm border border-slate-200 text-center max-w-md">
<div className="w-16 h-16 rounded-full bg-slate-50 flex items-center justify-center mx-auto mb-4 border border-slate-100">
<FileText className="w-8 h-8 text-slate-400" />
</div>
<h3 className="text-slate-900 font-semibold mb-2">Preview not available</h3>
<p className="text-slate-500 text-sm mb-6">
This file format cannot be previewed in the browser. You can download it to view locally.
</p>
<Button
className="bg-amber-600 hover:bg-amber-700 text-white gap-2"
onClick={() => window.open(fileUrl, '_blank')}
>
<Download className="w-4 h-4" />
Download file
</Button>
</div>
<img
src={fileUrl}
className="max-h-full max-w-full object-contain shadow-lg rounded-sm"
alt="Preview"
/>
)}
</div>
</div>
</>
) : (
<div className="flex items-center justify-center h-full text-slate-400">Loading document...</div>
)}
</DialogContent>
</Dialog>
);

View File

@ -0,0 +1,33 @@
import { API } from '../api/API';
export const collaborationService = {
getWorknotes: async (requestId: string, requestType: string) => {
const response: any = await API.getWorknotes(requestId, requestType);
if (!response.ok) throw new Error(response.data?.message || 'Failed to fetch worknotes');
return response.data?.data || response.data;
},
addWorknote: async (data: {
requestId?: string;
applicationId?: string;
requestType: string;
noteText: string;
noteType?: string;
attachments?: File[];
}) => {
// If attachments exist, we should use a different approach or multi-part
// For now, handling simple text.
const response: any = await API.addWorknote(data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to add worknote');
return response.data?.data || response.data;
},
addParticipant: async (data: any) => {
const response: any = await API.addParticipant(data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to add participant');
return response.data;
},
removeParticipant: async (id: string) => {
const response: any = await API.removeParticipant(id);
if (!response.ok) throw new Error(response.data?.message || 'Failed to remove participant');
return response.data;
}
};

View File

@ -1,13 +1,14 @@
import client from '../api/client';
export const eorService = {
getChecklist: async (applicationId: string) => {
const response = await client.get(`/eor/${applicationId}`);
getChecklist: async (applicationId?: string, relocationId?: string) => {
const path = relocationId ? `/eor/relocation/${relocationId}` : `/eor/application/${applicationId}`;
const response = await client.get(path);
return response.data as any;
},
createChecklist: async (applicationId: string) => {
const response = await client.post('/eor', { applicationId });
createChecklist: async (data: { applicationId?: string; relocationId?: string }) => {
const response = await client.post('/eor', data);
return response.data as any;
},