caht enhanced and made centralized componet separate FDD ui creted for document upload
This commit is contained in:
parent
c9de800c47
commit
0696756160
22
src/App.tsx
22
src/App.tsx
@ -14,6 +14,8 @@ import { Dashboard } from './components/dashboard/Dashboard';
|
|||||||
import { FinanceDashboard } from './components/dashboard/FinanceDashboard';
|
import { FinanceDashboard } from './components/dashboard/FinanceDashboard';
|
||||||
import { DealerDashboard } from './components/dashboard/DealerDashboard';
|
import { DealerDashboard } from './components/dashboard/DealerDashboard';
|
||||||
import { ProspectiveDashboardPage } from './components/dashboard/ProspectiveDashboardPage';
|
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 { ApplicationsPage } from './components/applications/ApplicationsPage';
|
||||||
import { AllApplicationsPage } from './components/applications/AllApplicationsPage';
|
import { AllApplicationsPage } from './components/applications/AllApplicationsPage';
|
||||||
import { OpportunityRequestsPage } from './components/applications/OpportunityRequestsPage';
|
import { OpportunityRequestsPage } from './components/applications/OpportunityRequestsPage';
|
||||||
@ -146,6 +148,7 @@ export default function App() {
|
|||||||
'/dealer-relocation': 'Dealer Relocation Requests',
|
'/dealer-relocation': 'Dealer Relocation Requests',
|
||||||
'/questionnaire-builder': 'Questionnaire Builder',
|
'/questionnaire-builder': 'Questionnaire Builder',
|
||||||
'/approval-policies': 'Approval Policies',
|
'/approval-policies': 'Approval Policies',
|
||||||
|
'/fdd-dashboard': 'FDD Dashboard',
|
||||||
};
|
};
|
||||||
return titles[pathname] || 'Dashboard';
|
return titles[pathname] || 'Dashboard';
|
||||||
};
|
};
|
||||||
@ -181,7 +184,7 @@ export default function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
{/* Prospective Dealer Route - STRICTLY ISOLATED */}
|
{/* Prospective Dealer Route - STRICTLY ISOLATED */}
|
||||||
<Route
|
<Route
|
||||||
path="/prospective-dashboard"
|
path="/prospective-dashboard/*"
|
||||||
element={
|
element={
|
||||||
<RoleGuard allowedRoles={['Prospective Dealer']}>
|
<RoleGuard allowedRoles={['Prospective Dealer']}>
|
||||||
<ProspectiveDashboardPage />
|
<ProspectiveDashboardPage />
|
||||||
@ -189,6 +192,8 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
|
{/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
|
||||||
<Route element={
|
<Route element={
|
||||||
<RoleGuard excludeRoles={['Prospective Dealer']} redirectTo="/prospective-dashboard">
|
<RoleGuard excludeRoles={['Prospective Dealer']} redirectTo="/prospective-dashboard">
|
||||||
@ -209,11 +214,10 @@ export default function App() {
|
|||||||
{/* Applications */}
|
{/* Applications */}
|
||||||
<Route path="/applications" element={<ApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" />} />
|
<Route path="/applications" element={<ApplicationsPage onViewDetails={(id) => navigate(`/applications/${id}`)} initialFilter="all" />} />
|
||||||
<Route path="/applications/:id" element={<ApplicationDetails />} />
|
<Route path="/applications/:id" element={<ApplicationDetails />} />
|
||||||
<Route path="/applications/:id/worknotes" element={
|
|
||||||
|
{/* Centralized Work Notes Route */}
|
||||||
|
<Route path="/worknotes/:type/:id" element={
|
||||||
<WorkNotesPage
|
<WorkNotesPage
|
||||||
applicationId={window.location.pathname.split('/')[2]}
|
|
||||||
applicationName=""
|
|
||||||
registrationNumber=""
|
|
||||||
onBack={() => window.history.back()}
|
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" />
|
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 */}
|
{/* Admin/Lead Routes */}
|
||||||
<Route path="/opportunity-requests" element={<OpportunityRequestsPage onViewDetails={(id) => navigate(`/applications/${id}`)} />} />
|
<Route path="/opportunity-requests" element={<OpportunityRequestsPage onViewDetails={(id) => navigate(`/applications/${id}`)} />} />
|
||||||
<Route path="/non-opportunities" element={<NonOpportunitiesPage 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="/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" 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" 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 */}
|
{/* Dealer Routes */}
|
||||||
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />} />
|
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id) => navigate(`/resignation/${id}`)} />} />
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
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 { onboardingService } from '../../services/onboarding.service';
|
||||||
import { auditService } from '../../services/audit.service';
|
import { auditService } from '../../services/audit.service';
|
||||||
import { eorService } from '../../services/eor.service';
|
import { eorService } from '../../services/eor.service';
|
||||||
@ -395,7 +395,6 @@ export function ApplicationDetails() {
|
|||||||
const [rejectionReason, setRejectionReason] = useState('');
|
const [rejectionReason, setRejectionReason] = useState('');
|
||||||
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
|
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
|
||||||
|
|
||||||
const [showWorkNoteModal, setShowWorkNoteModal] = useState(false);
|
|
||||||
const [showScheduleModal, setShowScheduleModal] = useState(false);
|
const [showScheduleModal, setShowScheduleModal] = useState(false);
|
||||||
const [showKTMatrixModal, setShowKTMatrixModal] = useState(false);
|
const [showKTMatrixModal, setShowKTMatrixModal] = useState(false);
|
||||||
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
|
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
|
||||||
@ -405,7 +404,6 @@ export function ApplicationDetails() {
|
|||||||
const [selectedStage, setSelectedStage] = useState<string | null>(null);
|
const [selectedStage, setSelectedStage] = useState<string | null>(null);
|
||||||
const [interviewMode, setInterviewMode] = useState('virtual');
|
const [interviewMode, setInterviewMode] = useState('virtual');
|
||||||
const [approvalRemark, setApprovalRemark] = useState('');
|
const [approvalRemark, setApprovalRemark] = useState('');
|
||||||
const [workNote, setWorkNote] = useState('');
|
|
||||||
const [expandedBranches, setExpandedBranches] = useState<{ [key: string]: boolean }>({
|
const [expandedBranches, setExpandedBranches] = useState<{ [key: string]: boolean }>({
|
||||||
'architectural-work': true,
|
'architectural-work': true,
|
||||||
'statutory-documents': 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 () => {
|
const handleAddParticipant = async () => {
|
||||||
if (!selectedUser) {
|
if (!selectedUser) {
|
||||||
toast.warning('Please select a user');
|
toast.warning('Please select a user');
|
||||||
@ -1663,7 +1651,20 @@ export function ApplicationDetails() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<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>
|
||||||
</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",
|
"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.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>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[10px] font-medium text-slate-700 leading-none">{approver.name}</span>
|
<span className="text-[10px] font-medium text-slate-700 leading-none">{approver.name}</span>
|
||||||
@ -2826,7 +2827,7 @@ export function ApplicationDetails() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => navigate(`/applications/${application.id}/worknotes`, {
|
onClick={() => navigate(`/worknotes/application/${application.id}`, {
|
||||||
state: {
|
state: {
|
||||||
applicationName: application.name,
|
applicationName: application.name,
|
||||||
registrationNumber: application.registrationNumber,
|
registrationNumber: application.registrationNumber,
|
||||||
@ -3028,39 +3029,6 @@ export function ApplicationDetails() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 >
|
||||||
</div >
|
</div >
|
||||||
|
|
||||||
@ -3218,48 +3186,6 @@ export function ApplicationDetails() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog >
|
</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 >
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -12,12 +12,12 @@ import { useState, useEffect } from 'react';
|
|||||||
import { User as UserType } from '../../lib/mock-data';
|
import { User as UserType } from '../../lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface ConstitutionalChangeDetailsProps {
|
interface ConstitutionalChangeDetailsProps {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
onOpenWorknote?: (requestId: string, requestType: 'relocation' | 'constitutional-change' | 'fnf' | 'resignation' | 'termination', requestTitle: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow stages as per the process flow
|
// 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';
|
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 [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
|
||||||
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
|
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
|
||||||
const [comments, setComments] = useState('');
|
const [comments, setComments] = useState('');
|
||||||
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
|
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
|
||||||
const [isWorknoteDialogOpen, setIsWorknoteDialogOpen] = useState(false);
|
|
||||||
const [request, setRequest] = useState<any>(null);
|
const [request, setRequest] = useState<any>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||||
const [newWorknote, setNewWorknote] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRequestDetails();
|
fetchRequestDetails();
|
||||||
@ -192,26 +191,6 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
|
|||||||
setIsUploadDialogOpen(false);
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -622,16 +601,16 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-blue- blue-700 hover:bg-blue-50"
|
className="w-full border-blue- blue-700 hover:bg-blue-50"
|
||||||
onClick={() => {
|
onClick={() => navigate(`/worknotes/constitutional-change/${requestId}`, {
|
||||||
if (onOpenWorknote) {
|
state: {
|
||||||
onOpenWorknote(requestId, 'constitutional-change', `${request.outlet?.name || 'N/A'} (${request.outlet?.code || 'N/A'}) - Constitutional Change Request`);
|
applicationName: request?.outlet?.name || 'Constitutional Change',
|
||||||
} else {
|
registrationNumber: requestId || '',
|
||||||
setIsWorknoteDialogOpen(true);
|
participants: request?.participants || []
|
||||||
}
|
}
|
||||||
}}
|
})}
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-4 h-4 mr-2" />
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
Worknotes ({(request.worknotes || []).length})
|
Worknotes ({(request?.worknotes || []).length})
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -699,87 +678,6 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
478
src/components/applications/FDDApplicationDetails.tsx
Normal file
478
src/components/applications/FDDApplicationDetails.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -46,7 +46,8 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
|
|||||||
const stage = app.currentStage;
|
const stage = app.currentStage;
|
||||||
return [
|
return [
|
||||||
'LOI In Progress', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
|
'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';
|
].includes(s) || stage === 'Finance';
|
||||||
});
|
});
|
||||||
setApplications(financeApps);
|
setApplications(financeApps);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Progress } from '../ui/progress';
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { User, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
|
import { User, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
import { WorkNotesPage } from './WorkNotesPage';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface FnFDetailsProps {
|
interface FnFDetailsProps {
|
||||||
@ -20,6 +20,7 @@ interface FnFDetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [fnfCase, setFnfCase] = useState<any>(null);
|
const [fnfCase, setFnfCase] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
|
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
|
||||||
@ -161,17 +162,32 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
{fnfCase.requestType}
|
{fnfCase.requestType}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Action Buttons */}
|
||||||
{/* Action Button */}
|
<div className="flex items-center gap-3">
|
||||||
{canSendToStakeholders && fnfCase.status === 'New' && (
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
onClick={() => navigate(`/worknotes/fnf/${fnfId}`, {
|
||||||
onClick={() => setSendStakeholdersDialog(true)}
|
state: {
|
||||||
|
applicationName: fnfCase.dealerName || 'F&F Settlement',
|
||||||
|
registrationNumber: fnfId || '',
|
||||||
|
participants: fnfCase.participants || []
|
||||||
|
}
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<Send className="w-4 h-4 mr-2" />
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
Send to Stakeholders
|
View Work Notes
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
|
{canSendToStakeholders && fnfCase.status === 'New' && (
|
||||||
|
<Button
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
onClick={() => setSendStakeholdersDialog(true)}
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4 mr-2" />
|
||||||
|
Send to Stakeholders
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Summary */}
|
{/* Progress Summary */}
|
||||||
@ -217,7 +233,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<TabsTrigger value="departments">Department Responses</TabsTrigger>
|
<TabsTrigger value="departments">Department Responses</TabsTrigger>
|
||||||
<TabsTrigger value="financial">Financial Summary</TabsTrigger>
|
<TabsTrigger value="financial">Financial Summary</TabsTrigger>
|
||||||
<TabsTrigger value="documents">Documents</TabsTrigger>
|
<TabsTrigger value="documents">Documents</TabsTrigger>
|
||||||
<TabsTrigger value="worknotes">Work Notes</TabsTrigger>
|
|
||||||
<TabsTrigger value="audit">Audit Trail</TabsTrigger>
|
<TabsTrigger value="audit">Audit Trail</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
@ -900,11 +916,6 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Work Notes Tab */}
|
|
||||||
<TabsContent value="worknotes">
|
|
||||||
<WorkNotesPage />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Audit Trail Tab */}
|
{/* Audit Trail Tab */}
|
||||||
<TabsContent value="audit">
|
<TabsContent value="audit">
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
295
src/components/applications/ProspectiveApplicationDetails.tsx
Normal file
295
src/components/applications/ProspectiveApplicationDetails.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||||
import { formatDateTime } from '../ui/utils';
|
import { formatDateTime } from '../ui/utils';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
@ -11,6 +11,7 @@ import { Textarea } from '../ui/textarea';
|
|||||||
import { Label } from '../ui/label';
|
import { Label } from '../ui/label';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { User as UserType } from '../../lib/mock-data';
|
import { User as UserType } from '../../lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
@ -19,7 +20,6 @@ interface RelocationRequestDetailsProps {
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
onOpenWorknote?: (requestId: string, requestType: 'relocation' | 'constitutional-change' | 'fnf' | 'resignation' | 'termination', requestTitle: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow stages configuration
|
// Workflow stages configuration
|
||||||
@ -60,7 +60,8 @@ const getStatusColor = (status: string) => {
|
|||||||
return 'bg-slate-100 text-slate-700 border-slate-300';
|
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 [request, setRequest] = useState<any>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
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 [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
|
||||||
const [comments, setComments] = useState('');
|
const [comments, setComments] = useState('');
|
||||||
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
|
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
|
||||||
const [isWorknoteDialogOpen, setIsWorknoteDialogOpen] = useState(false);
|
|
||||||
const [worknotes, setWorknotes] = useState<any[]>([]);
|
|
||||||
const [newWorknote, setNewWorknote] = useState('');
|
|
||||||
const [eorChecklist, setEorChecklist] = useState<any>(null);
|
const [eorChecklist, setEorChecklist] = useState<any>(null);
|
||||||
const [isEorLoading, setIsEorLoading] = useState(false);
|
const [isEorLoading, setIsEorLoading] = useState(false);
|
||||||
const [isSubmittingEor, setIsSubmittingEor] = 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;
|
const response = await API.getRelocationRequestById(requestId) as any;
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
setRequest(response.data.request);
|
setRequest(response.data.request);
|
||||||
setWorknotes(response.data.request.worknotes || []);
|
|
||||||
|
|
||||||
// Auto-fetch EOR checklist if in the correct stage
|
// Auto-fetch EOR checklist if in the correct stage
|
||||||
const currentStage = response.data.request.currentStage;
|
const currentStage = response.data.request.currentStage;
|
||||||
@ -194,7 +191,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
|
|||||||
if (!request || !currentUser) return false;
|
if (!request || !currentUser) return false;
|
||||||
|
|
||||||
// Check for Super Admin bypass
|
// 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;
|
if (isAdmin) return true;
|
||||||
|
|
||||||
// Check if user's role matches the role required for the current stage
|
// 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';
|
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') => {
|
const handleAction = (type: 'approve' | 'reject' | 'hold') => {
|
||||||
setActionType(type);
|
setActionType(type);
|
||||||
setIsActionDialogOpen(true);
|
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 () => {
|
const handleUploadDocument = async () => {
|
||||||
if (!selectedFile || !selectedDocType) {
|
if (!selectedFile || !selectedDocType) {
|
||||||
@ -356,9 +325,31 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge className={getStatusColor(request.status)}>
|
<div className="flex items-center gap-3">
|
||||||
{request.status}
|
<Button
|
||||||
</Badge>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Request Overview */}
|
{/* Request Overview */}
|
||||||
@ -938,16 +929,16 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-blue-300 text-blue-700 hover:bg-blue-50"
|
className="w-full border-blue-300 text-blue-700 hover:bg-blue-50"
|
||||||
onClick={() => {
|
onClick={() => navigate(`/worknotes/relocation/${requestId}`, {
|
||||||
if (onOpenWorknote) {
|
state: {
|
||||||
onOpenWorknote(requestId, 'relocation', `${request.outlet?.name} (${request.outlet?.code}) - Relocation Request`);
|
applicationName: request?.outlet?.name || 'Relocation',
|
||||||
} else {
|
registrationNumber: request?.requestId || '',
|
||||||
setIsWorknoteDialogOpen(true);
|
participants: request?.participants || []
|
||||||
}
|
}
|
||||||
}}
|
})}
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-4 h-4 mr-2" />
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
Worknotes ({worknotes.length})
|
Worknotes ({request?.worknotes?.length || 0})
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -1044,90 +1035,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
|
|||||||
|
|
||||||
|
|
||||||
{/* Worknotes Dialog */}
|
{/* Worknotes Dialog */}
|
||||||
<Dialog open={isWorknoteDialogOpen} onOpenChange={setIsWorknoteDialogOpen}>
|
{/* Worknotes Dialog - handled in Header */}
|
||||||
<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>
|
|
||||||
|
|
||||||
<DocumentPreviewModal
|
<DocumentPreviewModal
|
||||||
isOpen={isPreviewOpen}
|
isOpen={isPreviewOpen}
|
||||||
|
|||||||
@ -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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
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 { useState, useEffect } from 'react';
|
||||||
import { User } from '../../lib/mock-data';
|
import { User } from '../../lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -28,23 +23,8 @@ const getStatusColor = (status: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) {
|
export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
||||||
const [requests, setRequests] = useState<any[]>([]);
|
const [requests, setRequests] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchRequests();
|
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
|
// Statistics
|
||||||
const stats = [
|
const stats = [
|
||||||
@ -219,318 +84,10 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
|||||||
<p className="text-slate-600">
|
<p className="text-slate-600">
|
||||||
Manage dealer relocation requests - Moving dealership to a new location
|
Manage dealer relocation requests - Moving dealership to a new location
|
||||||
</p>
|
</p>
|
||||||
|
<span className="block mt-1 text-slate-500 text-sm">
|
||||||
|
• Note: Relocation requests are initiated by the dealer.
|
||||||
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Statistics Cards */}
|
{/* Statistics Cards */}
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import { Button } from '../ui/button';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||||
import { Badge } from '../ui/badge';
|
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 { Label } from '../ui/label';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { User as UserType } from '../../lib/mock-data';
|
import { User as UserType } from '../../lib/mock-data';
|
||||||
import { WorkNotesPage } from './WorkNotesPage';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { resignationService } from '../../services/resignation.service';
|
import { resignationService } from '../../services/resignation.service';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
@ -23,8 +23,8 @@ interface ResignationDetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ResignationDetails({ resignationId, onBack, currentUser }: 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 [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 [remarks, setRemarks] = useState('');
|
||||||
const [assignToUser, setAssignToUser] = useState('');
|
const [assignToUser, setAssignToUser] = useState('');
|
||||||
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
||||||
@ -248,37 +248,26 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<MessageSquare className="w-4 h-4 text-slate-500" />
|
<MessageSquare className="w-4 h-4 text-slate-500" />
|
||||||
<span className="text-sm text-slate-600">Communication & Notes</span>
|
<span className="text-sm text-slate-600">Communication & Notes</span>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={workNotesOpen} onOpenChange={setWorkNotesOpen}>
|
<Button
|
||||||
<DialogTrigger asChild>
|
size="sm"
|
||||||
<Button
|
variant="outline"
|
||||||
size="sm"
|
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm"
|
||||||
variant="outline"
|
onClick={() => navigate(`/worknotes/resignation/${resignationId}`, {
|
||||||
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all"
|
state: {
|
||||||
>
|
applicationName: resignationData?.outlet?.name || 'Resignation',
|
||||||
<MessageSquare className="w-4 h-4 mr-2" />
|
registrationNumber: resignationData?.resignationId || '',
|
||||||
View Work Notes
|
participants: resignationData?.participants || []
|
||||||
{resignationData?.worknotes?.length > 0 && (
|
}
|
||||||
<Badge className="ml-2 bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
|
})}
|
||||||
{resignationData.worknotes.length}
|
>
|
||||||
</Badge>
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
)}
|
View Work Notes
|
||||||
</Button>
|
{resignationData?.worknotes?.length > 0 && (
|
||||||
</DialogTrigger>
|
<Badge className="ml-2 bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
|
||||||
<DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
|
{resignationData.worknotes.length}
|
||||||
<DialogHeader>
|
</Badge>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
)}
|
||||||
<MessageSquare className="w-5 h-5 text-amber-600" />
|
</Button>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
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 { useState, useEffect } from 'react';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -26,19 +21,8 @@ const getStatusColor = (status: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ResignationPage({ currentUser, onViewDetails }: ResignationPageProps) {
|
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 [resignations, setResignations] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
resignationType: 'Voluntary',
|
|
||||||
lastOperationalDateSales: '',
|
|
||||||
lastOperationalDateServices: '',
|
|
||||||
resignationReason: '',
|
|
||||||
customerDescription: '',
|
|
||||||
document: null as File | null
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchResignations = async () => {
|
const fetchResignations = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -60,68 +44,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
|||||||
fetchResignations();
|
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
|
// Helper function to check if request is at current user's level
|
||||||
const isRequestAtMyLevel = (request: any) => {
|
const isRequestAtMyLevel = (request: any) => {
|
||||||
@ -201,153 +124,12 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
|||||||
<CardTitle>Resignation Requests</CardTitle>
|
<CardTitle>Resignation Requests</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Track and manage dealer resignation requests
|
Track and manage dealer resignation requests
|
||||||
{!isDDLead && (
|
<span className="block mt-1 text-slate-500">
|
||||||
<span className="block mt-1 text-amber-600">
|
• Note: Resignation requests are initiated by the dealer or via ASM.
|
||||||
• Note: Only DD Lead can create resignation requests. Current role: {currentUser?.role || 'Not logged in'}
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@ -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 { Button } from '../ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||||
import { Badge } from '../ui/badge';
|
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 { Label } from '../ui/label';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { terminationService } from '../../services/termination.service';
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { User, mockWorkNotes, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
|
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 {
|
interface TerminationDetailsProps {
|
||||||
terminationId: string;
|
terminationId: string;
|
||||||
@ -22,13 +22,13 @@ interface TerminationDetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TerminationDetails({ terminationId, onBack, currentUser }: 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 [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null });
|
||||||
const [remarks, setRemarks] = useState('');
|
const [remarks, setRemarks] = useState('');
|
||||||
const [assignToUser, setAssignToUser] = useState('');
|
const [assignToUser, setAssignToUser] = useState('');
|
||||||
const [workNotesOpen, setWorkNotesOpen] = useState(false);
|
|
||||||
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [terminationData, setTerminationData] = useState<any>(null);
|
const [terminationData, setTerminationData] = useState<any>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [showSCNDialog, setShowSCNDialog] = useState(false);
|
const [showSCNDialog, setShowSCNDialog] = useState(false);
|
||||||
const [scnFile, setScnFile] = useState<File | null>(null);
|
const [scnFile, setScnFile] = useState<File | null>(null);
|
||||||
const [scnRemarks, setScnRemarks] = useState('');
|
const [scnRemarks, setScnRemarks] = useState('');
|
||||||
@ -53,6 +53,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
fetchTermination();
|
fetchTermination();
|
||||||
}, [terminationId]);
|
}, [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 () => {
|
const handleIssueSCN = async () => {
|
||||||
try {
|
try {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
@ -443,37 +452,26 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<MessageSquare className="w-4 h-4 text-slate-500" />
|
<MessageSquare className="w-4 h-4 text-slate-500" />
|
||||||
<span className="text-sm text-slate-600">Communication & Notes</span>
|
<span className="text-sm text-slate-600">Communication & Notes</span>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={workNotesOpen} onOpenChange={setWorkNotesOpen}>
|
<Button
|
||||||
<DialogTrigger asChild>
|
size="sm"
|
||||||
<Button
|
variant="outline"
|
||||||
size="sm"
|
className="relative hover:bg-red-50 hover:border-red-300 hover:text-red-700 transition-all shadow-sm"
|
||||||
variant="outline"
|
onClick={() => navigate(`/worknotes/termination/${terminationId}`, {
|
||||||
className="relative hover:bg-red-50 hover:border-red-300 hover:text-red-700 transition-all"
|
state: {
|
||||||
>
|
applicationName: terminationData?.dealerName || 'Termination',
|
||||||
<MessageSquare className="w-4 h-4 mr-2" />
|
registrationNumber: terminationId || '',
|
||||||
View Work Notes
|
participants: terminationData?.participants || []
|
||||||
{workNotesCount > 0 && (
|
}
|
||||||
<Badge className="ml-2 bg-red-600 hover:bg-red-700 text-white h-5 px-2">
|
})}
|
||||||
{workNotesCount}
|
>
|
||||||
</Badge>
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
)}
|
View Work Notes
|
||||||
</Button>
|
{workNotesCount > 0 && (
|
||||||
</DialogTrigger>
|
<Badge className="ml-2 bg-red-600 hover:bg-red-700 text-white h-5 px-2">
|
||||||
<DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
|
{workNotesCount}
|
||||||
<DialogHeader>
|
</Badge>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
)}
|
||||||
<MessageSquare className="w-5 h-5 text-red-600" />
|
</Button>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { RootState } from '../../store';
|
|||||||
import { useParams, useLocation, useNavigate } from 'react-router-dom';
|
import { useParams, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
|
||||||
import { Avatar, AvatarFallback } from '../ui/avatar';
|
import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
@ -16,8 +15,15 @@ import {
|
|||||||
MessageSquare,
|
MessageSquare,
|
||||||
FileText,
|
FileText,
|
||||||
File as FileIcon,
|
File as FileIcon,
|
||||||
X
|
X,
|
||||||
|
RefreshCcw,
|
||||||
|
Users,
|
||||||
|
Search,
|
||||||
|
ChevronRight,
|
||||||
|
Info,
|
||||||
|
Clock as ClockIcon
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { Badge } from '../ui/badge';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -52,7 +58,9 @@ interface WorkNote {
|
|||||||
// Participant interface for mentions
|
// Participant interface for mentions
|
||||||
|
|
||||||
interface WorkNotesPageProps {
|
interface WorkNotesPageProps {
|
||||||
applicationId: string;
|
requestId: string;
|
||||||
|
requestType?: 'application' | 'relocation' | 'constitutional' | 'resignation' | 'termination' | 'fnf';
|
||||||
|
mode?: 'page' | 'modal';
|
||||||
applicationName: string;
|
applicationName: string;
|
||||||
registrationNumber: string;
|
registrationNumber: string;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
@ -66,18 +74,22 @@ interface ParticipantUI {
|
|||||||
email: string;
|
email: string;
|
||||||
initials: string;
|
initials: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
role?: string;
|
||||||
|
isOnline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BACKEND_URL = (import.meta as any).env?.VITE_API_URL?.replace('/api', '') || 'http://localhost:5000';
|
const BACKEND_URL = (import.meta as any).env?.VITE_API_URL?.replace('/api', '') || 'http://localhost:5000';
|
||||||
|
|
||||||
export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
||||||
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
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 location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Use props if provided (modal mode), otherwise use URL and state
|
// 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 [appName, setAppName] = useState(props.applicationName || location.state?.applicationName || 'Application');
|
||||||
const [regNumber, setRegNumber] = useState(props.registrationNumber || location.state?.registrationNumber || '');
|
const [regNumber, setRegNumber] = useState(props.registrationNumber || location.state?.registrationNumber || '');
|
||||||
const onBack = props.onBack || (() => navigate(-1));
|
const onBack = props.onBack || (() => navigate(-1));
|
||||||
@ -92,6 +104,8 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
|
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
|
||||||
const [attachedFiles, setAttachedFiles] = useState<Attachment[]>([]);
|
const [attachedFiles, setAttachedFiles] = useState<Attachment[]>([]);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||||
|
const [participantSearch, setParticipantSearch] = useState('');
|
||||||
|
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -162,46 +176,50 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
seenIds.add(id);
|
seenIds.add(id);
|
||||||
const name = p.user?.fullName || p.user?.name || p.fullName || p.name || 'Unknown User';
|
const name = p.user?.fullName || p.user?.name || p.fullName || p.name || 'Unknown User';
|
||||||
const email = p.user?.email || p.email || '';
|
const email = p.user?.email || p.email || '';
|
||||||
|
const role = p.user?.roleCode || p.roleCode || p.user?.role || p.role || 'Participant';
|
||||||
participantsList.push({
|
participantsList.push({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
initials: getInitials(name),
|
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 })));
|
console.log('Participants list for mentions:', participantsList.map(p => ({ id: p.id, name: p.name })));
|
||||||
|
|
||||||
|
const fetchNotes = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const res: any = await worknoteService.getWorknotes(requestId, requestType);
|
||||||
|
if (res.success) {
|
||||||
|
setNotes(res.data.map((n: any) => ({
|
||||||
|
id: n.id,
|
||||||
|
noteText: n.noteText,
|
||||||
|
noteType: n.noteType,
|
||||||
|
createdAt: n.createdAt,
|
||||||
|
userId: n.userId,
|
||||||
|
author: n.author || { name: 'System', email: '', role: 'system' },
|
||||||
|
attachments: n.attachments || []
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch notes error:', error);
|
||||||
|
toast.error('Failed to load work notes');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch Notes on load and join socket room
|
// Fetch Notes on load and join socket room
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchNotes = async () => {
|
|
||||||
try {
|
|
||||||
const res: any = await worknoteService.getWorknotes(applicationId, 'application');
|
|
||||||
if (res.success) {
|
|
||||||
setNotes(res.data.map((n: any) => ({
|
|
||||||
id: n.id,
|
|
||||||
noteText: n.noteText,
|
|
||||||
noteType: n.noteType,
|
|
||||||
createdAt: n.createdAt,
|
|
||||||
userId: n.userId,
|
|
||||||
author: n.author || { name: 'System', email: '', role: 'system' },
|
|
||||||
attachments: n.attachments || []
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fetch notes error:', error);
|
|
||||||
toast.error('Failed to load work notes');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchNotes();
|
fetchNotes();
|
||||||
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.emit('join_room', applicationId);
|
socket.emit('join_room', requestId);
|
||||||
|
|
||||||
socket.on('new_worknote', (newNote: any) => {
|
socket.on('new_worknote', (newNote: any) => {
|
||||||
setNotes(prev => {
|
setNotes(prev => {
|
||||||
@ -229,18 +247,18 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.emit('leave_room', applicationId);
|
socket.emit('leave_room', requestId);
|
||||||
socket.off('new_worknote');
|
socket.off('new_worknote');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [applicationId, socket]);
|
}, [requestId, requestType, socket]);
|
||||||
|
|
||||||
// Fetch application details if metadata or participants are missing (e.g. on refresh)
|
// Fetch application details if metadata or participants are missing (e.g. on refresh)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (applicationId) {
|
if (requestId && requestType === 'application') {
|
||||||
const fetchApplicationDetails = async () => {
|
const fetchApplicationDetails = async () => {
|
||||||
try {
|
try {
|
||||||
const appData = await onboardingService.getApplicationById(applicationId);
|
const appData = await onboardingService.getApplicationById(requestId);
|
||||||
if (appData) {
|
if (appData) {
|
||||||
// Update participants if not provided
|
// Update participants if not provided
|
||||||
if (externalParticipants.length === 0 && appData.participants) {
|
if (externalParticipants.length === 0 && appData.participants) {
|
||||||
@ -260,7 +278,7 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
};
|
};
|
||||||
fetchApplicationDetails();
|
fetchApplicationDetails();
|
||||||
}
|
}
|
||||||
}, [applicationId, externalParticipants.length, props.applicationName, props.registrationNumber, location.state]);
|
}, [requestId, requestType, externalParticipants.length, props.applicationName, props.registrationNumber, location.state]);
|
||||||
|
|
||||||
// Auto-scroll logic
|
// Auto-scroll logic
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@ -330,7 +348,7 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
for (const file of Array.from(files)) {
|
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) {
|
if (res.success) {
|
||||||
setAttachedFiles(prev => [...prev, res.data]);
|
setAttachedFiles(prev => [...prev, res.data]);
|
||||||
}
|
}
|
||||||
@ -403,8 +421,8 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
setNotes(prev => [tempNote, ...prev]);
|
setNotes(prev => [tempNote, ...prev]);
|
||||||
|
|
||||||
const res: any = await worknoteService.addWorknote({
|
const res: any = await worknoteService.addWorknote({
|
||||||
requestId: applicationId,
|
requestId: requestId,
|
||||||
requestType: 'application',
|
requestType: requestType,
|
||||||
noteText: processedMessage,
|
noteText: processedMessage,
|
||||||
noteType: 'General',
|
noteType: 'General',
|
||||||
tags: mentionedUserIds,
|
tags: mentionedUserIds,
|
||||||
@ -491,30 +509,56 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Participant Avatars */}
|
{/* Participant Avatars & Refresh & Toggle Sidebar */}
|
||||||
<div className="flex items-center -space-x-2">
|
<div className="flex items-center gap-4">
|
||||||
{participantsList.slice(0, 3).map((participant, index) => (
|
<div className="hidden sm:flex items-center -space-x-2 mr-2">
|
||||||
<Avatar
|
{participantsList.slice(0, 3).map((participant, index) => (
|
||||||
key={index}
|
<Avatar
|
||||||
className="w-8 h-8 border-2 border-white"
|
key={index}
|
||||||
>
|
className="w-8 h-8 border-2 border-white ring-1 ring-slate-100"
|
||||||
<AvatarFallback className={`${participant.color} text-white text-xs`}>
|
>
|
||||||
{participant.initials}
|
<AvatarFallback className={`${participant.color} text-white text-[10px]`}>
|
||||||
</AvatarFallback>
|
{participant.initials}
|
||||||
</Avatar>
|
</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">
|
{participantsList.length > 3 && (
|
||||||
<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">
|
||||||
</div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="flex-1 px-6 py-4 min-h-0">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
<div className="max-w-4xl mx-auto space-y-6 flex flex-col">
|
{/* Main Chat Engine */}
|
||||||
{[...notes].reverse().map((note) => {
|
<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()) ||
|
const isMe = (note?.author?.email && currentUser?.email && note.author.email.toLowerCase() === currentUser.email.toLowerCase()) ||
|
||||||
(note?.userId && currentUser?.id && note.userId === currentUser.id) ||
|
(note?.userId && currentUser?.id && note.userId === currentUser.id) ||
|
||||||
note.id.startsWith('temp-');
|
note.id.startsWith('temp-');
|
||||||
@ -609,12 +653,12 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</div>
|
||||||
|
|
||||||
{/* Input Area - Stays fixed because it's a sibling of ScrollArea */}
|
{/* Input Area */}
|
||||||
<div className="bg-white border-t border-slate-200 px-6 py-4 z-10">
|
<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">
|
<div className="max-w-4xl mx-auto space-y-4">
|
||||||
|
|
||||||
{/* Attachment Previews */}
|
{/* Attachment Previews */}
|
||||||
{attachedFiles.length > 0 && (
|
{attachedFiles.length > 0 && (
|
||||||
@ -757,11 +801,103 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-slate-400 text-[10px] px-1">
|
<p className="text-slate-400 text-[10px] px-1 flex items-center gap-1">
|
||||||
Press Enter to send • Use @ to mention someone • {isUploading ? 'Uploading files...' : 'Files attached appear above'}
|
<Info className="w-3 h-3" />
|
||||||
|
<span>Press Enter to send • Use @ to mention • {isUploading ? 'Uploading files...' : 'Files attached appear above'}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Preview Modal */}
|
||||||
<Dialog open={!!previewFile} onOpenChange={(open) => !open && setPreviewFile(null)}>
|
<Dialog open={!!previewFile} onOpenChange={(open) => !open && setPreviewFile(null)}>
|
||||||
@ -805,3 +941,5 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default WorkNotesPage;
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
207
src/components/dashboard/FDDDashboardPage.tsx
Normal file
207
src/components/dashboard/FDDDashboardPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -77,7 +77,8 @@ export function FinanceDashboard({ currentUser, onNavigate, onViewPaymentDetails
|
|||||||
const stage = app.currentStage;
|
const stage = app.currentStage;
|
||||||
return [
|
return [
|
||||||
'LOI In Progress', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
|
'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';
|
].includes(s) || stage === 'Finance';
|
||||||
});
|
});
|
||||||
setApplications(financeApps);
|
setApplications(financeApps);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
|
Calendar,
|
||||||
FileText,
|
FileText,
|
||||||
UserMinus,
|
UserMinus,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
@ -13,18 +14,16 @@ import {
|
|||||||
User,
|
User,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Upload,
|
|
||||||
Clock,
|
|
||||||
CheckCircle,
|
|
||||||
File,
|
|
||||||
X
|
X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
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 { RootState } from '../../store';
|
||||||
import { logout } from '../../store/slices/authSlice';
|
import { logout } from '../../store/slices/authSlice';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
|
import { Badge } from '../ui/badge';
|
||||||
|
import { ProspectiveApplicationDetails } from '../applications/ProspectiveApplicationDetails';
|
||||||
|
|
||||||
export function ProspectiveDashboardPage() {
|
export function ProspectiveDashboardPage() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -33,69 +32,6 @@ export function ProspectiveDashboardPage() {
|
|||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState('applicant');
|
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 = () => {
|
const handleLogout = () => {
|
||||||
dispatch(logout());
|
dispatch(logout());
|
||||||
toast.info('Logged out successfully');
|
toast.info('Logged out successfully');
|
||||||
@ -125,20 +61,17 @@ export function ProspectiveDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<nav className="flex-1 p-4 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<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'}`}
|
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" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -147,12 +80,12 @@ export function ProspectiveDashboardPage() {
|
|||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="px-4 py-2 bg-slate-800 rounded-lg mb-2">
|
<div className="px-4 py-2 bg-slate-800 rounded-lg mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-amber-600 rounded-full flex items-center justify-center">
|
<div className="w-10 h-10 bg-amber-600 rounded-full flex items-center justify-center text-white">
|
||||||
<span>{user?.name?.charAt(0) || 'A'}</span>
|
<span className="font-bold">{user?.name?.charAt(0) || 'A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="truncate">{user?.name || 'Amit Sharma'}</p>
|
<p className="truncate text-sm font-medium">{user?.name || 'Applicant'}</p>
|
||||||
<p className="text-slate-400 truncate">{user?.role || 'Prospective'}</p>
|
<p className="text-slate-400 truncate text-xs">{user?.role || 'Prospective'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -185,158 +118,127 @@ export function ProspectiveDashboardPage() {
|
|||||||
<p className="text-slate-600 text-xs">{user?.role || 'User'}</p>
|
<p className="text-slate-600 text-xs">{user?.role || 'User'}</p>
|
||||||
</div>
|
</div>
|
||||||
</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" />
|
<RefreshCw className="w-4 h-4 text-slate-600" />
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-6">
|
<main className="flex-1 overflow-y-auto p-6">
|
||||||
{activeTab === 'applicant' ? (
|
<Routes>
|
||||||
<div className="space-y-6">
|
<Route path="/" element={<ProspectiveApplicationList />} />
|
||||||
<div>
|
<Route path="/application/:id" element={<ProspectiveApplicationDetailsWrapper />} />
|
||||||
<h1 className="text-slate-900 text-2xl font-bold mb-2">Applicant Portal</h1>
|
</Routes>
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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>
|
</main>
|
||||||
</div >
|
</div>
|
||||||
</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-3xl font-bold mb-2">My Applications</h1>
|
||||||
|
<p className="text-slate-500 font-medium">Track and manage your dealership applications</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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>
|
||||||
|
<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="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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,8 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
{ id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus },
|
{ id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus },
|
||||||
{ id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw },
|
{ id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw },
|
||||||
{ id: 'dealer-relocation', label: 'Relocation Requests', icon: MapPin },
|
{ 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: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||||
{ id: 'applications', label: 'Dealership Requests', icon: FileText },
|
{ id: 'applications', label: 'Dealership Requests', icon: FileText },
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription
|
DialogDescription,
|
||||||
} from './dialog';
|
} from './dialog';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
import {
|
import {
|
||||||
@ -37,166 +37,53 @@ export const DocumentPreviewModal: React.FC<DocumentPreviewModalProps> = ({
|
|||||||
const [zoomScale, setZoomScale] = useState(1);
|
const [zoomScale, setZoomScale] = useState(1);
|
||||||
const [rotation, setRotation] = useState(0);
|
const [rotation, setRotation] = useState(0);
|
||||||
|
|
||||||
if (!document) return null;
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setZoomScale(1);
|
|
||||||
setRotation(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseUrl = 'http://localhost:5000';
|
const baseUrl = 'http://localhost:5000';
|
||||||
const fileUrl = `${baseUrl}/${document.filePath}`;
|
const fileUrl = document ? `${baseUrl}${document.filePath.startsWith('/') ? '' : '/'}${document.filePath}` : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={(open) => {
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
if (!open) {
|
<DialogContent className="max-w-4xl h-[85vh] flex flex-col p-0 overflow-hidden bg-white shadow-2xl border-none">
|
||||||
handleReset();
|
{document ? (
|
||||||
onClose();
|
<>
|
||||||
}
|
<div className="flex items-center justify-between p-4 border-b bg-slate-50">
|
||||||
}}>
|
<div className="flex items-center gap-3">
|
||||||
{/* Overriding sm:max-w-lg with sm:max-w-[95vw] and sm:h-[95vh] */}
|
<div className="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center border border-amber-200">
|
||||||
{/* Also hiding the default close button injected by DialogContent */}
|
<Eye className="w-5 h-5 text-amber-600" />
|
||||||
<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]">
|
</div>
|
||||||
{/* Simple Standard Header */}
|
<div>
|
||||||
<div className="flex items-center justify-between px-6 py-3 border-b bg-slate-50 flex-shrink-0">
|
<DialogTitle className="text-sm font-bold text-slate-900 leading-none mb-1">
|
||||||
<div className="flex items-center gap-3 min-w-0 pr-4">
|
{document.fileName}
|
||||||
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center border border-amber-200 flex-shrink-0">
|
</DialogTitle>
|
||||||
<Eye className="w-4 h-4 text-amber-600" />
|
<p className="text-[10px] text-slate-500 font-medium uppercase tracking-wider">{document.documentType}</p>
|
||||||
</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 && (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</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" />
|
|
||||||
</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">
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</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>
|
</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>
|
</div>
|
||||||
)}
|
<div className="flex items-center gap-2 pr-10">
|
||||||
</div>
|
<Button variant="outline" size="sm" className="h-8 gap-2" onClick={() => window.open(fileUrl, '_blank')}>
|
||||||
</div>
|
<Download className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Download</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 bg-white shadow-inner rounded-sm"
|
||||||
|
title="Preview"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={fileUrl}
|
||||||
|
className="max-h-full max-w-full object-contain shadow-lg rounded-sm"
|
||||||
|
alt="Preview"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full text-slate-400">Loading document...</div>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
33
src/services/collaboration.service.ts
Normal file
33
src/services/collaboration.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,13 +1,14 @@
|
|||||||
import client from '../api/client';
|
import client from '../api/client';
|
||||||
|
|
||||||
export const eorService = {
|
export const eorService = {
|
||||||
getChecklist: async (applicationId: string) => {
|
getChecklist: async (applicationId?: string, relocationId?: string) => {
|
||||||
const response = await client.get(`/eor/${applicationId}`);
|
const path = relocationId ? `/eor/relocation/${relocationId}` : `/eor/application/${applicationId}`;
|
||||||
|
const response = await client.get(path);
|
||||||
return response.data as any;
|
return response.data as any;
|
||||||
},
|
},
|
||||||
|
|
||||||
createChecklist: async (applicationId: string) => {
|
createChecklist: async (data: { applicationId?: string; relocationId?: string }) => {
|
||||||
const response = await client.post('/eor', { applicationId });
|
const response = await client.post('/eor', data);
|
||||||
return response.data as any;
|
return response.data as any;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user