schemaenhanced and tried to fill the gaps

This commit is contained in:
laxmanhalaki 2026-03-06 19:36:55 +05:30
parent d919e925c8
commit ad33de7e26
13 changed files with 1136 additions and 502 deletions

View File

@ -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 />} />

View File

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

View File

@ -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">

View 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>
);
}

View File

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

View File

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

View File

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

View File

@ -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 */}

View File

@ -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' }
]
});
}

View File

@ -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
}
];

View File

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

View 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;
}
}
};

View 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;
}
};