few more bugs fixed and dealer ide ui alignmen and visibibity chabges done F& F made manuall trigger
This commit is contained in:
parent
ec70f1d3f1
commit
1340f44485
176
src/App.tsx
176
src/App.tsx
@ -57,12 +57,20 @@ import { API } from '@/api/API';
|
|||||||
import { SocketProvider } from '@/context/SocketContext';
|
import { SocketProvider } from '@/context/SocketContext';
|
||||||
|
|
||||||
// Layout Component
|
// Layout Component
|
||||||
const AppLayout = ({ onLogout, title }: { onLogout: () => void, title: string }) => {
|
const AppLayout = ({
|
||||||
|
onLogout,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
}: {
|
||||||
|
onLogout: () => void;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-slate-50 overflow-hidden">
|
<div className="flex h-screen bg-slate-50 overflow-hidden">
|
||||||
<Sidebar onLogout={onLogout} />
|
<Sidebar onLogout={onLogout} />
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<Header title={title} onRefresh={() => window.location.reload()} />
|
<Header title={title} subtitle={subtitle} onRefresh={() => window.location.reload()} />
|
||||||
<main className="flex-1 overflow-y-auto p-6">
|
<main className="flex-1 overflow-y-auto p-6">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
@ -139,8 +147,18 @@ export default function App() {
|
|||||||
// Helper to determine page title based on path
|
// Helper to determine page title based on path
|
||||||
const getPageTitle = (pathname: string) => {
|
const getPageTitle = (pathname: string) => {
|
||||||
if (pathname.startsWith('/applications/') && pathname.length > 14) return 'Application Details';
|
if (pathname.startsWith('/applications/') && pathname.length > 14) return 'Application Details';
|
||||||
if (pathname.includes('/resignation/') && pathname.length > 13) return 'Resignation Details';
|
if (pathname.startsWith('/resignation/') && !pathname.startsWith('/dealer-resignation')) return 'Resignation Details';
|
||||||
// ... Add more dynamic title logic as needed
|
if (pathname.startsWith('/dealer-resignation/')) return 'Resignation Request Details';
|
||||||
|
if (pathname.startsWith('/termination/')) return 'Termination Details';
|
||||||
|
if (pathname.startsWith('/fnf/')) return 'F&F Request Details';
|
||||||
|
if (pathname.startsWith('/constitutional-change/')) return 'Constitutional Change Details';
|
||||||
|
if (pathname.startsWith('/relocation-requests/')) return 'Relocation Request Details';
|
||||||
|
if (pathname.startsWith('/finance-onboarding/')) return 'Payment Details';
|
||||||
|
if (pathname.startsWith('/finance-audit/')) return 'Finance Audit';
|
||||||
|
if (pathname.startsWith('/finance-fnf/')) return 'F&F Settlement Details';
|
||||||
|
if (pathname.startsWith('/fdd-details/')) return 'FDD Audit Workspace';
|
||||||
|
if (pathname.startsWith('/questionnaire-builder/')) return 'Questionnaire Builder';
|
||||||
|
if (pathname.startsWith('/worknotes/')) return 'Work Notes';
|
||||||
const titles: Record<string, string> = {
|
const titles: Record<string, string> = {
|
||||||
'/dashboard': 'Dashboard',
|
'/dashboard': 'Dashboard',
|
||||||
'/applications': 'Dealership Requests',
|
'/applications': 'Dealership Requests',
|
||||||
@ -166,11 +184,155 @@ export default function App() {
|
|||||||
'/approval-policies': 'Approval Policies',
|
'/approval-policies': 'Approval Policies',
|
||||||
'/fdd-dashboard': 'FDD Dashboard',
|
'/fdd-dashboard': 'FDD Dashboard',
|
||||||
'/fdd-details': 'Audit Workspace',
|
'/fdd-details': 'Audit Workspace',
|
||||||
|
'/questions': 'Questionnaires',
|
||||||
|
'/questionnaires': 'Questionnaire Templates',
|
||||||
|
'/interview-configs': 'Interview Configuration',
|
||||||
|
'/system-logs': 'System Logs',
|
||||||
|
'/sla-configurations': 'SLA Matrix',
|
||||||
'/notifications': 'Notifications',
|
'/notifications': 'Notifications',
|
||||||
};
|
};
|
||||||
return titles[pathname] || 'Dashboard';
|
return titles[pathname] || 'Dashboard';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Short context line under the main header title — varies by route (and sometimes role). */
|
||||||
|
const getPageSubtitle = (pathname: string, role: string) => {
|
||||||
|
const rl = (role || '').toLowerCase();
|
||||||
|
const isDealerRole = rl === 'dealer' || rl.includes('dealer');
|
||||||
|
const isFinanceRole = rl.includes('finance');
|
||||||
|
|
||||||
|
if (pathname.startsWith('/worknotes/')) {
|
||||||
|
return 'Collaborative notes and clarifications linked to this onboarding or offboarding record.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/applications/') && pathname !== '/applications') {
|
||||||
|
return 'Review stages, documents, interviews, and decisions for this single dealership onboarding application.';
|
||||||
|
}
|
||||||
|
if (pathname === '/applications') {
|
||||||
|
return 'Search, filter, and open dealership applications your role can work on.';
|
||||||
|
}
|
||||||
|
if (pathname === '/all-applications') {
|
||||||
|
return 'Cross-team view of every dealership application in the pipeline.';
|
||||||
|
}
|
||||||
|
if (pathname === '/opportunity-requests') {
|
||||||
|
return 'Applications tied to an opportunity location for DD prioritisation.';
|
||||||
|
}
|
||||||
|
if (pathname === '/non-opportunities') {
|
||||||
|
return 'Applications without a mapped opportunity; track for future reference or follow-up.';
|
||||||
|
}
|
||||||
|
if (pathname === '/dashboard') {
|
||||||
|
if (isDealerRole) {
|
||||||
|
return 'Your home for outlet actions: constitutional change (how your business is legally registered), relocation, and resignation requests.';
|
||||||
|
}
|
||||||
|
if (isFinanceRole) {
|
||||||
|
return 'Payment verification, finance audits, and F&F settlement work for dealership accounts.';
|
||||||
|
}
|
||||||
|
return 'Operational snapshot for dealership onboarding: workloads, alerts, and shortcuts for your role.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/dealer-resignation/')) {
|
||||||
|
return 'Read-only summary of the resignation you submitted for this outlet.';
|
||||||
|
}
|
||||||
|
if (pathname === '/dealer-resignation') {
|
||||||
|
return 'Start a new outlet resignation or open a request you already submitted.';
|
||||||
|
}
|
||||||
|
if (pathname === '/dealer-constitutional') {
|
||||||
|
return 'Constitutional change updates your outlet’s registered legal structure (for example sole proprietorship to private limited). Submit one request per outlet; Royal Enfield teams review documents and approve before records change.';
|
||||||
|
}
|
||||||
|
if (pathname === '/dealer-relocation') {
|
||||||
|
return 'Request a move of your dealership to a new address or territory, and track requests in progress.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/constitutional-change/')) {
|
||||||
|
return 'Review evidence, comments, and workflow for this constitutional change case.';
|
||||||
|
}
|
||||||
|
if (pathname === '/constitutional-change') {
|
||||||
|
return 'Process dealer requests to change registered legal constitution, supporting documents, and approvals.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/relocation-requests/')) {
|
||||||
|
return 'Assess feasibility, documents, and approvals for this relocation request.';
|
||||||
|
}
|
||||||
|
if (pathname === '/relocation-requests') {
|
||||||
|
return 'Manage dealer relocation proposals: new sites, handovers, and compliance checks.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/resignation/') && !pathname.startsWith('/dealer-resignation')) {
|
||||||
|
return 'HR workflow: clearances, handovers, and settlement steps for this resignation.';
|
||||||
|
}
|
||||||
|
if (pathname === '/resignation') {
|
||||||
|
return 'Queue of dealership resignation cases across your authorised outlets.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/termination/')) {
|
||||||
|
return 'Contractual exit details, evidence, and approvals for this termination case.';
|
||||||
|
}
|
||||||
|
if (pathname === '/termination') {
|
||||||
|
return 'Monitor dealership terminations, disputes, and mandated approvals.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/fnf/')) {
|
||||||
|
return 'Line items, deductions, and payout status for this full & final settlement.';
|
||||||
|
}
|
||||||
|
if (pathname === '/fnf') {
|
||||||
|
return 'Track F&F batches from clearance through finance payout.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/finance-onboarding/')) {
|
||||||
|
return 'Payment schedule, proofs, and audit notes for this onboarding application.';
|
||||||
|
}
|
||||||
|
if (pathname === '/finance-onboarding') {
|
||||||
|
return 'Validate security deposits, first fills, and related onboarding payments.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/finance-audit/')) {
|
||||||
|
return 'Finance audit trail and checklist for this application.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/finance-fnf/')) {
|
||||||
|
return 'Settlement calculations and release steps for this F&F record.';
|
||||||
|
}
|
||||||
|
if (pathname === '/finance-fnf') {
|
||||||
|
return 'Finance queue for F&F approvals and disbursements.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/fdd-details/')) {
|
||||||
|
return 'Field Development Director audit workspace for this application.';
|
||||||
|
}
|
||||||
|
if (pathname === '/fdd-dashboard') {
|
||||||
|
return 'FDD workload: audits due, flags raised, and follow-up actions.';
|
||||||
|
}
|
||||||
|
if (pathname.startsWith('/questionnaire-builder/') || pathname === '/questionnaire-builder') {
|
||||||
|
return 'Author and publish questionnaire versions used in dealership assessments.';
|
||||||
|
}
|
||||||
|
if (pathname === '/questionnaires' || pathname === '/questions') {
|
||||||
|
return 'List of published questionnaire templates and versions.';
|
||||||
|
}
|
||||||
|
if (pathname === '/master') {
|
||||||
|
return 'Hierarchy, geography, templates, and reference data shared across onboarding.';
|
||||||
|
}
|
||||||
|
if (pathname === '/users') {
|
||||||
|
return 'Create and maintain internal users, roles, and access for this portal.';
|
||||||
|
}
|
||||||
|
if (pathname === '/approval-policies') {
|
||||||
|
return 'Configure who must approve each onboarding stage or document type.';
|
||||||
|
}
|
||||||
|
if (pathname === '/sla-configurations') {
|
||||||
|
return 'Define turnaround targets and escalations for onboarding milestones.';
|
||||||
|
}
|
||||||
|
if (pathname === '/interview-configs') {
|
||||||
|
return 'Set up interview templates, panels, and scoring used during selection.';
|
||||||
|
}
|
||||||
|
if (pathname === '/system-logs') {
|
||||||
|
return 'Immutable record of configuration and administrative actions for compliance.';
|
||||||
|
}
|
||||||
|
if (pathname === '/notifications') {
|
||||||
|
return 'System and workflow alerts for your account.';
|
||||||
|
}
|
||||||
|
if (pathname === '/tasks') {
|
||||||
|
return 'Tasks assigned to you (placeholder module).';
|
||||||
|
}
|
||||||
|
if (pathname === '/reports') {
|
||||||
|
return 'Analytics and exports for onboarding performance (placeholder module).';
|
||||||
|
}
|
||||||
|
if (pathname === '/settings') {
|
||||||
|
return 'Profile, notifications, and security preferences for your account.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = getPageTitle(pathname);
|
||||||
|
return title === 'Dashboard'
|
||||||
|
? 'Operational snapshot for dealership onboarding: workloads, alerts, and shortcuts for your role.'
|
||||||
|
: `You are viewing: ${title}.`;
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen bg-slate-50">
|
<div className="flex items-center justify-center h-screen bg-slate-50">
|
||||||
@ -215,7 +377,11 @@ export default function App() {
|
|||||||
{/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
|
{/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
|
||||||
<Route element={
|
<Route element={
|
||||||
<RoleGuard excludeRoles={['Prospective Dealer']} redirectTo="/prospective-dashboard">
|
<RoleGuard excludeRoles={['Prospective Dealer']} redirectTo="/prospective-dashboard">
|
||||||
<AppLayout onLogout={handleLogout} title={getPageTitle(location.pathname)} />
|
<AppLayout
|
||||||
|
onLogout={handleLogout}
|
||||||
|
title={getPageTitle(location.pathname)}
|
||||||
|
subtitle={getPageSubtitle(location.pathname, currentRole)}
|
||||||
|
/>
|
||||||
</RoleGuard>
|
</RoleGuard>
|
||||||
}>
|
}>
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
|||||||
@ -198,8 +198,8 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
|
|||||||
file:mr-4 file:py-2 file:px-4
|
file:mr-4 file:py-2 file:px-4
|
||||||
file:rounded-full file:border-0
|
file:rounded-full file:border-0
|
||||||
file:text-sm file:font-semibold
|
file:text-sm file:font-semibold
|
||||||
file:bg-amber-50 file:text-amber-700
|
file:bg-red-50 file:text-re-red
|
||||||
hover:file:bg-amber-100"
|
hover:file:bg-red-100"
|
||||||
onChange={(e) => handleFileChange(q.id, e)}
|
onChange={(e) => handleFileChange(q.id, e)}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
/>
|
/>
|
||||||
@ -240,7 +240,7 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
|
|||||||
checked={responses[q.id] === val}
|
checked={responses[q.id] === val}
|
||||||
onChange={() => handleInputChange(q.id, val)}
|
onChange={() => handleInputChange(q.id, val)}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
className="text-amber-600 focus:ring-amber-500 w-4 h-4"
|
className="text-re-red focus:ring-re-red w-4 h-4"
|
||||||
/>
|
/>
|
||||||
<span className="text-gray-700">{val}</span>
|
<span className="text-gray-700">{val}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -18,10 +18,12 @@ import { formatDistanceToNow } from 'date-fns';
|
|||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
/** Context line under the title; changes per route in App layout. */
|
||||||
|
subtitle: string;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ title, onRefresh }: HeaderProps) {
|
export function Header({ title, subtitle, onRefresh }: HeaderProps) {
|
||||||
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
@ -101,7 +103,7 @@ export function Header({ title, onRefresh }: HeaderProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-slate-900">{title}</h1>
|
<h1 className="text-slate-900">{title}</h1>
|
||||||
<p className="text-slate-600">Manage and track dealership applications</p>
|
<p className="text-slate-600 text-sm leading-snug max-w-3xl">{subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@ -181,7 +181,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
<img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-6 w-auto" />
|
<img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-6 w-auto" />
|
||||||
<span className="text-[10px] uppercase tracking-[0.2em] font-bold text-slate-400 mt-1 whitespace-nowrap">
|
<span className="text-[10px] uppercase tracking-[0.2em] font-bold text-slate-400 mt-1 whitespace-nowrap">
|
||||||
Dealer Onboarding
|
Dealer Network
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[400px]">
|
<div className="flex flex-col items-center justify-center min-h-[400px]">
|
||||||
<Loader2 className="w-10 h-10 text-amber-600 animate-spin mb-4" />
|
<Loader2 className="w-10 h-10 text-re-red animate-spin mb-4" />
|
||||||
<p className="text-slate-600">Loading your dashboard...</p>
|
<p className="text-slate-600">Loading your dashboard...</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -58,7 +58,7 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
|
|||||||
title: 'Relocation Requests',
|
title: 'Relocation Requests',
|
||||||
value: statsSummary.relocation,
|
value: statsSummary.relocation,
|
||||||
icon: MapPin,
|
icon: MapPin,
|
||||||
color: 'bg-amber-500',
|
color: 'bg-re-red',
|
||||||
change: 'Active Requests',
|
change: 'Active Requests',
|
||||||
onClick: () => onNavigate('dealer-relocation')
|
onClick: () => onNavigate('dealer-relocation')
|
||||||
},
|
},
|
||||||
@ -96,8 +96,8 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
|
|||||||
title: 'Request Relocation',
|
title: 'Request Relocation',
|
||||||
description: 'Move dealership to new location',
|
description: 'Move dealership to new location',
|
||||||
icon: MapPin,
|
icon: MapPin,
|
||||||
color: 'bg-amber-50 hover:bg-amber-100 border-amber-200',
|
color: 'bg-red-50 hover:bg-red-100 border-red-200',
|
||||||
textColor: 'text-amber-700',
|
textColor: 'text-re-red',
|
||||||
onClick: () => onNavigate('dealer-relocation')
|
onClick: () => onNavigate('dealer-relocation')
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -105,14 +105,14 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Welcome Section */}
|
{/* Welcome Section */}
|
||||||
<div className="bg-gradient-to-r from-amber-500 to-amber-600 rounded-lg p-6 text-white">
|
<div className="rounded-lg bg-gradient-to-r from-re-red to-re-red-hover p-6 text-white">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-white mb-2">Welcome back, {profile.name || currentUser?.name}!</h1>
|
<h1 className="text-white mb-2">Welcome back, {profile.name || currentUser?.name}!</h1>
|
||||||
<p className="text-amber-100">
|
<p className="text-white/90">
|
||||||
Dealer Code: {profile.dealerCode} • {profile.businessName}
|
Dealer Code: {profile.dealerCode} • {profile.businessName}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-amber-100 text-sm mt-1">
|
<p className="text-white/90 text-sm mt-1">
|
||||||
{primaryOutlet.name} • {primaryOutlet.location}
|
{primaryOutlet.name} • {primaryOutlet.location}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -225,21 +225,21 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Clock className="w-5 h-5 text-amber-600" />
|
<Clock className="w-5 h-5 text-re-red" />
|
||||||
Important Reminders
|
Important Reminders
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<AlertCircle className="w-4 h-4 text-amber-600 mt-0.5" />
|
<AlertCircle className="w-4 h-4 text-re-red mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-900 text-sm">GST Filing Due</p>
|
<p className="text-slate-900 text-sm">GST Filing Due</p>
|
||||||
<p className="text-slate-600 text-xs">Due by Jan 15, 2026</p>
|
<p className="text-slate-600 text-xs">Due by Jan 15, 2026</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<AlertCircle className="w-4 h-4 text-amber-600 mt-0.5" />
|
<AlertCircle className="w-4 h-4 text-re-red mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-900 text-sm">Inventory Audit Scheduled</p>
|
<p className="text-slate-900 text-sm">Inventory Audit Scheduled</p>
|
||||||
<p className="text-slate-600 text-xs">Jan 20, 2026</p>
|
<p className="text-slate-600 text-xs">Jan 20, 2026</p>
|
||||||
|
|||||||
@ -8,18 +8,18 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useNavigate, Routes, Route, useParams } from 'react-router-dom';
|
import { useNavigate, Routes, Route, useParams, useLocation } from 'react-router-dom';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import { logout } from '@/store/slices/authSlice';
|
import { logout } from '@/store/slices/authSlice';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { API } from '@/api/API';
|
import { API } from '@/api/API';
|
||||||
import { formatDateTime } from '@/components/ui/utils';
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ProspectiveApplicationDetails } from '@/features/onboarding/pages/ProspectiveApplicationDetails';
|
import { ProspectiveApplicationDetails } from '@/features/onboarding/pages/ProspectiveApplicationDetails';
|
||||||
|
|
||||||
export function ProspectiveDashboardPage() {
|
export function ProspectiveDashboardPage() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const { user } = useSelector((state: RootState) => state.auth);
|
const { user } = useSelector((state: RootState) => state.auth);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState('applicant');
|
const [activeTab, setActiveTab] = useState('applicant');
|
||||||
@ -35,22 +35,46 @@ export function ProspectiveDashboardPage() {
|
|||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'}`}>
|
<div className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'}`}>
|
||||||
<div className="p-4 border-b border-slate-800">
|
<div className="p-4 border-b border-slate-800">
|
||||||
<div className="flex items-center justify-between">
|
{!collapsed ? (
|
||||||
{!collapsed && (
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex justify-end">
|
||||||
<div className="w-10 h-10 bg-amber-600 rounded-lg flex items-center justify-center">
|
<button
|
||||||
<FileText className="w-6 h-6 text-white" />
|
onClick={() => setCollapsed(true)}
|
||||||
</div>
|
className="p-1 hover:bg-slate-800 rounded transition-colors"
|
||||||
<span className="text-amber-600 font-bold">Applicant Portal</span>
|
title="Collapse sidebar"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="w-full">
|
||||||
<button
|
<img
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
src="/assets/images/Re_Logo.png"
|
||||||
className="p-1 hover:bg-slate-800 rounded transition-colors"
|
alt="Royal Enfield"
|
||||||
>
|
className="mx-auto block h-auto w-full max-h-14 object-contain"
|
||||||
{collapsed ? <ChevronRight className="w-5 h-5" /> : <ChevronLeft className="w-5 h-5" />}
|
/>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<p className="text-center text-[10px] font-bold uppercase tracking-[0.2em] text-slate-400">
|
||||||
|
Applicant Portal
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="w-full">
|
||||||
|
<img
|
||||||
|
src="/assets/images/Re_Logo.png"
|
||||||
|
alt="Royal Enfield"
|
||||||
|
className="block h-auto w-full max-h-10 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCollapsed(false)}
|
||||||
|
className="p-1 hover:bg-slate-800 rounded transition-colors"
|
||||||
|
title="Expand sidebar"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 p-4 space-y-2">
|
<nav className="flex-1 p-4 space-y-2">
|
||||||
@ -60,7 +84,7 @@ export function ProspectiveDashboardPage() {
|
|||||||
setActiveTab('applicant');
|
setActiveTab('applicant');
|
||||||
navigate('/prospective-dashboard');
|
navigate('/prospective-dashboard');
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${activeTab === 'applicant' ? 'bg-amber-600 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`}
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${activeTab === 'applicant' ? 'bg-re-red text-white hover:bg-re-red-hover' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
<FileText className="w-5 h-5 flex-shrink-0" />
|
<FileText className="w-5 h-5 flex-shrink-0" />
|
||||||
{!collapsed && <span className="flex-1 text-left">My Applications</span>}
|
{!collapsed && <span className="flex-1 text-left">My Applications</span>}
|
||||||
@ -72,7 +96,7 @@ export function ProspectiveDashboardPage() {
|
|||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="px-4 py-2 bg-slate-800 rounded-lg mb-2">
|
<div className="px-4 py-2 bg-slate-800 rounded-lg mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-amber-600 rounded-full flex items-center justify-center text-white">
|
<div className="w-10 h-10 bg-re-red rounded-full flex items-center justify-center text-white ring-2 ring-white/20">
|
||||||
<span className="font-bold">{user?.name?.charAt(0) || 'A'}</span>
|
<span className="font-bold">{user?.name?.charAt(0) || 'A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@ -97,12 +121,20 @@ export function ProspectiveDashboardPage() {
|
|||||||
<header className="bg-white border-b border-slate-200 px-6 py-4">
|
<header className="bg-white border-b border-slate-200 px-6 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-slate-900 text-xl font-semibold">Applicant Management</h1>
|
<h1 className="text-slate-900 text-xl font-semibold">
|
||||||
<p className="text-slate-600 text-sm">Manage and track dealership applications</p>
|
{location.pathname.includes('/application/')
|
||||||
|
? 'Application details'
|
||||||
|
: 'Applicant management'}
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-600 text-sm max-w-2xl leading-snug">
|
||||||
|
{location.pathname.includes('/application/')
|
||||||
|
? 'Review and update statutory information and required documents for this application. Progress is coordinated by Royal Enfield after you submit.'
|
||||||
|
: 'Start or continue your dealership application, upload documents, and use links we email you (for example the questionnaire).'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
|
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
|
||||||
<div className="w-8 h-8 bg-amber-600 rounded-full flex items-center justify-center">
|
<div className="w-8 h-8 bg-re-red rounded-full flex items-center justify-center ring-2 ring-re-red/20">
|
||||||
<User className="w-4 h-4 text-white" />
|
<User className="w-4 h-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
@ -175,40 +207,22 @@ function ProspectiveApplicationList() {
|
|||||||
{applications.map((app) => (
|
{applications.map((app) => (
|
||||||
<div
|
<div
|
||||||
key={app.id}
|
key={app.id}
|
||||||
className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm hover:shadow-md hover:border-amber-500 cursor-pointer transition-all group"
|
className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm hover:shadow-md hover:border-re-red cursor-pointer transition-all group"
|
||||||
onClick={() => navigate(`/prospective-dashboard/application/${app.id}`)}
|
onClick={() => navigate(`/prospective-dashboard/application/${app.id}`)}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="mb-4">
|
||||||
<div className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center group-hover:bg-amber-600 transition-colors">
|
<div className="w-12 h-12 bg-red-50 rounded-xl flex items-center justify-center group-hover:bg-re-red transition-colors">
|
||||||
<FileText className="w-6 h-6 text-amber-600 group-hover:text-white" />
|
<FileText className="w-6 h-6 text-re-red group-hover:text-white" />
|
||||||
</div>
|
</div>
|
||||||
<Badge className={`px-4 py-1.5 rounded-xl text-[10px] uppercase font-bold ${app.overallStatus === 'Completed' ? 'bg-green-100 text-green-700' :
|
|
||||||
app.overallStatus === 'Rejected' ? 'bg-red-100 text-red-700' :
|
|
||||||
'bg-amber-100 text-amber-700'}`}>
|
|
||||||
{app.overallStatus || 'Active'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-slate-900 mb-1 truncate">{app.applicationId}</h3>
|
<h3 className="text-xl font-bold text-slate-900 mb-1 truncate">{app.applicationId}</h3>
|
||||||
<p className="text-slate-500 text-sm mb-4 font-medium">{app.city}, {app.state}</p>
|
<p className="text-slate-500 text-sm mb-4 font-medium">{app.city}, {app.state}</p>
|
||||||
|
|
||||||
<div className="space-y-4 pt-6 border-t border-slate-100">
|
<div className="space-y-4 pt-6 border-t border-slate-100">
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-xs text-slate-500 font-medium">Current Stage</span>
|
|
||||||
<span className="text-xs font-bold text-slate-900 bg-slate-100 px-3 py-1 rounded-lg">{app.currentStage || 'Initial'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-xs text-slate-500 font-medium">Applied</span>
|
<span className="text-xs text-slate-500 font-medium">Applied</span>
|
||||||
<span className="text-xs font-bold text-slate-600">{formatDateTime(app.createdAt)}</span>
|
<span className="text-xs font-bold text-slate-600">{formatDateTime(app.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
|
||||||
<div className="flex justify-between items-center mb-1">
|
|
||||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Progress</span>
|
|
||||||
<span className="text-xs font-bold text-amber-600">{app.progressPercentage || 0}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-slate-100 rounded-full h-2">
|
|
||||||
<div className="bg-amber-500 h-2 rounded-full transition-all duration-1000" style={{ width: `${app.progressPercentage || 0}%` }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -9,13 +9,16 @@ import {
|
|||||||
Building,
|
Building,
|
||||||
Landmark,
|
Landmark,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
Check,
|
||||||
Info,
|
Info,
|
||||||
User,
|
User,
|
||||||
MapPin
|
MapPin
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { API } from '@/api/API';
|
import { API } from '@/api/API';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||||
|
import { cn } from '@/components/ui/utils';
|
||||||
|
import { Select, SelectContent, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@ -180,7 +183,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-20">
|
<div className="flex items-center justify-center py-20">
|
||||||
<RefreshCw className="w-8 h-8 animate-spin text-amber-600" />
|
<RefreshCw className="w-8 h-8 animate-spin text-re-red" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -189,7 +192,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
|
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
|
||||||
<p className="text-slate-600 mb-4">Application details not found.</p>
|
<p className="text-slate-600 mb-4">Application details not found.</p>
|
||||||
<button onClick={onBack} className="bg-amber-600 text-white px-4 py-2 rounded-md hover:bg-amber-700">Go Back</button>
|
<button onClick={onBack} className="bg-re-red text-white px-4 py-2 rounded-md hover:bg-re-red-hover">Go Back</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -224,19 +227,13 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<div className="animate-in fade-in duration-500 space-y-6">
|
<div className="animate-in fade-in duration-500 space-y-6">
|
||||||
{/* Status & Tracking Summary Card */}
|
{/* Status & Tracking Summary Card */}
|
||||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6" data-testid="onboarding-prospective-details-summary-card">
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6" data-testid="onboarding-prospective-details-summary-card">
|
||||||
<div className="flex items-center justify-between mb-4 border-b pb-2">
|
<div className="mb-4 border-b pb-2">
|
||||||
<h4 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
<h4 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||||||
<Info className="w-5 h-5 text-amber-600" /> Application Summary
|
<Info className="w-5 h-5 text-re-red" /> Application Summary
|
||||||
</h4>
|
</h4>
|
||||||
<div className="text-right">
|
|
||||||
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Current Stage</p>
|
|
||||||
<span className="bg-amber-100 text-amber-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide" data-testid="onboarding-prospective-details-current-stage">
|
|
||||||
{details.currentStage || details.overallStatus}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-start gap-3" data-testid="onboarding-prospective-details-applicant-info">
|
<div className="flex items-start gap-3" data-testid="onboarding-prospective-details-applicant-info">
|
||||||
<div className="p-2 bg-blue-50 rounded-lg">
|
<div className="p-2 bg-blue-50 rounded-lg">
|
||||||
@ -249,8 +246,8 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3" data-testid="onboarding-prospective-details-location-info">
|
<div className="flex items-start gap-3" data-testid="onboarding-prospective-details-location-info">
|
||||||
<div className="p-2 bg-amber-50 rounded-lg">
|
<div className="p-2 bg-red-50 rounded-lg">
|
||||||
<MapPin className="w-4 h-4 text-amber-600" />
|
<MapPin className="w-4 h-4 text-re-red" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] text-slate-500 uppercase font-bold">Proposed Location</p>
|
<p className="text-[10px] text-slate-500 uppercase font-bold">Proposed Location</p>
|
||||||
@ -283,21 +280,6 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-50 rounded-xl p-4 flex flex-col justify-center border border-slate-100" data-testid="onboarding-prospective-details-progress-card">
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<p className="text-xs font-bold text-slate-700 uppercase tracking-tight">Onboarding Progress</p>
|
|
||||||
<p className="text-xs font-black text-amber-600">{details.progressPercentage || 0}%</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-slate-200 rounded-full h-2 shadow-inner">
|
|
||||||
<div className="bg-amber-500 h-2 rounded-full transition-all duration-1000 ease-out shadow-sm" style={{ width: `${details.progressPercentage || 0}%` }}></div>
|
|
||||||
</div>
|
|
||||||
{details.statusHistory?.[0]?.changeReason && (
|
|
||||||
<p className="mt-3 text-[11px] text-slate-600 italic leading-relaxed border-l-2 border-amber-300 pl-2">
|
|
||||||
"{details.statusHistory[0].changeReason}"
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -307,12 +289,12 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" data-testid="onboarding-prospective-details-statutory-card">
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" data-testid="onboarding-prospective-details-statutory-card">
|
||||||
<div className="p-4 bg-slate-900 text-white flex justify-between items-center">
|
<div className="p-4 bg-slate-900 text-white flex justify-between items-center">
|
||||||
<h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest">
|
<h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest">
|
||||||
<CreditCard className="w-4 h-4 text-amber-400" /> Statutory & Bank Details
|
<CreditCard className="w-4 h-4 text-re-red" /> Statutory & Bank Details
|
||||||
</h4>
|
</h4>
|
||||||
<button
|
<button
|
||||||
onClick={handleSaveDetails}
|
onClick={handleSaveDetails}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
className={`text-xs text-white px-3 py-1 rounded font-bold transition-all flex items-center gap-1 disabled:opacity-50 ${isFormDirty ? 'bg-emerald-600 hover:bg-emerald-700 ring-2 ring-emerald-300 animate-pulse' : 'bg-amber-600 hover:bg-amber-700'}`}
|
className={`text-xs text-white px-3 py-1 rounded font-bold transition-all flex items-center gap-1 disabled:opacity-50 ${isFormDirty ? 'bg-emerald-600 hover:bg-emerald-700 ring-2 ring-emerald-300 animate-pulse' : 'bg-re-red hover:bg-re-red-hover'}`}
|
||||||
data-testid="onboarding-prospective-details-save-statutory-btn"
|
data-testid="onboarding-prospective-details-save-statutory-btn"
|
||||||
>
|
>
|
||||||
{isSaving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <CheckCircle2 className="w-3 h-3" />}
|
{isSaving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <CheckCircle2 className="w-3 h-3" />}
|
||||||
@ -325,7 +307,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Business Name</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Business Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
|
||||||
value={form.accountHolderName}
|
value={form.accountHolderName}
|
||||||
onChange={(e) => setForm({ ...form, accountHolderName: e.target.value })}
|
onChange={(e) => setForm({ ...form, accountHolderName: e.target.value })}
|
||||||
placeholder="As per legal documents"
|
placeholder="As per legal documents"
|
||||||
@ -336,7 +318,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Permanent Account Number (PAN)</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Permanent Account Number (PAN)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase"
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all uppercase"
|
||||||
value={form.panNumber}
|
value={form.panNumber}
|
||||||
onChange={(e) => setForm({ ...form, panNumber: e.target.value.toUpperCase() })}
|
onChange={(e) => setForm({ ...form, panNumber: e.target.value.toUpperCase() })}
|
||||||
placeholder="ABCDE1234F"
|
placeholder="ABCDE1234F"
|
||||||
@ -348,7 +330,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<label className="text-[10px] font-bold text-slate-500 uppercase">GST Identification Number (GSTIN)</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">GST Identification Number (GSTIN)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase"
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all uppercase"
|
||||||
value={form.gstNumber}
|
value={form.gstNumber}
|
||||||
onChange={(e) => setForm({ ...form, gstNumber: e.target.value.toUpperCase() })}
|
onChange={(e) => setForm({ ...form, gstNumber: e.target.value.toUpperCase() })}
|
||||||
placeholder="27ABCDE1234F1Z5"
|
placeholder="27ABCDE1234F1Z5"
|
||||||
@ -360,7 +342,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Office Address</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Office Address</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={1}
|
rows={1}
|
||||||
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
|
||||||
value={form.registeredAddress}
|
value={form.registeredAddress}
|
||||||
onChange={(e) => setForm({ ...form, registeredAddress: e.target.value })}
|
onChange={(e) => setForm({ ...form, registeredAddress: e.target.value })}
|
||||||
placeholder="Full legal address"
|
placeholder="Full legal address"
|
||||||
@ -378,7 +360,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Bank Name</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Bank Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
|
||||||
value={form.bankName}
|
value={form.bankName}
|
||||||
onChange={(e) => setForm({ ...form, bankName: e.target.value })}
|
onChange={(e) => setForm({ ...form, bankName: e.target.value })}
|
||||||
placeholder="e.g. HDFC Bank"
|
placeholder="e.g. HDFC Bank"
|
||||||
@ -389,7 +371,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Account Number</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Account Number</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
|
||||||
value={form.accountNumber}
|
value={form.accountNumber}
|
||||||
onChange={(e) => setForm({ ...form, accountNumber: e.target.value })}
|
onChange={(e) => setForm({ ...form, accountNumber: e.target.value })}
|
||||||
placeholder="Bank account number"
|
placeholder="Bank account number"
|
||||||
@ -400,7 +382,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<label className="text-[10px] font-bold text-slate-500 uppercase">IFSC Code</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">IFSC Code</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase"
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all uppercase"
|
||||||
value={form.ifscCode}
|
value={form.ifscCode}
|
||||||
onChange={(e) => setForm({ ...form, ifscCode: e.target.value.toUpperCase() })}
|
onChange={(e) => setForm({ ...form, ifscCode: e.target.value.toUpperCase() })}
|
||||||
placeholder="HDFC0001234"
|
placeholder="HDFC0001234"
|
||||||
@ -412,7 +394,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Branch Name</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Branch Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
|
||||||
value={form.branchName}
|
value={form.branchName}
|
||||||
onChange={(e) => setForm({ ...form, branchName: e.target.value })}
|
onChange={(e) => setForm({ ...form, branchName: e.target.value })}
|
||||||
placeholder="e.g. South Mumbai"
|
placeholder="e.g. South Mumbai"
|
||||||
@ -427,7 +409,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" data-testid="onboarding-prospective-details-upload-card">
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" data-testid="onboarding-prospective-details-upload-card">
|
||||||
<div className="p-4 border-b border-slate-200 bg-slate-50 flex justify-between items-center">
|
<div className="p-4 border-b border-slate-200 bg-slate-50 flex justify-between items-center">
|
||||||
<h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-slate-900">
|
<h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-slate-900">
|
||||||
<Upload className="w-4 h-4 text-blue-600" /> Required Documents
|
<Upload className="w-4 h-4 text-re-red" /> Required Documents
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
@ -436,7 +418,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label>
|
||||||
{selectedDocType && (
|
{selectedDocType && (
|
||||||
<span className={`rounded-full px-2 py-0.5 text-[10px] font-bold ${selectedDocAlreadyUploaded ? 'bg-blue-100 text-blue-700' : 'bg-amber-100 text-amber-700'}`}>
|
<span className="rounded-full bg-red-50 px-2 py-0.5 text-[10px] font-bold text-re-red">
|
||||||
{selectedDocAlreadyUploaded ? 'Already uploaded' : 'Pending upload'}
|
{selectedDocAlreadyUploaded ? 'Already uploaded' : 'Pending upload'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -444,24 +426,39 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CheckCircle2 className={`pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 ${selectedDocAlreadyUploaded ? 'text-green-600' : 'text-slate-300'}`} />
|
<CheckCircle2 className={`pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 ${selectedDocAlreadyUploaded ? 'text-green-600' : 'text-slate-300'}`} />
|
||||||
<Select value={selectedDocType} onValueChange={setSelectedDocType} disabled={isUploading}>
|
<Select value={selectedDocType} onValueChange={setSelectedDocType} disabled={isUploading}>
|
||||||
<SelectTrigger className="h-12 rounded-xl border-slate-200 bg-gradient-to-r from-white to-slate-50 pl-10 pr-3 text-sm font-medium text-slate-700 shadow-sm focus:border-amber-300 focus:ring-2 focus:ring-amber-500" data-testid="onboarding-prospective-details-doc-type-select">
|
<SelectTrigger className="h-12 rounded-xl border-slate-200 bg-gradient-to-r from-white to-slate-50 pl-10 pr-3 text-sm font-medium text-slate-700 shadow-sm focus:border-re-red/40 focus:ring-2 focus:ring-re-red" data-testid="onboarding-prospective-details-doc-type-select">
|
||||||
<SelectValue placeholder="Choose document type" />
|
<SelectValue placeholder="Choose document type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="rounded-xl border-slate-200 shadow-lg" data-testid="onboarding-prospective-details-doc-type-content">
|
<SelectContent className="rounded-xl border-slate-200 shadow-lg" data-testid="onboarding-prospective-details-doc-type-content">
|
||||||
{requiredDocumentTypes.map((docType) => {
|
{requiredDocumentTypes.map((docType) => {
|
||||||
const isUploaded = uploadedDocumentTypes.has(docType.toLowerCase());
|
const isUploaded = uploadedDocumentTypes.has(docType.toLowerCase());
|
||||||
return (
|
return (
|
||||||
<SelectItem
|
<SelectPrimitive.Item
|
||||||
key={docType}
|
key={docType}
|
||||||
value={docType}
|
value={docType}
|
||||||
className="rounded-lg px-3 py-2 text-sm text-slate-700 focus:bg-amber-50 focus:text-slate-900"
|
textValue={docType}
|
||||||
|
className={cn(
|
||||||
|
'relative flex w-full cursor-default select-none items-center gap-2 rounded-lg py-2 pl-2 pr-8 text-sm text-slate-700 outline-none',
|
||||||
|
'focus:bg-red-50 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
)}
|
||||||
data-testid={`onboarding-prospective-details-doc-type-item-${docType.replace(/\s+/g, '-').toLowerCase()}`}
|
data-testid={`onboarding-prospective-details-doc-type-item-${docType.replace(/\s+/g, '-').toLowerCase()}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
<CheckCircle2 className={`h-4 w-4 ${isUploaded ? 'text-green-600' : 'text-slate-300'}`} />
|
<SelectPrimitive.ItemIndicator>
|
||||||
{docType}
|
<Check className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
|
<CheckCircle2
|
||||||
|
className={cn(
|
||||||
|
'h-4 w-4 shrink-0',
|
||||||
|
isUploaded ? 'text-green-600' : 'text-slate-300',
|
||||||
|
)}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<SelectPrimitive.ItemText>{docType}</SelectPrimitive.ItemText>
|
||||||
|
</div>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -473,7 +470,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
className="w-full text-xs text-slate-600 file:mr-4 file:py-1 file:px-4 file:rounded-full file:border-0 file:text-xs file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100"
|
className="w-full text-xs text-slate-600 file:mr-4 file:py-1 file:px-4 file:rounded-full file:border-0 file:text-xs file:font-semibold file:bg-red-50 file:text-re-red hover:file:bg-red-100"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
data-testid="onboarding-prospective-details-file-input"
|
data-testid="onboarding-prospective-details-file-input"
|
||||||
@ -483,7 +480,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={!file || !selectedDocType || isUploading}
|
disabled={!file || !selectedDocType || isUploading}
|
||||||
className="bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50 text-xs font-bold transition-all shadow-sm flex items-center gap-2"
|
className="bg-re-red text-white px-5 py-2 rounded-md hover:bg-re-red-hover disabled:opacity-50 text-xs font-bold transition-all shadow-sm flex items-center gap-2"
|
||||||
data-testid="onboarding-prospective-details-upload-btn"
|
data-testid="onboarding-prospective-details-upload-btn"
|
||||||
>
|
>
|
||||||
{isUploading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
|
{isUploading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
|
||||||
@ -499,7 +496,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3" data-testid="onboarding-prospective-details-doc-library">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3" data-testid="onboarding-prospective-details-doc-library">
|
||||||
{documents.length > 0 ? documents.map((doc, idx) => (
|
{documents.length > 0 ? documents.map((doc, idx) => (
|
||||||
<div key={doc.id} className="flex justify-between items-center p-3 border border-slate-100 rounded-xl bg-slate-50 group hover:border-amber-200 transition-all" data-testid={`onboarding-prospective-details-doc-item-${idx}`}>
|
<div key={doc.id} className="flex justify-between items-center p-3 border border-slate-100 rounded-xl bg-slate-50 group hover:border-red-200 transition-all" data-testid={`onboarding-prospective-details-doc-item-${idx}`}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 rounded bg-white flex items-center justify-center border border-slate-200 group-hover:bg-blue-50">
|
<div className="w-8 h-8 rounded bg-white flex items-center justify-center border border-slate-200 group-hover:bg-blue-50">
|
||||||
<File className="w-4 h-4 text-slate-400 group-hover:text-blue-600" />
|
<File className="w-4 h-4 text-slate-400 group-hover:text-blue-600" />
|
||||||
@ -509,7 +506,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<p className="text-[10px] text-slate-400 truncate w-32">{doc.fileName}</p>
|
<p className="text-[10px] text-slate-400 truncate w-32">{doc.fileName}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-[9px] px-2 py-0.5 rounded-full font-black uppercase tracking-tighter ${doc.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`} data-testid={`onboarding-prospective-details-doc-status-${idx}`}>
|
<span className={`text-[9px] px-2 py-0.5 rounded-full font-black uppercase tracking-tighter ${doc.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-red-50 text-re-red'}`} data-testid={`onboarding-prospective-details-doc-status-${idx}`}>
|
||||||
{doc.status || 'Pending'}
|
{doc.status || 'Pending'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -218,7 +218,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
|
|||||||
{/* Loading Overlay */}
|
{/* Loading Overlay */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="min-h-[400px] flex items-center justify-center">
|
<div className="min-h-[400px] flex items-center justify-center">
|
||||||
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
|
<Loader2 className="w-8 h-8 text-re-red animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -235,7 +235,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
|
|||||||
|
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="bg-amber-600 hover:bg-amber-700 text-white">
|
<Button className="bg-re-red hover:bg-re-red-hover text-white">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
New Relocation Request
|
New Relocation Request
|
||||||
</Button>
|
</Button>
|
||||||
@ -404,7 +404,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-amber-600 hover:bg-amber-700 text-white"
|
className="bg-re-red hover:bg-re-red-hover text-white"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? (
|
{submitting ? (
|
||||||
@ -498,7 +498,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
|
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
|
||||||
<div
|
<div
|
||||||
className="bg-amber-500 h-2 rounded-full"
|
className="bg-re-red h-2 rounded-full"
|
||||||
style={{ width: `${request.progressPercentage || 0}%` }}
|
style={{ width: `${request.progressPercentage || 0}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,17 +1,12 @@
|
|||||||
import { ArrowLeft, Calendar, CheckCircle2, Clock, FileText, MapPin, MessageSquare, Upload, User, XCircle } from 'lucide-react';
|
import { ArrowLeft, CheckCircle2, Clock, MapPin, User, XCircle } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
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 { Badge } from '@/components/ui/badge';
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { resignationService } from '@/services/resignation.service';
|
import { resignationService } from '@/services/resignation.service';
|
||||||
import { formatDateTime } from '@/components/ui/utils';
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
import { RESIGNATION_STAGE_OPTIONS, RESIGNATION_DOCUMENT_TYPES } from '@/lib/offboardingDocumentOptions';
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -28,22 +23,9 @@ interface DealerResignationDetailsPageProps {
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
|
|
||||||
if (status === 'Rejected' || status === 'Withdrawn') return 'bg-red-100 text-red-700 border-red-300';
|
|
||||||
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
|
||||||
return 'bg-slate-100 text-slate-700 border-slate-300';
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DealerResignationDetailsPage({ resignationId, onBack }: DealerResignationDetailsPageProps) {
|
export function DealerResignationDetailsPage({ resignationId, onBack }: DealerResignationDetailsPageProps) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [details, setDetails] = useState<any>(null);
|
const [details, setDetails] = useState<any>(null);
|
||||||
const [uploading, setUploading] = useState(false);
|
|
||||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
|
||||||
const [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]);
|
|
||||||
const [uploadStage, setUploadStage] = useState<string>('');
|
|
||||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
|
||||||
const [withdrawing, setWithdrawing] = useState(false);
|
const [withdrawing, setWithdrawing] = useState(false);
|
||||||
const [isWithdrawDialogOpen, setIsWithdrawDialogOpen] = useState(false);
|
const [isWithdrawDialogOpen, setIsWithdrawDialogOpen] = useState(false);
|
||||||
const [withdrawalReason, setWithdrawalReason] = useState('User requested withdrawal');
|
const [withdrawalReason, setWithdrawalReason] = useState('User requested withdrawal');
|
||||||
@ -52,12 +34,8 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
|||||||
const fetchDetails = async () => {
|
const fetchDetails = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [data, audits] = await Promise.all([
|
const data = await resignationService.getResignationById(resignationId);
|
||||||
resignationService.getResignationById(resignationId),
|
|
||||||
fetchAuditLogs(resignationId)
|
|
||||||
]);
|
|
||||||
setDetails(data);
|
setDetails(data);
|
||||||
setAuditLogs(audits);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch resignation details:', error);
|
console.error('Failed to fetch resignation details:', error);
|
||||||
toast.error('Unable to load resignation details');
|
toast.error('Unable to load resignation details');
|
||||||
@ -71,53 +49,15 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
|||||||
}
|
}
|
||||||
}, [resignationId]);
|
}, [resignationId]);
|
||||||
|
|
||||||
const fetchAuditLogs = async (id: string) => {
|
|
||||||
try {
|
|
||||||
// Lazy import through existing API helper shape used in other modules.
|
|
||||||
const { API } = await import('@/api/API');
|
|
||||||
const response = await API.getAuditLogs('resignation', id) as any;
|
|
||||||
if (response?.data?.success) return response.data.data || [];
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshDetails = async () => {
|
const refreshDetails = async () => {
|
||||||
try {
|
try {
|
||||||
const [data, audits] = await Promise.all([
|
const data = await resignationService.getResignationById(resignationId);
|
||||||
resignationService.getResignationById(resignationId),
|
|
||||||
fetchAuditLogs(resignationId)
|
|
||||||
]);
|
|
||||||
setDetails(data);
|
setDetails(data);
|
||||||
setAuditLogs(audits);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Unable to refresh resignation details');
|
toast.error('Unable to refresh resignation details');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
|
||||||
if (!uploadFile) {
|
|
||||||
toast.error('Please choose a file');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setUploading(true);
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', uploadFile);
|
|
||||||
formData.append('documentType', uploadDocType);
|
|
||||||
if (uploadStage) formData.append('stage', uploadStage);
|
|
||||||
await resignationService.uploadDocument(resignationId, formData);
|
|
||||||
toast.success('Document uploaded successfully');
|
|
||||||
setUploadFile(null);
|
|
||||||
await refreshDetails();
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error?.response?.data?.message || 'Document upload failed');
|
|
||||||
} finally {
|
|
||||||
setUploading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWithdraw = async () => {
|
const handleWithdraw = async () => {
|
||||||
try {
|
try {
|
||||||
setWithdrawing(true);
|
setWithdrawing(true);
|
||||||
@ -135,7 +75,7 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[320px] flex items-center justify-center">
|
<div className="min-h-[320px] flex items-center justify-center">
|
||||||
<Clock className="w-8 h-8 animate-spin text-amber-600" />
|
<Clock className="w-8 h-8 animate-spin text-re-red" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -156,9 +96,6 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const docs = details.uploadedDocuments || [];
|
|
||||||
const timeline = Array.isArray(details.timeline) ? details.timeline : [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@ -169,13 +106,13 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h1 className="text-slate-900">Resignation Request Details</h1>
|
<h1 className="text-slate-900">Resignation Request Details</h1>
|
||||||
<p className="text-slate-600 text-sm">
|
<p className="text-slate-600 text-sm">
|
||||||
Track your request progress and uploaded documents
|
Review your resignation request details
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{details.status !== 'Withdrawn' &&
|
{details.status !== 'Withdrawn' &&
|
||||||
details.status !== 'Completed' &&
|
details.status !== 'Completed' &&
|
||||||
details.status !== 'Rejected' &&
|
details.status !== 'Rejected' &&
|
||||||
!['NBH', 'DD Admin', 'Legal', 'F&F Initiated'].includes(details.currentStage) && (
|
!['NBH', 'DD Admin', 'Legal', 'Awaiting F&F', 'F&F Initiated'].includes(details.currentStage) && (
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="bg-red-600 hover:bg-red-700"
|
className="bg-red-600 hover:bg-red-700"
|
||||||
@ -224,26 +161,16 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="w-5 h-5 text-amber-600" />
|
<CheckCircle2 className="w-5 h-5 text-re-red" />
|
||||||
Request Summary
|
Request Summary
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Current request status and key metadata</CardDescription>
|
<CardDescription>Key details about your resignation request</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500">Request ID</p>
|
<p className="text-xs text-slate-500">Request ID</p>
|
||||||
<p className="text-slate-900">{details.resignationId || details.id}</p>
|
<p className="text-slate-900">{details.resignationId || details.id}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500">Status</p>
|
|
||||||
<Badge className={`border ${getStatusColor(details.status || 'Pending')}`}>
|
|
||||||
{details.status || 'Pending'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500">Current Stage</p>
|
|
||||||
<p className="text-slate-900">{details.currentStage || 'Submitted'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500">Submitted On</p>
|
<p className="text-xs text-slate-500">Submitted On</p>
|
||||||
<p className="text-slate-900">{formatDateTime(details.submittedOn || details.createdAt)}</p>
|
<p className="text-slate-900">{formatDateTime(details.submittedOn || details.createdAt)}</p>
|
||||||
@ -252,10 +179,6 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
|||||||
<p className="text-xs text-slate-500">Resignation Type</p>
|
<p className="text-xs text-slate-500">Resignation Type</p>
|
||||||
<p className="text-slate-900">{details.resignationType || 'N/A'}</p>
|
<p className="text-slate-900">{details.resignationType || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500">Progress</p>
|
|
||||||
<p className="text-slate-900">{details.progressPercentage || 0}%</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -296,164 +219,6 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<FileText className="w-5 h-5 text-purple-600" />
|
|
||||||
Uploaded Documents
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>Dealer can upload resignation-related documents for review</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4 p-3 border rounded-lg bg-slate-50">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">Document Type</Label>
|
|
||||||
<Select value={uploadDocType} onValueChange={setUploadDocType}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{RESIGNATION_DOCUMENT_TYPES.map((docType) => (
|
|
||||||
<SelectItem key={docType} value={docType}>{docType}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">Stage (Optional)</Label>
|
|
||||||
<Select value={uploadStage || 'none'} onValueChange={(v) => setUploadStage(v === 'none' ? '' : v)}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select stage" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">No Stage</SelectItem>
|
|
||||||
{RESIGNATION_STAGE_OPTIONS.map((stage) => (
|
|
||||||
<SelectItem key={stage} value={stage}>{stage}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">File</Label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
className="h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
|
|
||||||
onChange={(e) => setUploadFile(e.target.files?.[0] || null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end">
|
|
||||||
<Button className="w-full" onClick={handleUpload} disabled={uploading}>
|
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
|
||||||
{uploading ? 'Uploading...' : 'Upload'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Document Type</TableHead>
|
|
||||||
<TableHead>File</TableHead>
|
|
||||||
<TableHead>Uploaded By</TableHead>
|
|
||||||
<TableHead>Uploaded On</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{docs.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={4} className="text-center text-slate-500 py-6">
|
|
||||||
No documents uploaded yet.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
docs.map((doc: any) => (
|
|
||||||
<TableRow key={doc.id}>
|
|
||||||
<TableCell>{doc.documentType || '-'}</TableCell>
|
|
||||||
<TableCell>{doc.fileName || '-'}</TableCell>
|
|
||||||
<TableCell>{doc.uploader?.fullName || '-'}</TableCell>
|
|
||||||
<TableCell>{formatDateTime(doc.createdAt)}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<MessageSquare className="w-5 h-5 text-blue-600" />
|
|
||||||
Work Notes Communication
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>Official channel for internal-dealer clarifications</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/worknotes/resignation/${resignationId}`, {
|
|
||||||
state: {
|
|
||||||
applicationName: details?.dealer?.fullName || 'Resignation Request',
|
|
||||||
registrationNumber: details?.resignationId || resignationId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MessageSquare className="w-4 h-4 mr-2" />
|
|
||||||
Open Work Notes
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Calendar className="w-5 h-5 text-amber-600" />
|
|
||||||
Progress Timeline
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{timeline.length === 0 ? (
|
|
||||||
<p className="text-sm text-slate-500">No timeline events available yet.</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{timeline.slice().reverse().map((entry: any, idx: number) => (
|
|
||||||
<div key={`${entry.timestamp || entry.createdAt}-${idx}`} className="p-3 border rounded-lg bg-slate-50">
|
|
||||||
<p className="text-sm text-slate-900">{entry.action || entry.stage || 'Stage Update'}</p>
|
|
||||||
<p className="text-xs text-slate-500">{formatDateTime(entry.timestamp || entry.createdAt)}</p>
|
|
||||||
<p className="text-xs text-slate-600">{entry.comments || entry.remarks || 'No remarks'}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Audit Trail</CardTitle>
|
|
||||||
<CardDescription>Traceability of status/actions on this request</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{auditLogs.length === 0 ? (
|
|
||||||
<p className="text-sm text-slate-500">No audit records found.</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{auditLogs.map((log: any) => (
|
|
||||||
<div key={log.id} className="p-3 border rounded-lg">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-sm text-slate-900">{log.action || 'Action'}</p>
|
|
||||||
<p className="text-xs text-slate-500">{formatDateTime(log.createdAt || log.timestamp)}</p>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-slate-600 mt-1">{log.remarks || log.description || 'No remarks'}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,13 +20,6 @@ interface DealerResignationPageProps {
|
|||||||
onViewDetails?: (id: string) => void;
|
onViewDetails?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
|
|
||||||
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
|
||||||
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
|
|
||||||
return 'bg-slate-100 text-slate-700 border-slate-300';
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DealerResignationPage({ onViewDetails }: DealerResignationPageProps) {
|
export function DealerResignationPage({ onViewDetails }: DealerResignationPageProps) {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
|
const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
|
||||||
@ -134,7 +127,7 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
|
|||||||
title: 'Pending Resignations',
|
title: 'Pending Resignations',
|
||||||
value: outlets.filter(o => o.status === 'Pending Resignation').length,
|
value: outlets.filter(o => o.status === 'Pending Resignation').length,
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
color: 'bg-amber-500',
|
color: 'bg-re-red',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -143,7 +136,7 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
|
|||||||
{/* Loading Overlay */}
|
{/* Loading Overlay */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="min-h-[400px] flex items-center justify-center">
|
<div className="min-h-[400px] flex items-center justify-center">
|
||||||
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
|
<Loader2 className="w-8 h-8 text-re-red animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -212,7 +205,7 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
|
|||||||
outlet.status === 'Active'
|
outlet.status === 'Active'
|
||||||
? 'bg-green-100 text-green-700 border-green-300'
|
? 'bg-green-100 text-green-700 border-green-300'
|
||||||
: outlet.status === 'Pending Resignation'
|
: outlet.status === 'Pending Resignation'
|
||||||
? 'bg-amber-100 text-amber-700 border-amber-300'
|
? 'bg-red-50 text-re-red border-red-200'
|
||||||
: 'bg-slate-100 text-slate-700 border-slate-300'
|
: 'bg-slate-100 text-slate-700 border-slate-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -236,8 +229,8 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasActiveResignation ? (
|
{hasActiveResignation ? (
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded p-3 text-sm">
|
<div className="bg-red-50 border border-red-200 rounded p-3 text-sm">
|
||||||
<p className="text-amber-800">
|
<p className="text-slate-800">
|
||||||
Resignation in progress - <span className="underline cursor-pointer" onClick={() => onViewDetails && resignation?.resignationId && onViewDetails(resignation.resignationId)}>View Request</span>
|
Resignation in progress - <span className="underline cursor-pointer" onClick={() => onViewDetails && resignation?.resignationId && onViewDetails(resignation.resignationId)}>View Request</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -362,9 +355,9 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Important Info */}
|
{/* Important Info */}
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
<h4 className="text-amber-900 mb-2">Important Information</h4>
|
<h4 className="text-re-red mb-2 font-semibold">Important Information</h4>
|
||||||
<ul className="text-amber-800 text-sm space-y-1">
|
<ul className="text-slate-700 text-sm space-y-1">
|
||||||
<li>• F&F settlement process will be initiated after submission</li>
|
<li>• F&F settlement process will be initiated after submission</li>
|
||||||
<li>• All department clearances must be obtained</li>
|
<li>• All department clearances must be obtained</li>
|
||||||
<li>• Final settlement will be processed after closure</li>
|
<li>• Final settlement will be processed after closure</li>
|
||||||
@ -407,7 +400,7 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>My Resignation Requests</CardTitle>
|
<CardTitle>My Resignation Requests</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Track the progress of your resignation requests
|
View your submitted resignation requests
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -418,15 +411,13 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
|
|||||||
<TableHead>Outlet</TableHead>
|
<TableHead>Outlet</TableHead>
|
||||||
<TableHead>Type</TableHead>
|
<TableHead>Type</TableHead>
|
||||||
<TableHead>Submitted On</TableHead>
|
<TableHead>Submitted On</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Progress</TableHead>
|
|
||||||
<TableHead>Actions</TableHead>
|
<TableHead>Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{resignations.length === 0 ? (
|
{resignations.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className="text-center py-4 text-slate-500">
|
<TableCell colSpan={5} className="text-center py-4 text-slate-500">
|
||||||
No resignation requests found
|
No resignation requests found
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -437,22 +428,6 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
|
|||||||
<TableCell>{request.outlet?.name}</TableCell>
|
<TableCell>{request.outlet?.name}</TableCell>
|
||||||
<TableCell>{request.resignationType}</TableCell>
|
<TableCell>{request.resignationType}</TableCell>
|
||||||
<TableCell>{formatDateTime(request.submittedOn)}</TableCell>
|
<TableCell>{formatDateTime(request.submittedOn)}</TableCell>
|
||||||
<TableCell>
|
|
||||||
<Badge className={`border ${getStatusColor(request.status)}`}>
|
|
||||||
{request.status}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
|
|
||||||
<div
|
|
||||||
className="bg-red-600 h-2 rounded-full"
|
|
||||||
style={{ width: `${request.progressPercentage}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-slate-600">{request.progressPercentage}%</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@ -52,6 +52,7 @@ const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
|
|||||||
'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'],
|
'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'],
|
||||||
'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
|
'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
|
||||||
'DD Admin': ['DD Admin', 'DD Admin Review'],
|
'DD Admin': ['DD Admin', 'DD Admin Review'],
|
||||||
|
'Awaiting F&F': ['Awaiting F&F', 'Awaiting F&F — manual initiation'],
|
||||||
'Legal': ['Legal', 'Legal - Resignation Letter'],
|
'Legal': ['Legal', 'Legal - Resignation Letter'],
|
||||||
'F&F Initiated': ['F&F Initiated', 'FNF_INITIATED', 'FNF Initiated'],
|
'F&F Initiated': ['F&F Initiated', 'FNF_INITIATED', 'FNF Initiated'],
|
||||||
'Completed': ['Completed']
|
'Completed': ['Completed']
|
||||||
@ -147,11 +148,12 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
{ id: 6, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
{ id: 6, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
||||||
{ id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
|
{ id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
|
||||||
{ id: 8, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification and final closure' },
|
{ id: 8, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification and final closure' },
|
||||||
{ id: 9, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
|
{ id: 9, name: 'Awaiting F&F', key: 'Awaiting F&F', description: 'Internal review complete — start Full & Final using Push to F&F when ready' },
|
||||||
{ id: 10, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
|
{ id: 10, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
|
||||||
|
{ id: 11, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const stagesOrdered = ['Request Submitted', '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', 'Awaiting F&F', 'F&F Initiated', 'Completed'];
|
||||||
|
|
||||||
const legalStageApproved = (() => {
|
const legalStageApproved = (() => {
|
||||||
if (!resignationData) return false;
|
if (!resignationData) return false;
|
||||||
@ -171,6 +173,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
const legalApprovedTransition =
|
const legalApprovedTransition =
|
||||||
targetStage === 'legal' ||
|
targetStage === 'legal' ||
|
||||||
targetStage === 'dd admin' ||
|
targetStage === 'dd admin' ||
|
||||||
|
targetStage === 'awaiting f&f' ||
|
||||||
targetStage === 'f&f initiated' ||
|
targetStage === 'f&f initiated' ||
|
||||||
targetStage === 'fnf_initiated' ||
|
targetStage === 'fnf_initiated' ||
|
||||||
action.includes('approved');
|
action.includes('approved');
|
||||||
@ -224,21 +227,27 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
return today >= lwd;
|
return today >= lwd;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const isAwaitingFnfGate = currentStage === 'Awaiting F&F';
|
||||||
|
|
||||||
const canApprove = isCurrentlyAssigned &&
|
const canApprove = isCurrentlyAssigned &&
|
||||||
!isFinalState &&
|
!isFinalState &&
|
||||||
!isSettlementPhase &&
|
!isSettlementPhase &&
|
||||||
!hasAlreadyPartiallyApproved &&
|
!hasAlreadyPartiallyApproved &&
|
||||||
!(currentStage === 'Legal' && legalStageApproved) &&
|
!(currentStage === 'Legal' && legalStageApproved) &&
|
||||||
!(isDDLead && isDDLeadStage && !hasUploadedPPT) &&
|
!(isDDLead && isDDLeadStage && !hasUploadedPPT) &&
|
||||||
!(currentStage === 'DD Admin' && !isLwdReached);
|
!(currentStage === 'DD Admin' && !isLwdReached) &&
|
||||||
|
!isAwaitingFnfGate;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canApprove,
|
canApprove,
|
||||||
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
|
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
|
||||||
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 && currentStage === 'Legal' && isLwdReached,
|
!isSettlementPhase &&
|
||||||
|
!isFinalState &&
|
||||||
|
(currentStage === 'Awaiting F&F' || currentStage === 'Legal') &&
|
||||||
|
isLwdReached,
|
||||||
canAssign: userRole !== 'Dealer' && !isFinalState
|
canAssign: userRole !== 'Dealer' && !isFinalState
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -253,6 +262,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
'DD Lead': ['DD Lead', 'DD Lead Review', 'Lead Review'],
|
'DD Lead': ['DD Lead', 'DD Lead Review', 'Lead Review'],
|
||||||
'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
|
'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
|
||||||
'DD Admin': ['DD Admin', 'DD Admin Review'],
|
'DD Admin': ['DD Admin', 'DD Admin Review'],
|
||||||
|
'Awaiting F&F': ['Awaiting F&F', 'Awaiting F&F — manual initiation'],
|
||||||
'Legal': ['Legal', 'Legal - Resignation Letter', 'Legal Review'],
|
'Legal': ['Legal', 'Legal - Resignation Letter', 'Legal Review'],
|
||||||
'F&F Initiated': ['F&F Initiated', 'FNF_INITIATED', 'F&F Settlement', 'Settled'],
|
'F&F Initiated': ['F&F Initiated', 'FNF_INITIATED', 'F&F Settlement', 'Settled'],
|
||||||
'Completed': ['Completed', 'Finalized']
|
'Completed': ['Completed', 'Finalized']
|
||||||
|
|||||||
@ -296,7 +296,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
|
|
||||||
const getProgressStatus = (stageName: string) => {
|
const getProgressStatus = (stageName: string) => {
|
||||||
const isTerminal = ['Rejected', 'Revoked', 'Withdrawn'].includes(request.status);
|
const isTerminal = ['Rejected', 'Revoked', 'Withdrawn'].includes(request.status);
|
||||||
const isSuccessFinal = ['Completed', 'Terminated', 'Settled', 'F&F Initiated', 'FNF_INITIATED'].includes(request.status) || request.currentStage === 'Terminated';
|
const isSuccessFinal = ['Completed', 'Terminated', 'Settled', 'F&F Initiated', 'FNF_INITIATED', 'Awaiting F&F', 'Awaiting F&F (LWD Pending)'].includes(request.status) || request.currentStage === 'Terminated';
|
||||||
|
|
||||||
// For terminal states, we determine the last active stage from the timeline to keep the track visible
|
// For terminal states, we determine the last active stage from the timeline to keep the track visible
|
||||||
let currentStageForProgress = request.currentStage || request.status;
|
let currentStageForProgress = request.currentStage || request.status;
|
||||||
|
|||||||
@ -7,9 +7,11 @@ import { RootState } from '../../store';
|
|||||||
import {
|
import {
|
||||||
User, RefreshCw, HelpCircle, ArrowLeft,
|
User, RefreshCw, HelpCircle, ArrowLeft,
|
||||||
Users, FileText, ChevronRight,
|
Users, FileText, ChevronRight,
|
||||||
CheckCircle
|
CheckCircle, Info
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
type SubmittedView = 'none' | 'success' | 'already';
|
||||||
|
|
||||||
const PublicQuestionnairePage: React.FC = () => {
|
const PublicQuestionnairePage: React.FC = () => {
|
||||||
const { applicationId } = useParams<{ applicationId: string }>();
|
const { applicationId } = useParams<{ applicationId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -21,7 +23,8 @@ const PublicQuestionnairePage: React.FC = () => {
|
|||||||
const [activeSection, setActiveSection] = useState<string>('');
|
const [activeSection, setActiveSection] = useState<string>('');
|
||||||
const [responses, setResponses] = useState<Record<string, any>>({});
|
const [responses, setResponses] = useState<Record<string, any>>({});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
/** End-of-flow screen: success = just submitted; already = reopened link / second visit */
|
||||||
|
const [submittedView, setSubmittedView] = useState<SubmittedView>('none');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchQuestionnaire = async () => {
|
const fetchQuestionnaire = async () => {
|
||||||
@ -47,7 +50,7 @@ const PublicQuestionnairePage: React.FC = () => {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error fetching questionnaire:", error);
|
console.error("Error fetching questionnaire:", error);
|
||||||
if (error.response?.data?.code === 'ALREADY_SUBMITTED') {
|
if (error.response?.data?.code === 'ALREADY_SUBMITTED') {
|
||||||
setIsSubmitted(true);
|
setSubmittedView('already');
|
||||||
} else {
|
} else {
|
||||||
toast.error("Failed to load questionnaire");
|
toast.error("Failed to load questionnaire");
|
||||||
}
|
}
|
||||||
@ -100,8 +103,7 @@ const PublicQuestionnairePage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Responses submitted successfully');
|
toast.success('Responses submitted successfully');
|
||||||
setIsSubmitted(true);
|
setSubmittedView('success');
|
||||||
setTimeout(() => navigate('/prospective-dashboard'), 3000);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error('Failed to submit responses');
|
toast.error('Failed to submit responses');
|
||||||
@ -116,24 +118,36 @@ const PublicQuestionnairePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSubmitted) {
|
if (submittedView !== 'none') {
|
||||||
|
const isAlready = submittedView === 'already';
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-6">
|
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-6">
|
||||||
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full text-center border-t-4 border-amber-600">
|
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full text-center border-t-4 border-re-red">
|
||||||
<div className="w-16 h-16 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div
|
||||||
<CheckCircle className="w-8 h-8" />
|
className={`w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 ${
|
||||||
</div>
|
isAlready ? 'bg-slate-100 text-slate-600' : 'bg-green-100 text-green-600'
|
||||||
<h2 className="text-2xl font-bold mb-2 text-slate-900">Assessment Submitted</h2>
|
}`}
|
||||||
<p className="text-slate-600 mb-6">
|
|
||||||
Thank you! Your assessment has been submitted successfully.
|
|
||||||
Redirecting to dashboard...
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/prospective-dashboard')}
|
|
||||||
className="w-full py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors"
|
|
||||||
>
|
>
|
||||||
Return to Dashboard
|
{isAlready ? <Info className="w-8 h-8" /> : <CheckCircle className="w-8 h-8" />}
|
||||||
</button>
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold mb-2 text-slate-900">
|
||||||
|
{isAlready ? 'Already submitted' : 'Thank you'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-600 leading-relaxed">
|
||||||
|
{isAlready ? (
|
||||||
|
<>
|
||||||
|
This questionnaire has already been submitted for your application. You do not need
|
||||||
|
to complete it again. If you think this is a mistake, contact support using the same
|
||||||
|
email you used to apply. You may close this page when you are done.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Your assessment has been submitted successfully. We have received your responses and
|
||||||
|
will review them as part of your application. You may close this page when you are
|
||||||
|
done.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -149,12 +163,14 @@ const PublicQuestionnairePage: React.FC = () => {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-slate-900 font-bold text-xl">Dealer Questionnaire Form</h1>
|
<h1 className="text-slate-900 font-bold text-xl">Dealer Questionnaire Form</h1>
|
||||||
<p className="text-slate-600 text-sm">Manage and track dealership applications</p>
|
<p className="text-slate-600 text-sm max-w-2xl leading-snug">
|
||||||
|
Answer each section accurately. Your responses are part of the dealership application assessment and may be verified.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{user && (
|
{user && (
|
||||||
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
|
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
|
||||||
<div className="w-8 h-8 bg-amber-600 rounded-full flex items-center justify-center">
|
<div className="w-8 h-8 bg-re-red rounded-full flex items-center justify-center">
|
||||||
<User className="w-4 h-4 text-white" />
|
<User className="w-4 h-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user