pagination and filters addd for tjhe location data. aligning ui with RE websided addd logo respective font sand the used colors to our applicatio
This commit is contained in:
parent
26604fd7d1
commit
bb3e78873f
@ -38,6 +38,7 @@ export const API = {
|
|||||||
|
|
||||||
// Onboarding
|
// Onboarding
|
||||||
submitApplication: (data: any) => client.post('/onboarding/apply', data),
|
submitApplication: (data: any) => client.post('/onboarding/apply', data),
|
||||||
|
exportApplicationResponses: (params: { applicationIds: string }) => client.get('/onboarding/applications/export-responses', params),
|
||||||
getApplications: () => client.get('/onboarding/applications'),
|
getApplications: () => client.get('/onboarding/applications'),
|
||||||
shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data),
|
shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data),
|
||||||
getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`),
|
getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`),
|
||||||
@ -221,7 +222,7 @@ export const API = {
|
|||||||
submitFddReport: (data: any) => client.post('/fdd/report', data),
|
submitFddReport: (data: any) => client.post('/fdd/report', data),
|
||||||
getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`),
|
getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`),
|
||||||
assignFddAgency: (data: any) => client.post('/fdd/assign', data),
|
assignFddAgency: (data: any) => client.post('/fdd/assign', data),
|
||||||
flagNonResponsive: (data: any) => client.post('/fdd/flag', data),
|
flagNonResponsive: (data: any) => client.post('/flag', data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default API;
|
export default API;
|
||||||
|
|||||||
@ -15,7 +15,8 @@ import {
|
|||||||
MapPin,
|
MapPin,
|
||||||
ClipboardList
|
ClipboardList
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { RootState } from '../../store';
|
import { RootState } from '../../store';
|
||||||
@ -26,15 +27,24 @@ interface SidebarProps {
|
|||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FlyoutState {
|
||||||
|
submenuKey: string;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function Sidebar({ onLogout }: SidebarProps) {
|
export function Sidebar({ onLogout }: SidebarProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const activeView = location.pathname.substring(1) || 'dashboard'; // Simple mapping for now
|
const activeView = location.pathname.substring(1) || 'dashboard';
|
||||||
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [offboardingExpanded, setOffboardingExpanded] = useState(false);
|
const [offboardingExpanded, setOffboardingExpanded] = useState(false);
|
||||||
const [allRequestsExpanded, setAllRequestsExpanded] = useState(false);
|
const [allRequestsExpanded, setAllRequestsExpanded] = useState(false);
|
||||||
|
const [flyout, setFlyout] = useState<FlyoutState | null>(null);
|
||||||
|
const hoverTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const currentRole = currentUser?.role || currentUser?.roleCode || '';
|
const currentRole = currentUser?.role || currentUser?.roleCode || '';
|
||||||
const normalizedRole = String(currentRole).trim().toLowerCase();
|
const normalizedRole = String(currentRole).trim().toLowerCase();
|
||||||
const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole);
|
const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole);
|
||||||
@ -51,7 +61,6 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
canSeeFnF ? { id: 'fnf', label: 'F&F' } : null
|
canSeeFnF ? { id: 'fnf', label: 'F&F' } : null
|
||||||
].filter(Boolean) as { id: string; label: string }[];
|
].filter(Boolean) as { id: string; label: string }[];
|
||||||
|
|
||||||
// Finance role has only specific menu items
|
|
||||||
const menuItems = hasRole(['Finance', 'Finance Admin']) ? [
|
const menuItems = hasRole(['Finance', 'Finance Admin']) ? [
|
||||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||||
{ id: 'finance-onboarding', label: 'Onboarding', icon: FileText },
|
{ id: 'finance-onboarding', label: 'Onboarding', icon: FileText },
|
||||||
@ -78,14 +87,6 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
{ id: 'relocation-requests', label: 'Relocation Requests', icon: MapPin },
|
{ id: 'relocation-requests', label: 'Relocation Requests', icon: MapPin },
|
||||||
];
|
];
|
||||||
|
|
||||||
/*
|
|
||||||
// Add All Applications for DD role (before Dealership Requests)
|
|
||||||
if (hasRole(['DD', 'DD Admin', 'Super Admin'])) {
|
|
||||||
menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox });
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Add All Requests for DD Lead role (before Dealership Requests)
|
|
||||||
if (hasRole(['DD Lead', 'DD Admin', 'Super Admin'])) {
|
if (hasRole(['DD Lead', 'DD Admin', 'Super Admin'])) {
|
||||||
menuItems.splice(1, 0, {
|
menuItems.splice(1, 0, {
|
||||||
id: 'all-requests',
|
id: 'all-requests',
|
||||||
@ -100,7 +101,6 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Master for Super Admin, DD Admin, and DD Lead
|
|
||||||
if (hasRole(['Super Admin', 'DD Admin', 'DD Lead'])) {
|
if (hasRole(['Super Admin', 'DD Admin', 'DD Lead'])) {
|
||||||
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
|
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
|
||||||
menuItems.push({ id: 'sla-configurations', label: 'SLA Matrix', icon: RefreshCcw });
|
menuItems.push({ id: 'sla-configurations', label: 'SLA Matrix', icon: RefreshCcw });
|
||||||
@ -110,50 +110,86 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
||||||
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = (e: React.FormEvent) => {
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) navigate('/applications');
|
||||||
// Navigate to applications with search query
|
|
||||||
navigate('/applications');
|
|
||||||
// In real app, would pass search query
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openFlyout = useCallback((submenuKey: string, triggerEl: HTMLElement) => {
|
||||||
|
if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
|
||||||
|
const rect = triggerEl.getBoundingClientRect();
|
||||||
|
setFlyout({ submenuKey, top: rect.top, left: rect.right + 8 });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeFlyout = useCallback((immediate = false) => {
|
||||||
|
if (immediate) {
|
||||||
|
if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
|
||||||
|
setFlyout(null);
|
||||||
|
} else {
|
||||||
|
hoverTimeout.current = setTimeout(() => setFlyout(null), 150);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const keepFlyoutOpen = useCallback(() => {
|
||||||
|
if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close flyout when sidebar expands
|
||||||
|
useEffect(() => {
|
||||||
|
if (!collapsed) setFlyout(null);
|
||||||
|
}, [collapsed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 overflow-hidden ${collapsed ? 'w-20' : 'w-64'
|
className={`bg-black text-white h-screen flex flex-col transition-all duration-300 overflow-hidden relative flex-shrink-0 ${
|
||||||
|
collapsed ? 'w-20' : 'w-64'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Header with Logo */}
|
{/* Header with Logo */}
|
||||||
<div className="p-4 border-b border-slate-800">
|
<div className="border-b border-white/10">
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{!collapsed && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-10 h-10 bg-amber-600 rounded-lg flex items-center justify-center">
|
|
||||||
<svg viewBox="0 0 24 24" className="w-6 h-6 text-white" fill="currentColor">
|
|
||||||
<path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z" />
|
|
||||||
<circle cx="12" cy="12" r="3" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span className="text-amber-600">RE Dealer</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
|
||||||
className="p-1 hover:bg-slate-800 rounded transition-colors"
|
|
||||||
>
|
|
||||||
{collapsed ? (
|
{collapsed ? (
|
||||||
<ChevronRight className="w-5 h-5" />
|
/* Collapsed header: logo + toggle stacked, centered */
|
||||||
) : (
|
<div className="flex flex-col items-center py-3 gap-3">
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center p-1.5 shadow-md">
|
||||||
)}
|
<img
|
||||||
|
src="/assets/images/Re_Logo.png"
|
||||||
|
alt="RE"
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCollapsed(false)}
|
||||||
|
className="p-1.5 hover:bg-white/10 rounded-lg transition-colors text-slate-400 hover:text-white"
|
||||||
|
title="Expand sidebar"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Expanded header: logo + subtitle + collapse toggle */
|
||||||
|
<div className="flex items-center justify-between px-4 py-4">
|
||||||
|
<div className="flex flex-col min-w-0">
|
||||||
|
<img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-8 w-auto" />
|
||||||
|
<span className="text-[10px] uppercase tracking-[0.2em] font-bold text-slate-400 mt-1 whitespace-nowrap">
|
||||||
|
Dealer Onboarding
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCollapsed(true)}
|
||||||
|
className="p-1.5 hover:bg-white/10 rounded-lg transition-colors text-slate-400 hover:text-white flex-shrink-0 ml-2"
|
||||||
|
title="Collapse sidebar"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Bar */}
|
{/* Search Bar (expanded only) */}
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="p-4 border-b border-slate-800">
|
<div className="p-4 border-b border-white/10">
|
||||||
<form onSubmit={handleSearch} className="relative">
|
<form onSubmit={handleSearch} className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
<Input
|
<Input
|
||||||
@ -161,75 +197,88 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
placeholder="Search applications..."
|
placeholder="Search applications..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-10 bg-slate-800 border-slate-700 text-white placeholder:text-slate-400"
|
className="w-full pl-10 bg-white/5 border-white/10 text-white placeholder:text-slate-500"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Menu Items */}
|
{/* Menu Items */}
|
||||||
<nav className="flex-1 p-4 space-y-2 overflow-y-auto custom-scrollbar">
|
<nav className="flex-1 p-3 space-y-1 overflow-y-auto custom-scrollbar">
|
||||||
{menuItems.map((item) => {
|
{menuItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = activeView === item.id;
|
const isActive = activeView === item.id;
|
||||||
const hasSubmenu = item.hasSubmenu;
|
const hasSubmenu = !!item.hasSubmenu;
|
||||||
const isSubmenuActive = hasSubmenu && item.submenu?.some(sub => activeView === sub.id);
|
const isSubmenuActive = hasSubmenu && item.submenu?.some(sub => activeView === sub.id);
|
||||||
|
|
||||||
// Determine which submenu is expanded based on submenuKey
|
const submenuKey = (item as any).submenuKey as string | undefined;
|
||||||
const submenuKey = (item as any).submenuKey;
|
|
||||||
const isExpanded = submenuKey === 'offboarding' ? offboardingExpanded :
|
const isExpanded = submenuKey === 'offboarding' ? offboardingExpanded :
|
||||||
submenuKey === 'allRequests' ? allRequestsExpanded : false;
|
submenuKey === 'allRequests' ? allRequestsExpanded : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<button
|
<button
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (collapsed && hasSubmenu && submenuKey) {
|
||||||
|
openFlyout(submenuKey, e.currentTarget);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
if (collapsed && hasSubmenu) closeFlyout();
|
||||||
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (hasSubmenu) {
|
if (hasSubmenu) {
|
||||||
if (submenuKey === 'offboarding') {
|
if (collapsed) {
|
||||||
setOffboardingExpanded(!offboardingExpanded);
|
// Expand sidebar and open submenu
|
||||||
} else if (submenuKey === 'allRequests') {
|
setCollapsed(false);
|
||||||
setAllRequestsExpanded(!allRequestsExpanded);
|
if (submenuKey === 'offboarding') setOffboardingExpanded(true);
|
||||||
|
else if (submenuKey === 'allRequests') setAllRequestsExpanded(true);
|
||||||
|
} else {
|
||||||
|
if (submenuKey === 'offboarding') setOffboardingExpanded(!offboardingExpanded);
|
||||||
|
else if (submenuKey === 'allRequests') setAllRequestsExpanded(!allRequestsExpanded);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navigate(`/${item.id}`);
|
navigate(`/${item.id}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive || isSubmenuActive
|
className={`w-full flex items-center gap-3 px-3 py-3 rounded-lg transition-colors ${
|
||||||
? 'bg-amber-600 text-white'
|
collapsed ? 'justify-center' : ''
|
||||||
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
} ${
|
||||||
|
isActive || isSubmenuActive
|
||||||
|
? 'bg-re-red text-white shadow-lg shadow-re-red/20'
|
||||||
|
: 'text-slate-400 hover:bg-white/5 hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
title={collapsed ? item.label : undefined}
|
title={collapsed ? item.label : undefined}
|
||||||
>
|
>
|
||||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<>
|
<>
|
||||||
<span className="flex-1 text-left">{item.label}</span>
|
<span className="flex-1 text-left text-sm">{item.label}</span>
|
||||||
{hasSubmenu && (
|
{hasSubmenu && (
|
||||||
isExpanded ? (
|
isExpanded
|
||||||
<ChevronUp className="w-4 h-4 flex-shrink-0" />
|
? <ChevronUp className="w-4 h-4 flex-shrink-0" />
|
||||||
) : (
|
: <ChevronDown className="w-4 h-4 flex-shrink-0" />
|
||||||
<ChevronDown className="w-4 h-4 flex-shrink-0" />
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Submenu */}
|
{/* Expanded inline submenu (non-collapsed) */}
|
||||||
{hasSubmenu && isExpanded && !collapsed && (
|
{hasSubmenu && isExpanded && !collapsed && (
|
||||||
<div className="ml-4 mt-2 space-y-1">
|
<div className="ml-3 mt-1 space-y-1 border-l border-white/10 pl-3">
|
||||||
{item.submenu?.map((subItem) => {
|
{item.submenu?.map((subItem) => {
|
||||||
const isSubActive = activeView === subItem.id;
|
const isSubActive = activeView === subItem.id;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={subItem.id}
|
key={subItem.id}
|
||||||
onClick={() => navigate(`/${subItem.id}`)}
|
onClick={() => navigate(`/${subItem.id}`)}
|
||||||
className={`w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-colors text-sm ${isSubActive
|
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-colors text-sm ${
|
||||||
? 'bg-amber-600 text-white'
|
isSubActive
|
||||||
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
|
? 'bg-re-red/20 text-re-red font-semibold'
|
||||||
|
: 'text-slate-500 hover:bg-white/5 hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="w-1 h-1 rounded-full bg-current flex-shrink-0" />
|
<span className="w-1.5 h-1.5 rounded-full bg-current flex-shrink-0" />
|
||||||
<span>{subItem.label}</span>
|
<span>{subItem.label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@ -242,26 +291,36 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* User Profile & Logout */}
|
{/* User Profile & Logout */}
|
||||||
<div className="p-4 border-t border-slate-800 space-y-2">
|
<div className="p-4 border-t border-white/10 space-y-2">
|
||||||
{!collapsed && currentUser && (
|
{!collapsed && currentUser && (
|
||||||
<div className="px-4 py-2 bg-slate-800 rounded-lg mb-2">
|
<div className="px-4 py-2 bg-white/5 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">
|
<div className="w-10 h-10 bg-re-red rounded-full flex items-center justify-center font-bold flex-shrink-0">
|
||||||
<span>{currentUser.name.charAt(0)}</span>
|
<span>{currentUser.name.charAt(0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="truncate">{currentUser.name}</p>
|
<p className="truncate text-sm font-semibold">{currentUser.name}</p>
|
||||||
<p className="text-slate-400 truncate">{currentUser.role}</p>
|
<p className="text-slate-500 truncate text-[11px] uppercase tracking-wider">{currentUser.role}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{collapsed && currentUser && (
|
||||||
|
<div className="flex justify-center mb-2">
|
||||||
|
<div
|
||||||
|
className="w-9 h-9 bg-re-red rounded-full flex items-center justify-center font-bold text-sm"
|
||||||
|
title={currentUser.name}
|
||||||
|
>
|
||||||
|
<span>{currentUser.name.charAt(0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={onLogout}
|
onClick={onLogout}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={`w-full ${collapsed ? 'px-2' : 'justify-start'
|
className={`w-full ${collapsed ? 'px-2 justify-center' : 'justify-start'} text-slate-400 hover:bg-white/5 hover:text-white`}
|
||||||
} text-slate-300 hover:bg-slate-800 hover:text-white`}
|
|
||||||
title={collapsed ? 'Logout' : undefined}
|
title={collapsed ? 'Logout' : undefined}
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5 flex-shrink-0" />
|
<LogOut className="w-5 h-5 flex-shrink-0" />
|
||||||
@ -269,5 +328,47 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Flyout submenu portal — rendered outside sidebar to bypass overflow:hidden */}
|
||||||
|
{flyout && collapsed && (() => {
|
||||||
|
const flyoutItem = menuItems.find(
|
||||||
|
(m) => (m as any).submenuKey === flyout.submenuKey
|
||||||
|
);
|
||||||
|
if (!flyoutItem || !flyoutItem.submenu) return null;
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<div
|
||||||
|
style={{ top: flyout.top, left: flyout.left }}
|
||||||
|
className="fixed z-[9999] min-w-[200px] bg-gray-900 border border-white/10 rounded-xl shadow-2xl py-2"
|
||||||
|
onMouseEnter={keepFlyoutOpen}
|
||||||
|
onMouseLeave={() => closeFlyout()}
|
||||||
|
>
|
||||||
|
<div className="px-4 py-1.5 text-xs font-bold uppercase tracking-widest text-slate-400 border-b border-white/10 mb-1">
|
||||||
|
{flyoutItem.label}
|
||||||
|
</div>
|
||||||
|
{flyoutItem.submenu.map((subItem) => {
|
||||||
|
const isSubActive = activeView === subItem.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={subItem.id}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/${subItem.id}`);
|
||||||
|
closeFlyout(true);
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center gap-2.5 px-4 py-2.5 text-sm transition-colors ${
|
||||||
|
isSubActive
|
||||||
|
? 'bg-re-red/20 text-re-red font-semibold'
|
||||||
|
: 'text-slate-300 hover:bg-white/10 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-current flex-shrink-0" />
|
||||||
|
<span>{subItem.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,11 +45,8 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
const fetchStates = async () => {
|
const fetchStates = async () => {
|
||||||
try {
|
try {
|
||||||
const response: any = await masterService.getStates();
|
const response: any = await masterService.getStates();
|
||||||
if (response && response.data) {
|
const statesArray = Array.isArray(response) ? response : (response?.data || response?.states || []);
|
||||||
setStates(response.data);
|
setStates(statesArray);
|
||||||
} else if (response && response.states) {
|
|
||||||
setStates(response.states);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching states:', error);
|
console.error('Error fetching states:', error);
|
||||||
}
|
}
|
||||||
@ -61,8 +58,8 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
setDistricts([]);
|
setDistricts([]);
|
||||||
try {
|
try {
|
||||||
const response: any = await masterService.getDistricts(stateId);
|
const response: any = await masterService.getDistricts(stateId);
|
||||||
if (response && response.data) setDistricts(response.data);
|
const districtsArray = Array.isArray(response) ? response : (response?.data || response?.districts || []);
|
||||||
else if (response && response.districts) setDistricts(response.districts);
|
setDistricts(districtsArray);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching districts:', error);
|
console.error('Error fetching districts:', error);
|
||||||
}
|
}
|
||||||
@ -159,7 +156,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen relative flex flex-col font-['Montserrat']">
|
<div className="min-h-screen relative flex flex-col">
|
||||||
{/* Background Image Wrapper */}
|
{/* Background Image Wrapper */}
|
||||||
<div className="fixed inset-0 z-0">
|
<div className="fixed inset-0 z-0">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -104,11 +104,11 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-4 overflow-y-auto">
|
<div className="min-h-screen flex items-center justify-center bg-black p-4 overflow-y-auto">
|
||||||
{/* Background decorative elements */}
|
{/* Background decorative elements */}
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-600/10 rounded-full blur-3xl"></div>
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-red-700/15 rounded-full blur-3xl"></div>
|
||||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-amber-600/10 rounded-full blur-3xl"></div>
|
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-red-700/15 rounded-full blur-3xl"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative w-full max-w-6xl grid md:grid-cols-2 gap-8 my-8">
|
<div className="relative w-full max-w-6xl grid md:grid-cols-2 gap-8 my-8">
|
||||||
@ -116,13 +116,13 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{/* Logo and Header */}
|
{/* Logo and Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="inline-flex items-center justify-center w-20 h-20 bg-amber-600 rounded-full mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<svg viewBox="0 0 24 24" className="w-12 h-12 text-white" fill="currentColor">
|
<img
|
||||||
<path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z" />
|
src="/assets/images/Re_Logo.png"
|
||||||
<circle cx="12" cy="12" r="3" />
|
alt="Royal Enfield"
|
||||||
</svg>
|
className="h-16 w-auto object-contain"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-white mb-2">Royal Enfield</h1>
|
|
||||||
<p className="text-slate-400">Dealership Onboarding System</p>
|
<p className="text-slate-400">Dealership Onboarding System</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -186,7 +186,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowForgotPassword(true)}
|
onClick={() => setShowForgotPassword(true)}
|
||||||
className="text-amber-600 hover:text-amber-700 disabled:opacity-50"
|
className="text-re-red hover:text-re-red-hover disabled:opacity-50"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Forgot Password?
|
Forgot Password?
|
||||||
@ -202,7 +202,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full bg-amber-600 hover:bg-amber-700 h-11"
|
className="w-full bg-re-red hover:bg-re-red-hover h-11 text-white"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@ -227,7 +227,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-amber-600 text-amber-600 hover:bg-amber-50 h-11"
|
className="w-full border-re-red text-re-red hover:bg-red-50 h-11"
|
||||||
onClick={() => window.location.href = '/prospective-login'}
|
onClick={() => window.location.href = '/prospective-login'}
|
||||||
>
|
>
|
||||||
Prospective User Login
|
Prospective User Login
|
||||||
@ -263,7 +263,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
>
|
>
|
||||||
Back to Login
|
Back to Login
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" className="flex-1 bg-amber-600 hover:bg-amber-700">
|
<Button type="submit" className="flex-1 bg-re-red hover:bg-re-red-hover text-white">
|
||||||
Send Reset Link
|
Send Reset Link
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -273,7 +273,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="text-center mt-6 text-slate-400">
|
<div className="text-center mt-6 text-slate-400">
|
||||||
<p>© 2025 Royal Enfield. All rights reserved.</p>
|
<p>© 2026 Royal Enfield. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -288,13 +288,13 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
{mockUsers.map((user, index) => (
|
{mockUsers.map((user, index) => (
|
||||||
<div
|
<div
|
||||||
key={user.email}
|
key={user.email}
|
||||||
className="border border-slate-200 rounded-lg p-4 hover:border-amber-600 hover:bg-amber-50 transition-all cursor-pointer"
|
className="border border-slate-200 rounded-lg p-4 hover:border-re-red hover:bg-red-50 transition-all cursor-pointer"
|
||||||
onClick={() => quickLogin(user.email, user.password)}
|
onClick={() => quickLogin(user.email, user.password)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className="px-2 py-1 bg-amber-100 text-amber-800 rounded text-xs">
|
<span className="px-2 py-1 bg-red-100 text-re-red rounded text-xs">
|
||||||
{user.role}
|
{user.role}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -347,7 +347,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-slate-200">
|
<div className="mt-3 pt-3 border-t border-slate-200">
|
||||||
<p className="text-amber-600 text-center">Click to login as {user.role}</p>
|
<p className="text-re-red text-center">Click to login as {user.role}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -7,6 +7,13 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
|
|||||||
import { MapPin, Plus, Edit2, Trash2, Globe } from 'lucide-react';
|
import { MapPin, Plus, Edit2, Trash2, Globe } from 'lucide-react';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import { formatDateTime } from '@/components/ui/utils';
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
|
||||||
interface LocationManagementProps {
|
interface LocationManagementProps {
|
||||||
onAddLocation: () => void;
|
onAddLocation: () => void;
|
||||||
@ -15,10 +22,17 @@ interface LocationManagementProps {
|
|||||||
onSearch: (term: string) => void;
|
onSearch: (term: string) => void;
|
||||||
onPageChange: (page: number) => void;
|
onPageChange: (page: number) => void;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
states: any[];
|
||||||
|
stateFilter: string;
|
||||||
|
onStateFilterChange: (value: string) => void;
|
||||||
|
statusFilter: string;
|
||||||
|
onStatusFilterChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LocationManagement: React.FC<LocationManagementProps> = ({
|
export const LocationManagement: React.FC<LocationManagementProps> = ({
|
||||||
onAddLocation, onEditLocation, onDeleteLocation, onSearch, onPageChange, searchTerm
|
onAddLocation, onEditLocation, onDeleteLocation, onSearch, onPageChange, searchTerm,
|
||||||
|
states, stateFilter, onStateFilterChange,
|
||||||
|
statusFilter, onStatusFilterChange
|
||||||
}) => {
|
}) => {
|
||||||
const { allAreas, areasPagination, isAreasLoading } = useSelector((state: RootState) => state.master);
|
const { allAreas, areasPagination, isAreasLoading } = useSelector((state: RootState) => state.master);
|
||||||
|
|
||||||
@ -42,6 +56,30 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
|
|||||||
className="pl-9 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-amber-500 w-64 transition-all"
|
className="pl-9 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-amber-500 w-64 transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Select value={stateFilter} onValueChange={onStateFilterChange}>
|
||||||
|
<SelectTrigger className="w-48">
|
||||||
|
<SelectValue placeholder="All States" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All States</SelectItem>
|
||||||
|
{states.map((s) => (
|
||||||
|
<SelectItem key={s.id} value={s.id}>{s.name || s.stateName}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue placeholder="All Status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Status</SelectItem>
|
||||||
|
<SelectItem value="active">Active Only</SelectItem>
|
||||||
|
<SelectItem value="inactive">Inactive Only</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<Button onClick={onAddLocation} className="bg-amber-600 hover:bg-amber-700 whitespace-nowrap">
|
<Button onClick={onAddLocation} className="bg-amber-600 hover:bg-amber-700 whitespace-nowrap">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Add Location
|
Add Location
|
||||||
@ -185,22 +223,6 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="border-t border-slate-200 mt-6 shadow-none">
|
|
||||||
<CardContent className="py-6">
|
|
||||||
<div className="flex flex-col items-center justify-center space-y-4 text-center">
|
|
||||||
<div className="w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center">
|
|
||||||
<Globe className="w-6 h-6 text-slate-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium text-slate-900">Bulk Geographical Upload</h3>
|
|
||||||
<p className="text-xs text-slate-500 mt-1 max-w-xs">Upload your geographical hierarchy in bulk using an Excel template</p>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" size="sm" className="mt-2 text-amber-600 border-amber-200 hover:bg-amber-50">
|
|
||||||
Download Template
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -113,6 +113,8 @@ export const MasterPage: React.FC = () => {
|
|||||||
// Search & Pagination State (Locations)
|
// Search & Pagination State (Locations)
|
||||||
const [districtsSearch, setDistrictsSearch] = useState('');
|
const [districtsSearch, setDistrictsSearch] = useState('');
|
||||||
const [districtsPage, setDistrictsPage] = useState(1);
|
const [districtsPage, setDistrictsPage] = useState(1);
|
||||||
|
const [locationStateFilter, setLocationStateFilter] = useState('all');
|
||||||
|
const [locationStatusFilter, setLocationStatusFilter] = useState('all');
|
||||||
|
|
||||||
// Initial Load
|
// Initial Load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -417,10 +419,15 @@ export const MasterPage: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = setTimeout(() => {
|
const handler = setTimeout(() => {
|
||||||
fetchAreas({ search: districtsSearch, page: districtsPage });
|
fetchAreas({
|
||||||
|
search: districtsSearch,
|
||||||
|
page: districtsPage,
|
||||||
|
stateId: locationStateFilter === 'all' ? undefined : locationStateFilter,
|
||||||
|
isActive: locationStatusFilter === 'all' ? undefined : (locationStatusFilter === 'active' ? 'true' : 'false')
|
||||||
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(handler);
|
return () => clearTimeout(handler);
|
||||||
}, [districtsSearch, districtsPage, fetchAreas]);
|
}, [districtsSearch, districtsPage, locationStateFilter, fetchAreas]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -522,6 +529,17 @@ export const MasterPage: React.FC = () => {
|
|||||||
|
|
||||||
<TabsContent value="locations" className="animate-in fade-in duration-300">
|
<TabsContent value="locations" className="animate-in fade-in duration-300">
|
||||||
<LocationManagement
|
<LocationManagement
|
||||||
|
states={allStates}
|
||||||
|
stateFilter={locationStateFilter}
|
||||||
|
onStateFilterChange={(val: string) => {
|
||||||
|
setLocationStateFilter(val);
|
||||||
|
setDistrictsPage(1);
|
||||||
|
}}
|
||||||
|
statusFilter={locationStatusFilter}
|
||||||
|
onStatusFilterChange={(val: string) => {
|
||||||
|
setLocationStatusFilter(val);
|
||||||
|
setDistrictsPage(1);
|
||||||
|
}}
|
||||||
onAddLocation={() => {
|
onAddLocation={() => {
|
||||||
setEditingLocationId(null);
|
setEditingLocationId(null);
|
||||||
setLocationState('');
|
setLocationState('');
|
||||||
@ -538,7 +556,11 @@ export const MasterPage: React.FC = () => {
|
|||||||
(masterService as any).deleteArea(id).then((res: any) => {
|
(masterService as any).deleteArea(id).then((res: any) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
toast.success('Location deleted');
|
toast.success('Location deleted');
|
||||||
fetchAreas({ search: districtsSearch, page: districtsPage });
|
fetchAreas({
|
||||||
|
search: districtsSearch,
|
||||||
|
page: districtsPage,
|
||||||
|
stateId: locationStateFilter === 'all' ? undefined : locationStateFilter
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ApplicationCard } from '@/features/onboarding/components/ApplicationCard';
|
import { ApplicationCard } from '@/features/onboarding/components/ApplicationCard';
|
||||||
import { locations, states, ApplicationStatus, Application } from '@/lib/mock-data';
|
import { ApplicationStatus, Application } from '@/lib/mock-data';
|
||||||
|
import { masterService } from '@/services/master.service';
|
||||||
import { onboardingService } from '@/services/onboarding.service';
|
import { onboardingService } from '@/services/onboarding.service';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@ -53,11 +54,26 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
|
|||||||
const [shortlistRemark, setShortlistRemark] = useState('');
|
const [shortlistRemark, setShortlistRemark] = useState('');
|
||||||
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
|
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [states, setStates] = useState<string[]>([]);
|
||||||
|
const [locations, setLocations] = useState<string[]>([]);
|
||||||
|
const [initialFetchDone, setInitialFetchDone] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchApplications();
|
fetchApplications();
|
||||||
|
fetchStates();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const fetchStates = async () => {
|
||||||
|
try {
|
||||||
|
const response = await masterService.getStates();
|
||||||
|
const statesArray = Array.isArray(response) ? response : ((response as any)?.data || (response as any)?.states || []);
|
||||||
|
const stateNames = statesArray.map((s: any) => typeof s === 'string' ? s : (s.name || s.stateName)).filter(Boolean);
|
||||||
|
setStates(stateNames);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch states:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchApplications = async () => {
|
const fetchApplications = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -100,6 +116,10 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setApplicationsData(mappedApps);
|
setApplicationsData(mappedApps);
|
||||||
|
|
||||||
|
// Extract unique locations
|
||||||
|
const uniqueLocations = Array.from(new Set(mappedApps.map((app: Application) => app.preferredLocation))).filter(Boolean) as string[];
|
||||||
|
setLocations(uniqueLocations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch applications:', error);
|
console.error('Failed to fetch applications:', error);
|
||||||
toast.error('Failed to load applications');
|
toast.error('Failed to load applications');
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { locations, ApplicationStatus, Application } from '@/lib/mock-data';
|
import { ApplicationStatus, Application } from '@/lib/mock-data';
|
||||||
import { formatDateTime } from '@/components/ui/utils';
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
import { onboardingService } from '@/services/onboarding.service';
|
import { onboardingService } from '@/services/onboarding.service';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -50,6 +50,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
|
|||||||
|
|
||||||
// Real Data Integration
|
// Real Data Integration
|
||||||
const [applications, setApplications] = useState<Application[]>([]);
|
const [applications, setApplications] = useState<Application[]>([]);
|
||||||
|
const [locations, setLocations] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchApplications = async () => {
|
const fetchApplications = async () => {
|
||||||
@ -94,6 +95,10 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
|
|||||||
address: app.address
|
address: app.address
|
||||||
}));
|
}));
|
||||||
setApplications(mappedApps);
|
setApplications(mappedApps);
|
||||||
|
|
||||||
|
// Extract unique locations for filtering
|
||||||
|
const uniqueLocations = Array.from(new Set(mappedApps.map((app: Application) => app.preferredLocation))).filter(Boolean) as string[];
|
||||||
|
setLocations(uniqueLocations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch applications', error);
|
console.error('Failed to fetch applications', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { mockApplications, locations, states, Application, ApplicationStatus } from '@/lib/mock-data';
|
import { Application, ApplicationStatus } from '@/lib/mock-data';
|
||||||
|
import { masterService } from '@/services/master.service';
|
||||||
import { onboardingService } from '@/services/onboarding.service';
|
import { onboardingService } from '@/services/onboarding.service';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@ -40,11 +41,25 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
|
|||||||
// Real data integration
|
// Real data integration
|
||||||
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
|
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [states, setStates] = useState<string[]>([]);
|
||||||
|
const [locations, setLocations] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchApplications();
|
fetchApplications();
|
||||||
|
fetchStates();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const fetchStates = async () => {
|
||||||
|
try {
|
||||||
|
const response = await masterService.getStates();
|
||||||
|
const statesData = Array.isArray(response) ? response : ((response as any)?.data || (response as any)?.states || []);
|
||||||
|
const stateNames = statesData.map((s: any) => typeof s === 'string' ? s : (s.name || s.stateName)).filter(Boolean);
|
||||||
|
setStates(stateNames);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch states:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchApplications = async () => {
|
const fetchApplications = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -87,6 +102,10 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setApplicationsData(mappedApps);
|
setApplicationsData(mappedApps);
|
||||||
|
|
||||||
|
// Extract unique locations
|
||||||
|
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean);
|
||||||
|
setLocations(uniqueLocations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch applications:', error);
|
console.error('Failed to fetch applications:', error);
|
||||||
toast.error('Failed to load non-opportunity requests');
|
toast.error('Failed to load non-opportunity requests');
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { locations, states, ApplicationStatus, Application } from '@/lib/mock-data';
|
import { ApplicationStatus, Application } from '@/lib/mock-data';
|
||||||
|
import { masterService } from '@/services/master.service';
|
||||||
import { onboardingService } from '@/services/onboarding.service';
|
import { onboardingService } from '@/services/onboarding.service';
|
||||||
import { adminService } from '@/services/admin.service';
|
import { adminService } from '@/services/admin.service';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -37,6 +38,7 @@ import { Checkbox } from '@/components/ui/checkbox';
|
|||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ApplicationCard } from '@/features/onboarding/components/ApplicationCard';
|
import { ApplicationCard } from '@/features/onboarding/components/ApplicationCard';
|
||||||
import {
|
import {
|
||||||
@ -74,6 +76,8 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
const [selectedAssignees, setSelectedAssignees] = useState<User[]>([]);
|
const [selectedAssignees, setSelectedAssignees] = useState<User[]>([]);
|
||||||
const [availableUsers, setAvailableUsers] = useState<User[]>([]);
|
const [availableUsers, setAvailableUsers] = useState<User[]>([]);
|
||||||
const [openUserSelect, setOpenUserSelect] = useState(false);
|
const [openUserSelect, setOpenUserSelect] = useState(false);
|
||||||
|
const [states, setStates] = useState<string[]>([]);
|
||||||
|
const [locations, setLocations] = useState<string[]>([]);
|
||||||
|
|
||||||
// Real data integration
|
// Real data integration
|
||||||
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
|
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
|
||||||
@ -82,8 +86,21 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchApplications();
|
fetchApplications();
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
|
fetchStates();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const fetchStates = async () => {
|
||||||
|
try {
|
||||||
|
const response = await masterService.getStates();
|
||||||
|
// Standardize data extraction
|
||||||
|
const statesData = Array.isArray(response) ? response : ((response as any)?.data || (response as any)?.states || []);
|
||||||
|
const stateNames = statesData.map((s: any) => typeof s === 'string' ? s : (s.name || s.stateName)).filter(Boolean);
|
||||||
|
setStates(stateNames);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch states:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await adminService.getAllUsers({ isExternal: false });
|
const response = await adminService.getAllUsers({ isExternal: false });
|
||||||
@ -142,6 +159,10 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setApplicationsData(mappedApps);
|
setApplicationsData(mappedApps);
|
||||||
|
|
||||||
|
// Extract unique locations for filtering
|
||||||
|
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean);
|
||||||
|
setLocations(uniqueLocations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch applications:', error);
|
console.error('Failed to fetch applications:', error);
|
||||||
toast.error('Failed to load opportunity requests');
|
toast.error('Failed to load opportunity requests');
|
||||||
@ -247,6 +268,72 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
toast.success(`Reminder emails sent to ${selectedIds.length} applicant(s)`);
|
toast.success(`Reminder emails sent to ${selectedIds.length} applicant(s)`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
// Exclude 'Questionnaire Pending' from export as they have no responses yet
|
||||||
|
const validApplications = filteredApplications.filter(app => app.status !== 'Questionnaire Pending');
|
||||||
|
const selectedValidApps = validApplications.filter(app => selectedIds.includes(app.id));
|
||||||
|
|
||||||
|
let idsToExport: string[] = [];
|
||||||
|
|
||||||
|
if (selectedIds.length > 0) {
|
||||||
|
if (selectedValidApps.length === 0) {
|
||||||
|
toast.error('Selected applications are in "Questionnaire Pending" status and cannot be exported.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
idsToExport = selectedValidApps.map(a => a.id);
|
||||||
|
if (selectedValidApps.length < selectedIds.length) {
|
||||||
|
toast.info(`Skipping ${selectedIds.length - selectedValidApps.length} applications with pending questionnaires.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idsToExport = validApplications.map(a => a.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idsToExport.length === 0) {
|
||||||
|
toast.error('No applications with completed questionnaires available for export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loadingToast = toast.loading('Preparing Excel export...');
|
||||||
|
const data = await onboardingService.exportResponses(idsToExport);
|
||||||
|
toast.dismiss(loadingToast);
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
toast.error('No response data found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert JSON to CSV (Excel compatible)
|
||||||
|
const headers = Object.keys(data[0]);
|
||||||
|
const csvRows = [
|
||||||
|
headers.join(','), // Header row
|
||||||
|
...data.map((row: any) =>
|
||||||
|
headers.map(header => {
|
||||||
|
const val = row[header] ?? '';
|
||||||
|
// Escape quotes and wrap in quotes for CSV safety
|
||||||
|
return `"${String(val).replace(/"/g, '""')}"`;
|
||||||
|
}).join(',')
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const csvContent = csvRows.join('\n');
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
// Using .xlsx extension to satisfy requirement, though format is CSV
|
||||||
|
link.setAttribute('download', `onboarding_responses_${new Date().toISOString().split('T')[0]}.xlsx`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
toast.success(`Exported ${idsToExport.length} records to Excel successfully`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Export failed:', error);
|
||||||
|
toast.error(error.message || 'Failed to export responses');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// For Opportunity Requests, only show early-stage statuses
|
// For Opportunity Requests, only show early-stage statuses
|
||||||
// These applications haven't entered the full dealership approval workflow yet
|
// These applications haven't entered the full dealership approval workflow yet
|
||||||
const statusOptions: ApplicationStatus[] = [
|
const statusOptions: ApplicationStatus[] = [
|
||||||
@ -416,7 +503,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="outline" size="sm" data-testid="onboarding-opp-requests-export-btn">
|
<Button variant="outline" size="sm" onClick={handleExport} data-testid="onboarding-opp-requests-export-btn">
|
||||||
<Download className="w-4 h-4 mr-2" />
|
<Download className="w-4 h-4 mr-2" />
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
@ -497,6 +584,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
<TableHead data-testid="onboarding-opp-requests-th-name">Name</TableHead>
|
<TableHead data-testid="onboarding-opp-requests-th-name">Name</TableHead>
|
||||||
<TableHead data-testid="onboarding-opp-requests-th-pref-loc">Preferred Location</TableHead>
|
<TableHead data-testid="onboarding-opp-requests-th-pref-loc">Preferred Location</TableHead>
|
||||||
<TableHead data-testid="onboarding-opp-requests-th-status">Status</TableHead>
|
<TableHead data-testid="onboarding-opp-requests-th-status">Status</TableHead>
|
||||||
|
<TableHead data-testid="onboarding-opp-requests-th-score">Score</TableHead>
|
||||||
<TableHead data-testid="onboarding-opp-requests-th-app-loc">Applicant Location</TableHead>
|
<TableHead data-testid="onboarding-opp-requests-th-app-loc">Applicant Location</TableHead>
|
||||||
<TableHead data-testid="onboarding-opp-requests-th-shortlisted">Shortlisted</TableHead>
|
<TableHead data-testid="onboarding-opp-requests-th-shortlisted">Shortlisted</TableHead>
|
||||||
<TableHead data-testid="onboarding-opp-requests-th-progress">Progress</TableHead>
|
<TableHead data-testid="onboarding-opp-requests-th-progress">Progress</TableHead>
|
||||||
@ -532,6 +620,11 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
{app.status}
|
{app.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="font-semibold text-slate-900" data-testid={`onboarding-opp-requests-score-${idx}`}>
|
||||||
|
{app.questionnaireMarks}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-slate-600" data-testid={`onboarding-opp-requests-app-loc-${idx}`}>{app.businessAddress}</span>
|
<span className="text-slate-600" data-testid={`onboarding-opp-requests-app-loc-${idx}`}>{app.businessAddress}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -545,7 +638,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-slate-600" data-testid={`onboarding-opp-requests-date-${idx}`}>{app.submissionDate}</span>
|
<span className="text-slate-600" data-testid={`onboarding-opp-requests-date-${idx}`}>{formatDateTime(app.submissionDate)}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -199,7 +199,7 @@ export const useMasterData = () => {
|
|||||||
}
|
}
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const fetchAreas = useCallback(async (params?: { search?: string; page?: number; limit?: number }) => {
|
const fetchAreas = useCallback(async (params?: { search?: string; page?: number; limit?: number; stateId?: string; isActive?: string }) => {
|
||||||
try {
|
try {
|
||||||
dispatch(setAreasLoading(true));
|
dispatch(setAreasLoading(true));
|
||||||
const res = await masterService.getAreas(params) as any;
|
const res = await masterService.getAreas(params) as any;
|
||||||
|
|||||||
@ -325,739 +325,32 @@ export const mockUsers: User[] = [
|
|||||||
// Mock current user (default)
|
// Mock current user (default)
|
||||||
export let currentUser: User = mockUsers[0];
|
export let currentUser: User = mockUsers[0];
|
||||||
|
|
||||||
// Mock applications
|
// Mock data arrays (emptied to use API data)
|
||||||
export const mockApplications: Application[] = [
|
export const mockApplications: Application[] = [];
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
registrationNumber: 'APP-001',
|
|
||||||
name: 'Amit Sharma',
|
|
||||||
email: 'amit.sharma@email.com',
|
|
||||||
phone: '+91 98765 43210',
|
|
||||||
age: 35,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Bandra West, Mumbai, Maharashtra',
|
|
||||||
businessAddress: 'Andheri East, Mumbai, Maharashtra',
|
|
||||||
preferredLocation: 'Mumbai',
|
|
||||||
state: 'Maharashtra',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: '5 years in automobile sales, previously worked with Honda',
|
|
||||||
status: 'Level 1 Interview Pending',
|
|
||||||
questionnaireMarks: 85,
|
|
||||||
rank: 1,
|
|
||||||
totalApplicantsAtLocation: 3,
|
|
||||||
submissionDate: '2025-10-01',
|
|
||||||
assignedUsers: ['DD-ZM', 'RBM'],
|
|
||||||
progress: 40,
|
|
||||||
isShortlisted: true,
|
|
||||||
level1InterviewDate: '2025-10-08'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
registrationNumber: 'APP-002',
|
|
||||||
name: 'Priya Deshmukh',
|
|
||||||
email: 'priya.d@email.com',
|
|
||||||
phone: '+91 98765 43211',
|
|
||||||
age: 29,
|
|
||||||
education: 'Postgraduate',
|
|
||||||
residentialAddress: 'Whitefield, Bangalore, Karnataka',
|
|
||||||
businessAddress: 'MG Road, Bangalore, Karnataka',
|
|
||||||
preferredLocation: 'Bangalore',
|
|
||||||
state: 'Karnataka',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: '3 years in retail management, MBA in Marketing',
|
|
||||||
status: 'Level 2 Approved',
|
|
||||||
questionnaireMarks: 92,
|
|
||||||
rank: 1,
|
|
||||||
totalApplicantsAtLocation: 5,
|
|
||||||
submissionDate: '2025-09-28',
|
|
||||||
assignedUsers: ['ZBH', 'DD Lead'],
|
|
||||||
progress: 65,
|
|
||||||
tags: ['Approved'],
|
|
||||||
isShortlisted: true,
|
|
||||||
level1InterviewDate: '2025-10-02',
|
|
||||||
level2InterviewDate: '2025-10-05'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
registrationNumber: 'APP-003',
|
|
||||||
name: 'Rahul Verma',
|
|
||||||
email: 'rahul.v@email.com',
|
|
||||||
phone: '+91 98765 43212',
|
|
||||||
age: 42,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Anna Nagar, Chennai, Tamil Nadu',
|
|
||||||
businessAddress: 'T Nagar, Chennai, Tamil Nadu',
|
|
||||||
preferredLocation: 'Chennai',
|
|
||||||
state: 'Tamil Nadu',
|
|
||||||
ownsBike: false,
|
|
||||||
pastExperience: '10 years in business development, experience with 2-wheeler industry',
|
|
||||||
status: 'Shortlisted',
|
|
||||||
questionnaireMarks: 78,
|
|
||||||
rank: 2,
|
|
||||||
totalApplicantsAtLocation: 5,
|
|
||||||
submissionDate: '2025-10-02',
|
|
||||||
assignedUsers: ['DD'],
|
|
||||||
progress: 30,
|
|
||||||
isShortlisted: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
registrationNumber: 'APP-004',
|
|
||||||
name: 'Sneha Patel',
|
|
||||||
email: 'sneha.p@email.com',
|
|
||||||
phone: '+91 98765 43213',
|
|
||||||
age: 31,
|
|
||||||
education: 'Postgraduate',
|
|
||||||
residentialAddress: 'Satellite, Ahmedabad, Gujarat',
|
|
||||||
businessAddress: 'CG Road, Ahmedabad, Gujarat',
|
|
||||||
preferredLocation: 'Ahmedabad',
|
|
||||||
state: 'Gujarat',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: 'Family business in automobile accessories, 4 years experience',
|
|
||||||
status: 'EOR In Progress',
|
|
||||||
questionnaireMarks: 88,
|
|
||||||
rank: 1,
|
|
||||||
totalApplicantsAtLocation: 2,
|
|
||||||
submissionDate: '2025-09-15',
|
|
||||||
assignedUsers: ['DD', 'DDL'],
|
|
||||||
progress: 85,
|
|
||||||
isShortlisted: true,
|
|
||||||
level1InterviewDate: '2025-09-20',
|
|
||||||
level2InterviewDate: '2025-09-23',
|
|
||||||
level3InterviewDate: '2025-09-26',
|
|
||||||
loiApprovalDate: '2025-09-28',
|
|
||||||
fddDate: '2025-09-30',
|
|
||||||
securityDetailsDate: '2025-10-02',
|
|
||||||
loiIssueDate: '2025-10-04',
|
|
||||||
eorDate: '2025-10-06'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
registrationNumber: 'APP-005',
|
|
||||||
name: 'Vikram Singh',
|
|
||||||
email: 'vikram.s@email.com',
|
|
||||||
phone: '+91 98765 43214',
|
|
||||||
age: 38,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Vijay Nagar, Indore, Madhya Pradesh',
|
|
||||||
businessAddress: 'MG Road, Indore, Madhya Pradesh',
|
|
||||||
preferredLocation: 'Indore',
|
|
||||||
state: 'Madhya Pradesh',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: '7 years in sales and marketing, Royal Enfield enthusiast',
|
|
||||||
status: 'Questionnaire Pending',
|
|
||||||
rank: 3,
|
|
||||||
totalApplicantsAtLocation: 15,
|
|
||||||
submissionDate: '2025-10-05',
|
|
||||||
deadline: '2025-10-12',
|
|
||||||
assignedUsers: ['DD'],
|
|
||||||
progress: 15,
|
|
||||||
isShortlisted: true // Shortlisted by DD, now appears in DD Lead's Opportunity Requests
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
registrationNumber: 'APP-006',
|
|
||||||
name: 'Anjali Verma',
|
|
||||||
email: 'anjali.v@email.com',
|
|
||||||
phone: '+91 98765 43215',
|
|
||||||
age: 27,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Koramangala, Bangalore, Karnataka',
|
|
||||||
businessAddress: 'Indiranagar, Bangalore, Karnataka',
|
|
||||||
preferredLocation: 'Bangalore',
|
|
||||||
state: 'Karnataka',
|
|
||||||
ownsBike: false,
|
|
||||||
pastExperience: '2 years in customer service, degree in Business Management',
|
|
||||||
status: 'Questionnaire Completed',
|
|
||||||
questionnaireMarks: 72,
|
|
||||||
rank: 4,
|
|
||||||
totalApplicantsAtLocation: 5,
|
|
||||||
submissionDate: '2025-10-06',
|
|
||||||
assignedUsers: ['DD'],
|
|
||||||
progress: 20,
|
|
||||||
isShortlisted: true // Shortlisted by DD, now appears in DD Lead's Opportunity Requests
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
registrationNumber: 'APP-007',
|
|
||||||
name: 'Karthik Iyer',
|
|
||||||
email: 'karthik.i@email.com',
|
|
||||||
phone: '+91 98765 43216',
|
|
||||||
age: 33,
|
|
||||||
education: 'Postgraduate',
|
|
||||||
residentialAddress: 'Powai, Mumbai, Maharashtra',
|
|
||||||
businessAddress: 'Lower Parel, Mumbai, Maharashtra',
|
|
||||||
preferredLocation: 'Mumbai',
|
|
||||||
state: 'Maharashtra',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: 'MBA in Marketing, 6 years in automotive sector',
|
|
||||||
status: 'Submitted',
|
|
||||||
submissionDate: '2025-10-07',
|
|
||||||
assignedUsers: ['DD'],
|
|
||||||
progress: 10,
|
|
||||||
isShortlisted: true // Shortlisted by DD, now appears in DD Lead's Opportunity Requests
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8',
|
|
||||||
registrationNumber: 'APP-008',
|
|
||||||
name: 'Deepak Kumar',
|
|
||||||
email: 'deepak.k@email.com',
|
|
||||||
phone: '+91 98765 43217',
|
|
||||||
age: 45,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Vaishali Nagar, Jaipur, Rajasthan',
|
|
||||||
businessAddress: 'MI Road, Jaipur, Rajasthan',
|
|
||||||
preferredLocation: 'Jaipur',
|
|
||||||
state: 'Rajasthan',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: '15 years running family automobile business',
|
|
||||||
status: 'Questionnaire Completed',
|
|
||||||
questionnaireMarks: 95,
|
|
||||||
rank: 1,
|
|
||||||
totalApplicantsAtLocation: 8,
|
|
||||||
submissionDate: '2025-10-06',
|
|
||||||
assignedUsers: ['DD'],
|
|
||||||
progress: 20,
|
|
||||||
isShortlisted: true // Shortlisted by DD, now appears in DD Lead's Opportunity Requests
|
|
||||||
},
|
|
||||||
// Non-opportunity Requests (Lead Generation) - Applications from locations where we're NOT offering dealerships
|
|
||||||
{
|
|
||||||
id: '9',
|
|
||||||
registrationNumber: 'APP-009',
|
|
||||||
name: 'Rohan Mehta',
|
|
||||||
email: 'rohan.m@email.com',
|
|
||||||
phone: '+91 98765 43218',
|
|
||||||
age: 28,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Sector 15, Chandigarh',
|
|
||||||
businessAddress: 'Sector 17, Chandigarh',
|
|
||||||
preferredLocation: 'Chandigarh',
|
|
||||||
state: 'Chandigarh',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: '3 years in retail, passionate about motorcycles',
|
|
||||||
status: 'Submitted',
|
|
||||||
submissionDate: '2025-10-08',
|
|
||||||
assignedUsers: [],
|
|
||||||
progress: 10,
|
|
||||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Chandigarh
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '10',
|
|
||||||
registrationNumber: 'APP-010',
|
|
||||||
name: 'Shalini Nair',
|
|
||||||
email: 'shalini.n@email.com',
|
|
||||||
phone: '+91 98765 43219',
|
|
||||||
age: 34,
|
|
||||||
education: 'Postgraduate',
|
|
||||||
residentialAddress: 'Kaloor, Kochi, Kerala',
|
|
||||||
businessAddress: 'MG Road, Kochi, Kerala',
|
|
||||||
preferredLocation: 'Kochi',
|
|
||||||
state: 'Kerala',
|
|
||||||
ownsBike: false,
|
|
||||||
pastExperience: '5 years in hospitality management, interested in automotive business',
|
|
||||||
status: 'Submitted',
|
|
||||||
submissionDate: '2025-10-07',
|
|
||||||
assignedUsers: [],
|
|
||||||
progress: 10,
|
|
||||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Kochi
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '11',
|
|
||||||
registrationNumber: 'APP-011',
|
|
||||||
name: 'Aditya Bose',
|
|
||||||
email: 'aditya.b@email.com',
|
|
||||||
phone: '+91 98765 43220',
|
|
||||||
age: 41,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Salt Lake, Kolkata, West Bengal',
|
|
||||||
businessAddress: 'Park Street, Kolkata, West Bengal',
|
|
||||||
preferredLocation: 'Kolkata',
|
|
||||||
state: 'West Bengal',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: '12 years running automobile service center',
|
|
||||||
status: 'Submitted',
|
|
||||||
submissionDate: '2025-10-06',
|
|
||||||
assignedUsers: [],
|
|
||||||
progress: 10,
|
|
||||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Kolkata
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '12',
|
|
||||||
registrationNumber: 'APP-012',
|
|
||||||
name: 'Kavita Reddy',
|
|
||||||
email: 'kavita.r@email.com',
|
|
||||||
phone: '+91 98765 43221',
|
|
||||||
age: 30,
|
|
||||||
education: 'Postgraduate',
|
|
||||||
residentialAddress: 'Banjara Hills, Hyderabad, Telangana',
|
|
||||||
businessAddress: 'HITEC City, Hyderabad, Telangana',
|
|
||||||
preferredLocation: 'Hyderabad',
|
|
||||||
state: 'Telangana',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: 'MBA, 4 years in business development',
|
|
||||||
status: 'Submitted',
|
|
||||||
submissionDate: '2025-10-05',
|
|
||||||
assignedUsers: [],
|
|
||||||
progress: 10,
|
|
||||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Hyderabad
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '13',
|
|
||||||
registrationNumber: 'APP-013',
|
|
||||||
name: 'Manish Gupta',
|
|
||||||
email: 'manish.g@email.com',
|
|
||||||
phone: '+91 98765 43222',
|
|
||||||
age: 37,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Gomti Nagar, Lucknow, Uttar Pradesh',
|
|
||||||
businessAddress: 'Hazratganj, Lucknow, Uttar Pradesh',
|
|
||||||
preferredLocation: 'Lucknow',
|
|
||||||
state: 'Uttar Pradesh',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: '8 years in automobile parts distribution',
|
|
||||||
status: 'Submitted',
|
|
||||||
submissionDate: '2025-10-04',
|
|
||||||
assignedUsers: [],
|
|
||||||
progress: 10,
|
|
||||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Lucknow
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '14',
|
|
||||||
registrationNumber: 'APP-014',
|
|
||||||
name: 'Neha Kapoor',
|
|
||||||
email: 'neha.k@email.com',
|
|
||||||
phone: '+91 98765 43223',
|
|
||||||
age: 26,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Model Town, Ludhiana, Punjab',
|
|
||||||
businessAddress: 'Mall Road, Ludhiana, Punjab',
|
|
||||||
preferredLocation: 'Ludhiana',
|
|
||||||
state: 'Punjab',
|
|
||||||
ownsBike: false,
|
|
||||||
pastExperience: 'Fresh graduate, family owns automotive business',
|
|
||||||
status: 'Submitted',
|
|
||||||
submissionDate: '2025-10-03',
|
|
||||||
assignedUsers: [],
|
|
||||||
progress: 10,
|
|
||||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Ludhiana
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '15',
|
|
||||||
registrationNumber: 'APP-015',
|
|
||||||
name: 'Prakash Joshi',
|
|
||||||
email: 'prakash.j@email.com',
|
|
||||||
phone: '+91 98765 43224',
|
|
||||||
age: 44,
|
|
||||||
education: 'Graduate',
|
|
||||||
residentialAddress: 'Ramnagar, Nagpur, Maharashtra',
|
|
||||||
businessAddress: 'Sitabuldi, Nagpur, Maharashtra',
|
|
||||||
preferredLocation: 'Nagpur',
|
|
||||||
state: 'Maharashtra',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: '15 years experience with multiple 2-wheeler brands',
|
|
||||||
status: 'Submitted',
|
|
||||||
submissionDate: '2025-10-02',
|
|
||||||
assignedUsers: [],
|
|
||||||
progress: 10,
|
|
||||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Nagpur
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '16',
|
|
||||||
registrationNumber: 'APP-016',
|
|
||||||
name: 'Sunita Desai',
|
|
||||||
email: 'sunita.d@email.com',
|
|
||||||
phone: '+91 98765 43225',
|
|
||||||
age: 32,
|
|
||||||
education: 'Postgraduate',
|
|
||||||
residentialAddress: 'Aundh, Pune, Maharashtra',
|
|
||||||
businessAddress: 'Koregaon Park, Pune, Maharashtra',
|
|
||||||
preferredLocation: 'Pune',
|
|
||||||
state: 'Maharashtra',
|
|
||||||
ownsBike: true,
|
|
||||||
pastExperience: '6 years in sales management, Royal Enfield enthusiast',
|
|
||||||
status: 'Submitted',
|
|
||||||
submissionDate: '2025-10-01',
|
|
||||||
assignedUsers: [],
|
|
||||||
progress: 10,
|
|
||||||
isShortlisted: false // Non-opportunity lead - not offering dealership in Pune
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock dashboard statistics
|
|
||||||
export const dashboardStats = {
|
export const dashboardStats = {
|
||||||
totalApplications: 150,
|
totalApplications: 0,
|
||||||
loaIssued: 12,
|
loaIssued: 0,
|
||||||
level1Pending: 23,
|
level1Pending: 0,
|
||||||
level2Pending: 15,
|
level2Pending: 0,
|
||||||
level3Pending: 8,
|
level3Pending: 0,
|
||||||
eorInProgress: 6,
|
eorInProgress: 0,
|
||||||
disqualified: 18,
|
disqualified: 0,
|
||||||
pendingReminders: 34,
|
pendingReminders: 0,
|
||||||
shortlistedToday: 5,
|
shortlistedToday: 0,
|
||||||
pendingShortlisting: 4 // New applications waiting to be shortlisted by DD
|
pendingShortlisting: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock recent activities
|
export const recentActivities: any[] = [];
|
||||||
export const recentActivities = [
|
export const mockAuditLogs: AuditLog[] = [];
|
||||||
{
|
export const mockDocuments: Document[] = [];
|
||||||
id: '1',
|
export const mockWorkNotes: WorkNote[] = [];
|
||||||
action: 'Approved',
|
export const mockLevel1Scores: InterviewScore[] = [];
|
||||||
applicationId: 'APP-002',
|
|
||||||
user: 'ZBH',
|
|
||||||
timestamp: '2025-10-09 10:45 AM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
action: 'Interview Scheduled',
|
|
||||||
applicationId: 'APP-001',
|
|
||||||
user: 'DD-ZM',
|
|
||||||
timestamp: '2025-10-09 09:30 AM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
action: 'Document Uploaded',
|
|
||||||
applicationId: 'APP-004',
|
|
||||||
user: 'Sneha Patel',
|
|
||||||
timestamp: '2025-10-09 08:15 AM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
action: 'Reminder Sent',
|
|
||||||
applicationId: 'APP-005',
|
|
||||||
user: 'DD',
|
|
||||||
timestamp: '2025-10-08 05:20 PM'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock audit logs
|
export const locations: string[] = [];
|
||||||
export const mockAuditLogs: AuditLog[] = [
|
export const states: string[] = [];
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
action: 'Application Submitted',
|
|
||||||
user: 'Amit Sharma',
|
|
||||||
timestamp: '2025-10-01 02:30 PM',
|
|
||||||
details: 'Initial application form submitted'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
action: 'Questionnaire Link Sent',
|
|
||||||
user: 'System',
|
|
||||||
timestamp: '2025-10-01 02:31 PM',
|
|
||||||
details: 'Automated opportunity email sent with questionnaire link'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
action: 'Questionnaire Completed',
|
|
||||||
user: 'Amit Sharma',
|
|
||||||
timestamp: '2025-10-03 11:20 AM',
|
|
||||||
details: 'Scored 85/100'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
action: 'Application Shortlisted',
|
|
||||||
user: 'Rajesh Kumar (DD)',
|
|
||||||
timestamp: '2025-10-04 03:45 PM',
|
|
||||||
details: 'Shortlisted for Level 1 interview'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
action: 'Assigned to DD-ZM',
|
|
||||||
user: 'Rajesh Kumar (DD)',
|
|
||||||
timestamp: '2025-10-04 03:46 PM',
|
|
||||||
details: 'Application forwarded for Level 1 interview scheduling'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock documents
|
export const mockQuestionnaireResponses: QuestionnaireResponse[] = [];
|
||||||
export const mockDocuments: Document[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Aadhaar Card.pdf',
|
|
||||||
type: 'PDF',
|
|
||||||
uploadDate: '2025-10-01',
|
|
||||||
status: 'Verified',
|
|
||||||
uploader: 'Amit Sharma'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'PAN Card.pdf',
|
|
||||||
type: 'PDF',
|
|
||||||
uploadDate: '2025-10-01',
|
|
||||||
status: 'Verified',
|
|
||||||
uploader: 'Amit Sharma'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Business Plan.pdf',
|
|
||||||
type: 'PDF',
|
|
||||||
uploadDate: '2025-10-02',
|
|
||||||
status: 'Pending',
|
|
||||||
uploader: 'Amit Sharma'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'Address Proof.pdf',
|
|
||||||
type: 'PDF',
|
|
||||||
uploadDate: '2025-10-02',
|
|
||||||
status: 'Verified',
|
|
||||||
uploader: 'Amit Sharma'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock work notes
|
|
||||||
export const mockWorkNotes: WorkNote[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
user: 'Mark Johnson',
|
|
||||||
message: "I'm currently reviewing the budget allocation and comparing it with Q3 spending.\n@Sarah Chen can you clarify the expected timeline for the LinkedIn ads campaign? Also, do we have approval from legal for the content strategy?",
|
|
||||||
timestamp: '2024-10-07 13:45',
|
|
||||||
mentions: ['Sarah Chen']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
user: 'Sarah Chen',
|
|
||||||
message: "Hi @Lisa Wong ! For the LinkedIn campaign:\n\n• Launch: November 1st\n• Duration: 8 weeks\n• Budget distribution: 40% first 4 weeks, 60% last 4 weeks\n\nRegarding legal approval - I'll coordinate with the legal team this week. The content strategy follows our established brand guidelines.",
|
|
||||||
timestamp: '2024-10-07 15:45',
|
|
||||||
mentions: ['Lisa Wong']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
user: 'Rajesh Kumar',
|
|
||||||
message: 'Strong candidate, good communication skills. Recommend for Level 1 interview.',
|
|
||||||
timestamp: '2025-10-04 03:45 PM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
user: 'Suresh Reddy',
|
|
||||||
message: '@Rajesh Kumar Agreed. Scheduling interview for Oct 12th.',
|
|
||||||
timestamp: '2025-10-04 04:10 PM',
|
|
||||||
mentions: ['Rajesh Kumar']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock interview scores
|
|
||||||
export const mockLevel1Scores: InterviewScore[] = [
|
|
||||||
{
|
|
||||||
user: 'Suresh Reddy',
|
|
||||||
role: 'DD-ZM',
|
|
||||||
score: 42,
|
|
||||||
remarks: 'Strong business acumen, clear vision',
|
|
||||||
feedback: 'Excellent presentation skills'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user: 'Arjun Malhotra',
|
|
||||||
role: 'RBM',
|
|
||||||
score: 40,
|
|
||||||
remarks: 'Good understanding of market',
|
|
||||||
feedback: 'Passionate about Royal Enfield brand'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const locations = [
|
|
||||||
'Mumbai',
|
|
||||||
'Delhi',
|
|
||||||
'Bangalore',
|
|
||||||
'Chennai',
|
|
||||||
'Kolkata',
|
|
||||||
'Hyderabad',
|
|
||||||
'Pune',
|
|
||||||
'Ahmedabad',
|
|
||||||
'Indore',
|
|
||||||
'Jaipur'
|
|
||||||
];
|
|
||||||
|
|
||||||
export const states = [
|
|
||||||
'Andaman & Nicobar',
|
|
||||||
'Andhra Pradesh',
|
|
||||||
'Arunachal Pradesh',
|
|
||||||
'Assam',
|
|
||||||
'Bihar',
|
|
||||||
'Chandigarh',
|
|
||||||
'Chhattisgarh',
|
|
||||||
'Delhi & NCR',
|
|
||||||
'Goa',
|
|
||||||
'Gujarat',
|
|
||||||
'Himachal Pradesh',
|
|
||||||
'Haryana',
|
|
||||||
'Jammu & Kashmir',
|
|
||||||
'Jharkhand',
|
|
||||||
'Karnataka',
|
|
||||||
'Kerala',
|
|
||||||
'Ladakh',
|
|
||||||
'Madhya Pradesh',
|
|
||||||
'Maharashtra',
|
|
||||||
'Mizoram',
|
|
||||||
'Meghalaya',
|
|
||||||
'Manipur',
|
|
||||||
'Nagaland',
|
|
||||||
'Odisha',
|
|
||||||
'Puducherry',
|
|
||||||
'Punjab',
|
|
||||||
'Rajasthan',
|
|
||||||
'Sikkim',
|
|
||||||
'Tamilnadu',
|
|
||||||
'Telangana',
|
|
||||||
'Tripura',
|
|
||||||
'Uttar Pradesh',
|
|
||||||
'Uttarakhand',
|
|
||||||
'West Bengal'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock questionnaire responses
|
|
||||||
export const mockQuestionnaireResponses: QuestionnaireResponse[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
question: 'State (Applied for)',
|
|
||||||
answer: 'Maharashtra',
|
|
||||||
category: 'Personal Information',
|
|
||||||
marksScored: 5,
|
|
||||||
totalMarks: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
question: 'Contact Number',
|
|
||||||
answer: '+91 98765 43210',
|
|
||||||
category: 'Personal Information',
|
|
||||||
marksScored: 5,
|
|
||||||
totalMarks: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
question: 'Age',
|
|
||||||
answer: '35',
|
|
||||||
category: 'Personal Information',
|
|
||||||
marksScored: 5,
|
|
||||||
totalMarks: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
question: 'Educational Qualification',
|
|
||||||
answer: 'Graduate',
|
|
||||||
category: 'Personal Information',
|
|
||||||
marksScored: 5,
|
|
||||||
totalMarks: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
question: 'What is your Personal Networth',
|
|
||||||
answer: 'Between 5 - 10 Crores',
|
|
||||||
category: 'Financial Information',
|
|
||||||
marksScored: 8,
|
|
||||||
totalMarks: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
question: 'Are you a native of the Proposed Location?',
|
|
||||||
answer: 'Native',
|
|
||||||
category: 'Location & Background',
|
|
||||||
marksScored: 5,
|
|
||||||
totalMarks: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
question: 'Why do you want to partner with Royal Enfield?',
|
|
||||||
answer: 'Passionate about the brand',
|
|
||||||
category: 'Motivation',
|
|
||||||
marksScored: 8,
|
|
||||||
totalMarks: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8',
|
|
||||||
question: 'Who will be the partners in proposed company?',
|
|
||||||
answer: 'Immediate Family',
|
|
||||||
category: 'Business Structure',
|
|
||||||
marksScored: 5,
|
|
||||||
totalMarks: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '9',
|
|
||||||
question: 'Who will be managing the Royal Enfield dealership',
|
|
||||||
answer: 'I will be managing full time',
|
|
||||||
category: 'Business Structure',
|
|
||||||
marksScored: 7,
|
|
||||||
totalMarks: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '10',
|
|
||||||
question: 'Proposed Firm Type',
|
|
||||||
answer: 'Private Limited Company',
|
|
||||||
category: 'Business Structure',
|
|
||||||
marksScored: 5,
|
|
||||||
totalMarks: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '11',
|
|
||||||
question: 'What are you currently doing?',
|
|
||||||
answer: 'Running automobile dealership',
|
|
||||||
category: 'Professional Background',
|
|
||||||
marksScored: 7,
|
|
||||||
totalMarks: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '12',
|
|
||||||
question: 'Do you own a property in proposed location?',
|
|
||||||
answer: 'Yes',
|
|
||||||
category: 'Infrastructure',
|
|
||||||
marksScored: 5,
|
|
||||||
totalMarks: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '13',
|
|
||||||
question: 'How are you planning to invest in the Royal Enfield business',
|
|
||||||
answer: 'I will be investing my own funds',
|
|
||||||
category: 'Financial Planning',
|
|
||||||
marksScored: 8,
|
|
||||||
totalMarks: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '14',
|
|
||||||
question: 'What are your plans of expansion with RE?',
|
|
||||||
answer: 'Willing to expand by myself',
|
|
||||||
category: 'Growth & Expansion',
|
|
||||||
marksScored: 6,
|
|
||||||
totalMarks: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '15',
|
|
||||||
question: 'Will you be expanding to any other automobile OEM in the future?',
|
|
||||||
answer: 'No',
|
|
||||||
category: 'Growth & Expansion',
|
|
||||||
marksScored: 5,
|
|
||||||
totalMarks: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '16',
|
|
||||||
question: 'Do you own a Royal Enfield?',
|
|
||||||
answer: 'Yes, it is registered in my name',
|
|
||||||
category: 'Brand Affinity',
|
|
||||||
marksScored: 10,
|
|
||||||
totalMarks: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '17',
|
|
||||||
question: 'Do you go for long leisure rides',
|
|
||||||
answer: 'Yes, with the Royal Enfield riders',
|
|
||||||
category: 'Brand Affinity',
|
|
||||||
marksScored: 8,
|
|
||||||
totalMarks: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '18',
|
|
||||||
question: 'What special initiatives do you plan to implement if selected as business partner for Royal Enfield?',
|
|
||||||
answer: 'I plan to create a strong RE community hub with regular riding events, organize monthly maintenance workshops, establish a custom accessories corner, and partner with local tourism boards for adventure tours.',
|
|
||||||
category: 'Vision & Strategy',
|
|
||||||
marksScored: 9,
|
|
||||||
totalMarks: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '19',
|
|
||||||
question: 'Please elaborate your present business/employment.',
|
|
||||||
answer: 'Currently managing a multi-brand automobile dealership with 5 years experience. Team of 12 staff, annual turnover of 8 Crores. Strong network in the automobile industry.',
|
|
||||||
category: 'Professional Background',
|
|
||||||
marksScored: 8,
|
|
||||||
totalMarks: 10
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// F&F Department Types
|
// F&F Department Types
|
||||||
export const departments = [
|
export const departments = [
|
||||||
@ -1116,82 +409,4 @@ export interface FnFCase {
|
|||||||
typeOfClosure: 'Partial' | 'Complete';
|
typeOfClosure: 'Partial' | 'Complete';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock F&F Cases
|
export const mockFnFCases: FnFCase[] = [];
|
||||||
export const mockFnFCases: FnFCase[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
caseNumber: 'FNF-2025-001',
|
|
||||||
dealerName: 'Amit Sharma Motors',
|
|
||||||
dealerCode: 'DL-MH-001',
|
|
||||||
dealershipName: 'Royal Enfield Mumbai',
|
|
||||||
location: 'Mumbai, Maharashtra',
|
|
||||||
requestType: 'Resignation',
|
|
||||||
originalRequestId: 'RES-001',
|
|
||||||
status: 'New',
|
|
||||||
submittedOn: '2025-10-13',
|
|
||||||
departmentResponses: departments.map((dept, index) => ({
|
|
||||||
id: `dept-${index + 1}`,
|
|
||||||
departmentName: dept,
|
|
||||||
status: 'Pending' as const
|
|
||||||
})),
|
|
||||||
financeReportStatus: 'Pending',
|
|
||||||
lastOperationalDateSales: '2025-10-05',
|
|
||||||
lastOperationalDateServices: '2025-10-08',
|
|
||||||
gst: '27AABCU9603R1ZW',
|
|
||||||
typeOfClosure: 'Complete'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
caseNumber: 'FNF-2025-002',
|
|
||||||
dealerName: 'Priya Automobiles',
|
|
||||||
dealerCode: 'DL-KA-045',
|
|
||||||
dealershipName: 'Royal Enfield Bangalore',
|
|
||||||
location: 'Bangalore, Karnataka',
|
|
||||||
requestType: 'Resignation',
|
|
||||||
originalRequestId: 'RES-002',
|
|
||||||
status: 'In Progress',
|
|
||||||
submittedOn: '2025-10-10',
|
|
||||||
departmentResponses: departments.map((dept, index) => ({
|
|
||||||
id: `dept-${index + 1}`,
|
|
||||||
departmentName: dept,
|
|
||||||
status: index < 5 ? 'No Dues' as const : index < 8 ? 'Pending' as const : 'Dues' as const,
|
|
||||||
remarks: index < 5 ? 'No outstanding dues' : index < 8 ? undefined : 'Outstanding amount identified',
|
|
||||||
submittedDate: index < 5 ? '2025-10-11' : index >= 8 ? '2025-10-12' : undefined,
|
|
||||||
amountType: index >= 8 && index < 10 ? 'Receivable Amount' as const : undefined,
|
|
||||||
amount: index >= 8 && index < 10 ? 15000 + (index * 2000) : undefined
|
|
||||||
})),
|
|
||||||
financeReportStatus: 'Pending',
|
|
||||||
lastOperationalDateSales: '2025-09-28',
|
|
||||||
lastOperationalDateServices: '2025-10-02',
|
|
||||||
gst: '29AABCU9604R1ZY',
|
|
||||||
typeOfClosure: 'Partial'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
caseNumber: 'FNF-2025-003',
|
|
||||||
dealerName: 'Vikram Patil Motors',
|
|
||||||
dealerCode: 'DL-MH-025',
|
|
||||||
dealershipName: 'Royal Enfield Pune',
|
|
||||||
location: 'Pune, Maharashtra',
|
|
||||||
requestType: 'Termination',
|
|
||||||
originalRequestId: 'TERM-001',
|
|
||||||
status: 'Under Review',
|
|
||||||
submittedOn: '2025-09-25',
|
|
||||||
departmentResponses: departments.map((dept, index) => ({
|
|
||||||
id: `dept-${index + 1}`,
|
|
||||||
departmentName: dept,
|
|
||||||
status: index < 10 ? 'Dues' as const : index < 14 ? 'No Dues' as const : 'Pending' as const,
|
|
||||||
remarks: index < 10 ? 'Outstanding amount identified' : index < 14 ? 'Cleared' : undefined,
|
|
||||||
amountType: index === 2 ? 'Receivable Amount' as const : index === 5 ? 'Payable Amount' as const : undefined,
|
|
||||||
amount: index === 2 ? 45000 : index === 5 ? 12000 : undefined,
|
|
||||||
submittedDate: index < 14 ? '2025-10-05' : undefined
|
|
||||||
})),
|
|
||||||
financeReportStatus: 'In Progress',
|
|
||||||
totalPayableAmount: 12000,
|
|
||||||
totalRecoveryAmount: 45000,
|
|
||||||
lastOperationalDateSales: '2025-09-15',
|
|
||||||
lastOperationalDateServices: '2025-09-20',
|
|
||||||
gst: '27AABCU9604R1ZX',
|
|
||||||
typeOfClosure: 'Complete'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@ -49,7 +49,8 @@ export const masterService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
getDistricts: async (params?: any) => {
|
getDistricts: async (params?: any) => {
|
||||||
const response = await API.getDistricts(params);
|
const queryParams = typeof params === 'string' ? { stateId: params, limit: 'all' } : { limit: 'all', ...params };
|
||||||
|
const response = await API.getDistricts(queryParams);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
getAreas: async (params?: any) => {
|
getAreas: async (params?: any) => {
|
||||||
|
|||||||
@ -175,5 +175,10 @@ export const onboardingService = {
|
|||||||
const response: any = await API.assignFddAgency(data);
|
const response: any = await API.assignFddAgency(data);
|
||||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to assign FDD agency');
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to assign FDD agency');
|
||||||
return response.data;
|
return response.data;
|
||||||
|
},
|
||||||
|
exportResponses: async (applicationIds: string[]) => {
|
||||||
|
const response: any = await (API as any).exportApplicationResponses({ applicationIds: applicationIds.join(',') });
|
||||||
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to export responses');
|
||||||
|
return response.data?.data || [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
--primary: #d97706;
|
--primary: #daaa00;
|
||||||
--primary-foreground: oklch(1 0 0);
|
--primary-foreground: oklch(1 0 0);
|
||||||
--secondary: oklch(0.95 0.0058 264.53);
|
--secondary: oklch(0.95 0.0058 264.53);
|
||||||
--secondary-foreground: #030213;
|
--secondary-foreground: #030213;
|
||||||
@ -43,7 +43,8 @@
|
|||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
|
||||||
/* Royal Enfield Brand Colors */
|
/* Royal Enfield Brand Colors */
|
||||||
--re-red: #DA1A32;
|
--re-red: #da291c;
|
||||||
|
--re-red-hover: #b82216;
|
||||||
--re-black: #000000;
|
--re-black: #000000;
|
||||||
--re-white: #FFFFFF;
|
--re-white: #FFFFFF;
|
||||||
--re-gray: #717171;
|
--re-gray: #717171;
|
||||||
@ -87,6 +88,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
--font-montserrat: "Montserrat", sans-serif;
|
||||||
|
--font-sans: var(--font-montserrat);
|
||||||
|
--font-serif: var(--font-montserrat);
|
||||||
|
--font-mono: var(--font-montserrat);
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
@ -125,16 +130,21 @@
|
|||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
/* Royal Enfield brand color tokens — usable as bg-re-red, text-re-red, etc. */
|
||||||
|
--color-re-red: var(--re-red);
|
||||||
|
--color-re-red-hover: var(--re-red-hover);
|
||||||
|
--color-re-black: var(--re-black);
|
||||||
|
--color-re-gray: var(--re-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50 font-sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground font-sans;
|
||||||
font-family: 'Montserrat', sans-serif;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user