@@ -230,23 +254,44 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
-
-
+
+
-
+
-
-
-
-
-
-
-
+ {(l3Fields || []).map((field: any, idx: number) => (
+
+
+ {field.type === 'select' ? (
+
+ ) : field.type === 'number' ? (
+ handleLevel3Change(field.itemKey, e.target.value)} />
+ ) : (
+
+ ))}
@@ -285,7 +330,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
-
+
@@ -306,7 +351,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
-
+
-
+
-
+
setUploadFile(e.target.files ? e.target.files[0] : null)} data-testid="onboarding-documents-file-input" />
@@ -377,7 +422,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
{(currentUser?.role !== 'FDD' && currentUser?.roleCode !== 'FDD') && (
-
+
{['Recommended', 'Qualified with Observations', 'Not Recommended'].map((rec) => (
@@ -483,7 +528,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
-
+
{/* Applications Grid/Table */}
- {viewMode === 'grid' ? (
+ {loading ? (
+
+
+
+ ) : viewMode === 'grid' ? (
{filteredApplications.map((app, idx) => (
@@ -474,6 +489,61 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
)}
+ {/* Pagination */}
+ {paginationMeta && paginationMeta.totalPages > 1 && (
+
+
+
+
+ setCurrentPage(p => Math.max(1, p - 1))}
+ className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
+ />
+
+
+ {[...Array(paginationMeta.totalPages)].map((_, i) => {
+ const pageNum = i + 1;
+ // Complex pagination: show first, last, and current +/- 1
+ if (
+ pageNum === 1 ||
+ pageNum === paginationMeta.totalPages ||
+ (pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
+ ) {
+ return (
+
+ setCurrentPage(pageNum)}
+ className="cursor-pointer"
+ >
+ {pageNum}
+
+
+ );
+ } else if (
+ pageNum === currentPage - 2 ||
+ pageNum === currentPage + 2
+ ) {
+ return (
+
+
+
+ );
+ }
+ return null;
+ })}
+
+
+ setCurrentPage(p => Math.min(paginationMeta.totalPages, p + 1))}
+ className={currentPage === paginationMeta.totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
+ />
+
+
+
+
+ )}
+
{/* Shortlist Modal */}
@@ -300,7 +302,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
0}
onCheckedChange={toggleSelectAll}
data-testid="onboarding-applications-header-checkbox"
/>
@@ -359,6 +361,57 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
))}
+
+ {paginationMeta && paginationMeta.totalPages > 1 && (
+
+
+
+
+ setCurrentPage(prev => Math.max(1, prev - 1))}
+ className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
+ />
+
+
+ {[...Array(paginationMeta.totalPages)].map((_, i) => {
+ const page = i + 1;
+ // Show current, first, last, and pages around current
+ if (
+ page === 1 ||
+ page === paginationMeta.totalPages ||
+ (page >= currentPage - 1 && page <= currentPage + 1)
+ ) {
+ return (
+
+ setCurrentPage(page)}
+ className="cursor-pointer"
+ >
+ {page}
+
+
+ );
+ }
+ if (
+ (page === 2 && currentPage > 3) ||
+ (page === paginationMeta.totalPages - 1 && currentPage < paginationMeta.totalPages - 2)
+ ) {
+ return ;
+ }
+ return null;
+ })}
+
+
+ setCurrentPage(prev => Math.min(paginationMeta.totalPages, prev + 1))}
+ className={currentPage === paginationMeta.totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
+ />
+
+
+
+
+ )}
);
diff --git a/src/features/onboarding/pages/OpportunityRequestsPage.tsx b/src/features/onboarding/pages/OpportunityRequestsPage.tsx
index 044082e..e6feb5b 100644
--- a/src/features/onboarding/pages/OpportunityRequestsPage.tsx
+++ b/src/features/onboarding/pages/OpportunityRequestsPage.tsx
@@ -2,7 +2,6 @@ import { useState, useEffect } from 'react';
import { ApplicationStatus, Application } from '@/lib/mock-data';
import { masterService } from '@/services/master.service';
import { onboardingService } from '@/services/onboarding.service';
-import { adminService } from '@/services/admin.service';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
@@ -12,6 +11,15 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
+import {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+} from "@/components/ui/pagination";
import {
Search,
Download,
@@ -21,8 +29,8 @@ import {
List,
AlertCircle,
Loader2,
- X,
- User as UserIcon
+ Calendar,
+ ArrowUpDown
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import {
@@ -41,41 +49,28 @@ import { Textarea } from '@/components/ui/textarea';
import { formatDateTime } from '@/components/ui/utils';
import { toast } from 'sonner';
import { ApplicationCard } from '@/features/onboarding/components/ApplicationCard';
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from '@/components/ui/command';
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+
interface OpportunityRequestsPageProps {
onViewDetails: (id: string) => void;
}
-interface User {
- id: string;
- fullName: string;
- email: string;
- role: string;
-}
-
export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPageProps) {
const [viewMode, setViewMode] = useState<'grid' | 'table'>('table');
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState
('all');
const [locationFilter, setLocationFilter] = useState('all');
const [stateFilter, setStateFilter] = useState('all');
+ const [fromDate, setFromDate] = useState('');
+ const [toDate, setToDate] = useState('');
+ const [sortBy, setSortBy] = useState('date-desc');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [paginationMeta, setPaginationMeta] = useState(null);
const [selectedIds, setSelectedIds] = useState([]);
const [showShortlistModal, setShowShortlistModal] = useState(false);
const [shortlistRemark, setShortlistRemark] = useState('');
// Assignee Selection
- const [selectedAssignees, setSelectedAssignees] = useState([]);
- const [availableUsers, setAvailableUsers] = useState([]);
- const [openUserSelect, setOpenUserSelect] = useState(false);
const [states, setStates] = useState([]);
const [locations, setLocations] = useState([]);
@@ -85,7 +80,13 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
useEffect(() => {
fetchApplications();
- fetchUsers();
+ }, [fromDate, toDate, statusFilter, searchQuery, currentPage, locationFilter, stateFilter]);
+
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [fromDate, toDate, statusFilter, searchQuery, locationFilter, stateFilter]);
+
+ useEffect(() => {
fetchStates();
}, []);
@@ -101,26 +102,25 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
}
};
- const fetchUsers = async () => {
- try {
- const response = await adminService.getAllUsers({ isExternal: false });
- // Defensive check for array data
- const users = (response && response.success && Array.isArray(response.data))
- ? response.data
- : (Array.isArray(response) ? response : []);
-
- // Filter out any invalid user objects
- setAvailableUsers(users.filter((u: any) => u && u.id));
- } catch (error) {
- console.error('Failed to fetch users:', error);
- }
- };
const fetchApplications = async () => {
try {
setLoading(true);
- const response = await onboardingService.getApplications();
- const rawData = response.data || (Array.isArray(response) ? response : []);
+ const opportunityStatuses = ['Questionnaire Pending', 'Questionnaire Completed', 'Shortlisted'];
+ const response = await onboardingService.getApplications({
+ fromDate,
+ toDate,
+ status: statusFilter === 'all' ? opportunityStatuses.join(',') : statusFilter,
+ location: locationFilter !== 'all' ? locationFilter : undefined,
+ state: stateFilter !== 'all' ? stateFilter : undefined,
+ ddLeadShortlisted: 'false',
+ isShortlisted: 'true',
+ search: searchQuery,
+ page: currentPage,
+ limit: 10
+ });
+ const rawData = response.data || [];
+ setPaginationMeta(response.meta);
// Map backend data to Application interface
const mappedApps: Application[] = rawData.map((app: any) => ({
@@ -176,23 +176,12 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
// Shows applications shortlisted by DD but NOT yet shortlisted by DD Lead
// IMPORTANT: Only shows applications in early stages (before they enter full workflow)
// UPDATED LOGIC: Opportunities start at 'Questionnaire Pending'. 'Submitted' means Non-Opportunity.
- const filteredApplications = applicationsData.filter((app) => {
- // Only show applications that are:
- // 1. Not Shortlisted by DD Lead yet (ddLeadShortlisted !== true) - waiting for action
- const waitingForDDLead = !(app as any).ddLeadShortlisted;
-
- // Only show applications with Opportunity statuses
- // 'Submitted' is EXCLUDED because it represents Non-Opportunity (Leads)
- const validStatuses: ApplicationStatus[] = ['Questionnaire Pending', 'Questionnaire Completed', 'Shortlisted'];
- const isOpportunityStatus = validStatuses.includes(app.status);
-
- const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- app.registrationNumber.toLowerCase().includes(searchQuery.toLowerCase());
- const matchesStatus = statusFilter === 'all' || app.status === statusFilter;
- const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter;
- const matchesState = stateFilter === 'all' || app.state === stateFilter;
-
- return waitingForDDLead && isOpportunityStatus && matchesSearch && matchesStatus && matchesLocation && matchesState;
+ const filteredApplications = applicationsData.sort((a, b) => {
+ if (sortBy === 'score-desc') return (b.questionnaireMarks || 0) - (a.questionnaireMarks || 0);
+ if (sortBy === 'score-asc') return (a.questionnaireMarks || 0) - (b.questionnaireMarks || 0);
+ if (sortBy === 'date-desc') return new Date(b.submissionDate).getTime() - new Date(a.submissionDate).getTime();
+ if (sortBy === 'date-asc') return new Date(a.submissionDate).getTime() - new Date(b.submissionDate).getTime();
+ return 0;
});
const handleSelectAll = (checked: boolean) => {
@@ -220,16 +209,9 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
};
const confirmShortlist = async () => {
- if (selectedAssignees.length === 0) {
- toast.error('Please assign at least one user');
- return;
- }
-
- const assignedUserIds = selectedAssignees.map(u => u.id);
-
try {
- // Call Backend API
- const response = await onboardingService.shortlistApplications(selectedIds, assignedUserIds, shortlistRemark);
+ // Call Backend API - assignedTo is now empty as it's handled automatically
+ const response = await onboardingService.shortlistApplications(selectedIds, [], shortlistRemark);
if (response && response.success) {
// Update local state and show success only if API succeeded
@@ -237,8 +219,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
if (selectedIds.includes(app.id)) {
return {
...app,
- ddLeadShortlisted: true,
- assignedTo: assignedUserIds[0] // Optimistically update with first assignee
+ ddLeadShortlisted: true
} as any;
}
return app;
@@ -248,9 +229,8 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
setSelectedIds([]);
setShowShortlistModal(false);
setShortlistRemark('');
- setSelectedAssignees([]);
- toast.success(`${selectedIds.length} application(s) shortlisted and assigned to ${selectedAssignees.length} user(s)`);
+ toast.success(`${selectedIds.length} application(s) shortlisted successfully. Users will be assigned automatically.`);
} else {
throw new Error(response?.message || 'Failed to process shortlisting');
}
@@ -337,9 +317,9 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
// For Opportunity Requests, only show early-stage statuses
// These applications haven't entered the full dealership approval workflow yet
const statusOptions: ApplicationStatus[] = [
- 'Submitted',
'Questionnaire Pending',
- 'Questionnaire Completed'
+ 'Questionnaire Completed',
+ 'Shortlisted'
];
const getStatusColor = (status: ApplicationStatus) => {
@@ -476,6 +456,47 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
))}
+
+
+
+
+ setFromDate(e.target.value)}
+ className="pl-10 text-xs"
+ placeholder="From"
+ data-testid="onboarding-opp-requests-from-date"
+ />
+
+
to
+
+
+ setToDate(e.target.value)}
+ className="pl-10 text-xs"
+ placeholder="To"
+ data-testid="onboarding-opp-requests-to-date"
+ />
+
+
+
+
+
+
+
+
+ Newest Applied
+ Oldest Applied
+ Highest Score
+ Lowest Score
+
+
{/* Action Buttons */}
@@ -534,7 +555,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa