stage names modified and calendar addd in opportunity requests
This commit is contained in:
parent
2f82699572
commit
b357dbdcbb
@ -43,6 +43,7 @@ export const API = {
|
|||||||
exportApplicationResponses: (params: { applicationIds: string }) => client.get('/onboarding/applications/export-responses', params),
|
exportApplicationResponses: (params: { applicationIds: string }) => client.get('/onboarding/applications/export-responses', params),
|
||||||
getApplications: (params?: any) => client.get('/onboarding/applications', params),
|
getApplications: (params?: any) => client.get('/onboarding/applications', params),
|
||||||
shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data),
|
shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data),
|
||||||
|
sendBulkReminders: (data: { applicationIds: string[] }) => client.post('/onboarding/applications/reminders', data),
|
||||||
getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`),
|
getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`),
|
||||||
updateApplication: (id: string, data: any) => client.put(`/onboarding/applications/${id}`, data),
|
updateApplication: (id: string, data: any) => client.put(`/onboarding/applications/${id}`, data),
|
||||||
getLatestQuestionnaire: () => client.get('/questionnaire/latest'),
|
getLatestQuestionnaire: () => client.get('/questionnaire/latest'),
|
||||||
|
|||||||
@ -26,7 +26,6 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Grid3x3,
|
Grid3x3,
|
||||||
List,
|
List,
|
||||||
Mail,
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Loader2
|
Loader2
|
||||||
@ -193,9 +192,6 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkReminders = () => {
|
|
||||||
toast.success(`Reminder emails sent to ${selectedIds.length} applicant(s)`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// For DD's All Applications page, only show initial statuses
|
// For DD's All Applications page, only show initial statuses
|
||||||
const statusOptions: ApplicationStatus[] = [
|
const statusOptions: ApplicationStatus[] = [
|
||||||
@ -356,16 +352,6 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
|
|||||||
|
|
||||||
{selectedIds.length > 0 && (
|
{selectedIds.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleBulkReminders}
|
|
||||||
data-testid="onboarding-all-apps-reminders-btn"
|
|
||||||
>
|
|
||||||
<Mail className="w-4 h-4 mr-2" />
|
|
||||||
Send Reminders ({selectedIds.length})
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleShortlist}
|
onClick={handleShortlist}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { ApplicationStatus, Application } from '@/lib/mock-data';
|
import { ApplicationStatus, Application } from '@/lib/mock-data';
|
||||||
import { formatDateTime } from '@/components/ui/utils';
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
import { onboardingService } from '@/services/onboarding.service';
|
import { onboardingService } from '@/services/onboarding.service';
|
||||||
@ -14,7 +15,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Download,
|
Download,
|
||||||
Mail
|
Mail,
|
||||||
|
Loader2
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
@ -53,6 +55,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
|
|||||||
const [statusFilter, setStatusFilter] = useState<string>(initialFilter || 'all');
|
const [statusFilter, setStatusFilter] = useState<string>(initialFilter || 'all');
|
||||||
|
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
const [isSendingReminders, setIsSendingReminders] = useState(false);
|
||||||
const [sortBy, setSortBy] = useState<'date'>('date');
|
const [sortBy, setSortBy] = useState<'date'>('date');
|
||||||
const [showNewApplicationModal, setShowNewApplicationModal] = useState(false);
|
const [showNewApplicationModal, setShowNewApplicationModal] = useState(false);
|
||||||
const [showMyAssignments, setShowMyAssignments] = useState(false);
|
const [showMyAssignments, setShowMyAssignments] = useState(false);
|
||||||
@ -153,9 +156,22 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkReminders = () => {
|
const handleBulkReminders = async () => {
|
||||||
alert(`Sending reminders to ${selectedIds.length} applicants`);
|
if (selectedIds.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSendingReminders(true);
|
||||||
|
const res = await onboardingService.sendBulkReminders(selectedIds);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success(res.message || `Reminder emails sent to ${selectedIds.length} applicant(s)`);
|
||||||
setSelectedIds([]);
|
setSelectedIds([]);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to send reminders:', error);
|
||||||
|
toast.error(error.message || 'Failed to send reminders');
|
||||||
|
} finally {
|
||||||
|
setIsSendingReminders(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
@ -283,9 +299,14 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleBulkReminders}
|
onClick={handleBulkReminders}
|
||||||
|
disabled={isSendingReminders}
|
||||||
data-testid="onboarding-applications-reminders-button"
|
data-testid="onboarding-applications-reminders-button"
|
||||||
>
|
>
|
||||||
|
{isSendingReminders ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
<Mail className="w-4 h-4 mr-2" />
|
<Mail className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
Send Reminders ({selectedIds.length})
|
Send Reminders ({selectedIds.length})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -11,6 +11,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { Calendar as CalendarPicker } from '@/components/ui/calendar';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { cn } from '@/components/ui/utils';
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Download,
|
Download,
|
||||||
@ -182,9 +190,12 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
|
|||||||
|
|
||||||
setApplicationsData(mappedApps);
|
setApplicationsData(mappedApps);
|
||||||
|
|
||||||
// Extract unique locations
|
// Extract unique locations and states from the returned data
|
||||||
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean);
|
const newLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean) as string[];
|
||||||
setLocations(uniqueLocations);
|
setLocations(prev => Array.from(new Set([...prev, ...newLocations])));
|
||||||
|
|
||||||
|
const newStates = Array.from(new Set(mappedApps.map(app => app.state))).filter(Boolean) as string[];
|
||||||
|
setStates(prev => Array.from(new Set([...prev, ...newStates])));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch applications:', error);
|
console.error('Failed to fetch applications:', error);
|
||||||
toast.error('Failed to load non-opportunity requests');
|
toast.error('Failed to load non-opportunity requests');
|
||||||
@ -247,29 +258,55 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative w-full md:w-36">
|
<Popover>
|
||||||
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
|
<PopoverTrigger asChild>
|
||||||
<Input
|
<Button
|
||||||
type="date"
|
variant="outline"
|
||||||
value={fromDate}
|
className={cn(
|
||||||
onChange={(e) => setFromDate(e.target.value)}
|
"w-full md:w-36 justify-start text-left font-normal h-10 px-3",
|
||||||
className="pl-10 text-xs"
|
!fromDate && "text-muted-foreground"
|
||||||
placeholder="From"
|
)}
|
||||||
data-testid="onboarding-non-opps-from-date"
|
data-testid="onboarding-non-opps-from-date-trigger"
|
||||||
|
>
|
||||||
|
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
|
||||||
|
{fromDate ? format(new Date(fromDate), "PP") : <span className="text-xs text-slate-500">From Date</span>}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<CalendarPicker
|
||||||
|
mode="single"
|
||||||
|
selected={fromDate ? new Date(fromDate) : undefined}
|
||||||
|
onSelect={(date) => setFromDate(date ? date.toISOString().split('T')[0] : '')}
|
||||||
|
initialFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<span className="text-slate-400">to</span>
|
<span className="text-slate-400">to</span>
|
||||||
<div className="relative w-full md:w-36">
|
|
||||||
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
|
<Popover>
|
||||||
<Input
|
<PopoverTrigger asChild>
|
||||||
type="date"
|
<Button
|
||||||
value={toDate}
|
variant="outline"
|
||||||
onChange={(e) => setToDate(e.target.value)}
|
className={cn(
|
||||||
className="pl-10 text-xs"
|
"w-full md:w-36 justify-start text-left font-normal h-10 px-3",
|
||||||
placeholder="To"
|
!toDate && "text-muted-foreground"
|
||||||
data-testid="onboarding-non-opps-to-date"
|
)}
|
||||||
|
data-testid="onboarding-non-opps-to-date-trigger"
|
||||||
|
>
|
||||||
|
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
|
||||||
|
{toDate ? format(new Date(toDate), "PP") : <span className="text-xs text-slate-500">To Date</span>}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<CalendarPicker
|
||||||
|
mode="single"
|
||||||
|
selected={toDate ? new Date(toDate) : undefined}
|
||||||
|
onSelect={(date) => setToDate(date ? date.toISOString().split('T')[0] : '')}
|
||||||
|
initialFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select value={locationFilter} onValueChange={setLocationFilter}>
|
<Select value={locationFilter} onValueChange={setLocationFilter}>
|
||||||
@ -284,6 +321,22 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-slate-500 hover:text-slate-700 h-10 px-3"
|
||||||
|
onClick={() => {
|
||||||
|
setFromDate('');
|
||||||
|
setToDate('');
|
||||||
|
setLocationFilter('all');
|
||||||
|
setStateFilter('all');
|
||||||
|
setSearchQuery('');
|
||||||
|
}}
|
||||||
|
data-testid="onboarding-non-opps-clear-filters"
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Select value={stateFilter} onValueChange={setStateFilter}>
|
<Select value={stateFilter} onValueChange={setStateFilter}>
|
||||||
<SelectTrigger className="w-full lg:w-48" data-testid="onboarding-non-opps-state-select">
|
<SelectTrigger className="w-full lg:w-48" data-testid="onboarding-non-opps-state-select">
|
||||||
<SelectValue placeholder="All States" />
|
<SelectValue placeholder="All States" />
|
||||||
|
|||||||
@ -11,6 +11,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { Calendar as CalendarPicker } from '@/components/ui/calendar';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { cn } from '@/components/ui/utils';
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
@ -67,6 +75,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [paginationMeta, setPaginationMeta] = useState<any>(null);
|
const [paginationMeta, setPaginationMeta] = useState<any>(null);
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
const [isSendingReminders, setIsSendingReminders] = useState(false);
|
||||||
const [showShortlistModal, setShowShortlistModal] = useState(false);
|
const [showShortlistModal, setShowShortlistModal] = useState(false);
|
||||||
const [shortlistRemark, setShortlistRemark] = useState('');
|
const [shortlistRemark, setShortlistRemark] = useState('');
|
||||||
|
|
||||||
@ -160,9 +169,13 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
|
|
||||||
setApplicationsData(mappedApps);
|
setApplicationsData(mappedApps);
|
||||||
|
|
||||||
// Extract unique locations for filtering
|
// Extract unique locations and states from the returned data
|
||||||
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean);
|
// Note: This appends new ones to the existing list to ensure all found locations are selectable
|
||||||
setLocations(uniqueLocations);
|
const newLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean) as string[];
|
||||||
|
setLocations(prev => Array.from(new Set([...prev, ...newLocations])));
|
||||||
|
|
||||||
|
const newStates = Array.from(new Set(mappedApps.map(app => app.state))).filter(Boolean) as string[];
|
||||||
|
setStates(prev => Array.from(new Set([...prev, ...newStates])));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch applications:', error);
|
console.error('Failed to fetch applications:', error);
|
||||||
toast.error('Failed to load opportunity requests');
|
toast.error('Failed to load opportunity requests');
|
||||||
@ -231,12 +244,25 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkReminders = () => {
|
const handleBulkReminders = async () => {
|
||||||
if (selectedIds.length === 0) {
|
if (selectedIds.length === 0) {
|
||||||
toast.error('Please select at least one application');
|
toast.error('Please select at least one application');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.success(`Reminder emails sent to ${selectedIds.length} applicant(s)`);
|
|
||||||
|
try {
|
||||||
|
setIsSendingReminders(true);
|
||||||
|
const res = await onboardingService.sendBulkReminders(selectedIds);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success(res.message || `Reminder emails sent to ${selectedIds.length} applicant(s)`);
|
||||||
|
setSelectedIds([]);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to send reminders:', error);
|
||||||
|
toast.error(error.message || 'Failed to send reminders');
|
||||||
|
} finally {
|
||||||
|
setIsSendingReminders(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
@ -380,20 +406,6 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Info Banner */}
|
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4" data-testid="onboarding-opp-requests-banner">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h3 className="text-amber-900 mb-1">DD Lead Workflow - Opportunity Requests</h3>
|
|
||||||
<p className="text-amber-800">
|
|
||||||
This page shows <strong>applications where dealerships are being offered</strong> at the applicant's preferred location.
|
|
||||||
These have been shortlisted by DD and are waiting for your review. Select and <strong>Shortlist</strong> promising candidates
|
|
||||||
to move them to the <strong>Dealership Requests</strong> page for further processing.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Header with Filters */}
|
{/* Header with Filters */}
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
||||||
@ -424,6 +436,23 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-slate-500 hover:text-slate-700 h-9"
|
||||||
|
onClick={() => {
|
||||||
|
setFromDate('');
|
||||||
|
setToDate('');
|
||||||
|
setStatusFilter('all');
|
||||||
|
setLocationFilter('all');
|
||||||
|
setStateFilter('all');
|
||||||
|
setSearchQuery('');
|
||||||
|
}}
|
||||||
|
data-testid="onboarding-opp-requests-clear-filters"
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Select value={stateFilter} onValueChange={setStateFilter}>
|
<Select value={stateFilter} onValueChange={setStateFilter}>
|
||||||
<SelectTrigger className="w-full md:w-48" data-testid="onboarding-opp-requests-state-select">
|
<SelectTrigger className="w-full md:w-48" data-testid="onboarding-opp-requests-state-select">
|
||||||
<SelectValue placeholder="Filter by state" />
|
<SelectValue placeholder="Filter by state" />
|
||||||
@ -449,29 +478,55 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 flex-1 md:flex-none">
|
<div className="flex items-center gap-2 flex-1 md:flex-none">
|
||||||
<div className="relative w-full md:w-40">
|
<Popover>
|
||||||
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
|
<PopoverTrigger asChild>
|
||||||
<Input
|
<Button
|
||||||
type="date"
|
variant="outline"
|
||||||
value={fromDate}
|
className={cn(
|
||||||
onChange={(e) => setFromDate(e.target.value)}
|
"w-full md:w-40 justify-start text-left font-normal h-9 px-3",
|
||||||
className="pl-10 text-xs"
|
!fromDate && "text-muted-foreground"
|
||||||
placeholder="From"
|
)}
|
||||||
data-testid="onboarding-opp-requests-from-date"
|
data-testid="onboarding-opp-requests-from-date-trigger"
|
||||||
|
>
|
||||||
|
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
|
||||||
|
{fromDate ? format(new Date(fromDate), "PPP") : <span className="text-xs">From Date</span>}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<CalendarPicker
|
||||||
|
mode="single"
|
||||||
|
selected={fromDate ? new Date(fromDate) : undefined}
|
||||||
|
onSelect={(date) => setFromDate(date ? date.toISOString().split('T')[0] : '')}
|
||||||
|
initialFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<span className="text-slate-400">to</span>
|
<span className="text-slate-400">to</span>
|
||||||
<div className="relative w-full md:w-40">
|
|
||||||
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
|
<Popover>
|
||||||
<Input
|
<PopoverTrigger asChild>
|
||||||
type="date"
|
<Button
|
||||||
value={toDate}
|
variant="outline"
|
||||||
onChange={(e) => setToDate(e.target.value)}
|
className={cn(
|
||||||
className="pl-10 text-xs"
|
"w-full md:w-40 justify-start text-left font-normal h-9 px-3",
|
||||||
placeholder="To"
|
!toDate && "text-muted-foreground"
|
||||||
data-testid="onboarding-opp-requests-to-date"
|
)}
|
||||||
|
data-testid="onboarding-opp-requests-to-date-trigger"
|
||||||
|
>
|
||||||
|
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
|
||||||
|
{toDate ? format(new Date(toDate), "PPP") : <span className="text-xs">To Date</span>}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<CalendarPicker
|
||||||
|
mode="single"
|
||||||
|
selected={toDate ? new Date(toDate) : undefined}
|
||||||
|
onSelect={(date) => setToDate(date ? date.toISOString().split('T')[0] : '')}
|
||||||
|
initialFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
@ -526,9 +581,14 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleBulkReminders}
|
onClick={handleBulkReminders}
|
||||||
|
disabled={isSendingReminders}
|
||||||
data-testid="onboarding-opp-requests-bulk-reminder-btn"
|
data-testid="onboarding-opp-requests-bulk-reminder-btn"
|
||||||
>
|
>
|
||||||
|
{isSendingReminders ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
<Mail className="w-4 h-4 mr-2" />
|
<Mail className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
Send Reminders ({selectedIds.length})
|
Send Reminders ({selectedIds.length})
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,8 @@ const STAGE_TO_ROLE_MAP: Record<string, string> = {
|
|||||||
const TERMINAL_STAGE_LABELS = ['REJECTED', 'Rejected', 'REVOKED', 'Revoked', 'WITHDRAWN', 'Withdrawn'];
|
const TERMINAL_STAGE_LABELS = ['REJECTED', 'Rejected', 'REVOKED', 'Revoked', 'WITHDRAWN', 'Withdrawn'];
|
||||||
|
|
||||||
const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
|
const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
|
||||||
'ASM': ['ASM', 'ASM Review', 'Submission', 'Submitted'],
|
'Request Submitted': ['Submission', 'Submitted', 'Initiation'],
|
||||||
|
'ASM': ['ASM', 'ASM Review'],
|
||||||
'RBM': ['RBM', 'RBM Review', 'Regional Review', 'RBM + DD-ZM Review'],
|
'RBM': ['RBM', 'RBM Review', 'Regional Review', 'RBM + DD-ZM Review'],
|
||||||
'ZBH': ['ZBH', 'ZBH Review', 'ZM Review'],
|
'ZBH': ['ZBH', 'ZBH Review', 'ZM Review'],
|
||||||
'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'],
|
'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'],
|
||||||
@ -137,18 +138,19 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
|
|
||||||
// Progress stages logic based on live data
|
// Progress stages logic based on live data
|
||||||
const progressStages = [
|
const progressStages = [
|
||||||
{ id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
|
{ id: 1, name: 'Request Submitted', key: 'Request Submitted', description: 'Dealer submitted the resignation request' },
|
||||||
{ id: 2, name: 'RBM + DD-ZM Review', key: 'RBM', description: 'Joint approval by Regional Business Manager and DD-ZM' },
|
{ id: 2, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
|
||||||
{ id: 3, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
|
{ id: 3, name: 'RBM + DD-ZM Review', key: 'RBM', description: 'Joint approval by Regional Business Manager and DD-ZM' },
|
||||||
{ id: 4, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
|
{ id: 4, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
|
||||||
{ id: 5, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
{ id: 5, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
|
||||||
{ id: 6, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
|
{ id: 6, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
||||||
{ id: 7, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification and final closure' },
|
{ id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
|
||||||
{ id: 8, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
|
{ id: 8, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification and final closure' },
|
||||||
{ id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
|
{ id: 9, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
|
||||||
|
{ id: 10, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'Legal', 'DD Admin', 'F&F Initiated', 'Completed'];
|
const stagesOrdered = ['Request Submitted', 'ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'Legal', 'DD Admin', 'F&F Initiated', 'Completed'];
|
||||||
|
|
||||||
const legalStageApproved = (() => {
|
const legalStageApproved = (() => {
|
||||||
if (!resignationData) return false;
|
if (!resignationData) return false;
|
||||||
@ -216,7 +218,18 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
!isSettlementPhase &&
|
!isSettlementPhase &&
|
||||||
!hasAlreadyPartiallyApproved &&
|
!hasAlreadyPartiallyApproved &&
|
||||||
!(currentStage === 'Legal' && legalStageApproved) &&
|
!(currentStage === 'Legal' && legalStageApproved) &&
|
||||||
!(isDDLead && isDDLeadStage && !hasUploadedPPT);
|
!(isDDLead && isDDLeadStage && !hasUploadedPPT) &&
|
||||||
|
!(currentStage === 'DD Admin' && !isLwdReached);
|
||||||
|
|
||||||
|
const isLwdReached = (() => {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const lwdString = resignationData.lastOperationalDateServices || resignationData.lastOperationalDateSales;
|
||||||
|
if (!lwdString) return true;
|
||||||
|
const lwd = new Date(lwdString);
|
||||||
|
lwd.setHours(0, 0, 0, 0);
|
||||||
|
return today >= lwd;
|
||||||
|
})();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canApprove,
|
canApprove,
|
||||||
@ -224,7 +237,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState,
|
canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState,
|
||||||
canRevoke: (userRoleCode === 'SUPER_ADMIN' || userRole === 'DD Admin') && !isFinalState && !isSettlementPhase,
|
canRevoke: (userRoleCode === 'SUPER_ADMIN' || userRole === 'DD Admin') && !isFinalState && !isSettlementPhase,
|
||||||
canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) &&
|
canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) &&
|
||||||
!isSettlementPhase && !isFinalState,
|
!isSettlementPhase && !isFinalState && currentStage === 'Legal' && isLwdReached,
|
||||||
canAssign: userRole !== 'Dealer' && !isFinalState
|
canAssign: userRole !== 'Dealer' && !isFinalState
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1020,29 +1033,29 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
<Table className="w-full border-collapse">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow className="bg-slate-50/50">
|
||||||
<TableHead>Stage</TableHead>
|
<TableHead className="min-w-[120px]">Stage</TableHead>
|
||||||
<TableHead>Approver</TableHead>
|
<TableHead className="min-w-[120px]">Approver</TableHead>
|
||||||
<TableHead>Action</TableHead>
|
<TableHead className="min-w-[200px]">Action</TableHead>
|
||||||
<TableHead>Remarks</TableHead>
|
<TableHead className="w-full min-w-[300px]">Remarks</TableHead>
|
||||||
<TableHead>Date</TableHead>
|
<TableHead className="min-w-[180px] text-right">Date</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(resignationData?.timeline || []).length > 0 ? (
|
{(resignationData?.timeline || []).length > 0 ? (
|
||||||
resignationData.timeline.map((entry: any, index: number) => (
|
resignationData.timeline.map((entry: any, index: number) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
<TableCell className="font-medium whitespace-nowrap">{entry.stage}</TableCell>
|
<TableCell className="font-medium">{entry.stage}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline">{entry.user || 'System'}</Badge>
|
<Badge variant="outline">{entry.user || 'System'}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="whitespace-nowrap">{entry.action}</TableCell>
|
<TableCell className="whitespace-normal break-words">{entry.action}</TableCell>
|
||||||
<TableCell className="max-w-[400px]">
|
<TableCell className="whitespace-normal break-words">
|
||||||
{entry.remarks || entry.comments || '-'}
|
{entry.remarks || entry.comments || '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-500 whitespace-nowrap">
|
<TableCell className="text-slate-500 whitespace-nowrap text-right">
|
||||||
{formatDateTime(entry.timestamp || entry.createdAt)}
|
{formatDateTime(entry.timestamp || entry.createdAt)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck, Loader2, Upload } from 'lucide-react';
|
import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck, Loader2, Upload, PauseCircle } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
@ -16,6 +16,11 @@ import { terminationService } from '@/services/termination.service';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { API } from '@/api/API';
|
import { API } from '@/api/API';
|
||||||
import { formatDateTime } from '@/components/ui/utils';
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
|
import { formatTerminationStatusLabel } from '@/lib/terminationDisplay';
|
||||||
|
import {
|
||||||
|
getJointRoundCutoffMsFromTimeline,
|
||||||
|
isAuditLogInCurrentJointRound
|
||||||
|
} from '@/lib/terminationJointReviewRound';
|
||||||
import { TERMINATION_DOCUMENT_TYPES, TERMINATION_STAGE_OPTIONS } from '@/lib/offboardingDocumentOptions';
|
import { TERMINATION_DOCUMENT_TYPES, TERMINATION_STAGE_OPTIONS } from '@/lib/offboardingDocumentOptions';
|
||||||
import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal';
|
import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal';
|
||||||
import { WIDE_DIALOG_CLASS } from '@/lib/dialogStyles';
|
import { WIDE_DIALOG_CLASS } from '@/lib/dialogStyles';
|
||||||
@ -27,7 +32,7 @@ interface TerminationDetailsProps {
|
|||||||
|
|
||||||
export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) {
|
export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | null }>({ open: false, type: null });
|
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'hold' | null }>({ open: false, type: null });
|
||||||
const [remarks, setRemarks] = useState('');
|
const [remarks, setRemarks] = useState('');
|
||||||
const [assignToUser, setAssignToUser] = useState('');
|
const [assignToUser, setAssignToUser] = useState('');
|
||||||
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
||||||
@ -155,6 +160,40 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
// Check if user can push to F&F (DD Lead and above)
|
// Check if user can push to F&F (DD Lead and above)
|
||||||
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'DD_HEAD', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role || currentUser.roleCode);
|
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'DD_HEAD', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role || currentUser.roleCode);
|
||||||
|
|
||||||
|
const stageAliases: Record<string, string[]> = {
|
||||||
|
'Submitted': ['Submitted'],
|
||||||
|
'RBM + DD-ZM Review': ['RBM + DD-ZM Review'],
|
||||||
|
'ZBH Review': ['ZBH Review'],
|
||||||
|
'DD Lead Review': ['DD Lead Review'],
|
||||||
|
'Legal Verification': ['Legal Verification'],
|
||||||
|
'DD Head Review': ['DD Head Review'],
|
||||||
|
'NBH Evaluation': ['NBH Evaluation'],
|
||||||
|
'Show Cause Notice (SCN)': ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'],
|
||||||
|
'Evaluation of Dealer SCN Response': ['Evaluation of Dealer SCN Response', 'Personal Hearing'],
|
||||||
|
'NBH Final Approval': ['NBH Final Approval'],
|
||||||
|
'CCO Approval': ['CCO Approval'],
|
||||||
|
'CEO Final Approval': ['CEO Final Approval'],
|
||||||
|
'Legal - Termination Letter': ['Legal - Termination Letter'],
|
||||||
|
'Dealer Terminated': ['Terminated', 'Dealer Terminated']
|
||||||
|
};
|
||||||
|
|
||||||
|
const stageSequence = [
|
||||||
|
'Submitted',
|
||||||
|
'RBM + DD-ZM Review',
|
||||||
|
'ZBH Review',
|
||||||
|
'DD Lead Review',
|
||||||
|
'Legal Verification',
|
||||||
|
'DD Head Review',
|
||||||
|
'NBH Evaluation',
|
||||||
|
'Show Cause Notice (SCN)',
|
||||||
|
'Evaluation of Dealer SCN Response',
|
||||||
|
'NBH Final Approval',
|
||||||
|
'CCO Approval',
|
||||||
|
'CEO Final Approval',
|
||||||
|
'Legal - Termination Letter',
|
||||||
|
'Dealer Terminated'
|
||||||
|
];
|
||||||
|
|
||||||
// Centralized Permissions Utility for Termination logic (Robust Validation)
|
// Centralized Permissions Utility for Termination logic (Robust Validation)
|
||||||
const getTerminationPermissions = () => {
|
const getTerminationPermissions = () => {
|
||||||
if (!terminationData || !currentUser) {
|
if (!terminationData || !currentUser) {
|
||||||
@ -164,54 +203,67 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
const currentStage = terminationData.currentStage;
|
const currentStage = terminationData.currentStage;
|
||||||
const status = terminationData.status;
|
const status = terminationData.status;
|
||||||
const userRole = currentUser.role || currentUser.roleCode;
|
const userRole = currentUser.role || currentUser.roleCode;
|
||||||
const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage);
|
const isScnStage = ['Show Cause Notice (SCN)', 'SCN'].includes(currentStage);
|
||||||
|
|
||||||
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated';
|
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated';
|
||||||
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
|
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
|
||||||
|
|
||||||
|
const scnJointRoundCutoffMs = getJointRoundCutoffMsFromTimeline(terminationData.timeline, 'scn_response_eval');
|
||||||
|
const rbmJointRoundCutoffMs = getJointRoundCutoffMsFromTimeline(terminationData.timeline, 'rbm_review');
|
||||||
|
const isScnResponseEvalStage =
|
||||||
|
currentStage === 'Evaluation of Dealer SCN Response' || currentStage === 'Personal Hearing';
|
||||||
|
const jointRoundCutoffMs =
|
||||||
|
currentStage === 'RBM + DD-ZM Review' ? rbmJointRoundCutoffMs : isScnResponseEvalStage ? scnJointRoundCutoffMs : null;
|
||||||
|
|
||||||
const userHasApprovedJointly = auditLogs.some(log => {
|
const userHasApprovedJointly = auditLogs.some(log => {
|
||||||
|
if (!isAuditLogInCurrentJointRound(log, jointRoundCutoffMs)) return false;
|
||||||
const logUserId = log.userId || log.user?.id || log.actor?.id || log.actorId;
|
const logUserId = log.userId || log.user?.id || log.actor?.id || log.actorId;
|
||||||
const isThisUser = String(logUserId) === String(currentUser.id);
|
const isThisUser = String(logUserId) === String(currentUser.id);
|
||||||
const actionText = (log.action || log.description || '').toUpperCase();
|
const actionText = (log.action || log.description || '').toUpperCase();
|
||||||
const isPartialApprove = actionText.includes('PARTIAL_APPROVE') || actionText.includes('PARTIAL APPROVED');
|
const isPartialApprove = actionText.includes('PARTIAL_APPROVE') || actionText.includes('PARTIAL APPROVED');
|
||||||
|
|
||||||
|
const logStage = log.details?.stage || log.stage || '';
|
||||||
|
const isRbmReviewLog = logStage === 'RBM + DD-ZM Review' || (log.remarks || '').includes('Partial approval by');
|
||||||
|
const isPersonalHearingLog =
|
||||||
|
logStage === 'Evaluation of Dealer SCN Response' ||
|
||||||
|
logStage === 'Personal Hearing' ||
|
||||||
|
(log.remarks || '').includes('SCN Response Review by');
|
||||||
|
|
||||||
const stageMatches =
|
const stageMatches =
|
||||||
log.details?.stage === 'RBM + DD-ZM Review' ||
|
(currentStage === 'RBM + DD-ZM Review' && isRbmReviewLog) ||
|
||||||
log.stage === 'RBM + DD-ZM Review' ||
|
(isScnResponseEvalStage && isPersonalHearingLog);
|
||||||
(log.remarks || '').includes('Partial approval by');
|
|
||||||
|
|
||||||
const result = isThisUser && isPartialApprove && stageMatches;
|
return isThisUser && isPartialApprove && stageMatches;
|
||||||
if (result) console.log('[TerminationDebug] Found matching partial approval log:', log);
|
|
||||||
return result;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentStage === 'RBM + DD-ZM Review' && (userRole === 'RBM' || userRole === 'DD-ZM')) {
|
const isStage = (stageName: string) => {
|
||||||
console.log('[TerminationDebug] Joint Stage Detection:', {
|
const aliases = stageAliases[stageName] || [stageName];
|
||||||
currentStage,
|
return aliases.includes(currentStage);
|
||||||
userRole,
|
};
|
||||||
userId: currentUser.id,
|
|
||||||
userHasApprovedJointly,
|
|
||||||
auditLogsCount: auditLogs.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === 'DD Admin' || (
|
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === 'DD Admin' || (
|
||||||
(currentStage === 'RBM + DD-ZM Review' && (userRole === 'RBM' || userRole === 'DD-ZM') && !userHasApprovedJointly) ||
|
(isStage('RBM + DD-ZM Review') && (userRole === 'RBM' || userRole === 'DD-ZM') && !userHasApprovedJointly) ||
|
||||||
(currentStage === 'ZBH Review' && userRole === 'ZBH') ||
|
(isStage('ZBH Review') && userRole === 'ZBH') ||
|
||||||
(currentStage === 'DD Lead Review' && userRole === 'DD Lead') ||
|
(isStage('DD Lead Review') && userRole === 'DD Lead') ||
|
||||||
(currentStage === 'Legal Verification' && userRole === 'Legal Admin') ||
|
(isStage('Legal Verification') && userRole === 'Legal Admin') ||
|
||||||
(currentStage === 'DD Head Review' && (userRole === 'DD Head' || userRole === 'DD_HEAD')) ||
|
(isStage('DD Head Review') && (userRole === 'DD Head' || userRole === 'DD_HEAD')) ||
|
||||||
(currentStage === 'NBH Evaluation' && userRole === 'NBH') ||
|
(isStage('NBH Evaluation') && userRole === 'NBH') ||
|
||||||
(currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
|
(isStage('Evaluation of Dealer SCN Response') && ['DD Lead', 'ZBH', 'RBM', 'DD Head', 'DD_HEAD'].includes(userRole) && !userHasApprovedJointly) ||
|
||||||
(currentStage === 'CCO Approval' && userRole === 'CCO') ||
|
(isStage('NBH Final Approval') && userRole === 'NBH') ||
|
||||||
(currentStage === 'CEO Final Approval' && userRole === 'CEO') ||
|
(isStage('CCO Approval') && userRole === 'CCO') ||
|
||||||
(currentStage === 'Legal - Termination Letter' && userRole === 'Legal Admin')
|
(isStage('CEO Final Approval') && userRole === 'CEO') ||
|
||||||
|
(isStage('Legal - Termination Letter') && userRole === 'Legal Admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && ![...['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'], 'Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage),
|
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && ![...['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'], 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage),
|
||||||
canIssueSCN: currentStage === 'NBH Evaluation' && (userRole === 'NBH' || userRole === 'Super Admin') && !isFinalState,
|
canIssueSCN: currentStage === 'NBH Evaluation' && (userRole === 'NBH' || userRole === 'Super Admin') && !isFinalState,
|
||||||
canUploadSCNResponse: isScnStage && (['Legal Admin', 'DD Admin', 'Super Admin'].includes(userRole)) && !isFinalState,
|
canUploadSCNResponse: isScnStage && (['Legal Admin', 'DD Admin', 'DD Lead', 'Super Admin'].includes(userRole)) && !isFinalState,
|
||||||
|
canHold:
|
||||||
|
(isStage('NBH Evaluation') || isStage('NBH Final Approval')) &&
|
||||||
|
(userRole === 'NBH' || userRole === 'Super Admin') &&
|
||||||
|
status !== 'On Hold' &&
|
||||||
|
!isFinalState,
|
||||||
canFinalize: (
|
canFinalize: (
|
||||||
(currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
|
(currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
|
||||||
(currentStage === 'CCO Approval' && userRole === 'CCO') ||
|
(currentStage === 'CCO Approval' && userRole === 'CCO') ||
|
||||||
@ -231,39 +283,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
const request = terminationData || {};
|
const request = terminationData || {};
|
||||||
const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(request.currentStage);
|
const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(request.currentStage);
|
||||||
|
|
||||||
const stageAliases: Record<string, string[]> = {
|
|
||||||
'Submitted': ['Submitted'],
|
|
||||||
'RBM + DD-ZM Review': ['RBM + DD-ZM Review'],
|
|
||||||
'ZBH Review': ['ZBH Review'],
|
|
||||||
'DD Lead Review': ['DD Lead Review'],
|
|
||||||
'Legal Verification': ['Legal Verification'],
|
|
||||||
'DD Head Review': ['DD Head Review'],
|
|
||||||
'NBH Evaluation': ['NBH Evaluation'],
|
|
||||||
'Show Cause Notice (SCN)': ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'],
|
|
||||||
'Personal Hearing': ['Personal Hearing'],
|
|
||||||
'NBH Final Approval': ['NBH Final Approval'],
|
|
||||||
'CCO Approval': ['CCO Approval'],
|
|
||||||
'CEO Final Approval': ['CEO Final Approval'],
|
|
||||||
'Legal - Termination Letter': ['Legal - Termination Letter'],
|
|
||||||
'Dealer Terminated': ['Terminated', 'Dealer Terminated']
|
|
||||||
};
|
|
||||||
|
|
||||||
const stageSequence = [
|
|
||||||
'Submitted',
|
|
||||||
'RBM + DD-ZM Review',
|
|
||||||
'ZBH Review',
|
|
||||||
'DD Lead Review',
|
|
||||||
'Legal Verification',
|
|
||||||
'DD Head Review',
|
|
||||||
'NBH Evaluation',
|
|
||||||
'Show Cause Notice (SCN)',
|
|
||||||
'Personal Hearing',
|
|
||||||
'NBH Final Approval',
|
|
||||||
'CCO Approval',
|
|
||||||
'CEO Final Approval',
|
|
||||||
'Legal - Termination Letter',
|
|
||||||
'Dealer Terminated'
|
|
||||||
];
|
|
||||||
|
|
||||||
const resolveCanonicalStage = (currentStage?: string) => {
|
const resolveCanonicalStage = (currentStage?: string) => {
|
||||||
if (!currentStage) return '';
|
if (!currentStage) return '';
|
||||||
@ -396,9 +415,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
name: 'Personal Hearing',
|
name: 'Evaluation of Dealer SCN Response',
|
||||||
status: getProgressStatus('Personal Hearing'),
|
status: getProgressStatus('Evaluation of Dealer SCN Response'),
|
||||||
description: 'Evaluation of SCN response & Hearing'
|
description: 'Joint evaluation of SCN response by DD-Lead, ZBH, RBM, and DD-Head'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
@ -442,7 +461,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
setStageDocumentsDialog({ open: true, stageName, documents });
|
setStageDocumentsDialog({ open: true, stageName, documents });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAction = (type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke') => {
|
const handleAction = (type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'hold') => {
|
||||||
setActionDialog({ open: true, type });
|
setActionDialog({ open: true, type });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -471,7 +490,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
try {
|
try {
|
||||||
let response: any;
|
let response: any;
|
||||||
if (actionType === 'approve' || actionType === 'sendBack' || actionType === 'withdrawal' || actionType === 'revoke') {
|
if (actionType === 'approve' || actionType === 'sendBack' || actionType === 'withdrawal' || actionType === 'revoke' || actionType === 'hold') {
|
||||||
response = await terminationService.updateTerminationStatus(terminationId, effectiveAction, remarks);
|
response = await terminationService.updateTerminationStatus(terminationId, effectiveAction, remarks);
|
||||||
} else if (actionType === 'pushfnf') {
|
} else if (actionType === 'pushfnf') {
|
||||||
response = await terminationService.updateTerminationStatus(terminationId, 'pushfnf', remarks);
|
response = await terminationService.updateTerminationStatus(terminationId, 'pushfnf', remarks);
|
||||||
@ -556,7 +575,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
? 'bg-red-100 text-red-700 border-red-300'
|
? 'bg-red-100 text-red-700 border-red-300'
|
||||||
: 'bg-yellow-100 text-yellow-700 border-yellow-300'
|
: 'bg-yellow-100 text-yellow-700 border-yellow-300'
|
||||||
}>
|
}>
|
||||||
{request.status === 'Settled' ? 'Completed' : (request.status || 'Pending')}
|
{request.status === 'Settled' ? 'Completed' : formatTerminationStatusLabel(request.status || 'Pending')}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -565,6 +584,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<Card className="border-amber-200 shadow-sm">
|
<Card className="border-amber-200 shadow-sm">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
{(request.currentStage === 'Evaluation of Dealer SCN Response' || request.currentStage === 'Personal Hearing') && (
|
||||||
|
<Alert className="mb-4 bg-blue-50 border-blue-200">
|
||||||
|
<AlertTitle className="text-blue-800 text-sm font-semibold">Joint Review Stage</AlertTitle>
|
||||||
|
<AlertDescription className="text-blue-700 text-xs">
|
||||||
|
This stage requires a joint evaluation of the SCN response by the <strong>DD-Lead, ZBH, RBM, and DD-Head</strong>.
|
||||||
|
The case will only advance to NBH Final Approval once all four stakeholders have recorded their review.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
{/* Primary Actions Row */}
|
{/* Primary Actions Row */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -619,6 +647,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{permissions.canHold && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="border-orange-200 text-orange-700 hover:bg-orange-50"
|
||||||
|
onClick={() => handleAction('hold')}
|
||||||
|
>
|
||||||
|
<PauseCircle className="w-4 h-4 mr-2" />
|
||||||
|
Hold Decision
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{permissions.canFinalize && (
|
{permissions.canFinalize && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -945,7 +984,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
{entry.action || 'Action'}
|
{entry.action || 'Action'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-[10px] text-slate-500 font-medium">
|
<span className="text-[10px] text-slate-500 font-medium">
|
||||||
by {entry.user || 'System'} • {formatDateTime(entry.timestamp)}
|
by {entry.user || 'System'}{entry.role ? ` (${entry.role})` : ''} • {formatDateTime(entry.timestamp)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`
|
<div className={`
|
||||||
@ -1123,6 +1162,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
{actionDialog.type === 'revoke' && 'Revoke Termination Request'}
|
{actionDialog.type === 'revoke' && 'Revoke Termination Request'}
|
||||||
{actionDialog.type === 'assign' && 'Assign to User'}
|
{actionDialog.type === 'assign' && 'Assign to User'}
|
||||||
{actionDialog.type === 'pushfnf' && 'Push to Full & Final Settlement'}
|
{actionDialog.type === 'pushfnf' && 'Push to Full & Final Settlement'}
|
||||||
|
{actionDialog.type === 'hold' && 'Hold Termination Case'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{actionDialog.type === 'assign'
|
{actionDialog.type === 'assign'
|
||||||
@ -1185,6 +1225,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
className={
|
className={
|
||||||
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
||||||
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
|
||||||
|
actionDialog.type === 'hold' ? 'bg-orange-600 hover:bg-orange-700' :
|
||||||
'bg-blue-600 hover:bg-blue-700'
|
'bg-blue-600 hover:bg-blue-700'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
} from "@/components/ui/pagination";
|
} from "@/components/ui/pagination";
|
||||||
import { User } from '@/lib/mock-data';
|
import { User } from '@/lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { formatTerminationStatusLabel } from '@/lib/terminationDisplay';
|
||||||
|
|
||||||
interface TerminationPageProps {
|
interface TerminationPageProps {
|
||||||
currentUser: User | null;
|
currentUser: User | null;
|
||||||
@ -51,6 +52,8 @@ const getStatusColor = (status: string) => {
|
|||||||
return 'bg-blue-100 text-blue-700 border-blue-300';
|
return 'bg-blue-100 text-blue-700 border-blue-300';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatStatus = formatTerminationStatusLabel;
|
||||||
|
|
||||||
export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) {
|
export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [dealers, setDealers] = useState<any[]>([]);
|
const [dealers, setDealers] = useState<any[]>([]);
|
||||||
@ -103,7 +106,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDialogOpen || !isDDLead) return;
|
if (!isDialogOpen || !canCreateTermination) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -254,7 +257,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDDLead = currentUser?.role === 'DD Lead';
|
const allowedRoles = ['DD Lead', 'ASM', 'DD Admin', 'DD AM', 'Super Admin'];
|
||||||
|
const canCreateTermination = currentUser?.role && allowedRoles.includes(currentUser.role);
|
||||||
|
|
||||||
// Map terminations to tab-specific views (already filtered by backend, but need variables for render)
|
// Map terminations to tab-specific views (already filtered by backend, but need variables for render)
|
||||||
const openRequests = activeTab === 'open' || activeTab === 'all' ? terminations : [];
|
const openRequests = activeTab === 'open' || activeTab === 'all' ? terminations : [];
|
||||||
@ -312,14 +316,9 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
<CardTitle>Termination Requests</CardTitle>
|
<CardTitle>Termination Requests</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Manage dealer termination proceedings and legal compliance
|
Manage dealer termination proceedings and legal compliance
|
||||||
{!isDDLead && (
|
|
||||||
<span className="block mt-1 text-red-600">
|
|
||||||
• Note: Only DD Lead can create termination requests. Current role: {currentUser?.role || 'Not logged in'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{isDDLead && (
|
{canCreateTermination && (
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="bg-red-600 hover:bg-red-700">
|
<Button className="bg-red-600 hover:bg-red-700">
|
||||||
@ -408,7 +407,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="Working Capital">Working Capital</SelectItem>
|
<SelectItem value="Working Capital">Working Capital</SelectItem>
|
||||||
<SelectItem value="Performance Issues">Performance Issues</SelectItem>
|
<SelectItem value="Performance Issues">Performance Issues</SelectItem>
|
||||||
<SelectItem value="Unethical Practical">Unethical Practical</SelectItem>
|
<SelectItem value="Unethical Practice">Unethical Practice</SelectItem>
|
||||||
<SelectItem value="Unforeseen Circumstances">Unforeseen Circumstances</SelectItem>
|
<SelectItem value="Unforeseen Circumstances">Unforeseen Circumstances</SelectItem>
|
||||||
<SelectItem value="Others">Others</SelectItem>
|
<SelectItem value="Others">Others</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -495,12 +494,12 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
||||||
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
|
|
||||||
<Badge className={getSeverityColor(request.severity || 'Medium')}>
|
<Badge className={getSeverityColor(request.severity || 'Medium')}>
|
||||||
{request.severity || 'Normal'}
|
{request.severity || 'Normal'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge className={getStatusColor(request.status)}>
|
<Badge className={getStatusColor(request.status)}>
|
||||||
{request.status}
|
{formatStatus(request.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
@ -518,7 +517,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Current Stage</p>
|
<p className="text-slate-600">Current Stage</p>
|
||||||
<p>{request.currentStage}</p>
|
<p>{formatStatus(request.currentStage)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Proposed LWD</p>
|
<p className="text-slate-600">Proposed LWD</p>
|
||||||
@ -570,9 +569,9 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
||||||
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
|
|
||||||
<Badge className={getStatusColor(request.status)}>
|
<Badge className={getStatusColor(request.status)}>
|
||||||
{request.status}
|
{formatStatus(request.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
@ -586,7 +585,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Current Stage</p>
|
<p className="text-slate-600">Current Stage</p>
|
||||||
<p>{request.currentStage}</p>
|
<p>{formatStatus(request.currentStage)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Submitted On</p>
|
<p className="text-slate-600">Submitted On</p>
|
||||||
@ -632,9 +631,9 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
||||||
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
|
|
||||||
<Badge className={getStatusColor(request.status)}>
|
<Badge className={getStatusColor(request.status)}>
|
||||||
{request.status}
|
{formatStatus(request.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export const RESIGNATION_DOCUMENT_TYPES = [
|
|||||||
"Resignation Letter",
|
"Resignation Letter",
|
||||||
"Dealer Undertaking",
|
"Dealer Undertaking",
|
||||||
"Approval Note",
|
"Approval Note",
|
||||||
"Legal Communication",
|
"Resignation Acceptance Letter",
|
||||||
"Handover Document",
|
"Handover Document",
|
||||||
"Settlement Supporting Document",
|
"Settlement Supporting Document",
|
||||||
"PPT Presentation",
|
"PPT Presentation",
|
||||||
@ -10,6 +10,7 @@ export const RESIGNATION_DOCUMENT_TYPES = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const RESIGNATION_STAGE_OPTIONS = [
|
export const RESIGNATION_STAGE_OPTIONS = [
|
||||||
|
"Request Submitted",
|
||||||
"ASM",
|
"ASM",
|
||||||
"RBM",
|
"RBM",
|
||||||
"ZBH",
|
"ZBH",
|
||||||
|
|||||||
5
src/lib/terminationDisplay.ts
Normal file
5
src/lib/terminationDisplay.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/** Legacy workflow used "Personal Hearing"; UI and newer APIs use "SCN Response Evaluation" wording. */
|
||||||
|
export function formatTerminationStatusLabel(value: string | null | undefined): string {
|
||||||
|
if (!value) return 'Pending';
|
||||||
|
return value.replace(/Personal Hearing/gi, 'SCN Response Evaluation');
|
||||||
|
}
|
||||||
64
src/lib/terminationJointReviewRound.ts
Normal file
64
src/lib/terminationJointReviewRound.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/** Mirrors backend terminationJointReviewRound.util.ts — keep send-back / reconsider detection aligned. */
|
||||||
|
|
||||||
|
const norm = (s: string | undefined | null) =>
|
||||||
|
String(s || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const SCN_CANONICAL = 'Evaluation of Dealer SCN Response';
|
||||||
|
|
||||||
|
export const isScnResponseJointTargetStage = (targetStage: string | undefined | null): boolean => {
|
||||||
|
const n = norm(targetStage);
|
||||||
|
if (!n) return false;
|
||||||
|
if (n === norm(SCN_CANONICAL)) return true;
|
||||||
|
if (n.includes('evaluation') && n.includes('scn') && n.includes('response')) return true;
|
||||||
|
if (n.includes('personal hearing')) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isRbmJointTargetStage = (targetStage: string | undefined | null): boolean => {
|
||||||
|
const n = norm(targetStage);
|
||||||
|
return n.includes('rbm') && (n.includes('dd-zm') || n.includes('dd zm'));
|
||||||
|
};
|
||||||
|
|
||||||
|
function isSendBackOrReconsiderTimelineAction(action: string | undefined | null): boolean {
|
||||||
|
const a = norm(action);
|
||||||
|
return (
|
||||||
|
a.includes('sent back') ||
|
||||||
|
a.includes('send back') ||
|
||||||
|
a.includes('reconsider') ||
|
||||||
|
a.includes('reconsideration')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JointRoundTimelineMode = 'scn_response_eval' | 'rbm_review';
|
||||||
|
|
||||||
|
export function getJointRoundCutoffMsFromTimeline(
|
||||||
|
timeline: unknown,
|
||||||
|
mode: JointRoundTimelineMode
|
||||||
|
): number | null {
|
||||||
|
if (!Array.isArray(timeline) || timeline.length === 0) return null;
|
||||||
|
const matcher = mode === 'scn_response_eval' ? isScnResponseJointTargetStage : isRbmJointTargetStage;
|
||||||
|
const arr = timeline as Record<string, unknown>[];
|
||||||
|
for (let i = arr.length - 1; i >= 0; i--) {
|
||||||
|
const e = arr[i];
|
||||||
|
if (!isSendBackOrReconsiderTimelineAction(e?.action as string)) continue;
|
||||||
|
if (!matcher(e?.targetStage as string)) continue;
|
||||||
|
const t = e?.timestamp != null ? new Date(e.timestamp as string | number | Date).getTime() : NaN;
|
||||||
|
if (!Number.isNaN(t)) return t;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditLogTimestampMs(log: { createdAt?: string | Date; timestamp?: string | Date }): number {
|
||||||
|
const raw = log.createdAt ?? log.timestamp;
|
||||||
|
if (raw == null) return 0;
|
||||||
|
const t = new Date(raw).getTime();
|
||||||
|
return Number.isNaN(t) ? 0 : t;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAuditLogInCurrentJointRound(log: { createdAt?: string | Date; timestamp?: string | Date }, cutoffMs: number | null): boolean {
|
||||||
|
if (cutoffMs == null) return true;
|
||||||
|
return auditLogTimestampMs(log) >= cutoffMs;
|
||||||
|
}
|
||||||
@ -115,6 +115,11 @@ export const onboardingService = {
|
|||||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to perform bulk conversion');
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to perform bulk conversion');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
sendBulkReminders: async (applicationIds: string[]) => {
|
||||||
|
const response: any = await API.sendBulkReminders({ applicationIds });
|
||||||
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to send reminders');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
createDealer: async (data: any) => {
|
createDealer: async (data: any) => {
|
||||||
const response: any = await API.createDealer(data);
|
const response: any = await API.createDealer(data);
|
||||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to create dealer profile');
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to create dealer profile');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user