schemaenhanced and tried to fill the gaps
This commit is contained in:
parent
d919e925c8
commit
ad33de7e26
@ -17,7 +17,7 @@ import { ProspectiveDashboardPage } from './components/dashboard/ProspectiveDash
|
|||||||
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';
|
||||||
import { UnopportunityRequestsPage } from './components/applications/UnopportunityRequestsPage';
|
import { NonOpportunitiesPage } from './components/applications/NonOpportunitiesPage';
|
||||||
import { ApplicationDetails } from './components/applications/ApplicationDetails';
|
import { ApplicationDetails } from './components/applications/ApplicationDetails';
|
||||||
import { ResignationPage } from './components/applications/ResignationPage';
|
import { ResignationPage } from './components/applications/ResignationPage';
|
||||||
import { TerminationPage } from './components/applications/TerminationPage';
|
import { TerminationPage } from './components/applications/TerminationPage';
|
||||||
@ -127,7 +127,7 @@ export default function App() {
|
|||||||
'/applications': 'Dealership Requests',
|
'/applications': 'Dealership Requests',
|
||||||
'/all-applications': 'All Applications',
|
'/all-applications': 'All Applications',
|
||||||
'/opportunity-requests': 'Opportunity Requests',
|
'/opportunity-requests': 'Opportunity Requests',
|
||||||
'/unopportunity-requests': 'Unopportunity Requests',
|
'/non-opportunities': 'Non-opportunities',
|
||||||
'/tasks': 'My Tasks',
|
'/tasks': 'My Tasks',
|
||||||
'/reports': 'Reports & Analytics',
|
'/reports': 'Reports & Analytics',
|
||||||
'/settings': 'Settings',
|
'/settings': 'Settings',
|
||||||
@ -222,7 +222,7 @@ export default function App() {
|
|||||||
|
|
||||||
{/* 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="/unopportunity-requests" element={<UnopportunityRequestsPage onViewDetails={(id) => navigate(`/applications/${id}`)} />} />
|
<Route path="/non-opportunities" element={<NonOpportunitiesPage onViewDetails={(id) => navigate(`/applications/${id}`)} />} />
|
||||||
|
|
||||||
{/* Other Modules */}
|
{/* Other Modules */}
|
||||||
<Route path="/users" element={<UserManagementPage />} />
|
<Route path="/users" element={<UserManagementPage />} />
|
||||||
|
|||||||
@ -34,6 +34,8 @@ export const API = {
|
|||||||
submitQuestionnaireResponse: (data: any) => client.post('/questionnaire/response', data),
|
submitQuestionnaireResponse: (data: any) => client.post('/questionnaire/response', data),
|
||||||
getAllQuestionnaires: () => client.get('/onboarding/questionnaires'),
|
getAllQuestionnaires: () => client.get('/onboarding/questionnaires'),
|
||||||
getQuestionnaireById: (id: string) => client.get(`/onboarding/questionnaires/${id}`),
|
getQuestionnaireById: (id: string) => client.get(`/onboarding/questionnaires/${id}`),
|
||||||
|
assignArchitectureTeam: (applicationId: string, assignedTo: string) => client.post(`/onboarding/applications/${applicationId}/assign-architecture`, { assignedTo }),
|
||||||
|
updateArchitectureStatus: (applicationId: string, status: string, remarks?: string) => client.post(`/onboarding/applications/${applicationId}/architecture-status`, { status, remarks }),
|
||||||
|
|
||||||
// Documents
|
// Documents
|
||||||
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {
|
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {
|
||||||
@ -86,6 +88,18 @@ export const API = {
|
|||||||
// Prospective Login
|
// Prospective Login
|
||||||
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
|
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
|
||||||
verifyOtp: (phone: string, otp: string) => client.post('/prospective-login/verify-otp', { phone, otp }),
|
verifyOtp: (phone: string, otp: string) => client.post('/prospective-login/verify-otp', { phone, otp }),
|
||||||
|
// Resignation
|
||||||
|
getResignationById: (id: string) => client.get(`/resignation/${id}`),
|
||||||
|
updateClearance: (id: string, data: any) => client.post(`/resignation/${id}/clearance`, data),
|
||||||
|
|
||||||
|
// Termination
|
||||||
|
getTerminationById: (id: string) => client.get(`/termination/${id}`),
|
||||||
|
updateTerminationStatus: (id: string, data: any) => client.post(`/termination/${id}/status`, data),
|
||||||
|
issueSCN: (id: string, data: any) => client.post(`/termination/${id}/scn`, data),
|
||||||
|
uploadSCNResponse: (id: string, data: any) => client.post(`/termination/${id}/scn-response`, data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
}),
|
||||||
|
finalizeTermination: (id: string, data: any) => client.post(`/termination/${id}/finalize`, data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default API;
|
export default API;
|
||||||
|
|||||||
@ -382,6 +382,13 @@ export function ApplicationDetails() {
|
|||||||
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
|
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
|
||||||
const [interviews, setInterviews] = useState<any[]>([]);
|
const [interviews, setInterviews] = useState<any[]>([]);
|
||||||
const [isScheduling, setIsScheduling] = useState(false);
|
const [isScheduling, setIsScheduling] = useState(false);
|
||||||
|
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
|
||||||
|
const [architectureLeadId, setArchitectureLeadId] = useState<string>('');
|
||||||
|
const [isAssigningArchitecture, setIsAssigningArchitecture] = useState(false);
|
||||||
|
const [showArchitectureStatusModal, setShowArchitectureStatusModal] = useState(false);
|
||||||
|
const [architectureStatus, setArchitectureStatus] = useState<string>('COMPLETED');
|
||||||
|
const [architectureRemarks, setArchitectureRemarks] = useState<string>('');
|
||||||
|
const [isUpdatingArchitecture, setIsUpdatingArchitecture] = useState(false);
|
||||||
|
|
||||||
// KT Matrix State
|
// KT Matrix State
|
||||||
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
|
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
|
||||||
@ -1123,6 +1130,38 @@ export function ApplicationDetails() {
|
|||||||
setRejectionReason('');
|
setRejectionReason('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAssignArchitecture = async () => {
|
||||||
|
if (!architectureLeadId) {
|
||||||
|
alert('Please select an architecture lead');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsAssigningArchitecture(true);
|
||||||
|
await onboardingService.assignArchitectureTeam(applicationId!, architectureLeadId);
|
||||||
|
toast.success('Architecture team assigned successfully');
|
||||||
|
setShowAssignArchitectureModal(false);
|
||||||
|
fetchApplication(); // Refresh to update status
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to assign architecture team');
|
||||||
|
} finally {
|
||||||
|
setIsAssigningArchitecture(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateArchitectureStatus = async () => {
|
||||||
|
try {
|
||||||
|
setIsUpdatingArchitecture(true);
|
||||||
|
await onboardingService.updateArchitectureStatus(applicationId!, architectureStatus, architectureRemarks);
|
||||||
|
toast.success('Architecture status updated successfully');
|
||||||
|
setShowArchitectureStatusModal(false);
|
||||||
|
fetchApplication();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update architecture status');
|
||||||
|
} finally {
|
||||||
|
setIsUpdatingArchitecture(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleWorkNote = () => {
|
const handleWorkNote = () => {
|
||||||
if (!workNote.trim()) {
|
if (!workNote.trim()) {
|
||||||
alert('Please enter a note');
|
alert('Please enter a note');
|
||||||
@ -1367,7 +1406,7 @@ export function ApplicationDetails() {
|
|||||||
|
|
||||||
{/* Tabs Section */}
|
{/* Tabs Section */}
|
||||||
{/* Only show tabs for shortlisted applications (opportunity requests and regular dealership requests) */}
|
{/* Only show tabs for shortlisted applications (opportunity requests and regular dealership requests) */}
|
||||||
{/* Hide tabs for unopportunity requests (lead generation) */}
|
{/* Hide tabs for non-opportunity requests (lead generation) */}
|
||||||
{application.isShortlisted !== false && (
|
{application.isShortlisted !== false && (
|
||||||
<Card>
|
<Card>
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
@ -1939,7 +1978,7 @@ export function ApplicationDetails() {
|
|||||||
|
|
||||||
{/* Actions Card */}
|
{/* Actions Card */}
|
||||||
{/* Only show Actions card for shortlisted applications (opportunity requests and regular dealership requests) */}
|
{/* Only show Actions card for shortlisted applications (opportunity requests and regular dealership requests) */}
|
||||||
{/* Hide Actions for unopportunity requests (lead generation) - these are read-only records */}
|
{/* Hide Actions for non-opportunity requests (lead generation) - these are read-only records */}
|
||||||
{application.isShortlisted !== false && (
|
{application.isShortlisted !== false && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -2002,6 +2041,28 @@ export function ApplicationDetails() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-blue-200 hover:bg-blue-50 text-blue-700"
|
||||||
|
onClick={() => setShowAssignArchitectureModal(true)}
|
||||||
|
>
|
||||||
|
<GitBranch className="w-4 h-4 mr-2" />
|
||||||
|
Assign Architecture Team
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{((currentUser && currentUser.id === application.architectureAssignedTo) || (currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role))) &&
|
||||||
|
application.architectureStatus === 'IN_PROGRESS' && (
|
||||||
|
<Button
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||||
|
onClick={() => setShowArchitectureStatusModal(true)}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
Complete Architecture Work
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Show Interview Feedback only if active interview exists AND feedback NOT submitted */}
|
{/* Show Interview Feedback only if active interview exists AND feedback NOT submitted */}
|
||||||
{activeInterviewForUser && !hasSubmittedFeedback && (
|
{activeInterviewForUser && !hasSubmittedFeedback && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -2099,7 +2160,7 @@ export function ApplicationDetails() {
|
|||||||
|
|
||||||
{/* Work Notes Chat */}
|
{/* Work Notes Chat */}
|
||||||
{/* Only show Work Notes card for shortlisted applications (opportunity requests and regular dealership requests) */}
|
{/* Only show Work Notes card for shortlisted applications (opportunity requests and regular dealership requests) */}
|
||||||
{/* Hide Work Notes for unopportunity requests (lead generation) - no workflow tracking needed */}
|
{/* Hide Work Notes for non-opportunity requests (lead generation) - no workflow tracking needed */}
|
||||||
{
|
{
|
||||||
application.isShortlisted !== false && (
|
application.isShortlisted !== false && (
|
||||||
<Card>
|
<Card>
|
||||||
@ -2395,6 +2456,111 @@ export function ApplicationDetails() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Assign Architecture Team Modal */}
|
||||||
|
<Dialog open={showAssignArchitectureModal} onOpenChange={setShowAssignArchitectureModal}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Assign Architecture Team</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select an architecture team lead for site planning and blueprints.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Select Architecture Lead</Label>
|
||||||
|
<Select value={architectureLeadId} onValueChange={setArchitectureLeadId}>
|
||||||
|
<SelectTrigger className="mt-2">
|
||||||
|
<SelectValue placeholder="Search users..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{users.filter(u => u.role === 'Architecture' || u.roleCode === 'ARCHITECTURE').map((u) => (
|
||||||
|
<SelectItem key={u.id} value={u.id}>
|
||||||
|
{u.fullName} ({u.email})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
{/* Fallback if no specific architecture users found */}
|
||||||
|
{users.filter(u => u.role === 'Architecture' || u.roleCode === 'ARCHITECTURE').length === 0 && users.map((u) => (
|
||||||
|
<SelectItem key={u.id} value={u.id}>
|
||||||
|
{u.fullName} ({u.roleCode || u.role})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => setShowAssignArchitectureModal(false)}
|
||||||
|
disabled={isAssigningArchitecture}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-700"
|
||||||
|
onClick={handleAssignArchitecture}
|
||||||
|
disabled={isAssigningArchitecture}
|
||||||
|
>
|
||||||
|
{isAssigningArchitecture ? 'Assigning...' : 'Assign Team'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Architecture Status Modal */}
|
||||||
|
<Dialog open={showArchitectureStatusModal} onOpenChange={setShowArchitectureStatusModal}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Architecture Status</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Mark the architectural work as completed and optionally add remarks.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Status</Label>
|
||||||
|
<Select value={architectureStatus} onValueChange={setArchitectureStatus}>
|
||||||
|
<SelectTrigger className="mt-2">
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="COMPLETED">Completed</SelectItem>
|
||||||
|
<SelectItem value="REJECTED">Rejected / Needs Revision</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Remarks (Optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Enter any planning or site-visit remarks..."
|
||||||
|
value={architectureRemarks}
|
||||||
|
onChange={(e) => setArchitectureRemarks(e.target.value)}
|
||||||
|
className="mt-2"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => setShowArchitectureStatusModal(false)}
|
||||||
|
disabled={isUpdatingArchitecture}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-700"
|
||||||
|
onClick={handleUpdateArchitectureStatus}
|
||||||
|
disabled={isUpdatingArchitecture}
|
||||||
|
>
|
||||||
|
{isUpdatingArchitecture ? 'Updating...' : 'Update Status'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* KT Matrix Modal */}
|
{/* KT Matrix Modal */}
|
||||||
< Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal} >
|
< Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal} >
|
||||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
|||||||
260
src/components/applications/NonOpportunitiesPage.tsx
Normal file
260
src/components/applications/NonOpportunitiesPage.tsx
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { mockApplications, locations, states, Application, ApplicationStatus } from '../../lib/mock-data';
|
||||||
|
import { onboardingService } from '../../services/onboarding.service';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Input } from '../ui/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '../ui/select';
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Download,
|
||||||
|
Database,
|
||||||
|
Loader2
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Badge } from '../ui/badge';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '../ui/table';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface NonOpportunitiesPageProps {
|
||||||
|
onViewDetails: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [locationFilter, setLocationFilter] = useState<string>('all');
|
||||||
|
const [stateFilter, setStateFilter] = useState<string>('all');
|
||||||
|
|
||||||
|
// Real data integration
|
||||||
|
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchApplications();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchApplications = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await onboardingService.getApplications();
|
||||||
|
const rawData = response.data || (Array.isArray(response) ? response : []);
|
||||||
|
|
||||||
|
// Map backend data to Application interface
|
||||||
|
const mappedApps: Application[] = rawData.map((app: any) => ({
|
||||||
|
id: app.id,
|
||||||
|
registrationNumber: app.applicationId || 'N/A',
|
||||||
|
name: app.applicantName,
|
||||||
|
email: app.email,
|
||||||
|
phone: app.phone,
|
||||||
|
age: app.age,
|
||||||
|
education: app.education,
|
||||||
|
residentialAddress: app.address || app.city || '',
|
||||||
|
businessAddress: app.address || '',
|
||||||
|
preferredLocation: app.preferredLocation,
|
||||||
|
state: app.state,
|
||||||
|
ownsBike: app.ownRoyalEnfield === 'yes',
|
||||||
|
pastExperience: app.experienceYears ? `${app.experienceYears} years` : (app.description || ''),
|
||||||
|
status: app.overallStatus as ApplicationStatus,
|
||||||
|
questionnaireMarks: app.score || app.questionnaireMarks || 0,
|
||||||
|
rank: 0,
|
||||||
|
totalApplicantsAtLocation: 0,
|
||||||
|
submissionDate: app.createdAt,
|
||||||
|
assignedUsers: [],
|
||||||
|
progress: app.progressPercentage || 0,
|
||||||
|
isShortlisted: app.isShortlisted, // Backend provides this
|
||||||
|
// Add other fields to match interface
|
||||||
|
companyName: app.companyName,
|
||||||
|
source: app.source,
|
||||||
|
existingDealer: app.existingDealer,
|
||||||
|
royalEnfieldModel: app.royalEnfieldModel,
|
||||||
|
description: app.description,
|
||||||
|
pincode: app.pincode,
|
||||||
|
locationType: app.locationType,
|
||||||
|
ownRoyalEnfield: app.ownRoyalEnfield,
|
||||||
|
address: app.address
|
||||||
|
}));
|
||||||
|
|
||||||
|
setApplicationsData(mappedApps);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch applications:', error);
|
||||||
|
toast.error('Failed to load non-opportunity requests');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter non-opportunity leads - These are lead generation submissions
|
||||||
|
// People who expressed interest but received non-opportunity email because
|
||||||
|
// we're currently not offering dealerships in their preferred location
|
||||||
|
// UPDATED LOGIC: 'Submitted' status specifically implies Non-Opportunity (Lead)
|
||||||
|
const filteredLeads = applicationsData.filter((app) => {
|
||||||
|
// Only show applications with 'Submitted' status
|
||||||
|
const isNonOpportunity = app.status === 'Submitted';
|
||||||
|
|
||||||
|
const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
app.registrationNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
app.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
app.phone.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter;
|
||||||
|
const matchesState = stateFilter === 'all' || app.state === stateFilter;
|
||||||
|
|
||||||
|
return isNonOpportunity && matchesSearch && matchesLocation && matchesState;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl mb-2">Non-opportunities (Lead Generation)</h2>
|
||||||
|
<p className="text-slate-600">
|
||||||
|
Interest submissions from regions where dealerships are currently not being offered. These leads received non-opportunity notification and are stored for future reference.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Total Leads</p>
|
||||||
|
<p className="text-2xl text-slate-900 mt-1">{filteredLeads.length}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-blue-100 rounded-lg">
|
||||||
|
<Database className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
||||||
|
<p className="text-slate-600">Unique Locations</p>
|
||||||
|
<p className="text-2xl text-slate-900 mt-1">
|
||||||
|
{new Set(filteredLeads.map(app => app.preferredLocation)).size}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
||||||
|
<p className="text-slate-600">With Experience</p>
|
||||||
|
<p className="text-2xl text-amber-600 mt-1">
|
||||||
|
{filteredLeads.filter(app => app.pastExperience && app.pastExperience !== 'No').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters and Actions */}
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<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 name, email, phone, or registration number..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select value={locationFilter} onValueChange={setLocationFilter}>
|
||||||
|
<SelectTrigger className="w-full lg:w-48">
|
||||||
|
<SelectValue placeholder="All Locations" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Locations</SelectItem>
|
||||||
|
{locations.map((location) => (
|
||||||
|
<SelectItem key={location} value={location}>{location}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select value={stateFilter} onValueChange={setStateFilter}>
|
||||||
|
<SelectTrigger className="w-full lg:w-48">
|
||||||
|
<SelectValue placeholder="All States" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All States</SelectItem>
|
||||||
|
{states.map((state) => (
|
||||||
|
<SelectItem key={state} value={state}>{state}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button variant="outline" size="icon">
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lead Generation Table */}
|
||||||
|
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Phone</TableHead>
|
||||||
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>Preferred Location</TableHead>
|
||||||
|
<TableHead>Main Address</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
<TableHead>Experience</TableHead>
|
||||||
|
<TableHead>Education</TableHead>
|
||||||
|
<TableHead>Applied On</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredLeads.map((lead) => (
|
||||||
|
<TableRow key={lead.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-900">{lead.name}</p>
|
||||||
|
<p className="text-slate-500 text-sm">{lead.registrationNumber}</p>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-slate-900">{lead.phone}</TableCell>
|
||||||
|
<TableCell className="text-slate-600">{lead.email}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-900">{lead.preferredLocation}</p>
|
||||||
|
<p className="text-slate-500 text-sm">{lead.state}</p>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-slate-600 max-w-xs truncate">{lead.residentialAddress}</TableCell>
|
||||||
|
<TableCell className="text-slate-900">{lead.age}</TableCell>
|
||||||
|
<TableCell className="text-slate-600">{lead.pastExperience}</TableCell>
|
||||||
|
<TableCell className="text-slate-900">{lead.education}</TableCell>
|
||||||
|
<TableCell className="text-slate-600">
|
||||||
|
{new Date(lead.submissionDate).toLocaleDateString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onViewDetails(lead.id)}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{filteredLeads.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={10} className="text-center py-12 text-slate-500">
|
||||||
|
<Database className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
||||||
|
<p className="text-lg mb-2">No lead generation data found</p>
|
||||||
|
<p className="text-sm">Try adjusting your filters</p>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, TrendingUp, Send } from 'lucide-react';
|
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send } 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';
|
||||||
@ -8,10 +8,12 @@ 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 } 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 { WorkNotesPage } from './WorkNotesPage';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { resignationService } from '../../services/resignation.service';
|
||||||
|
import { ShieldCheck, Info } from 'lucide-react';
|
||||||
|
|
||||||
interface ResignationDetailsProps {
|
interface ResignationDetailsProps {
|
||||||
resignationId: string;
|
resignationId: string;
|
||||||
@ -25,6 +27,29 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
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: [] });
|
||||||
|
const [showClearanceDialog, setShowClearanceDialog] = useState(false);
|
||||||
|
const [selectedDept, setSelectedDept] = useState<string | null>(null);
|
||||||
|
const [clearanceStatus, setClearanceStatus] = useState<'Cleared' | 'Pending' | 'Rejected'>('Cleared');
|
||||||
|
const [clearanceRemarks, setClearanceRemarks] = useState('');
|
||||||
|
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
|
||||||
|
const [resignationData, setResignationData] = useState<any>(null); // Real data from API
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const fetchResignation = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const data = await resignationService.getResignationById(resignationId);
|
||||||
|
setResignationData(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching resignation:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchResignation();
|
||||||
|
}, [resignationId]);
|
||||||
|
|
||||||
// Check if user can push to F&F (DD Lead and above)
|
// Check if user can push to F&F (DD Lead and above)
|
||||||
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
||||||
@ -111,6 +136,12 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
|
name: 'Departmental Clearances',
|
||||||
|
status: request.currentStage === 'ASM' ? 'pending' : request.currentStage === 'Clearance' ? 'active' : 'completed',
|
||||||
|
description: 'Clearance from all relevant departments'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
name: 'RBM + DD ZM Review',
|
name: 'RBM + DD ZM Review',
|
||||||
status: request.currentStage === 'RBM' || request.currentStage === 'DD ZM' ? 'active' : ['Legal', 'NBH', 'DD Lead', 'ZBH'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'RBM' || request.currentStage === 'DD ZM' ? 'active' : ['Legal', 'NBH', 'DD Lead', 'ZBH'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'Regional Business Manager and DD ZM evaluation'
|
description: 'Regional Business Manager and DD ZM evaluation'
|
||||||
@ -146,7 +177,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
setStageDocumentsDialog({ open: true, stageName, documents });
|
setStageDocumentsDialog({ open: true, stageName, documents });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAction = (type: 'approve' | 'withdrawal' | 'sendback' | 'assign') => {
|
const handleAction = (type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf') => {
|
||||||
setActionDialog({ open: true, type });
|
setActionDialog({ open: true, type });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -160,7 +191,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionMessages = {
|
const actionMessages: Record<string, string> = {
|
||||||
approve: 'Request approved successfully',
|
approve: 'Request approved successfully',
|
||||||
withdrawal: 'Request withdrawn successfully',
|
withdrawal: 'Request withdrawn successfully',
|
||||||
sendback: 'Request sent back for clarification',
|
sendback: 'Request sent back for clarification',
|
||||||
@ -174,6 +205,37 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
setAssignToUser('');
|
setAssignToUser('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearanceUpdate = async () => {
|
||||||
|
if (!selectedDept) return;
|
||||||
|
try {
|
||||||
|
setIsUpdatingClearance(true);
|
||||||
|
await resignationService.updateClearance(resignationId, {
|
||||||
|
department: selectedDept,
|
||||||
|
status: clearanceStatus,
|
||||||
|
remarks: clearanceRemarks
|
||||||
|
});
|
||||||
|
toast.success(`${selectedDept} clearance updated`);
|
||||||
|
setShowClearanceDialog(false);
|
||||||
|
// fetchResignation(); // Refresh data
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update clearance');
|
||||||
|
} finally {
|
||||||
|
setIsUpdatingClearance(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const departments = ['Sales', 'Service', 'Spares', 'Fin-Accounts', 'GSA', 'Legal'];
|
||||||
|
|
||||||
|
// Mock clearance data if not available from API yet
|
||||||
|
const departmentalClearances = resignationData?.departmentalClearances || [
|
||||||
|
{ department: 'Sales', status: 'Cleared', remarks: 'All units settled' },
|
||||||
|
{ department: 'Service', status: 'Pending', remarks: '' },
|
||||||
|
{ department: 'Spares', status: 'Cleared', remarks: 'Inventory returned' },
|
||||||
|
{ department: 'Fin-Accounts', status: 'Pending', remarks: '' },
|
||||||
|
{ department: 'GSA', status: 'Pending', remarks: '' },
|
||||||
|
{ department: 'Legal', status: 'Pending', remarks: '' }
|
||||||
|
];
|
||||||
|
|
||||||
const workNotesCount = mockWorkNotes.length;
|
const workNotesCount = mockWorkNotes.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -308,6 +370,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<TabsList className="bg-slate-100 p-1">
|
<TabsList className="bg-slate-100 p-1">
|
||||||
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
|
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
|
||||||
<TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger>
|
<TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger>
|
||||||
|
<TabsTrigger value="clearances" className="data-[state=active]:bg-white">Clearances</TabsTrigger>
|
||||||
<TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger>
|
<TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger>
|
||||||
<TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
|
<TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@ -465,11 +528,10 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
return (
|
return (
|
||||||
<div key={stage.id} className="flex gap-4">
|
<div key={stage.id} className="flex gap-4">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${stage.status === 'completed' ? 'bg-green-100 text-green-600' :
|
||||||
stage.status === 'completed' ? 'bg-green-100 text-green-600' :
|
|
||||||
stage.status === 'active' ? 'bg-blue-100 text-blue-600' :
|
stage.status === 'active' ? 'bg-blue-100 text-blue-600' :
|
||||||
'bg-slate-100 text-slate-400'
|
'bg-slate-100 text-slate-400'
|
||||||
}`}>
|
}`}>
|
||||||
{stage.status === 'completed' ? (
|
{stage.status === 'completed' ? (
|
||||||
<Check className="w-5 h-5" />
|
<Check className="w-5 h-5" />
|
||||||
) : (
|
) : (
|
||||||
@ -477,11 +539,9 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{index < progressStages.length - 1 && (
|
{index < progressStages.length - 1 && (
|
||||||
<div className={`w-0.5 ${
|
<div className={`w-0.5 ${stage.remarks ? 'h-32' : 'h-16'
|
||||||
stage.remarks ? 'h-32' : 'h-16'
|
} ${stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
|
||||||
} ${
|
}`} />
|
||||||
stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
|
|
||||||
}`} />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 pb-8">
|
<div className="flex-1 pb-8">
|
||||||
@ -489,8 +549,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className={
|
<h3 className={
|
||||||
stage.status === 'completed' ? 'text-green-600' :
|
stage.status === 'completed' ? 'text-green-600' :
|
||||||
stage.status === 'active' ? 'text-blue-600' :
|
stage.status === 'active' ? 'text-blue-600' :
|
||||||
'text-slate-400'
|
'text-slate-400'
|
||||||
}>{stage.name}</h3>
|
}>{stage.name}</h3>
|
||||||
{documentCount > 0 && (
|
{documentCount > 0 && (
|
||||||
<button
|
<button
|
||||||
@ -517,9 +577,9 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge className={
|
<Badge className={
|
||||||
stage.actionType === 'approved' ? 'bg-green-100 text-green-700 border-green-300' :
|
stage.actionType === 'approved' ? 'bg-green-100 text-green-700 border-green-300' :
|
||||||
stage.actionType === 'sendback' ? 'bg-orange-100 text-orange-700 border-orange-300' :
|
stage.actionType === 'sendback' ? 'bg-orange-100 text-orange-700 border-orange-300' :
|
||||||
stage.actionType === 'withdrawal' ? 'bg-red-100 text-red-700 border-red-300' :
|
stage.actionType === 'withdrawal' ? 'bg-red-100 text-red-700 border-red-300' :
|
||||||
'bg-blue-100 text-blue-700 border-blue-300'
|
'bg-blue-100 text-blue-700 border-blue-300'
|
||||||
}>
|
}>
|
||||||
{stage.actionType === 'approved' && '✓ Approved'}
|
{stage.actionType === 'approved' && '✓ Approved'}
|
||||||
{stage.actionType === 'sendback' && '↩ Sent Back'}
|
{stage.actionType === 'sendback' && '↩ Sent Back'}
|
||||||
@ -554,6 +614,56 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Clearances Tab */}
|
||||||
|
<TabsContent value="clearances">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Departmental Clearances</CardTitle>
|
||||||
|
<CardDescription>Status of clearances from various departments</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{departmentalClearances.map((clearance: any) => (
|
||||||
|
<Card key={clearance.department} className="border border-slate-200">
|
||||||
|
<CardHeader className="pb-2 flex flex-row items-center justify-between">
|
||||||
|
<CardTitle className="text-base font-medium">{clearance.department}</CardTitle>
|
||||||
|
<Badge className={
|
||||||
|
clearance.status === 'Cleared' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
|
||||||
|
clearance.status === 'Rejected' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
|
||||||
|
'bg-yellow-100 text-yellow-700 hover:bg-yellow-100'
|
||||||
|
}>
|
||||||
|
{clearance.status}
|
||||||
|
</Badge>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-slate-600 line-clamp-2 min-h-[2.5rem]">
|
||||||
|
{clearance.remarks || 'No remarks provided'}
|
||||||
|
</p>
|
||||||
|
{currentUser && (currentUser.role === 'Super Admin' || currentUser.role === 'DD Admin' || (currentUser.role.includes(clearance.department) && request.currentStage === 'Clearance')) && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2 text-blue-600 hover:text-blue-700 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDept(clearance.department);
|
||||||
|
setClearanceStatus(clearance.status);
|
||||||
|
setClearanceRemarks(clearance.remarks);
|
||||||
|
setShowClearanceDialog(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update Status
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
{/* Documents Tab */}
|
{/* Documents Tab */}
|
||||||
<TabsContent value="documents">
|
<TabsContent value="documents">
|
||||||
<Card>
|
<Card>
|
||||||
@ -638,8 +748,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
{actionDialog.type === 'assign'
|
{actionDialog.type === 'assign'
|
||||||
? 'Select a user to assign this request to'
|
? 'Select a user to assign this request to'
|
||||||
: actionDialog.type === 'pushfnf'
|
: actionDialog.type === 'pushfnf'
|
||||||
? 'This will move the resignation request to F&F for dues clearance'
|
? 'This will move the resignation request to F&F for dues clearance'
|
||||||
: 'Please provide remarks for this action'
|
: 'Please provide remarks for this action'
|
||||||
}
|
}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -692,8 +802,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
onClick={handleSubmitAction}
|
onClick={handleSubmitAction}
|
||||||
className={
|
className={
|
||||||
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
||||||
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
||||||
'bg-blue-600 hover:bg-blue-700'
|
'bg-blue-600 hover:bg-blue-700'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
@ -760,6 +870,60 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Clearance Update Modal */}
|
||||||
|
<Dialog open={showClearanceDialog} onOpenChange={setShowClearanceDialog}>
|
||||||
|
<DialogContent className="bg-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update {selectedDept} Clearance</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Provide clearance status and observations for this resignation.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Clearance Status</Label>
|
||||||
|
<Select value={clearanceStatus} onValueChange={(val: any) => setClearanceStatus(val)}>
|
||||||
|
<SelectTrigger className="mt-2 text-slate-900 border-slate-300">
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-white border-slate-200 shadow-xl overflow-visible z-[9999]">
|
||||||
|
<SelectItem value="Cleared" className="text-green-600 focus:bg-green-50">Cleared</SelectItem>
|
||||||
|
<SelectItem value="Pending" className="text-yellow-600 focus:bg-yellow-50">Pending / In-Review</SelectItem>
|
||||||
|
<SelectItem value="Rejected" className="text-red-600 focus:bg-red-50">Rejected / Dues Owed</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Remarks/Details</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="List any dues, remaining tasks, or observations..."
|
||||||
|
value={clearanceRemarks}
|
||||||
|
onChange={(e) => setClearanceRemarks(e.target.value)}
|
||||||
|
className="mt-2 border-slate-300"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1 border-slate-300"
|
||||||
|
onClick={() => setShowClearanceDialog(false)}
|
||||||
|
disabled={isUpdatingClearance}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="flex-1 bg-slate-900 hover:bg-slate-800 text-white"
|
||||||
|
onClick={handleClearanceUpdate}
|
||||||
|
disabled={isUpdatingClearance}
|
||||||
|
>
|
||||||
|
{isUpdatingClearance ? 'Updating...' : 'Update Clearance'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send } from 'lucide-react';
|
import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck } 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';
|
||||||
@ -9,10 +9,11 @@ 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 { useState } from 'react';
|
import { toast } from 'sonner';
|
||||||
|
import { terminationService } from '../../services/termination.service';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { User, mockWorkNotes, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
|
import { User, mockWorkNotes, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
|
||||||
import { WorkNotesPage } from './WorkNotesPage';
|
import { WorkNotesPage } from './WorkNotesPage';
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
interface TerminationDetailsProps {
|
interface TerminationDetailsProps {
|
||||||
terminationId: string;
|
terminationId: string;
|
||||||
@ -22,10 +23,78 @@ interface TerminationDetailsProps {
|
|||||||
|
|
||||||
export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) {
|
export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) {
|
||||||
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 [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 [terminationData, setTerminationData] = useState<any>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showSCNDialog, setShowSCNDialog] = useState(false);
|
||||||
|
const [scnFile, setScnFile] = useState<File | null>(null);
|
||||||
|
const [scnRemarks, setScnRemarks] = useState('');
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [showFinalizeDialog, setShowFinalizeDialog] = useState(false);
|
||||||
|
const [finalDecision, setFinalDecision] = useState<'Approve' | 'Reject' | 'Reconsider'>('Approve');
|
||||||
|
const [finalRemarks, setFinalRemarks] = useState('');
|
||||||
|
|
||||||
|
const fetchTermination = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const data = await terminationService.getTerminationById(terminationId);
|
||||||
|
setTerminationData(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching termination:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTermination();
|
||||||
|
}, [terminationId]);
|
||||||
|
|
||||||
|
const handleIssueSCN = async () => {
|
||||||
|
try {
|
||||||
|
setIsProcessing(true);
|
||||||
|
await terminationService.issueSCN(terminationId, { remarks: scnRemarks });
|
||||||
|
toast.success('SCN issued successfully');
|
||||||
|
setShowSCNDialog(false);
|
||||||
|
fetchTermination();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to issue SCN');
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadSCNResponse = async () => {
|
||||||
|
if (!scnFile) return;
|
||||||
|
try {
|
||||||
|
setIsProcessing(true);
|
||||||
|
await terminationService.uploadSCNResponse(terminationId, scnFile, scnRemarks);
|
||||||
|
toast.success('SCN response uploaded');
|
||||||
|
setShowSCNDialog(false);
|
||||||
|
fetchTermination();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to upload response');
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinalize = async () => {
|
||||||
|
try {
|
||||||
|
setIsProcessing(true);
|
||||||
|
await terminationService.finalizeTermination(terminationId, finalDecision, finalRemarks);
|
||||||
|
toast.success(`Termination ${finalDecision.toLowerCase()}ed`);
|
||||||
|
setShowFinalizeDialog(false);
|
||||||
|
fetchTermination();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to finalize termination');
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Check if user can push to F&F (DD Lead and above)
|
// Check if user can push to F&F (DD Lead and above)
|
||||||
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
||||||
@ -206,7 +275,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
setStageDocumentsDialog({ open: true, stageName, documents });
|
setStageDocumentsDialog({ open: true, stageName, documents });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAction = (type: 'approve' | 'withdrawal' | 'sendback' | 'assign') => {
|
const handleAction = (type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf') => {
|
||||||
setActionDialog({ open: true, type });
|
setActionDialog({ open: true, type });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -287,32 +356,60 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-slate-600 mr-2">Termination Actions:</span>
|
<span className="text-sm text-slate-600 mr-2">Termination Actions:</span>
|
||||||
<Button
|
{currentUser?.role !== 'Dealer' && (
|
||||||
size="sm"
|
<>
|
||||||
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
<Button
|
||||||
onClick={() => handleAction('approve')}
|
size="sm"
|
||||||
>
|
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
||||||
<Check className="w-4 h-4 mr-2" />
|
onClick={() => handleAction('approve')}
|
||||||
Approve
|
>
|
||||||
</Button>
|
<Check className="w-4 h-4 mr-2" />
|
||||||
<Button
|
Approve
|
||||||
size="sm"
|
</Button>
|
||||||
variant="outline"
|
{request.currentStage === 'NBH' && (currentUser?.role === 'NBH' || currentUser?.role === 'Super Admin') && (
|
||||||
className="text-red-600 border-red-300 hover:bg-red-50 transition-all"
|
<Button
|
||||||
onClick={() => handleAction('withdrawal')}
|
size="sm"
|
||||||
>
|
className="bg-purple-600 hover:bg-purple-700 transition-all shadow-sm"
|
||||||
<X className="w-4 h-4 mr-2" />
|
onClick={() => setShowSCNDialog(true)}
|
||||||
Withdrawal
|
>
|
||||||
</Button>
|
<AlertTriangle className="w-4 h-4 mr-2" />
|
||||||
<Button
|
Issue SCN
|
||||||
size="sm"
|
</Button>
|
||||||
variant="outline"
|
)}
|
||||||
className="hover:bg-slate-50 transition-all"
|
{request.currentStage === 'SCN' && (['Legal', 'DD Admin', 'Super Admin'].includes(currentUser?.role || '')) && (
|
||||||
onClick={() => handleAction('sendback')}
|
<Button
|
||||||
>
|
size="sm"
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
className="bg-amber-600 hover:bg-amber-700 transition-all shadow-sm"
|
||||||
Send Back
|
onClick={() => {
|
||||||
</Button>
|
setScnFile(null);
|
||||||
|
setShowSCNDialog(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
|
Upload SCN Response
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{(['NBH Final', 'CCO', 'CEO'].includes(request.currentStage)) && (currentUser?.role === request.currentStage || currentUser?.role === 'Super Admin') && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700 transition-all shadow-sm"
|
||||||
|
onClick={() => setShowFinalizeDialog(true)}
|
||||||
|
>
|
||||||
|
<ShieldCheck className="w-4 h-4 mr-2" />
|
||||||
|
Final Authorization
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="hover:bg-slate-50 transition-all"
|
||||||
|
onClick={() => handleAction('sendback')}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
Send Back
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Secondary Actions */}
|
{/* Secondary Actions */}
|
||||||
@ -559,11 +656,10 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
return (
|
return (
|
||||||
<div key={stage.id} className="flex gap-4">
|
<div key={stage.id} className="flex gap-4">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${stage.status === 'completed' ? 'bg-green-100 text-green-600' :
|
||||||
stage.status === 'completed' ? 'bg-green-100 text-green-600' :
|
|
||||||
stage.status === 'active' ? 'bg-red-100 text-red-600' :
|
stage.status === 'active' ? 'bg-red-100 text-red-600' :
|
||||||
'bg-slate-100 text-slate-400'
|
'bg-slate-100 text-slate-400'
|
||||||
}`}>
|
}`}>
|
||||||
{stage.status === 'completed' ? (
|
{stage.status === 'completed' ? (
|
||||||
<Check className="w-5 h-5" />
|
<Check className="w-5 h-5" />
|
||||||
) : stage.status === 'active' ? (
|
) : stage.status === 'active' ? (
|
||||||
@ -573,11 +669,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{index < progressStages.length - 1 && (
|
{index < progressStages.length - 1 && (
|
||||||
<div className={`w-0.5 ${
|
<div className={`w-0.5 ${stage.remarks ? 'h-32' : 'h-16'
|
||||||
stage.remarks ? 'h-32' : 'h-16'
|
} ${stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
|
||||||
} ${
|
}`} />
|
||||||
stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
|
|
||||||
}`} />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 pb-8">
|
<div className="flex-1 pb-8">
|
||||||
@ -585,8 +679,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className={
|
<h3 className={
|
||||||
stage.status === 'completed' ? 'text-green-600' :
|
stage.status === 'completed' ? 'text-green-600' :
|
||||||
stage.status === 'active' ? 'text-red-600' :
|
stage.status === 'active' ? 'text-red-600' :
|
||||||
'text-slate-400'
|
'text-slate-400'
|
||||||
}>{stage.name}</h3>
|
}>{stage.name}</h3>
|
||||||
{documentCount > 0 && (
|
{documentCount > 0 && (
|
||||||
<button
|
<button
|
||||||
@ -613,9 +707,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge className={
|
<Badge className={
|
||||||
stage.actionType === 'approved' ? 'bg-green-100 text-green-700 border-green-300' :
|
stage.actionType === 'approved' ? 'bg-green-100 text-green-700 border-green-300' :
|
||||||
stage.actionType === 'sendback' ? 'bg-orange-100 text-orange-700 border-orange-300' :
|
stage.actionType === 'sendback' ? 'bg-orange-100 text-orange-700 border-orange-300' :
|
||||||
stage.actionType === 'withdrawal' ? 'bg-red-100 text-red-700 border-red-300' :
|
stage.actionType === 'withdrawal' ? 'bg-red-100 text-red-700 border-red-300' :
|
||||||
'bg-blue-100 text-blue-700 border-blue-300'
|
'bg-blue-100 text-blue-700 border-blue-300'
|
||||||
}>
|
}>
|
||||||
{stage.actionType === 'approved' && '✓ Approved'}
|
{stage.actionType === 'approved' && '✓ Approved'}
|
||||||
{stage.actionType === 'sendback' && '↩ Sent Back'}
|
{stage.actionType === 'sendback' && '↩ Sent Back'}
|
||||||
@ -734,8 +828,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
{actionDialog.type === 'assign'
|
{actionDialog.type === 'assign'
|
||||||
? 'Select a user to assign this request to'
|
? 'Select a user to assign this request to'
|
||||||
: actionDialog.type === 'pushfnf'
|
: actionDialog.type === 'pushfnf'
|
||||||
? 'This will move the termination case to F&F for dues clearance'
|
? 'This will move the termination case to F&F for dues clearance'
|
||||||
: 'Please provide remarks for this action'
|
: 'Please provide remarks for this action'
|
||||||
}
|
}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -790,8 +884,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
onClick={handleSubmitAction}
|
onClick={handleSubmitAction}
|
||||||
className={
|
className={
|
||||||
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
||||||
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
||||||
'bg-blue-600 hover:bg-blue-700'
|
'bg-blue-600 hover:bg-blue-700'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
@ -858,6 +952,113 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* SCN Dialog */}
|
||||||
|
<Dialog open={showSCNDialog} onOpenChange={setShowSCNDialog}>
|
||||||
|
<DialogContent className="bg-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{request.currentStage === 'SCN' ? 'Upload SCN Response' : 'Issue Show Cause Notice (SCN)'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{request.currentStage === 'SCN'
|
||||||
|
? 'Upload the response received from the dealer regarding the SCN.'
|
||||||
|
: 'Confirm the issuance of a formal Show Cause Notice to the dealer.'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 pt-4">
|
||||||
|
{request.currentStage === 'SCN' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>SCN Response File</Label>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
id="scn-file-upload"
|
||||||
|
onChange={(e) => setScnFile(e.target.files?.[0] || null)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-dashed"
|
||||||
|
onClick={() => document.getElementById('scn-file-upload')?.click()}
|
||||||
|
>
|
||||||
|
{scnFile ? scnFile.name : 'Select PDF or Image'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Remarks/Details</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Add any internal remarks or justification..."
|
||||||
|
value={scnRemarks}
|
||||||
|
onChange={(e) => setScnRemarks(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button variant="outline" onClick={() => setShowSCNDialog(false)} disabled={isProcessing}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={request.currentStage === 'SCN' ? 'bg-amber-600 hover:bg-amber-700' : 'bg-purple-600 hover:bg-purple-700'}
|
||||||
|
onClick={request.currentStage === 'SCN' ? handleUploadSCNResponse : handleIssueSCN}
|
||||||
|
disabled={isProcessing || (request.currentStage === 'SCN' && !scnFile)}
|
||||||
|
>
|
||||||
|
{isProcessing ? 'Processing...' : request.currentStage === 'SCN' ? 'Upload Response' : 'Issue SCN'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Finalize Termination Dialog */}
|
||||||
|
<Dialog open={showFinalizeDialog} onOpenChange={setShowFinalizeDialog}>
|
||||||
|
<DialogContent className="bg-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Final Termination Authorization</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Provide your final decision on this termination case.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 pt-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Final Decision</Label>
|
||||||
|
<Select value={finalDecision} onValueChange={(val: any) => setFinalDecision(val)}>
|
||||||
|
<SelectTrigger className="mt-2 text-slate-900 border-slate-300">
|
||||||
|
<SelectValue placeholder="Select decision" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-white border-slate-200 shadow-xl overflow-visible z-[9999]">
|
||||||
|
<SelectItem value="Approve" className="text-red-600 focus:bg-red-50">Confirm Termination</SelectItem>
|
||||||
|
<SelectItem value="Reject" className="text-slate-600 focus:bg-slate-50">Reject Termination</SelectItem>
|
||||||
|
<SelectItem value="Reconsider" className="text-amber-600 focus:bg-amber-50">Reconsider / Give More Time</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Authorization Remarks</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Provide your rationale for this decision..."
|
||||||
|
value={finalRemarks}
|
||||||
|
onChange={(e) => setFinalRemarks(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button variant="outline" onClick={() => setShowFinalizeDialog(false)} disabled={isProcessing}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700"
|
||||||
|
onClick={handleFinalize}
|
||||||
|
disabled={isProcessing || !finalRemarks}
|
||||||
|
>
|
||||||
|
{isProcessing ? 'Authorizing...' : 'Submit Decision'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,260 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { mockApplications, locations, states, Application, ApplicationStatus } from '../../lib/mock-data';
|
|
||||||
import { onboardingService } from '../../services/onboarding.service';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Input } from '../ui/input';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '../ui/select';
|
|
||||||
import {
|
|
||||||
Search,
|
|
||||||
Download,
|
|
||||||
Database,
|
|
||||||
Loader2
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Badge } from '../ui/badge';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '../ui/table';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
interface UnopportunityRequestsPageProps {
|
|
||||||
onViewDetails: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UnopportunityRequestsPage({ onViewDetails }: UnopportunityRequestsPageProps) {
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [locationFilter, setLocationFilter] = useState<string>('all');
|
|
||||||
const [stateFilter, setStateFilter] = useState<string>('all');
|
|
||||||
|
|
||||||
// Real data integration
|
|
||||||
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchApplications();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchApplications = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const response = await onboardingService.getApplications();
|
|
||||||
const rawData = response.data || (Array.isArray(response) ? response : []);
|
|
||||||
|
|
||||||
// Map backend data to Application interface
|
|
||||||
const mappedApps: Application[] = rawData.map((app: any) => ({
|
|
||||||
id: app.id,
|
|
||||||
registrationNumber: app.applicationId || 'N/A',
|
|
||||||
name: app.applicantName,
|
|
||||||
email: app.email,
|
|
||||||
phone: app.phone,
|
|
||||||
age: app.age,
|
|
||||||
education: app.education,
|
|
||||||
residentialAddress: app.address || app.city || '',
|
|
||||||
businessAddress: app.address || '',
|
|
||||||
preferredLocation: app.preferredLocation,
|
|
||||||
state: app.state,
|
|
||||||
ownsBike: app.ownRoyalEnfield === 'yes',
|
|
||||||
pastExperience: app.experienceYears ? `${app.experienceYears} years` : (app.description || ''),
|
|
||||||
status: app.overallStatus as ApplicationStatus,
|
|
||||||
questionnaireMarks: app.score || app.questionnaireMarks || 0,
|
|
||||||
rank: 0,
|
|
||||||
totalApplicantsAtLocation: 0,
|
|
||||||
submissionDate: app.createdAt,
|
|
||||||
assignedUsers: [],
|
|
||||||
progress: app.progressPercentage || 0,
|
|
||||||
isShortlisted: app.isShortlisted, // Backend provides this
|
|
||||||
// Add other fields to match interface
|
|
||||||
companyName: app.companyName,
|
|
||||||
source: app.source,
|
|
||||||
existingDealer: app.existingDealer,
|
|
||||||
royalEnfieldModel: app.royalEnfieldModel,
|
|
||||||
description: app.description,
|
|
||||||
pincode: app.pincode,
|
|
||||||
locationType: app.locationType,
|
|
||||||
ownRoyalEnfield: app.ownRoyalEnfield,
|
|
||||||
address: app.address
|
|
||||||
}));
|
|
||||||
|
|
||||||
setApplicationsData(mappedApps);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch applications:', error);
|
|
||||||
toast.error('Failed to load unopportunity requests');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter unopportunity leads - These are lead generation submissions
|
|
||||||
// People who expressed interest but received unopportunity email because
|
|
||||||
// we're currently not offering dealerships in their preferred location
|
|
||||||
// UPDATED LOGIC: 'Submitted' status specifically implies Non-Opportunity (Lead)
|
|
||||||
const filteredLeads = applicationsData.filter((app) => {
|
|
||||||
// Only show applications with 'Submitted' status
|
|
||||||
const isUnopportunity = app.status === 'Submitted';
|
|
||||||
|
|
||||||
const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
app.registrationNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
app.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
app.phone.toLowerCase().includes(searchQuery.toLowerCase());
|
|
||||||
const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter;
|
|
||||||
const matchesState = stateFilter === 'all' || app.state === stateFilter;
|
|
||||||
|
|
||||||
return isUnopportunity && matchesSearch && matchesLocation && matchesState;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl mb-2">Unopportunity Requests (Lead Generation)</h2>
|
|
||||||
<p className="text-slate-600">
|
|
||||||
Interest submissions from regions where dealerships are currently not being offered. These leads received unopportunity notification and are stored for future reference.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-slate-600">Total Leads</p>
|
|
||||||
<p className="text-2xl text-slate-900 mt-1">{filteredLeads.length}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-blue-100 rounded-lg">
|
|
||||||
<Database className="w-6 h-6 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
|
||||||
<p className="text-slate-600">Unique Locations</p>
|
|
||||||
<p className="text-2xl text-slate-900 mt-1">
|
|
||||||
{new Set(filteredLeads.map(app => app.preferredLocation)).size}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
|
||||||
<p className="text-slate-600">With Experience</p>
|
|
||||||
<p className="text-2xl text-amber-600 mt-1">
|
|
||||||
{filteredLeads.filter(app => app.pastExperience && app.pastExperience !== 'No').length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters and Actions */}
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<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 name, email, phone, or registration number..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Select value={locationFilter} onValueChange={setLocationFilter}>
|
|
||||||
<SelectTrigger className="w-full lg:w-48">
|
|
||||||
<SelectValue placeholder="All Locations" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All Locations</SelectItem>
|
|
||||||
{locations.map((location) => (
|
|
||||||
<SelectItem key={location} value={location}>{location}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select value={stateFilter} onValueChange={setStateFilter}>
|
|
||||||
<SelectTrigger className="w-full lg:w-48">
|
|
||||||
<SelectValue placeholder="All States" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All States</SelectItem>
|
|
||||||
{states.map((state) => (
|
|
||||||
<SelectItem key={state} value={state}>{state}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button variant="outline" size="icon">
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lead Generation Table */}
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>Phone</TableHead>
|
|
||||||
<TableHead>Email</TableHead>
|
|
||||||
<TableHead>Preferred Location</TableHead>
|
|
||||||
<TableHead>Main Address</TableHead>
|
|
||||||
<TableHead>Age</TableHead>
|
|
||||||
<TableHead>Experience</TableHead>
|
|
||||||
<TableHead>Education</TableHead>
|
|
||||||
<TableHead>Applied On</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredLeads.map((lead) => (
|
|
||||||
<TableRow key={lead.id}>
|
|
||||||
<TableCell>
|
|
||||||
<div>
|
|
||||||
<p className="text-slate-900">{lead.name}</p>
|
|
||||||
<p className="text-slate-500 text-sm">{lead.registrationNumber}</p>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-slate-900">{lead.phone}</TableCell>
|
|
||||||
<TableCell className="text-slate-600">{lead.email}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div>
|
|
||||||
<p className="text-slate-900">{lead.preferredLocation}</p>
|
|
||||||
<p className="text-slate-500 text-sm">{lead.state}</p>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-slate-600 max-w-xs truncate">{lead.residentialAddress}</TableCell>
|
|
||||||
<TableCell className="text-slate-900">{lead.age}</TableCell>
|
|
||||||
<TableCell className="text-slate-600">{lead.pastExperience}</TableCell>
|
|
||||||
<TableCell className="text-slate-900">{lead.education}</TableCell>
|
|
||||||
<TableCell className="text-slate-600">
|
|
||||||
{new Date(lead.submissionDate).toLocaleDateString()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onViewDetails(lead.id)}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
{filteredLeads.length === 0 && (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={10} className="text-center py-12 text-slate-500">
|
|
||||||
<Database className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
|
||||||
<p className="text-lg mb-2">No lead generation data found</p>
|
|
||||||
<p className="text-sm">Try adjusting your filters</p>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -78,8 +78,8 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
|
|
||||||
// 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 applicationId = props.applicationId || id || '';
|
||||||
const applicationName = props.applicationName || location.state?.applicationName || 'Application';
|
const [appName, setAppName] = useState(props.applicationName || location.state?.applicationName || 'Application');
|
||||||
const registrationNumber = 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));
|
||||||
const externalParticipantsInit = props.participants || location.state?.participants || [];
|
const externalParticipantsInit = props.participants || location.state?.participants || [];
|
||||||
const [externalParticipants, setExternalParticipants] = useState<any[]>(externalParticipantsInit);
|
const [externalParticipants, setExternalParticipants] = useState<any[]>(externalParticipantsInit);
|
||||||
@ -96,7 +96,6 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [previewFile, setPreviewFile] = useState<Attachment | null>(null);
|
const [previewFile, setPreviewFile] = useState<Attachment | null>(null);
|
||||||
|
|
||||||
const getFileIcon = (mimeType: string, filePath?: string) => {
|
const getFileIcon = (mimeType: string, filePath?: string) => {
|
||||||
@ -229,24 +228,41 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
}
|
}
|
||||||
}, [applicationId, socket]);
|
}, [applicationId, socket]);
|
||||||
|
|
||||||
// Fetch application details if participants are missing (e.g. on refresh)
|
// Fetch application details if metadata or participants are missing (e.g. on refresh)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (externalParticipants.length === 0 && applicationId) {
|
if (applicationId) {
|
||||||
const fetchApplicationDetails = async () => {
|
const fetchApplicationDetails = async () => {
|
||||||
try {
|
try {
|
||||||
const appData = await onboardingService.getApplicationById(applicationId);
|
const appData = await onboardingService.getApplicationById(applicationId);
|
||||||
if (appData && appData.participants) {
|
if (appData) {
|
||||||
setExternalParticipants(appData.participants);
|
// Update participants if not provided
|
||||||
|
if (externalParticipants.length === 0 && appData.participants) {
|
||||||
|
setExternalParticipants(appData.participants);
|
||||||
|
}
|
||||||
|
// Update metadata if not provided via props/state
|
||||||
|
if (!props.applicationName && !location.state?.applicationName && appData.companyName) {
|
||||||
|
setAppName(appData.companyName);
|
||||||
|
}
|
||||||
|
if (!props.registrationNumber && !location.state?.registrationNumber && appData.registrationNumber) {
|
||||||
|
setRegNumber(appData.registrationNumber);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch application details for participants:', error);
|
console.error('Failed to fetch application details:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchApplicationDetails();
|
fetchApplicationDetails();
|
||||||
}
|
}
|
||||||
}, [applicationId, externalParticipants.length]);
|
}, [applicationId, externalParticipants.length, props.applicationName, props.registrationNumber, location.state]);
|
||||||
|
|
||||||
// Scroll logic removed - handled by flex-col-reverse anchoring
|
// Auto-scroll logic
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messagesEndRef.current) {
|
||||||
|
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}, [notes]);
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
@ -440,9 +456,9 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-slate-50">
|
<div className="h-full flex flex-col bg-slate-50 overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header - Stays fixed because it's a sibling of ScrollArea */}
|
||||||
<div className="bg-white border-b border-slate-200 px-6 py-4">
|
<div className="bg-white border-b border-slate-200 px-6 py-4 z-10">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
@ -461,9 +477,9 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-slate-900">Work Notes</h1>
|
<h1 className="text-slate-900">Work Notes</h1>
|
||||||
<div className="flex items-center gap-2 text-slate-600">
|
<div className="flex items-center gap-2 text-slate-600">
|
||||||
<span>{applicationName}</span>
|
<span>{appName}</span>
|
||||||
<span className="text-slate-400">|</span>
|
<span className="text-slate-400">|</span>
|
||||||
<span className="text-slate-500">{registrationNumber}</span>
|
<span className="text-slate-500">{regNumber}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -489,10 +505,9 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages Area */}
|
<ScrollArea className="flex-1 px-6 py-4 min-h-0">
|
||||||
<ScrollArea className="flex-1 px-6 py-4">
|
<div className="max-w-4xl mx-auto space-y-6 flex flex-col">
|
||||||
<div className="max-w-4xl mx-auto space-y-6 flex flex-col-reverse" ref={scrollRef}>
|
{[...notes].reverse().map((note) => {
|
||||||
{notes.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-');
|
||||||
@ -586,11 +601,12 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
<span className="text-slate-500">Loading notes...</span>
|
<span className="text-slate-500">Loading notes...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Input Area */}
|
{/* Input Area - Stays fixed because it's a sibling of ScrollArea */}
|
||||||
<div className="bg-white border-t border-slate-200 px-6 py-4">
|
<div className="bg-white border-t border-slate-200 px-6 py-4 z-10">
|
||||||
<div className="max-w-4xl mx-auto space-y-4">
|
<div className="max-w-4xl mx-auto space-y-4">
|
||||||
|
|
||||||
{/* Attachment Previews */}
|
{/* Attachment Previews */}
|
||||||
|
|||||||
@ -81,7 +81,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
submenuKey: 'allRequests',
|
submenuKey: 'allRequests',
|
||||||
submenu: [
|
submenu: [
|
||||||
{ id: 'opportunity-requests', label: 'Opportunity Requests' },
|
{ id: 'opportunity-requests', label: 'Opportunity Requests' },
|
||||||
{ id: 'unopportunity-requests', label: 'Unopportunity Requests' }
|
{ id: 'non-opportunities', label: 'Non-opportunities' }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,6 +106,8 @@ export interface Application {
|
|||||||
inaugurationDate?: string;
|
inaugurationDate?: string;
|
||||||
questionnaireResponses?: any[]; // added for response view
|
questionnaireResponses?: any[]; // added for response view
|
||||||
participants?: Participant[];
|
participants?: Participant[];
|
||||||
|
architectureAssignedTo?: string;
|
||||||
|
architectureStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Participant {
|
export interface Participant {
|
||||||
@ -425,7 +427,7 @@ export const mockApplications: Application[] = [
|
|||||||
progress: 20,
|
progress: 20,
|
||||||
isShortlisted: true // Shortlisted by DD, now appears in DD Lead's Opportunity Requests
|
isShortlisted: true // Shortlisted by DD, now appears in DD Lead's Opportunity Requests
|
||||||
},
|
},
|
||||||
// Unopportunity Requests (Lead Generation) - Applications from locations where we're NOT offering dealerships
|
// Non-opportunity Requests (Lead Generation) - Applications from locations where we're NOT offering dealerships
|
||||||
{
|
{
|
||||||
id: '9',
|
id: '9',
|
||||||
registrationNumber: 'APP-009',
|
registrationNumber: 'APP-009',
|
||||||
@ -444,7 +446,7 @@ export const mockApplications: Application[] = [
|
|||||||
submissionDate: '2025-10-08',
|
submissionDate: '2025-10-08',
|
||||||
assignedUsers: [],
|
assignedUsers: [],
|
||||||
progress: 10,
|
progress: 10,
|
||||||
isShortlisted: false // Unopportunity lead - not offering dealership in Chandigarh
|
isShortlisted: false // Non-opportunity lead - not offering dealership in Chandigarh
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '10',
|
id: '10',
|
||||||
@ -464,7 +466,7 @@ export const mockApplications: Application[] = [
|
|||||||
submissionDate: '2025-10-07',
|
submissionDate: '2025-10-07',
|
||||||
assignedUsers: [],
|
assignedUsers: [],
|
||||||
progress: 10,
|
progress: 10,
|
||||||
isShortlisted: false // Unopportunity lead - not offering dealership in Kochi
|
isShortlisted: false // Non-opportunity lead - not offering dealership in Kochi
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '11',
|
id: '11',
|
||||||
@ -484,7 +486,7 @@ export const mockApplications: Application[] = [
|
|||||||
submissionDate: '2025-10-06',
|
submissionDate: '2025-10-06',
|
||||||
assignedUsers: [],
|
assignedUsers: [],
|
||||||
progress: 10,
|
progress: 10,
|
||||||
isShortlisted: false // Unopportunity lead - not offering dealership in Kolkata
|
isShortlisted: false // Non-opportunity lead - not offering dealership in Kolkata
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '12',
|
id: '12',
|
||||||
@ -504,7 +506,7 @@ export const mockApplications: Application[] = [
|
|||||||
submissionDate: '2025-10-05',
|
submissionDate: '2025-10-05',
|
||||||
assignedUsers: [],
|
assignedUsers: [],
|
||||||
progress: 10,
|
progress: 10,
|
||||||
isShortlisted: false // Unopportunity lead - not offering dealership in Hyderabad
|
isShortlisted: false // Non-opportunity lead - not offering dealership in Hyderabad
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '13',
|
id: '13',
|
||||||
@ -524,7 +526,7 @@ export const mockApplications: Application[] = [
|
|||||||
submissionDate: '2025-10-04',
|
submissionDate: '2025-10-04',
|
||||||
assignedUsers: [],
|
assignedUsers: [],
|
||||||
progress: 10,
|
progress: 10,
|
||||||
isShortlisted: false // Unopportunity lead - not offering dealership in Lucknow
|
isShortlisted: false // Non-opportunity lead - not offering dealership in Lucknow
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '14',
|
id: '14',
|
||||||
@ -544,7 +546,7 @@ export const mockApplications: Application[] = [
|
|||||||
submissionDate: '2025-10-03',
|
submissionDate: '2025-10-03',
|
||||||
assignedUsers: [],
|
assignedUsers: [],
|
||||||
progress: 10,
|
progress: 10,
|
||||||
isShortlisted: false // Unopportunity lead - not offering dealership in Ludhiana
|
isShortlisted: false // Non-opportunity lead - not offering dealership in Ludhiana
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '15',
|
id: '15',
|
||||||
@ -564,7 +566,7 @@ export const mockApplications: Application[] = [
|
|||||||
submissionDate: '2025-10-02',
|
submissionDate: '2025-10-02',
|
||||||
assignedUsers: [],
|
assignedUsers: [],
|
||||||
progress: 10,
|
progress: 10,
|
||||||
isShortlisted: false // Unopportunity lead - not offering dealership in Nagpur
|
isShortlisted: false // Non-opportunity lead - not offering dealership in Nagpur
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '16',
|
id: '16',
|
||||||
@ -584,7 +586,7 @@ export const mockApplications: Application[] = [
|
|||||||
submissionDate: '2025-10-01',
|
submissionDate: '2025-10-01',
|
||||||
assignedUsers: [],
|
assignedUsers: [],
|
||||||
progress: 10,
|
progress: 10,
|
||||||
isShortlisted: false // Unopportunity lead - not offering dealership in Pune
|
isShortlisted: false // Non-opportunity lead - not offering dealership in Pune
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -143,5 +143,23 @@ export const onboardingService = {
|
|||||||
console.error('Update interview decision error:', error);
|
console.error('Update interview decision error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
assignArchitectureTeam: async (applicationId: string, assignedTo: string) => {
|
||||||
|
try {
|
||||||
|
const response: any = await API.assignArchitectureTeam(applicationId, assignedTo);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Assign architecture team error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateArchitectureStatus: async (applicationId: string, status: string, remarks?: string) => {
|
||||||
|
try {
|
||||||
|
const response: any = await API.updateArchitectureStatus(applicationId, status, remarks);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update architecture status error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
22
src/services/resignation.service.ts
Normal file
22
src/services/resignation.service.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { API } from '../api/API';
|
||||||
|
|
||||||
|
export const resignationService = {
|
||||||
|
getResignationById: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response: any = await API.getResignationById(id);
|
||||||
|
return response.data?.data || response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get resignation error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateClearance: async (id: string, data: any) => {
|
||||||
|
try {
|
||||||
|
const response: any = await API.updateClearance(id, data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update clearance error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
31
src/services/termination.service.ts
Normal file
31
src/services/termination.service.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { API } from '../api/API';
|
||||||
|
|
||||||
|
export const terminationService = {
|
||||||
|
getTerminationById: async (id: string) => {
|
||||||
|
const response = await API.getTerminationById(id);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTerminationStatus: async (id: string, status: string, remarks: string) => {
|
||||||
|
const response = await API.updateTerminationStatus(id, { status, remarks });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
issueSCN: async (id: string, scnData: any) => {
|
||||||
|
const response = await API.issueSCN(id, scnData);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadSCNResponse: async (id: string, file: File, remarks?: string) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
if (remarks) formData.append('remarks', remarks);
|
||||||
|
const response = await API.uploadSCNResponse(id, formData);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
finalizeTermination: async (id: string, decision: 'Approve' | 'Reject' | 'Reconsider', remarks: string) => {
|
||||||
|
const response = await API.finalizeTermination(id, { decision, remarks });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user