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 { AllApplicationsPage } from './components/applications/AllApplicationsPage';
|
||||
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 { ResignationPage } from './components/applications/ResignationPage';
|
||||
import { TerminationPage } from './components/applications/TerminationPage';
|
||||
@ -127,7 +127,7 @@ export default function App() {
|
||||
'/applications': 'Dealership Requests',
|
||||
'/all-applications': 'All Applications',
|
||||
'/opportunity-requests': 'Opportunity Requests',
|
||||
'/unopportunity-requests': 'Unopportunity Requests',
|
||||
'/non-opportunities': 'Non-opportunities',
|
||||
'/tasks': 'My Tasks',
|
||||
'/reports': 'Reports & Analytics',
|
||||
'/settings': 'Settings',
|
||||
@ -222,7 +222,7 @@ export default function App() {
|
||||
|
||||
{/* Admin/Lead Routes */}
|
||||
<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 */}
|
||||
<Route path="/users" element={<UserManagementPage />} />
|
||||
|
||||
@ -34,6 +34,8 @@ export const API = {
|
||||
submitQuestionnaireResponse: (data: any) => client.post('/questionnaire/response', data),
|
||||
getAllQuestionnaires: () => client.get('/onboarding/questionnaires'),
|
||||
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
|
||||
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {
|
||||
@ -86,6 +88,18 @@ export const API = {
|
||||
// Prospective Login
|
||||
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
|
||||
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;
|
||||
|
||||
@ -382,6 +382,13 @@ export function ApplicationDetails() {
|
||||
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
|
||||
const [interviews, setInterviews] = useState<any[]>([]);
|
||||
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
|
||||
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
|
||||
@ -1123,6 +1130,38 @@ export function ApplicationDetails() {
|
||||
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 = () => {
|
||||
if (!workNote.trim()) {
|
||||
alert('Please enter a note');
|
||||
@ -1367,7 +1406,7 @@ export function ApplicationDetails() {
|
||||
|
||||
{/* Tabs Section */}
|
||||
{/* 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 && (
|
||||
<Card>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
@ -1939,7 +1978,7 @@ export function ApplicationDetails() {
|
||||
|
||||
{/* Actions Card */}
|
||||
{/* 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 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@ -2002,6 +2041,28 @@ export function ApplicationDetails() {
|
||||
</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 */}
|
||||
{activeInterviewForUser && !hasSubmittedFeedback && (
|
||||
<DropdownMenu>
|
||||
@ -2099,7 +2160,7 @@ export function ApplicationDetails() {
|
||||
|
||||
{/* Work Notes Chat */}
|
||||
{/* 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 && (
|
||||
<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 */}
|
||||
< Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal} >
|
||||
<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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
@ -8,10 +8,12 @@ import { Label } from '../ui/label';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { User, mockWorkNotes, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
|
||||
import { WorkNotesPage } from './WorkNotesPage';
|
||||
import { toast } from 'sonner';
|
||||
import { resignationService } from '../../services/resignation.service';
|
||||
import { ShieldCheck, Info } from 'lucide-react';
|
||||
|
||||
interface ResignationDetailsProps {
|
||||
resignationId: string;
|
||||
@ -25,7 +27,30 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
const [remarks, setRemarks] = useState('');
|
||||
const [assignToUser, setAssignToUser] = useState('');
|
||||
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)
|
||||
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
||||
|
||||
@ -87,66 +112,72 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
};
|
||||
|
||||
const progressStages = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Request Submitted',
|
||||
status: 'completed',
|
||||
date: '2025-10-08',
|
||||
{
|
||||
id: 1,
|
||||
name: 'Request Submitted',
|
||||
status: 'completed',
|
||||
date: '2025-10-08',
|
||||
description: 'Resignation request created by DD Lead',
|
||||
actionType: 'approved',
|
||||
actionBy: 'DD Lead',
|
||||
remarks: 'Initial resignation request submitted with all required documentation.',
|
||||
feedback: 'Request is complete and ready for ASM review.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'ASM Review',
|
||||
status: request.currentStage === 'ASM' ? 'active' : request.currentStage === 'ASM' ? 'pending' : 'completed',
|
||||
date: request.currentStage === 'ASM' ? '2025-10-09' : undefined,
|
||||
{
|
||||
id: 2,
|
||||
name: 'ASM Review',
|
||||
status: request.currentStage === 'ASM' ? 'active' : request.currentStage === 'ASM' ? 'pending' : 'completed',
|
||||
date: request.currentStage === 'ASM' ? '2025-10-09' : undefined,
|
||||
description: 'Area Sales Manager review',
|
||||
actionType: request.currentStage === 'ASM' ? undefined : 'approved',
|
||||
actionBy: request.currentStage === 'ASM' ? undefined : 'ASM - Mumbai',
|
||||
remarks: request.currentStage === 'ASM' ? undefined : 'Reviewed dealer performance and resignation request. All documentation verified.',
|
||||
feedback: request.currentStage === 'ASM' ? undefined : 'Dealer has maintained good performance. Recommended for approval at next level.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'RBM + DD ZM Review',
|
||||
status: request.currentStage === 'RBM' || request.currentStage === 'DD ZM' ? 'active' : ['Legal', 'NBH', 'DD Lead', 'ZBH'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
{
|
||||
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',
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'ZBH Review',
|
||||
status: request.currentStage === 'ZBH' ? 'active' : ['Legal', 'NBH', 'DD Lead'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
{
|
||||
id: 4,
|
||||
name: 'ZBH Review',
|
||||
status: request.currentStage === 'ZBH' ? 'active' : ['Legal', 'NBH', 'DD Lead'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Zonal Business Head approval'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'DD Lead Review',
|
||||
status: request.currentStage === 'DD Lead' ? 'active' : ['Legal', 'NBH'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
{
|
||||
id: 5,
|
||||
name: 'DD Lead Review',
|
||||
status: request.currentStage === 'DD Lead' ? 'active' : ['Legal', 'NBH'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'DD Lead final review'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'NBH Approval',
|
||||
status: request.currentStage === 'NBH' ? 'active' : request.currentStage === 'Legal' ? 'completed' : 'pending',
|
||||
{
|
||||
id: 6,
|
||||
name: 'NBH Approval',
|
||||
status: request.currentStage === 'NBH' ? 'active' : request.currentStage === 'Legal' ? 'completed' : 'pending',
|
||||
description: 'National Business Head approval'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Legal - Resignation Letter',
|
||||
status: request.currentStage === 'Legal' ? 'active' : 'pending',
|
||||
{
|
||||
id: 7,
|
||||
name: 'Legal - Resignation Letter',
|
||||
status: request.currentStage === 'Legal' ? 'active' : 'pending',
|
||||
description: 'Legal team issues resignation approval letter'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const handleViewStageDocuments = (stageName: string) => {
|
||||
const documents = stageDocuments[stageName] || [];
|
||||
setStageDocumentsDialog({ open: true, stageName, documents });
|
||||
};
|
||||
|
||||
const handleAction = (type: 'approve' | 'withdrawal' | 'sendback' | 'assign') => {
|
||||
const handleAction = (type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf') => {
|
||||
setActionDialog({ open: true, type });
|
||||
};
|
||||
|
||||
@ -160,7 +191,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
return;
|
||||
}
|
||||
|
||||
const actionMessages = {
|
||||
const actionMessages: Record<string, string> = {
|
||||
approve: 'Request approved successfully',
|
||||
withdrawal: 'Request withdrawn successfully',
|
||||
sendback: 'Request sent back for clarification',
|
||||
@ -174,6 +205,37 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
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;
|
||||
|
||||
return (
|
||||
@ -204,17 +266,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<span className="text-sm text-slate-600 mr-2">Workflow Actions:</span>
|
||||
{currentUser?.role !== 'Dealer' && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
||||
onClick={() => handleAction('approve')}
|
||||
>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="hover:bg-slate-50 transition-all"
|
||||
onClick={() => handleAction('sendback')}
|
||||
>
|
||||
@ -223,9 +285,9 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-red-600 border-red-300 hover:bg-red-50 transition-all"
|
||||
onClick={() => handleAction('withdrawal')}
|
||||
>
|
||||
@ -238,9 +300,9 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
{currentUser?.role !== 'Dealer' && (
|
||||
<div className="flex items-center gap-2">
|
||||
{canPushToFnF && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-blue-600 border-blue-300 hover:bg-blue-50 transition-all"
|
||||
onClick={() => handleAction('pushfnf')}
|
||||
>
|
||||
@ -248,8 +310,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
Push to F&F
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="hover:bg-slate-50 transition-all"
|
||||
onClick={() => handleAction('assign')}
|
||||
@ -269,7 +331,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
</div>
|
||||
<Dialog open={workNotesOpen} onOpenChange={setWorkNotesOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all"
|
||||
@ -308,6 +370,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<TabsList className="bg-slate-100 p-1">
|
||||
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</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="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
|
||||
</TabsList>
|
||||
@ -465,11 +528,10 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
return (
|
||||
<div key={stage.id} className="flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
stage.status === 'completed' ? 'bg-green-100 text-green-600' :
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${stage.status === 'completed' ? 'bg-green-100 text-green-600' :
|
||||
stage.status === 'active' ? 'bg-blue-100 text-blue-600' :
|
||||
'bg-slate-100 text-slate-400'
|
||||
}`}>
|
||||
'bg-slate-100 text-slate-400'
|
||||
}`}>
|
||||
{stage.status === 'completed' ? (
|
||||
<Check className="w-5 h-5" />
|
||||
) : (
|
||||
@ -477,11 +539,9 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
)}
|
||||
</div>
|
||||
{index < progressStages.length - 1 && (
|
||||
<div className={`w-0.5 ${
|
||||
stage.remarks ? 'h-32' : 'h-16'
|
||||
} ${
|
||||
stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
|
||||
}`} />
|
||||
<div className={`w-0.5 ${stage.remarks ? 'h-32' : 'h-16'
|
||||
} ${stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 pb-8">
|
||||
@ -489,8 +549,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className={
|
||||
stage.status === 'completed' ? 'text-green-600' :
|
||||
stage.status === 'active' ? 'text-blue-600' :
|
||||
'text-slate-400'
|
||||
stage.status === 'active' ? 'text-blue-600' :
|
||||
'text-slate-400'
|
||||
}>{stage.name}</h3>
|
||||
{documentCount > 0 && (
|
||||
<button
|
||||
@ -510,16 +570,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
)}
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm">{stage.description}</p>
|
||||
|
||||
|
||||
{/* Action Badge and Remarks */}
|
||||
{stage.actionType && stage.remarks && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={
|
||||
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 === 'withdrawal' ? 'bg-red-100 text-red-700 border-red-300' :
|
||||
'bg-blue-100 text-blue-700 border-blue-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' :
|
||||
'bg-blue-100 text-blue-700 border-blue-300'
|
||||
}>
|
||||
{stage.actionType === 'approved' && '✓ Approved'}
|
||||
{stage.actionType === 'sendback' && '↩ Sent Back'}
|
||||
@ -554,6 +614,56 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
</Card>
|
||||
</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 */}
|
||||
<TabsContent value="documents">
|
||||
<Card>
|
||||
@ -635,15 +745,15 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
{actionDialog.type === 'pushfnf' && 'Push to Full & Final Settlement'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{actionDialog.type === 'assign'
|
||||
{actionDialog.type === 'assign'
|
||||
? 'Select a user to assign this request to'
|
||||
: actionDialog.type === 'pushfnf'
|
||||
? 'This will move the resignation request to F&F for dues clearance'
|
||||
: 'Please provide remarks for this action'
|
||||
? 'This will move the resignation request to F&F for dues clearance'
|
||||
: 'Please provide remarks for this action'
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
{actionDialog.type === 'assign' ? (
|
||||
<div className="space-y-2">
|
||||
@ -688,12 +798,12 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<Button variant="outline" onClick={() => setActionDialog({ open: false, type: null })}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleSubmitAction}
|
||||
className={
|
||||
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
||||
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
||||
'bg-blue-600 hover:bg-blue-700'
|
||||
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
||||
'bg-blue-600 hover:bg-blue-700'
|
||||
}
|
||||
>
|
||||
Confirm
|
||||
@ -714,7 +824,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
Documents uploaded for this stage ({stageDocumentsDialog.documents.length} {stageDocumentsDialog.documents.length === 1 ? 'document' : 'documents'})
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{stageDocumentsDialog.documents.length > 0 ? (
|
||||
<Table>
|
||||
@ -760,6 +870,60 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||
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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||
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 { WorkNotesPage } from './WorkNotesPage';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface TerminationDetailsProps {
|
||||
terminationId: string;
|
||||
@ -22,11 +23,79 @@ interface 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 [workNotesOpen, setWorkNotesOpen] = useState(false);
|
||||
const [remarks, setRemarks] = useState('');
|
||||
const [assignToUser, setAssignToUser] = useState('');
|
||||
const [workNotesOpen, setWorkNotesOpen] = useState(false);
|
||||
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
||||
|
||||
const [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)
|
||||
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
||||
|
||||
@ -106,107 +175,107 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
};
|
||||
|
||||
const progressStages = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Request Initiated',
|
||||
status: 'completed',
|
||||
date: '2025-10-15',
|
||||
{
|
||||
id: 1,
|
||||
name: 'Request Initiated',
|
||||
status: 'completed',
|
||||
date: '2025-10-15',
|
||||
description: 'Termination request created by ASM/Initiator',
|
||||
actionType: 'approved',
|
||||
actionBy: 'ASM - Mumbai Region',
|
||||
remarks: 'Termination request initiated due to severe breach of agreement. Multiple violations documented.',
|
||||
feedback: 'All evidence and documentation attached. Case requires urgent attention due to severity of violations.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'RBM Review',
|
||||
status: request.currentStage === 'RBM' ? 'active' : ['ZBH', 'DD Lead', 'Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
{
|
||||
id: 2,
|
||||
name: 'RBM Review',
|
||||
status: request.currentStage === 'RBM' ? 'active' : ['ZBH', 'DD Lead', 'Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Regional Business Manager review',
|
||||
actionType: request.currentStage === 'RBM' ? undefined : undefined,
|
||||
actionBy: request.currentStage === 'RBM' ? undefined : undefined,
|
||||
remarks: request.currentStage === 'RBM' ? undefined : undefined,
|
||||
feedback: request.currentStage === 'RBM' ? undefined : undefined
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'ZBH Review',
|
||||
status: request.currentStage === 'ZBH' ? 'active' : ['DD Lead', 'Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Zonal Business Head evaluation'
|
||||
{
|
||||
id: 3,
|
||||
name: 'ZBH Review',
|
||||
status: request.currentStage === 'ZBH' ? 'active' : ['DD Lead', 'Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Zonal Business Head evaluation'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'DD Lead Review',
|
||||
status: request.currentStage === 'DD Lead' ? 'active' : ['Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'DD Lead validation'
|
||||
{
|
||||
id: 4,
|
||||
name: 'DD Lead Review',
|
||||
status: request.currentStage === 'DD Lead' ? 'active' : ['Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'DD Lead validation'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Legal Verification',
|
||||
status: request.currentStage === 'Legal' ? 'active' : ['NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Legal team validates termination grounds'
|
||||
{
|
||||
id: 5,
|
||||
name: 'Legal Verification',
|
||||
status: request.currentStage === 'Legal' ? 'active' : ['NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Legal team validates termination grounds'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'NBH Evaluation',
|
||||
status: request.currentStage === 'NBH' ? 'active' : ['SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'National Business Head decision'
|
||||
{
|
||||
id: 6,
|
||||
name: 'NBH Evaluation',
|
||||
status: request.currentStage === 'NBH' ? 'active' : ['SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'National Business Head decision'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Show Cause Notice (SCN)',
|
||||
status: request.currentStage === 'SCN' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'SCN sent to dealer, awaiting response'
|
||||
{
|
||||
id: 7,
|
||||
name: 'Show Cause Notice (SCN)',
|
||||
status: request.currentStage === 'SCN' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'SCN sent to dealer, awaiting response'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'DD Lead & Legal Review',
|
||||
status: request.currentStage === 'DD Lead Legal' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Evaluation of SCN response'
|
||||
{
|
||||
id: 8,
|
||||
name: 'DD Lead & Legal Review',
|
||||
status: request.currentStage === 'DD Lead Legal' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Evaluation of SCN response'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'NBH Termination Approval',
|
||||
status: request.currentStage === 'NBH Final' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'NBH approves termination'
|
||||
{
|
||||
id: 9,
|
||||
name: 'NBH Termination Approval',
|
||||
status: request.currentStage === 'NBH Final' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'NBH approves termination'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'CCO Approval',
|
||||
status: request.currentStage === 'CCO' ? 'active' : ['CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Chief Commercial Officer approval'
|
||||
{
|
||||
id: 10,
|
||||
name: 'CCO Approval',
|
||||
status: request.currentStage === 'CCO' ? 'active' : ['CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Chief Commercial Officer approval'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'CEO Final Approval',
|
||||
status: request.currentStage === 'CEO' ? 'active' : ['Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'CEO final authorization'
|
||||
{
|
||||
id: 11,
|
||||
name: 'CEO Final Approval',
|
||||
status: request.currentStage === 'CEO' ? 'active' : ['Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'CEO final authorization'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Legal - Termination Letter',
|
||||
status: request.currentStage === 'Legal Letter' ? 'active' : ['DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Legal team shares termination letter to DD-Lead and DD Admin'
|
||||
{
|
||||
id: 12,
|
||||
name: 'Legal - Termination Letter',
|
||||
status: request.currentStage === 'Legal Letter' ? 'active' : ['DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Legal team shares termination letter to DD-Lead and DD Admin'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'DD Admin - Share with Dealer',
|
||||
status: request.currentStage === 'DD Admin Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
||||
description: 'DD Admin shares termination letter with dealer (Proceed to F&F)'
|
||||
{
|
||||
id: 13,
|
||||
name: 'DD Admin - Share with Dealer',
|
||||
status: request.currentStage === 'DD Admin Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
||||
description: 'DD Admin shares termination letter with dealer (Proceed to F&F)'
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'Dealer Terminated',
|
||||
status: request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
||||
description: 'Dealership termination effective'
|
||||
{
|
||||
id: 14,
|
||||
name: 'Dealer Terminated',
|
||||
status: request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
||||
description: 'Dealership termination effective'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const handleViewStageDocuments = (stageName: string) => {
|
||||
const documents = stageDocuments[stageName] || [];
|
||||
setStageDocumentsDialog({ open: true, stageName, documents });
|
||||
};
|
||||
|
||||
const handleAction = (type: 'approve' | 'withdrawal' | 'sendback' | 'assign') => {
|
||||
const handleAction = (type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf') => {
|
||||
setActionDialog({ open: true, type });
|
||||
};
|
||||
|
||||
@ -287,40 +356,68 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-slate-600 mr-2">Termination Actions:</span>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
||||
onClick={() => handleAction('approve')}
|
||||
>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-red-600 border-red-300 hover:bg-red-50 transition-all"
|
||||
onClick={() => handleAction('withdrawal')}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Withdrawal
|
||||
</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>
|
||||
{currentUser?.role !== 'Dealer' && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
||||
onClick={() => handleAction('approve')}
|
||||
>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Approve
|
||||
</Button>
|
||||
{request.currentStage === 'NBH' && (currentUser?.role === 'NBH' || currentUser?.role === 'Super Admin') && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-purple-600 hover:bg-purple-700 transition-all shadow-sm"
|
||||
onClick={() => setShowSCNDialog(true)}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 mr-2" />
|
||||
Issue SCN
|
||||
</Button>
|
||||
)}
|
||||
{request.currentStage === 'SCN' && (['Legal', 'DD Admin', 'Super Admin'].includes(currentUser?.role || '')) && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-amber-600 hover:bg-amber-700 transition-all shadow-sm"
|
||||
onClick={() => {
|
||||
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>
|
||||
|
||||
{/* Secondary Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
{canPushToFnF && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-blue-600 border-blue-300 hover:bg-blue-50 transition-all"
|
||||
onClick={() => handleAction('pushfnf')}
|
||||
>
|
||||
@ -328,8 +425,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
Push to F&F
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="hover:bg-slate-50 transition-all"
|
||||
onClick={() => handleAction('assign')}
|
||||
@ -348,7 +445,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
</div>
|
||||
<Dialog open={workNotesOpen} onOpenChange={setWorkNotesOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="relative hover:bg-red-50 hover:border-red-300 hover:text-red-700 transition-all"
|
||||
@ -559,11 +656,10 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
return (
|
||||
<div key={stage.id} className="flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
stage.status === 'completed' ? 'bg-green-100 text-green-600' :
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${stage.status === 'completed' ? 'bg-green-100 text-green-600' :
|
||||
stage.status === 'active' ? 'bg-red-100 text-red-600' :
|
||||
'bg-slate-100 text-slate-400'
|
||||
}`}>
|
||||
'bg-slate-100 text-slate-400'
|
||||
}`}>
|
||||
{stage.status === 'completed' ? (
|
||||
<Check className="w-5 h-5" />
|
||||
) : stage.status === 'active' ? (
|
||||
@ -573,11 +669,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
)}
|
||||
</div>
|
||||
{index < progressStages.length - 1 && (
|
||||
<div className={`w-0.5 ${
|
||||
stage.remarks ? 'h-32' : 'h-16'
|
||||
} ${
|
||||
stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
|
||||
}`} />
|
||||
<div className={`w-0.5 ${stage.remarks ? 'h-32' : 'h-16'
|
||||
} ${stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 pb-8">
|
||||
@ -585,8 +679,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className={
|
||||
stage.status === 'completed' ? 'text-green-600' :
|
||||
stage.status === 'active' ? 'text-red-600' :
|
||||
'text-slate-400'
|
||||
stage.status === 'active' ? 'text-red-600' :
|
||||
'text-slate-400'
|
||||
}>{stage.name}</h3>
|
||||
{documentCount > 0 && (
|
||||
<button
|
||||
@ -606,16 +700,16 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
)}
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm">{stage.description}</p>
|
||||
|
||||
|
||||
{/* Action Badge and Remarks */}
|
||||
{stage.actionType && stage.remarks && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={
|
||||
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 === 'withdrawal' ? 'bg-red-100 text-red-700 border-red-300' :
|
||||
'bg-blue-100 text-blue-700 border-blue-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' :
|
||||
'bg-blue-100 text-blue-700 border-blue-300'
|
||||
}>
|
||||
{stage.actionType === 'approved' && '✓ Approved'}
|
||||
{stage.actionType === 'sendback' && '↩ Sent Back'}
|
||||
@ -731,15 +825,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
{actionDialog.type === 'pushfnf' && 'Push to Full & Final Settlement'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{actionDialog.type === 'assign'
|
||||
{actionDialog.type === 'assign'
|
||||
? 'Select a user to assign this request to'
|
||||
: actionDialog.type === 'pushfnf'
|
||||
? 'This will move the termination case to F&F for dues clearance'
|
||||
: 'Please provide remarks for this action'
|
||||
? 'This will move the termination case to F&F for dues clearance'
|
||||
: 'Please provide remarks for this action'
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
{actionDialog.type === 'assign' ? (
|
||||
<div className="space-y-2">
|
||||
@ -786,12 +880,12 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<Button variant="outline" onClick={() => setActionDialog({ open: false, type: null })}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleSubmitAction}
|
||||
className={
|
||||
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
||||
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
||||
'bg-blue-600 hover:bg-blue-700'
|
||||
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
||||
'bg-blue-600 hover:bg-blue-700'
|
||||
}
|
||||
>
|
||||
Confirm
|
||||
@ -812,7 +906,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
Documents uploaded for this stage ({stageDocumentsDialog.documents.length} {stageDocumentsDialog.documents.length === 1 ? 'document' : 'documents'})
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{stageDocumentsDialog.documents.length > 0 ? (
|
||||
<Table>
|
||||
@ -858,6 +952,113 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
const applicationId = props.applicationId || id || '';
|
||||
const applicationName = props.applicationName || location.state?.applicationName || 'Application';
|
||||
const registrationNumber = props.registrationNumber || location.state?.registrationNumber || '';
|
||||
const [appName, setAppName] = useState(props.applicationName || location.state?.applicationName || 'Application');
|
||||
const [regNumber, setRegNumber] = useState(props.registrationNumber || location.state?.registrationNumber || '');
|
||||
const onBack = props.onBack || (() => navigate(-1));
|
||||
const externalParticipantsInit = props.participants || location.state?.participants || [];
|
||||
const [externalParticipants, setExternalParticipants] = useState<any[]>(externalParticipantsInit);
|
||||
@ -96,7 +96,6 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
||||
const { socket } = useSocket();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [previewFile, setPreviewFile] = useState<Attachment | null>(null);
|
||||
|
||||
const getFileIcon = (mimeType: string, filePath?: string) => {
|
||||
@ -229,24 +228,41 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
||||
}
|
||||
}, [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(() => {
|
||||
if (externalParticipants.length === 0 && applicationId) {
|
||||
if (applicationId) {
|
||||
const fetchApplicationDetails = async () => {
|
||||
try {
|
||||
const appData = await onboardingService.getApplicationById(applicationId);
|
||||
if (appData && appData.participants) {
|
||||
setExternalParticipants(appData.participants);
|
||||
if (appData) {
|
||||
// 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) {
|
||||
console.error('Failed to fetch application details for participants:', error);
|
||||
console.error('Failed to fetch application details:', error);
|
||||
}
|
||||
};
|
||||
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 value = e.target.value;
|
||||
@ -440,9 +456,9 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-slate-50">
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b border-slate-200 px-6 py-4">
|
||||
<div className="h-full flex flex-col bg-slate-50 overflow-hidden">
|
||||
{/* Header - Stays fixed because it's a sibling of ScrollArea */}
|
||||
<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 gap-4">
|
||||
<Button
|
||||
@ -461,9 +477,9 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
||||
<div>
|
||||
<h1 className="text-slate-900">Work Notes</h1>
|
||||
<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-500">{registrationNumber}</span>
|
||||
<span className="text-slate-500">{regNumber}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -489,10 +505,9 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<ScrollArea className="flex-1 px-6 py-4">
|
||||
<div className="max-w-4xl mx-auto space-y-6 flex flex-col-reverse" ref={scrollRef}>
|
||||
{notes.map((note) => {
|
||||
<ScrollArea className="flex-1 px-6 py-4 min-h-0">
|
||||
<div className="max-w-4xl mx-auto space-y-6 flex flex-col">
|
||||
{[...notes].reverse().map((note) => {
|
||||
const isMe = (note?.author?.email && currentUser?.email && note.author.email.toLowerCase() === currentUser.email.toLowerCase()) ||
|
||||
(note?.userId && currentUser?.id && note.userId === currentUser.id) ||
|
||||
note.id.startsWith('temp-');
|
||||
@ -586,11 +601,12 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
||||
<span className="text-slate-500">Loading notes...</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="bg-white border-t border-slate-200 px-6 py-4">
|
||||
{/* Input Area - Stays fixed because it's a sibling of ScrollArea */}
|
||||
<div className="bg-white border-t border-slate-200 px-6 py-4 z-10">
|
||||
<div className="max-w-4xl mx-auto space-y-4">
|
||||
|
||||
{/* Attachment Previews */}
|
||||
|
||||
@ -81,7 +81,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
||||
submenuKey: 'allRequests',
|
||||
submenu: [
|
||||
{ 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;
|
||||
questionnaireResponses?: any[]; // added for response view
|
||||
participants?: Participant[];
|
||||
architectureAssignedTo?: string;
|
||||
architectureStatus?: string;
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
@ -425,7 +427,7 @@ export const mockApplications: Application[] = [
|
||||
progress: 20,
|
||||
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',
|
||||
registrationNumber: 'APP-009',
|
||||
@ -444,7 +446,7 @@ export const mockApplications: Application[] = [
|
||||
submissionDate: '2025-10-08',
|
||||
assignedUsers: [],
|
||||
progress: 10,
|
||||
isShortlisted: false // Unopportunity lead - not offering dealership in Chandigarh
|
||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Chandigarh
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
@ -464,7 +466,7 @@ export const mockApplications: Application[] = [
|
||||
submissionDate: '2025-10-07',
|
||||
assignedUsers: [],
|
||||
progress: 10,
|
||||
isShortlisted: false // Unopportunity lead - not offering dealership in Kochi
|
||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Kochi
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
@ -484,7 +486,7 @@ export const mockApplications: Application[] = [
|
||||
submissionDate: '2025-10-06',
|
||||
assignedUsers: [],
|
||||
progress: 10,
|
||||
isShortlisted: false // Unopportunity lead - not offering dealership in Kolkata
|
||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Kolkata
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
@ -504,7 +506,7 @@ export const mockApplications: Application[] = [
|
||||
submissionDate: '2025-10-05',
|
||||
assignedUsers: [],
|
||||
progress: 10,
|
||||
isShortlisted: false // Unopportunity lead - not offering dealership in Hyderabad
|
||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Hyderabad
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
@ -524,7 +526,7 @@ export const mockApplications: Application[] = [
|
||||
submissionDate: '2025-10-04',
|
||||
assignedUsers: [],
|
||||
progress: 10,
|
||||
isShortlisted: false // Unopportunity lead - not offering dealership in Lucknow
|
||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Lucknow
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
@ -544,7 +546,7 @@ export const mockApplications: Application[] = [
|
||||
submissionDate: '2025-10-03',
|
||||
assignedUsers: [],
|
||||
progress: 10,
|
||||
isShortlisted: false // Unopportunity lead - not offering dealership in Ludhiana
|
||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Ludhiana
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
@ -564,7 +566,7 @@ export const mockApplications: Application[] = [
|
||||
submissionDate: '2025-10-02',
|
||||
assignedUsers: [],
|
||||
progress: 10,
|
||||
isShortlisted: false // Unopportunity lead - not offering dealership in Nagpur
|
||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Nagpur
|
||||
},
|
||||
{
|
||||
id: '16',
|
||||
@ -584,7 +586,7 @@ export const mockApplications: Application[] = [
|
||||
submissionDate: '2025-10-01',
|
||||
assignedUsers: [],
|
||||
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);
|
||||
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