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:
laxman h 2026-04-23 18:58:47 +05:30
parent 26604fd7d1
commit bb3e78873f
15 changed files with 535 additions and 1024 deletions

View File

@ -38,6 +38,7 @@ export const API = {
// Onboarding
submitApplication: (data: any) => client.post('/onboarding/apply', data),
exportApplicationResponses: (params: { applicationIds: string }) => client.get('/onboarding/applications/export-responses', params),
getApplications: () => client.get('/onboarding/applications'),
shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data),
getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`),
@ -221,7 +222,7 @@ export const API = {
submitFddReport: (data: any) => client.post('/fdd/report', data),
getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`),
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;

View File

@ -15,7 +15,8 @@ import {
MapPin,
ClipboardList
} 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 { useSelector } from 'react-redux';
import { RootState } from '../../store';
@ -26,15 +27,24 @@ interface SidebarProps {
onLogout: () => void;
}
interface FlyoutState {
submenuKey: string;
top: number;
left: number;
}
export function Sidebar({ onLogout }: SidebarProps) {
const navigate = useNavigate();
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 [collapsed, setCollapsed] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [offboardingExpanded, setOffboardingExpanded] = 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 normalizedRole = String(currentRole).trim().toLowerCase();
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
].filter(Boolean) as { id: string; label: string }[];
// Finance role has only specific menu items
const menuItems = hasRole(['Finance', 'Finance Admin']) ? [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'finance-onboarding', label: 'Onboarding', icon: FileText },
@ -78,14 +87,6 @@ export function Sidebar({ onLogout }: SidebarProps) {
{ 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'])) {
menuItems.splice(1, 0, {
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'])) {
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
menuItems.push({ id: 'sla-configurations', label: 'SLA Matrix', icon: RefreshCcw });
@ -110,164 +110,265 @@ export function Sidebar({ onLogout }: SidebarProps) {
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
// Navigate to applications with search query
navigate('/applications');
// In real app, would pass search query
}
if (searchQuery.trim()) navigate('/applications');
};
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 (
<div
className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 overflow-hidden ${collapsed ? 'w-20' : 'w-64'
<>
<div
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 */}
<div className="p-4 border-b border-slate-800">
<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>
>
{/* Header with Logo */}
<div className="border-b border-white/10">
{collapsed ? (
/* Collapsed header: logo + toggle stacked, centered */
<div className="flex flex-col items-center py-3 gap-3">
<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>
<span className="text-amber-600">RE Dealer</span>
<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>
</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>
)}
<button
onClick={() => setCollapsed(!collapsed)}
className="p-1 hover:bg-slate-800 rounded transition-colors"
>
{collapsed ? (
<ChevronRight className="w-5 h-5" />
) : (
<ChevronLeft className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* Search Bar */}
{!collapsed && (
<div className="p-4 border-b border-slate-800">
<form onSubmit={handleSearch} className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
type="text"
placeholder="Search applications..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 bg-slate-800 border-slate-700 text-white placeholder:text-slate-400"
/>
</form>
</div>
)}
{/* Menu Items */}
<nav className="flex-1 p-4 space-y-2 overflow-y-auto custom-scrollbar">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = activeView === item.id;
const hasSubmenu = item.hasSubmenu;
const isSubmenuActive = hasSubmenu && item.submenu?.some(sub => activeView === sub.id);
// Determine which submenu is expanded based on submenuKey
const submenuKey = (item as any).submenuKey;
const isExpanded = submenuKey === 'offboarding' ? offboardingExpanded :
submenuKey === 'allRequests' ? allRequestsExpanded : false;
return (
<div key={item.id}>
<button
onClick={() => {
if (hasSubmenu) {
if (submenuKey === 'offboarding') {
setOffboardingExpanded(!offboardingExpanded);
} else if (submenuKey === 'allRequests') {
setAllRequestsExpanded(!allRequestsExpanded);
}
} else {
navigate(`/${item.id}`);
}
}}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive || isSubmenuActive
? 'bg-amber-600 text-white'
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
}`}
title={collapsed ? item.label : undefined}
>
<Icon className="w-5 h-5 flex-shrink-0" />
{!collapsed && (
<>
<span className="flex-1 text-left">{item.label}</span>
{hasSubmenu && (
isExpanded ? (
<ChevronUp className="w-4 h-4 flex-shrink-0" />
) : (
<ChevronDown className="w-4 h-4 flex-shrink-0" />
)
)}
</>
)}
</button>
{/* Submenu */}
{hasSubmenu && isExpanded && !collapsed && (
<div className="ml-4 mt-2 space-y-1">
{item.submenu?.map((subItem) => {
const isSubActive = activeView === subItem.id;
return (
<button
key={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
? 'bg-amber-600 text-white'
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
}`}
>
<span className="w-1 h-1 rounded-full bg-current flex-shrink-0" />
<span>{subItem.label}</span>
</button>
);
})}
</div>
)}
</div>
);
})}
</nav>
{/* User Profile & Logout */}
<div className="p-4 border-t border-slate-800 space-y-2">
{!collapsed && currentUser && (
<div className="px-4 py-2 bg-slate-800 rounded-lg mb-2">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-600 rounded-full flex items-center justify-center">
<span>{currentUser.name.charAt(0)}</span>
</div>
<div className="flex-1 min-w-0">
<p className="truncate">{currentUser.name}</p>
<p className="text-slate-400 truncate">{currentUser.role}</p>
</div>
</div>
{/* Search Bar (expanded only) */}
{!collapsed && (
<div className="p-4 border-b border-white/10">
<form onSubmit={handleSearch} className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
type="text"
placeholder="Search applications..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 bg-white/5 border-white/10 text-white placeholder:text-slate-500"
/>
</form>
</div>
)}
<Button
onClick={onLogout}
variant="ghost"
className={`w-full ${collapsed ? 'px-2' : 'justify-start'
} text-slate-300 hover:bg-slate-800 hover:text-white`}
title={collapsed ? 'Logout' : undefined}
>
<LogOut className="w-5 h-5 flex-shrink-0" />
{!collapsed && <span className="ml-3">Logout</span>}
</Button>
{/* Menu Items */}
<nav className="flex-1 p-3 space-y-1 overflow-y-auto custom-scrollbar">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = activeView === item.id;
const hasSubmenu = !!item.hasSubmenu;
const isSubmenuActive = hasSubmenu && item.submenu?.some(sub => activeView === sub.id);
const submenuKey = (item as any).submenuKey as string | undefined;
const isExpanded = submenuKey === 'offboarding' ? offboardingExpanded :
submenuKey === 'allRequests' ? allRequestsExpanded : false;
return (
<div key={item.id}>
<button
onMouseEnter={(e) => {
if (collapsed && hasSubmenu && submenuKey) {
openFlyout(submenuKey, e.currentTarget);
}
}}
onMouseLeave={() => {
if (collapsed && hasSubmenu) closeFlyout();
}}
onClick={() => {
if (hasSubmenu) {
if (collapsed) {
// Expand sidebar and open submenu
setCollapsed(false);
if (submenuKey === 'offboarding') setOffboardingExpanded(true);
else if (submenuKey === 'allRequests') setAllRequestsExpanded(true);
} else {
if (submenuKey === 'offboarding') setOffboardingExpanded(!offboardingExpanded);
else if (submenuKey === 'allRequests') setAllRequestsExpanded(!allRequestsExpanded);
}
} else {
navigate(`/${item.id}`);
}
}}
className={`w-full flex items-center gap-3 px-3 py-3 rounded-lg transition-colors ${
collapsed ? 'justify-center' : ''
} ${
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}
>
<Icon className="w-5 h-5 flex-shrink-0" />
{!collapsed && (
<>
<span className="flex-1 text-left text-sm">{item.label}</span>
{hasSubmenu && (
isExpanded
? <ChevronUp className="w-4 h-4 flex-shrink-0" />
: <ChevronDown className="w-4 h-4 flex-shrink-0" />
)}
</>
)}
</button>
{/* Expanded inline submenu (non-collapsed) */}
{hasSubmenu && isExpanded && !collapsed && (
<div className="ml-3 mt-1 space-y-1 border-l border-white/10 pl-3">
{item.submenu?.map((subItem) => {
const isSubActive = activeView === subItem.id;
return (
<button
key={subItem.id}
onClick={() => navigate(`/${subItem.id}`)}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-colors text-sm ${
isSubActive
? 'bg-re-red/20 text-re-red font-semibold'
: 'text-slate-500 hover:bg-white/5 hover:text-white'
}`}
>
<span className="w-1.5 h-1.5 rounded-full bg-current flex-shrink-0" />
<span>{subItem.label}</span>
</button>
);
})}
</div>
)}
</div>
);
})}
</nav>
{/* User Profile & Logout */}
<div className="p-4 border-t border-white/10 space-y-2">
{!collapsed && currentUser && (
<div className="px-4 py-2 bg-white/5 rounded-lg mb-2">
<div className="flex items-center gap-3">
<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>
</div>
<div className="flex-1 min-w-0">
<p className="truncate text-sm font-semibold">{currentUser.name}</p>
<p className="text-slate-500 truncate text-[11px] uppercase tracking-wider">{currentUser.role}</p>
</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
onClick={onLogout}
variant="ghost"
className={`w-full ${collapsed ? 'px-2 justify-center' : 'justify-start'} text-slate-400 hover:bg-white/5 hover:text-white`}
title={collapsed ? 'Logout' : undefined}
>
<LogOut className="w-5 h-5 flex-shrink-0" />
{!collapsed && <span className="ml-3">Logout</span>}
</Button>
</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
);
})()}
</>
);
}

View File

@ -45,11 +45,8 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
const fetchStates = async () => {
try {
const response: any = await masterService.getStates();
if (response && response.data) {
setStates(response.data);
} else if (response && response.states) {
setStates(response.states);
}
const statesArray = Array.isArray(response) ? response : (response?.data || response?.states || []);
setStates(statesArray);
} catch (error) {
console.error('Error fetching states:', error);
}
@ -61,8 +58,8 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
setDistricts([]);
try {
const response: any = await masterService.getDistricts(stateId);
if (response && response.data) setDistricts(response.data);
else if (response && response.districts) setDistricts(response.districts);
const districtsArray = Array.isArray(response) ? response : (response?.data || response?.districts || []);
setDistricts(districtsArray);
} catch (error) {
console.error('Error fetching districts:', error);
}
@ -159,7 +156,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
];
return (
<div className="min-h-screen relative flex flex-col font-['Montserrat']">
<div className="min-h-screen relative flex flex-col">
{/* Background Image Wrapper */}
<div className="fixed inset-0 z-0">
<img

View File

@ -104,11 +104,11 @@ export function LoginPage({ onLogin }: LoginPageProps) {
};
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 */}
<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 -bottom-40 -left-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-red-700/15 rounded-full blur-3xl"></div>
</div>
<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">
{/* Logo and Header */}
<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">
<svg viewBox="0 0 24 24" className="w-12 h-12 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 className="flex justify-center mb-4">
<img
src="/assets/images/Re_Logo.png"
alt="Royal Enfield"
className="h-16 w-auto object-contain"
/>
</div>
<h1 className="text-white mb-2">Royal Enfield</h1>
<p className="text-slate-400">Dealership Onboarding System</p>
</div>
@ -186,7 +186,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
<button
type="button"
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}
>
Forgot Password?
@ -202,7 +202,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
<Button
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}
>
{isLoading ? (
@ -227,7 +227,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
<Button
type="button"
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'}
>
Prospective User Login
@ -263,7 +263,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
>
Back to Login
</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
</Button>
</div>
@ -273,7 +273,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
{/* Footer */}
<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>
@ -288,13 +288,13 @@ export function LoginPage({ onLogin }: LoginPageProps) {
{mockUsers.map((user, index) => (
<div
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)}
>
<div className="flex items-start justify-between mb-2">
<div className="flex-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}
</span>
</div>
@ -347,7 +347,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
</div>
<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>
))}

View File

@ -7,6 +7,13 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { MapPin, Plus, Edit2, Trash2, Globe } from 'lucide-react';
import { RootState } from '@/store';
import { formatDateTime } from '@/components/ui/utils';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
interface LocationManagementProps {
onAddLocation: () => void;
@ -15,10 +22,17 @@ interface LocationManagementProps {
onSearch: (term: string) => void;
onPageChange: (page: number) => void;
searchTerm: string;
states: any[];
stateFilter: string;
onStateFilterChange: (value: string) => void;
statusFilter: string;
onStatusFilterChange: (value: string) => void;
}
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);
@ -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"
/>
</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">
<Plus className="w-4 h-4 mr-2" />
Add Location
@ -185,22 +223,6 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
</CardContent>
</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>
);
};

View File

@ -113,6 +113,8 @@ export const MasterPage: React.FC = () => {
// Search & Pagination State (Locations)
const [districtsSearch, setDistrictsSearch] = useState('');
const [districtsPage, setDistrictsPage] = useState(1);
const [locationStateFilter, setLocationStateFilter] = useState('all');
const [locationStatusFilter, setLocationStatusFilter] = useState('all');
// Initial Load
useEffect(() => {
@ -417,10 +419,15 @@ export const MasterPage: React.FC = () => {
useEffect(() => {
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);
return () => clearTimeout(handler);
}, [districtsSearch, districtsPage, fetchAreas]);
}, [districtsSearch, districtsPage, locationStateFilter, fetchAreas]);
return (
<div className="space-y-6">
@ -522,6 +529,17 @@ export const MasterPage: React.FC = () => {
<TabsContent value="locations" className="animate-in fade-in duration-300">
<LocationManagement
states={allStates}
stateFilter={locationStateFilter}
onStateFilterChange={(val: string) => {
setLocationStateFilter(val);
setDistrictsPage(1);
}}
statusFilter={locationStatusFilter}
onStatusFilterChange={(val: string) => {
setLocationStatusFilter(val);
setDistrictsPage(1);
}}
onAddLocation={() => {
setEditingLocationId(null);
setLocationState('');
@ -538,7 +556,11 @@ export const MasterPage: React.FC = () => {
(masterService as any).deleteArea(id).then((res: any) => {
if (res.success) {
toast.success('Location deleted');
fetchAreas({ search: districtsSearch, page: districtsPage });
fetchAreas({
search: districtsSearch,
page: districtsPage,
stateId: locationStateFilter === 'all' ? undefined : locationStateFilter
});
}
});
}

View File

@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -53,11 +54,26 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
const [shortlistRemark, setShortlistRemark] = useState('');
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
const [loading, setLoading] = useState(true);
const [states, setStates] = useState<string[]>([]);
const [locations, setLocations] = useState<string[]>([]);
const [initialFetchDone, setInitialFetchDone] = useState(false);
useEffect(() => {
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 () => {
try {
setLoading(true);
@ -100,6 +116,10 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
}));
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) {
console.error('Failed to fetch applications:', error);
toast.error('Failed to load applications');

View File

@ -1,5 +1,5 @@
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 { onboardingService } from '@/services/onboarding.service';
import { Button } from '@/components/ui/button';
@ -50,6 +50,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
// Real Data Integration
const [applications, setApplications] = useState<Application[]>([]);
const [locations, setLocations] = useState<string[]>([]);
useEffect(() => {
const fetchApplications = async () => {
@ -94,6 +95,10 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
address: app.address
}));
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) {
console.error('Failed to fetch applications', error);
} finally {

View File

@ -1,5 +1,6 @@
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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -40,11 +41,25 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
// Real data integration
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
const [loading, setLoading] = useState(true);
const [states, setStates] = useState<string[]>([]);
const [locations, setLocations] = useState<string[]>([]);
useEffect(() => {
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 () => {
try {
setLoading(true);
@ -87,6 +102,10 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
}));
setApplicationsData(mappedApps);
// Extract unique locations
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean);
setLocations(uniqueLocations);
} catch (error) {
console.error('Failed to fetch applications:', error);
toast.error('Failed to load non-opportunity requests');

View File

@ -1,5 +1,6 @@
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 { adminService } from '@/services/admin.service';
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 { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { formatDateTime } from '@/components/ui/utils';
import { toast } from 'sonner';
import { ApplicationCard } from '@/features/onboarding/components/ApplicationCard';
import {
@ -74,6 +76,8 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
const [selectedAssignees, setSelectedAssignees] = useState<User[]>([]);
const [availableUsers, setAvailableUsers] = useState<User[]>([]);
const [openUserSelect, setOpenUserSelect] = useState(false);
const [states, setStates] = useState<string[]>([]);
const [locations, setLocations] = useState<string[]>([]);
// Real data integration
const [applicationsData, setApplicationsData] = useState<Application[]>([]);
@ -82,8 +86,21 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
useEffect(() => {
fetchApplications();
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 () => {
try {
const response = await adminService.getAllUsers({ isExternal: false });
@ -142,6 +159,10 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
}));
setApplicationsData(mappedApps);
// Extract unique locations for filtering
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean);
setLocations(uniqueLocations);
} catch (error) {
console.error('Failed to fetch applications:', error);
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)`);
};
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
// These applications haven't entered the full dealership approval workflow yet
const statusOptions: ApplicationStatus[] = [
@ -416,10 +503,10 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
</Button>
</div>
<Button variant="outline" size="sm" data-testid="onboarding-opp-requests-export-btn">
<Download className="w-4 h-4 mr-2" />
Export
</Button>
<Button variant="outline" size="sm" onClick={handleExport} data-testid="onboarding-opp-requests-export-btn">
<Download className="w-4 h-4 mr-2" />
Export
</Button>
{selectedIds.length > 0 && (
<>
@ -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-pref-loc">Preferred Location</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-shortlisted">Shortlisted</TableHead>
<TableHead data-testid="onboarding-opp-requests-th-progress">Progress</TableHead>
@ -532,6 +620,11 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
{app.status}
</Badge>
</TableCell>
<TableCell>
<span className="font-semibold text-slate-900" data-testid={`onboarding-opp-requests-score-${idx}`}>
{app.questionnaireMarks}
</span>
</TableCell>
<TableCell>
<span className="text-slate-600" data-testid={`onboarding-opp-requests-app-loc-${idx}`}>{app.businessAddress}</span>
</TableCell>
@ -545,7 +638,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
</div>
</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>
</TableRow>
))}

View File

@ -199,7 +199,7 @@ export const useMasterData = () => {
}
}, [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 {
dispatch(setAreasLoading(true));
const res = await masterService.getAreas(params) as any;

View File

@ -325,739 +325,32 @@ export const mockUsers: User[] = [
// Mock current user (default)
export let currentUser: User = mockUsers[0];
// Mock applications
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 data arrays (emptied to use API data)
export const mockApplications: Application[] = [];
// Mock dashboard statistics
export const dashboardStats = {
totalApplications: 150,
loaIssued: 12,
level1Pending: 23,
level2Pending: 15,
level3Pending: 8,
eorInProgress: 6,
disqualified: 18,
pendingReminders: 34,
shortlistedToday: 5,
pendingShortlisting: 4 // New applications waiting to be shortlisted by DD
totalApplications: 0,
loaIssued: 0,
level1Pending: 0,
level2Pending: 0,
level3Pending: 0,
eorInProgress: 0,
disqualified: 0,
pendingReminders: 0,
shortlistedToday: 0,
pendingShortlisting: 0
};
// Mock recent activities
export const recentActivities = [
{
id: '1',
action: 'Approved',
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'
}
];
export const recentActivities: any[] = [];
export const mockAuditLogs: AuditLog[] = [];
export const mockDocuments: Document[] = [];
export const mockWorkNotes: WorkNote[] = [];
export const mockLevel1Scores: InterviewScore[] = [];
// Mock audit logs
export const mockAuditLogs: AuditLog[] = [
{
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'
}
];
export const locations: string[] = [];
export const states: string[] = [];
// Mock documents
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
}
];
export const mockQuestionnaireResponses: QuestionnaireResponse[] = [];
// F&F Department Types
export const departments = [
@ -1116,82 +409,4 @@ export interface FnFCase {
typeOfClosure: 'Partial' | 'Complete';
}
// Mock F&F Cases
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'
}
];
export const mockFnFCases: FnFCase[] = [];

View File

@ -49,7 +49,8 @@ export const masterService = {
return response.data;
},
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;
},
getAreas: async (params?: any) => {

View File

@ -175,5 +175,10 @@ export const onboardingService = {
const response: any = await API.assignFddAgency(data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to assign FDD agency');
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 || [];
}
};

View File

@ -10,7 +10,7 @@
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: #d97706;
--primary: #daaa00;
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.95 0.0058 264.53);
--secondary-foreground: #030213;
@ -43,7 +43,8 @@
--sidebar-ring: oklch(0.708 0 0);
/* Royal Enfield Brand Colors */
--re-red: #DA1A32;
--re-red: #da291c;
--re-red-hover: #b82216;
--re-black: #000000;
--re-white: #FFFFFF;
--re-gray: #717171;
@ -87,6 +88,10 @@
}
@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-foreground: var(--foreground);
--color-card: var(--card);
@ -125,16 +130,21 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--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 {
* {
@apply border-border outline-ring/50;
@apply border-border outline-ring/50 font-sans;
}
body {
@apply bg-background text-foreground;
font-family: 'Montserrat', sans-serif;
@apply bg-background text-foreground font-sans;
}
}