flow created upto outlet creation

This commit is contained in:
laxmanhalaki 2026-03-20 19:31:58 +05:30
parent 9f13f8056e
commit 2d0a5fce22
9 changed files with 1173 additions and 1088 deletions

View File

@ -80,6 +80,7 @@ export const API = {
createDealer: (data: any) => client.post('/dealer', data), createDealer: (data: any) => client.post('/dealer', data),
getDealerById: (id: string) => client.get(`/dealer/${id}`), getDealerById: (id: string) => client.get(`/dealer/${id}`),
updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data), updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data),
getDealerDashboard: () => client.get('/dealer/dashboard'),
// Email Templates // Email Templates
getEmailTemplates: () => client.get('/admin/email-templates'), getEmailTemplates: () => client.get('/admin/email-templates'),
@ -91,13 +92,14 @@ export const API = {
// Audit Trail // Audit Trail
getAuditLogs: (entityType: string, entityId: string, page: number = 1, limit: number = 50) => getAuditLogs: (entityType: string, entityId: string, page: number = 1, limit: number = 50) =>
client.get('/audit/logs', { entityType, entityId, page, limit }), client.get('/audit/logs', { params: { entityType, entityId, page, limit } }),
getAuditSummary: (entityType: string, entityId: string) => getAuditSummary: (entityType: string, entityId: string) =>
client.get('/audit/summary', { entityType, entityId }), client.get('/audit/summary', { params: { entityType, entityId } }),
// Prospective Login // Prospective Login
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }), sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
verifyOtp: (phone: string, otp: string) => client.post('/prospective-login/verify-otp', { phone, otp }), verifyOtp: (phone: string, otp: string) => client.post('/prospective-login/verify-otp', { phone, otp }),
// Resignation // Resignation
getResignationById: (id: string) => client.get(`/resignation/${id}`), getResignationById: (id: string) => client.get(`/resignation/${id}`),
updateClearance: (id: string, data: any) => client.post(`/resignation/${id}/clearance`, data), updateClearance: (id: string, data: any) => client.post(`/resignation/${id}/clearance`, data),

View File

@ -2409,14 +2409,14 @@ export function ApplicationDetails() {
)} )}
{/* Dedicated Onboarding Button - Appears when logic is ready to onboard as a dealer */} {/* Dedicated Onboarding Button - Appears when logic is ready to onboard as a dealer */}
{isAdmin && ['Dealer Code Generation', 'Architecture Team Completion', 'LOA Pending', 'EOR Complete', 'Inauguration'].includes(application.status) && ( {isAdmin && ['Dealer Code Generation', 'Architecture Team Completion', 'LOA Pending', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) && !application.dealer && (
<Button <Button
className="w-full bg-amber-600 hover:bg-amber-700 font-bold" className="w-full bg-amber-600 hover:bg-amber-700 font-bold"
onClick={async () => { onClick={async () => {
if (window.confirm('Do you want to finalize and onboard this dealer?')) { if (window.confirm('Do you want to finalize and onboard this dealer?')) {
try { try {
await onboardingService.createDealer({ applicationId }); await onboardingService.createDealer({ applicationId });
toast.success('Dealer record created successfully!'); toast.success('Dealer profile and login account created successfully!');
fetchApplication(); fetchApplication();
} catch (error) { } catch (error) {
toast.error('Failed to create dealer profile'); toast.error('Failed to create dealer profile');
@ -2429,6 +2429,32 @@ export function ApplicationDetails() {
</Button> </Button>
)} )}
{/* Dealer Onboarded Status & Link */}
{application.dealer && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg space-y-3">
<div className="flex items-center gap-2 text-green-800 font-semibold">
<CheckCircle className="w-5 h-5 text-green-600" />
Dealer Profile Active
</div>
<div className="text-sm text-green-700">
This application has been successfully onboarded as a dealer. A user account has been created for the dealer.
</div>
{application.dealerCode && (
<div className="flex items-center justify-between text-xs font-mono bg-white p-2 rounded border border-green-100">
<span className="text-slate-500">Dealer Code:</span>
<span className="font-bold text-slate-900">{application.dealerCode.code}</span>
</div>
)}
<Button
className="w-full bg-green-600 hover:bg-green-700 text-white"
onClick={() => navigate('/dashboard')}
>
<Zap className="w-4 h-4 mr-2" />
Go to Dealer Dashboard
</Button>
</div>
)}
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && ( {currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && (
<Dialog open={showAssignModal} onOpenChange={setShowAssignModal}> <Dialog open={showAssignModal} onOpenChange={setShowAssignModal}>
<DialogTrigger asChild> <DialogTrigger asChild>

View File

@ -1,8 +1,10 @@
import { FileText, RefreshCcw, MapPin, Users, TrendingUp, Clock, CheckCircle, AlertCircle } from 'lucide-react'; import { useState, useEffect } from 'react';
import { RefreshCcw, MapPin, Users, TrendingUp, Clock, CheckCircle, AlertCircle, ShoppingBag, Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { User } from '../../lib/mock-data'; import { User } from '../../lib/mock-data';
import { dealerService } from '../../services/dealer.service';
interface DealerDashboardProps { interface DealerDashboardProps {
currentUser: User | null; currentUser: User | null;
@ -10,27 +12,67 @@ interface DealerDashboardProps {
} }
export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProps) { export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProps) {
// Dealer stats - showing their own requests const [loading, setLoading] = useState(true);
const [data, setData] = useState<any>(null);
useEffect(() => {
const fetchDashboard = async () => {
try {
const dashboardData = await dealerService.getDashboardData();
setData(dashboardData);
} catch (error) {
console.error('Failed to fetch dashboard:', error);
} finally {
setLoading(false);
}
};
fetchDashboard();
}, []);
if (loading) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px]">
<Loader2 className="w-10 h-10 text-amber-600 animate-spin mb-4" />
<p className="text-slate-600">Loading your dashboard...</p>
</div>
);
}
const dashboardData = data || {};
const profile = dashboardData.profile || {};
const statsSummary = dashboardData.stats || { constitutional: 0, relocation: 0, resignation: 0, total: 0 };
const recentRequests = dashboardData.recentRequests || [];
const primaryOutlet = dashboardData.outlets?.[0] || {};
// Dealer stats
const stats = [ const stats = [
{ {
title: 'Constitutional Changes', title: 'Constitutional Changes',
value: 1, value: statsSummary.constitutional,
icon: RefreshCcw, icon: RefreshCcw,
color: 'bg-blue-500', color: 'bg-blue-500',
change: 'In Review', change: 'Active Requests',
onClick: () => onNavigate('dealer-constitutional') onClick: () => onNavigate('dealer-constitutional')
}, },
{ {
title: 'Relocation Requests', title: 'Relocation Requests',
value: 1, value: statsSummary.relocation,
icon: MapPin, icon: MapPin,
color: 'bg-amber-500', color: 'bg-amber-500',
change: 'Pending Approval', change: 'Active Requests',
onClick: () => onNavigate('dealer-relocation') onClick: () => onNavigate('dealer-relocation')
}, },
{
title: 'My Outlets',
value: dashboardData.outlets?.length || 0,
icon: ShoppingBag,
color: 'bg-purple-500',
change: 'Registered',
onClick: () => {}
},
{ {
title: 'Total Requests', title: 'Total Requests',
value: 2, value: statsSummary.total,
icon: TrendingUp, icon: TrendingUp,
color: 'bg-green-500', color: 'bg-green-500',
change: 'All time', change: 'All time',
@ -39,24 +81,6 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
]; ];
// Recent requests by the dealer // Recent requests by the dealer
const recentRequests = [
{
id: 'CON-001',
type: 'Constitutional Change',
title: 'Change from Proprietorship to Partnership',
status: 'RBM Review',
date: '2025-12-15',
color: 'bg-blue-100 text-blue-700 border-blue-300'
},
{
id: 'RLO-001',
type: 'Relocation',
title: 'Moving to Andheri East, Mumbai',
status: 'DD ZM Review',
date: '2025-12-10',
color: 'bg-amber-100 text-amber-700 border-amber-300'
},
];
// Quick actions for dealer // Quick actions for dealer
const quickActions = [ const quickActions = [
@ -84,12 +108,12 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
<div className="bg-gradient-to-r from-amber-500 to-amber-600 rounded-lg p-6 text-white"> <div className="bg-gradient-to-r from-amber-500 to-amber-600 rounded-lg p-6 text-white">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-white mb-2">Welcome back, {currentUser?.name}!</h1> <h1 className="text-white mb-2">Welcome back, {profile.name || currentUser?.name}!</h1>
<p className="text-amber-100"> <p className="text-amber-100">
Dealer Code: DL-MH-001 Royal Enfield Mumbai Dealer Code: {profile.dealerCode} {profile.businessName}
</p> </p>
<p className="text-amber-100 text-sm mt-1"> <p className="text-amber-100 text-sm mt-1">
Bandra West, Mumbai, Maharashtra {primaryOutlet.name} {primaryOutlet.location}
</p> </p>
</div> </div>
<div className="text-right"> <div className="text-right">
@ -167,7 +191,7 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-3"> <div className="space-y-3">
{recentRequests.map((request) => ( {recentRequests.map((request: any) => (
<div <div
key={request.id} key={request.id}
className="flex items-center justify-between p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors" className="flex items-center justify-between p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"

View File

@ -1,4 +1,4 @@
import { RefreshCcw, Plus, Eye, Calendar, FileText } from 'lucide-react'; import { RefreshCcw, Plus, Eye, Calendar, FileText, Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
@ -8,31 +8,16 @@ import { Input } from '../ui/input';
import { Label } from '../ui/label'; import { Label } from '../ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data'; import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { dealerService } from '../../services/dealer.service';
interface DealerConstitutionalChangePageProps { interface DealerConstitutionalChangePageProps {
currentUser: UserType | null; currentUser: UserType | null;
onViewDetails?: (id: string) => void; onViewDetails?: (id: string) => void;
} }
// Mock constitutional change requests for this dealer
const mockDealerConstitutionalChanges = [
{
id: 'CON-001',
dealerCode: 'DL-MH-001',
dealerName: 'Amit Sharma Motors',
currentConstitution: 'Proprietorship',
proposedConstitution: 'Partnership',
reason: 'Adding family members as partners',
status: 'RBM Review',
submittedOn: '2025-12-15',
currentStage: 'RBM',
progressPercentage: 25
},
];
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300'; if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300'; if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
@ -44,13 +29,39 @@ const constitutionTypes = ['Proprietorship', 'Partnership', 'LLP', 'Pvt Ltd'];
export function DealerConstitutionalChangePage({ currentUser, onViewDetails }: DealerConstitutionalChangePageProps) { export function DealerConstitutionalChangePage({ currentUser, onViewDetails }: DealerConstitutionalChangePageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [currentConstitution, setCurrentConstitution] = useState('Proprietorship'); // Pre-filled const [currentConstitution, setCurrentConstitution] = useState('');
const [proposedConstitution, setProposedConstitution] = useState(''); const [proposedConstitution, setProposedConstitution] = useState('');
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
const [newPartners, setNewPartners] = useState(''); const [newPartners, setNewPartners] = useState('');
const [shareholdingPattern, setShareholdingPattern] = useState(''); const [shareholdingPattern, setShareholdingPattern] = useState('');
const [requests, setRequests] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [profile, setProfile] = useState<any>(null);
const handleSubmitRequest = (e: React.FormEvent) => { useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
setLoading(true);
const dashboard = await dealerService.getDashboardData();
const constitutionalRes = await dealerService.getConstitutionalChanges();
setProfile(dashboard.profile);
setCurrentConstitution(dashboard.profile?.constitutionType || 'Proprietorship');
setRequests(constitutionalRes.requests || []);
} catch (error) {
console.error('Fetch constitutional data error:', error);
toast.error('Failed to load requests');
} finally {
setLoading(false);
}
};
const handleSubmitRequest = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!proposedConstitution) { if (!proposedConstitution) {
@ -68,32 +79,50 @@ export function DealerConstitutionalChangePage({ currentUser, onViewDetails }: D
return; return;
} }
toast.success('Constitutional change request submitted successfully'); try {
setIsDialogOpen(false); setSubmitting(true);
const payload = {
// Reset form currentConstitution,
setProposedConstitution(''); changeType: proposedConstitution,
setReason(''); reason,
setNewPartners(''); newPartnersDetails: newPartners,
setShareholdingPattern(''); shareholdingPattern
};
await dealerService.submitConstitutionalChange(payload);
toast.success('Constitutional change request submitted successfully');
setIsDialogOpen(false);
fetchData(); // Refresh list
// Reset form
setProposedConstitution('');
setReason('');
setNewPartners('');
setShareholdingPattern('');
} catch (error) {
console.error('Submit constitutional change error:', error);
toast.error('Failed to submit constitutional change request');
} finally {
setSubmitting(false);
}
}; };
const stats = [ const stats = [
{ {
title: 'Total Requests', title: 'Total Requests',
value: mockDealerConstitutionalChanges.length, value: requests.length,
icon: RefreshCcw, icon: RefreshCcw,
color: 'bg-blue-500', color: 'bg-blue-500',
}, },
{ {
title: 'Pending', title: 'Pending',
value: mockDealerConstitutionalChanges.filter(r => r.status !== 'Completed' && r.status !== 'Rejected').length, value: requests.filter(r => r.status !== 'Completed' && r.status !== 'Rejected').length,
icon: Calendar, icon: Calendar,
color: 'bg-yellow-500', color: 'bg-yellow-500',
}, },
{ {
title: 'Completed', title: 'Completed',
value: mockDealerConstitutionalChanges.filter(r => r.status === 'Completed').length, value: requests.filter(r => r.status === 'Completed').length,
icon: FileText, icon: FileText,
color: 'bg-green-500', color: 'bg-green-500',
}, },
@ -101,244 +130,271 @@ export function DealerConstitutionalChangePage({ currentUser, onViewDetails }: D
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Loading Overlay */}
<div className="flex items-center justify-between"> {loading && (
<div> <div className="min-h-[400px] flex items-center justify-center">
<h1 className="text-slate-900 mb-2">My Constitutional Change Requests</h1> <Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
<p className="text-slate-600">
Submit and track requests for changing your business constitution
</p>
</div> </div>
)}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> {!loading && (
<Button className="bg-blue-600 hover:bg-blue-700"> <>
<Plus className="w-4 h-4 mr-2" /> {/* Header */}
New Constitutional Change <div className="flex items-center justify-between">
</Button> <div>
</DialogTrigger> <h1 className="text-slate-900 mb-2">My Constitutional Change Requests</h1>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> <p className="text-slate-600">
<DialogHeader> Submit and track requests for changing your business constitution
<DialogTitle>Submit Constitutional Change Request</DialogTitle> </p>
<DialogDescription> </div>
Request to change your dealership's business constitution structure
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmitRequest} className="space-y-4"> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
{/* Dealer Info */} <DialogTrigger asChild>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2"> <Button className="bg-blue-600 hover:bg-blue-700">
<h3 className="text-slate-900">Current Dealership Information</h3> <Plus className="w-4 h-4 mr-2" />
<div className="grid grid-cols-2 gap-3 text-sm"> New Constitutional Change
<div>
<span className="text-slate-600">Dealer Code:</span>
<p className="text-slate-900">DL-MH-001</p>
</div>
<div>
<span className="text-slate-600">Dealer Name:</span>
<p className="text-slate-900">Amit Sharma Motors</p>
</div>
<div className="col-span-2">
<span className="text-slate-600">Current Constitution:</span>
<p className="text-slate-900">Proprietorship</p>
</div>
</div>
</div>
{/* Constitution Change */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="currentConstitution">Current Constitution *</Label>
<Input
id="currentConstitution"
value={currentConstitution}
disabled
className="bg-slate-100"
/>
</div>
<div className="space-y-2">
<Label htmlFor="proposedConstitution">Proposed Constitution *</Label>
<Select value={proposedConstitution} onValueChange={setProposedConstitution} required>
<SelectTrigger>
<SelectValue placeholder="Select new constitution" />
</SelectTrigger>
<SelectContent>
{constitutionTypes
.filter(type => type !== currentConstitution)
.map(type => (
<SelectItem key={type} value={type}>{type}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Reason */}
<div className="space-y-2">
<Label htmlFor="reason">Reason for Change *</Label>
<Textarea
id="reason"
placeholder="Please provide detailed reason for constitutional change..."
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={4}
required
/>
</div>
{/* New Partners (if applicable) */}
{(proposedConstitution === 'Partnership' || proposedConstitution === 'LLP') && (
<div className="space-y-2">
<Label htmlFor="newPartners">Details of New Partners/Members</Label>
<Textarea
id="newPartners"
placeholder="Name, relationship, and experience of new partners..."
value={newPartners}
onChange={(e) => setNewPartners(e.target.value)}
rows={3}
/>
</div>
)}
{/* Shareholding Pattern */}
{(proposedConstitution === 'Pvt Ltd' || proposedConstitution === 'LLP') && (
<div className="space-y-2">
<Label htmlFor="shareholdingPattern">Proposed Shareholding Pattern</Label>
<Textarea
id="shareholdingPattern"
placeholder="Details of share distribution among partners/directors..."
value={shareholdingPattern}
onChange={(e) => setShareholdingPattern(e.target.value)}
rows={3}
/>
</div>
)}
{/* Document Requirements */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-blue-900 mb-2">Documents Required (to be uploaded later)</h4>
<ul className="text-blue-800 text-sm space-y-1">
<li> GST Registration Certificate</li>
<li> Firm PAN Copy</li>
<li> Partnership Deed (if applicable)</li>
<li> LLP Agreement (if applicable)</li>
<li> Certificate of Incorporation (if applicable)</li>
<li> MOA & AOA (if applicable)</li>
<li> Board Resolution</li>
<li> Aadhaar & PAN of all partners/directors</li>
</ul>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setIsDialogOpen(false)}
>
Cancel
</Button> </Button>
<Button </DialogTrigger>
type="submit" <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
className="bg-blue-600 hover:bg-blue-700" <DialogHeader>
> <DialogTitle>Submit Constitutional Change Request</DialogTitle>
Submit Request <DialogDescription>
</Button> Request to change your dealership's business constitution structure
</DialogFooter> </DialogDescription>
</form> </DialogHeader>
</DialogContent>
</Dialog> <form onSubmit={handleSubmitRequest} className="space-y-4">
</div> {/* Dealer Info */}
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2">
{/* Stats */} <h3 className="text-slate-900">Current Dealership Information</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-2 gap-3 text-sm">
{stats.map((stat, index) => { <div>
const Icon = stat.icon; <span className="text-slate-600">Dealer Code:</span>
return ( <p className="text-slate-900">{profile?.dealerCode || 'N/A'}</p>
<Card key={index}> </div>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <div>
<CardTitle className="text-sm">{stat.title}</CardTitle> <span className="text-slate-600">Dealer Name:</span>
<div className={`${stat.color} p-2 rounded-lg`}> <p className="text-slate-900">{profile?.businessName || 'N/A'}</p>
<Icon className="h-4 w-4 text-white" /> </div>
</div> <div className="col-span-2">
</CardHeader> <span className="text-slate-600">Current Constitution:</span>
<CardContent> <p className="text-slate-900">{currentConstitution}</p>
<div className="text-slate-900 text-2xl">{stat.value}</div>
</CardContent>
</Card>
);
})}
</div>
{/* Requests Table */}
<Card>
<CardHeader>
<CardTitle>My Constitutional Change Requests</CardTitle>
<CardDescription>
View and track all your constitutional change requests
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Request ID</TableHead>
<TableHead>Current</TableHead>
<TableHead>Proposed</TableHead>
<TableHead>Submitted On</TableHead>
<TableHead>Current Status</TableHead>
<TableHead>Progress</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockDealerConstitutionalChanges.map((request) => (
<TableRow key={request.id}>
<TableCell>
<span className="text-slate-900">{request.id}</span>
</TableCell>
<TableCell>
<Badge variant="outline">{request.currentConstitution}</Badge>
</TableCell>
<TableCell>
<Badge className="bg-blue-100 text-blue-700 border-blue-300">
{request.proposedConstitution}
</Badge>
</TableCell>
<TableCell className="text-slate-600">
{request.submittedOn}
</TableCell>
<TableCell>
<Badge className={`border ${getStatusColor(request.status)}`}>
{request.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${request.progressPercentage}%` }}
/>
</div> </div>
<span className="text-xs text-slate-600">{request.progressPercentage}%</span>
</div> </div>
</TableCell> </div>
<TableCell>
{/* Constitution Change */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="currentConstitution">Current Constitution *</Label>
<Input
id="currentConstitution"
value={currentConstitution}
disabled
className="bg-slate-100"
/>
</div>
<div className="space-y-2">
<Label htmlFor="proposedConstitution">Proposed Constitution *</Label>
<Select value={proposedConstitution} onValueChange={setProposedConstitution} required>
<SelectTrigger>
<SelectValue placeholder="Select new constitution" />
</SelectTrigger>
<SelectContent>
{constitutionTypes
.filter(type => type !== currentConstitution)
.map(type => (
<SelectItem key={type} value={type}>{type}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Reason */}
<div className="space-y-2">
<Label htmlFor="reason">Reason for Change *</Label>
<Textarea
id="reason"
placeholder="Please provide detailed reason for constitutional change..."
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={4}
required
/>
</div>
{/* New Partners (if applicable) */}
{(proposedConstitution === 'Partnership' || proposedConstitution === 'LLP') && (
<div className="space-y-2">
<Label htmlFor="newPartners">Details of New Partners/Members</Label>
<Textarea
id="newPartners"
placeholder="Name, relationship, and experience of new partners..."
value={newPartners}
onChange={(e) => setNewPartners(e.target.value)}
rows={3}
/>
</div>
)}
{/* Shareholding Pattern */}
{(proposedConstitution === 'Pvt Ltd' || proposedConstitution === 'LLP') && (
<div className="space-y-2">
<Label htmlFor="shareholdingPattern">Proposed Shareholding Pattern</Label>
<Textarea
id="shareholdingPattern"
placeholder="Details of share distribution among partners/directors..."
value={shareholdingPattern}
onChange={(e) => setShareholdingPattern(e.target.value)}
rows={3}
/>
</div>
)}
{/* Document Requirements */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-blue-900 mb-2">Documents Required (to be uploaded later)</h4>
<ul className="text-blue-800 text-sm space-y-1">
<li> GST Registration Certificate</li>
<li> Firm PAN Copy</li>
<li> Partnership Deed (if applicable)</li>
<li> LLP Agreement (if applicable)</li>
<li> Certificate of Incorporation (if applicable)</li>
<li> MOA & AOA (if applicable)</li>
<li> Board Resolution</li>
<li> Aadhaar & PAN of all partners/directors</li>
</ul>
</div>
<DialogFooter>
<Button <Button
variant="ghost" type="button"
size="sm" variant="outline"
onClick={() => onViewDetails && onViewDetails(request.id)} onClick={() => setIsDialogOpen(false)}
> >
<Eye className="w-4 h-4 mr-1" /> Cancel
View
</Button> </Button>
</TableCell> <Button
</TableRow> type="submit"
))} className="bg-blue-600 hover:bg-blue-700"
</TableBody> disabled={submitting}
</Table> >
</CardContent> {submitting ? (
</Card> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
'Submit Request'
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{stats.map((stat, index) => {
const Icon = stat.icon;
return (
<Card key={index}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm">{stat.title}</CardTitle>
<div className={`${stat.color} p-2 rounded-lg`}>
<Icon className="h-4 w-4 text-white" />
</div>
</CardHeader>
<CardContent>
<div className="text-slate-900 text-2xl">{stat.value}</div>
</CardContent>
</Card>
);
})}
</div>
{/* Requests Table */}
<Card>
<CardHeader>
<CardTitle>My Constitutional Change Requests</CardTitle>
<CardDescription>
View and track all your constitutional change requests
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Request ID</TableHead>
<TableHead>Current</TableHead>
<TableHead>Proposed</TableHead>
<TableHead>Submitted On</TableHead>
<TableHead>Current Status</TableHead>
<TableHead>Progress</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{requests.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-4 text-slate-500">
No constitutional change requests found
</TableCell>
</TableRow>
) : (
requests.map((request) => (
<TableRow key={request.id}>
<TableCell>
<span className="text-slate-900">{request.requestId}</span>
</TableCell>
<TableCell>
<Badge variant="outline">{request.currentConstitution}</Badge>
</TableCell>
<TableCell>
<Badge className="bg-blue-100 text-blue-700 border-blue-300">
{request.changeType}
</Badge>
</TableCell>
<TableCell className="text-slate-600">
{new Date(request.createdAt).toLocaleDateString()}
</TableCell>
<TableCell>
<Badge className={`border ${getStatusColor(request.status)}`}>
{request.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${request.progressPercentage || 0}%` }}
/>
</div>
<span className="text-xs text-slate-600">{request.progressPercentage || 0}%</span>
</div>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetails && onViewDetails(request.requestId)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
</>
)}
</div> </div>
); );
} }

View File

@ -1,4 +1,4 @@
import { MapPin, Plus, Eye, Calendar, FileText, Building, Navigation } from 'lucide-react'; import { MapPin, Plus, Eye, Calendar, Building2, Loader2, ArrowRight } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
@ -8,32 +8,16 @@ import { Input } from '../ui/input';
import { Label } from '../ui/label'; import { Label } from '../ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data'; import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { dealerService } from '../../services/dealer.service';
interface DealerRelocationPageProps { interface DealerRelocationPageProps {
currentUser: UserType | null; currentUser: UserType | null;
onViewDetails?: (id: string) => void; onViewDetails?: (id: string) => void;
} }
// Mock relocation requests for this dealer
const mockDealerRelocations = [
{
id: 'RLO-001',
dealerCode: 'DL-MH-001',
dealerName: 'Amit Sharma Motors',
currentLocation: 'Bandra West, Mumbai',
proposedLocation: 'Andheri East, Mumbai',
distance: '12 km',
reason: 'Better connectivity and higher footfall area',
status: 'DD ZM Review',
submittedOn: '2025-12-20',
currentStage: 'DD-ZM',
progressPercentage: 25
},
];
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300'; if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300'; if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
@ -43,58 +27,54 @@ const getStatusColor = (status: string) => {
export function DealerRelocationPage({ currentUser, onViewDetails }: DealerRelocationPageProps) { export function DealerRelocationPage({ currentUser, onViewDetails }: DealerRelocationPageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [proposedAddress, setProposedAddress] = useState(''); const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
const [proposedCity, setProposedCity] = useState(''); const [newCity, setNewCity] = useState('');
const [proposedState, setProposedState] = useState(''); const [newState, setNewState] = useState('');
const [proposedPincode, setProposedPincode] = useState(''); const [newAddress, setNewAddress] = useState('');
const [distance, setDistance] = useState('');
const [propertyType, setPropertyType] = useState('');
const [expectedDate, setExpectedDate] = useState('');
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
const [locationMode, setLocationMode] = useState<'manual' | 'map'>('manual');
const [mapCoordinates, setMapCoordinates] = useState({ lat: 19.0760, lng: 72.8777 }); const [outlets, setOutlets] = useState<any[]>([]);
const [selectedLocation, setSelectedLocation] = useState<{ lat: number; lng: number } | null>(null); const [requests, setRequests] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [profile, setProfile] = useState<any>(null);
const handleMapClick = (e: React.MouseEvent<HTMLDivElement>) => { useEffect(() => {
const rect = e.currentTarget.getBoundingClientRect(); fetchData();
const x = e.clientX - rect.left; }, []);
const y = e.clientY - rect.top;
const fetchData = async () => {
const lat = mapCoordinates.lat + (y - rect.height / 2) / 1000; try {
const lng = mapCoordinates.lng + (x - rect.width / 2) / 1000; setLoading(true);
const dashboard = await dealerService.getDashboardData();
setSelectedLocation({ lat, lng }); const relocationRes = await dealerService.getRelocationRequests();
const mockLocations = [ setOutlets(dashboard.outlets || []);
{ city: 'Mumbai', state: 'Maharashtra', pincode: '400001', address: 'Nariman Point, South Mumbai' }, setProfile(dashboard.profile);
{ city: 'Mumbai', state: 'Maharashtra', pincode: '400051', address: 'Andheri East, Mumbai' }, setRequests(relocationRes.requests || []);
{ city: 'Mumbai', state: 'Maharashtra', pincode: '400070', address: 'Powai, Mumbai' }, } catch (error) {
]; console.error('Fetch relocation data error:', error);
toast.error('Failed to load outlets and requests');
const randomLocation = mockLocations[Math.floor(Math.random() * mockLocations.length)]; } finally {
setProposedAddress(randomLocation.address); setLoading(false);
setProposedCity(randomLocation.city); }
setProposedState(randomLocation.state);
setProposedPincode(randomLocation.pincode);
toast.success('Location selected from map');
}; };
const handleSubmitRequest = (e: React.FormEvent) => { const handleOpenRelocationDialog = (outlet: any) => {
setSelectedOutlet(outlet);
setIsDialogOpen(true);
};
const handleSubmitRequest = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!proposedAddress.trim() || !proposedCity.trim() || !proposedState.trim() || !proposedPincode.trim()) { if (!selectedOutlet) {
toast.error('Please enter complete proposed location details'); toast.error('Please select an outlet');
return; return;
} }
if (!distance.trim()) { if (!newCity.trim() || !newState.trim() || !newAddress.trim()) {
toast.error('Please enter distance from current location'); toast.error('Please provide complete relocation details');
return;
}
if (!propertyType) {
toast.error('Please select property type');
return; return;
} }
@ -103,422 +83,311 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
return; return;
} }
toast.success('Relocation request submitted successfully'); try {
setIsDialogOpen(false); setSubmitting(true);
const payload = {
// Reset form outletId: selectedOutlet.id,
setProposedAddress(''); currentLocation: selectedOutlet.location,
setProposedCity(''); newCity,
setProposedState(''); newState,
setProposedPincode(''); newAddress,
setDistance(''); reason
setPropertyType(''); };
setExpectedDate('');
setReason(''); await dealerService.submitRelocationRequest(payload);
setLocationMode('manual'); toast.success(`Relocation request submitted successfully for ${selectedOutlet.name}`);
setSelectedLocation(null); setIsDialogOpen(false);
fetchData(); // Refresh list
// Reset form
setSelectedOutlet(null);
setNewCity('');
setNewState('');
setNewAddress('');
setReason('');
} catch (error) {
console.error('Submit relocation error:', error);
toast.error('Failed to submit relocation request');
} finally {
setSubmitting(false);
}
}; };
const stats = [ const stats = [
{ {
title: 'Total Requests', title: 'Total Requests',
value: mockDealerRelocations.length, value: requests.length,
icon: MapPin, icon: MapPin,
color: 'bg-amber-500', color: 'bg-blue-500',
}, },
{ {
title: 'Pending', title: 'Pending',
value: mockDealerRelocations.filter(r => r.status !== 'Completed' && r.status !== 'Rejected').length, value: requests.filter(r => r.status !== 'Completed' && r.status !== 'Rejected').length,
icon: Calendar, icon: Calendar,
color: 'bg-yellow-500', color: 'bg-yellow-500',
}, },
{ {
title: 'Completed', title: 'Approved',
value: mockDealerRelocations.filter(r => r.status === 'Completed').length, value: requests.filter(r => r.status === 'Completed').length,
icon: FileText, icon: Building2,
color: 'bg-green-500', color: 'bg-green-500',
}, },
]; ];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Loading Overlay */}
<div className="flex items-center justify-between"> {loading && (
<div> <div className="min-h-[400px] flex items-center justify-center">
<h1 className="text-slate-900 mb-2">My Relocation Requests</h1> <Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
<p className="text-slate-600">
Submit and track requests for relocating your dealership
</p>
</div> </div>
)}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> {!loading && (
<Button className="bg-amber-600 hover:bg-amber-700"> <>
<Plus className="w-4 h-4 mr-2" /> {/* Header */}
New Relocation Request <div className="flex items-center justify-between">
</Button> <div>
</DialogTrigger> <h1 className="text-slate-900 mb-2">Relocation Requests</h1>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto"> <p className="text-slate-600">
<DialogHeader> Request to relocate your existing dealership or studio to a new location
<DialogTitle>Submit Relocation Request</DialogTitle> </p>
<DialogDescription> </div>
Request to relocate your dealership to a new location
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmitRequest} className="space-y-4"> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
{/* Current Dealer Info */} <DialogTrigger asChild>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-3"> <Button className="bg-amber-600 hover:bg-amber-700 text-white">
<h3 className="text-slate-900">Current Dealership Details</h3> <Plus className="w-4 h-4 mr-2" />
<div className="grid grid-cols-2 gap-3 text-sm"> New Relocation Request
<div> </Button>
<span className="text-slate-600">Dealer Code:</span> </DialogTrigger>
<p className="text-slate-900">DL-MH-001</p> <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
</div> <DialogHeader>
<div> <DialogTitle>Submit Relocation Request</DialogTitle>
<span className="text-slate-600">Dealer Name:</span> <DialogDescription>
<p className="text-slate-900">Amit Sharma Motors</p> Provide details about the outlet you want to relocate and its proposed new location
</div> </DialogDescription>
<div className="col-span-2"> </DialogHeader>
<span className="text-slate-600">Current Location:</span>
<p className="text-slate-900">123, MG Road, Bandra West, Mumbai, Maharashtra - 400050</p>
</div>
</div>
</div>
{/* Proposed New Location */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-slate-900">Proposed New Location *</h3>
{/* Location Mode Toggle */}
<div className="flex items-center gap-2 bg-slate-100 rounded-lg p-1">
<button
type="button"
onClick={() => setLocationMode('manual')}
className={`px-3 py-1 rounded text-sm transition-colors ${
locationMode === 'manual'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
Manual Entry
</button>
<button
type="button"
onClick={() => setLocationMode('map')}
className={`px-3 py-1 rounded text-sm transition-colors flex items-center gap-1 ${
locationMode === 'map'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<MapPin className="w-3 h-3" />
Map Location
</button>
</div>
</div>
{/* Map Mode */}
{locationMode === 'map' && (
<div className="space-y-3">
<div className="border-2 border-amber-300 rounded-lg overflow-hidden">
<div
onClick={handleMapClick}
className="relative h-64 bg-gradient-to-br from-green-100 via-blue-50 to-amber-50 cursor-crosshair"
style={{
backgroundImage: `
linear-gradient(to right, rgba(148, 163, 184, 0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(148, 163, 184, 0.1) 1px, transparent 1px)
`,
backgroundSize: '20px 20px'
}}
>
<div className="absolute inset-0">
<div className="absolute top-1/4 left-0 right-0 h-1 bg-slate-300 opacity-30" />
<div className="absolute top-1/2 left-0 right-0 h-2 bg-slate-400 opacity-40" />
<div className="absolute top-3/4 left-0 right-0 h-1 bg-slate-300 opacity-30" />
<div className="absolute left-1/4 top-0 bottom-0 w-1 bg-slate-300 opacity-30" />
<div className="absolute left-1/2 top-0 bottom-0 w-2 bg-slate-400 opacity-40" />
<div className="absolute left-3/4 top-0 bottom-0 w-1 bg-slate-300 opacity-30" />
</div>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className="flex flex-col items-center">
<Building className="w-6 h-6 text-blue-600" />
<div className="text-xs text-blue-900 bg-white px-2 py-1 rounded shadow-sm mt-1">
Current Location
</div>
</div>
</div>
{selectedLocation && (
<div className="absolute top-1/3 left-2/3 transform -translate-x-1/2 -translate-y-full">
<div className="flex flex-col items-center animate-bounce">
<MapPin className="w-8 h-8 text-amber-600 drop-shadow-lg" />
<div className="text-xs text-amber-900 bg-amber-100 px-2 py-1 rounded shadow-md border border-amber-300">
New Location
</div>
</div>
</div>
)}
<div className="absolute bottom-2 left-2 bg-white/90 px-3 py-2 rounded shadow-sm border border-slate-200">
<p className="text-xs text-slate-700">
<MapPin className="w-3 h-3 inline mr-1" />
Click anywhere on the map to select new location
</p>
</div>
{selectedLocation && (
<div className="absolute top-2 right-2 bg-amber-600 text-white px-3 py-2 rounded shadow-md text-xs">
Lat: {selectedLocation.lat.toFixed(4)}, Lng: {selectedLocation.lng.toFixed(4)}
</div>
)}
</div>
</div>
{selectedLocation && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3 text-sm text-green-800">
Location selected! Address details auto-filled below.
</div>
)}
</div>
)}
{/* Address Fields */} <form onSubmit={handleSubmitRequest} className="space-y-4">
<div className="space-y-2"> {/* Select Outlet */}
<Label htmlFor="proposedAddress">Complete Address *</Label>
<Input
id="proposedAddress"
placeholder="Building/Shop number, Street, Locality"
value={proposedAddress}
onChange={(e) => setProposedAddress(e.target.value)}
required
readOnly={locationMode === 'map' && !!selectedLocation}
className={locationMode === 'map' && selectedLocation ? 'bg-green-50' : ''}
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="proposedCity">City *</Label> <Label htmlFor="outlet">Select Outlet to Relocate *</Label>
<Input <Select
id="proposedCity" value={selectedOutlet?.id}
placeholder="City" onValueChange={(val) => setSelectedOutlet(outlets.find(o => o.id === val))}
value={proposedCity}
onChange={(e) => setProposedCity(e.target.value)}
required required
readOnly={locationMode === 'map' && !!selectedLocation}
className={locationMode === 'map' && selectedLocation ? 'bg-green-50' : ''}
/>
</div>
<div className="space-y-2">
<Label htmlFor="proposedState">State *</Label>
<Input
id="proposedState"
placeholder="State"
value={proposedState}
onChange={(e) => setProposedState(e.target.value)}
required
readOnly={locationMode === 'map' && !!selectedLocation}
className={locationMode === 'map' && selectedLocation ? 'bg-green-50' : ''}
/>
</div>
<div className="space-y-2">
<Label htmlFor="proposedPincode">Pincode *</Label>
<Input
id="proposedPincode"
placeholder="Pincode"
value={proposedPincode}
onChange={(e) => setProposedPincode(e.target.value)}
required
readOnly={locationMode === 'map' && !!selectedLocation}
className={locationMode === 'map' && selectedLocation ? 'bg-green-50' : ''}
/>
</div>
</div>
</div>
{/* Distance & Property Details */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="distance">Distance from Current Location *</Label>
<Input
id="distance"
placeholder="e.g., 12 km"
value={distance}
onChange={(e) => setDistance(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="propertyType">Property Type *</Label>
<Select value={propertyType} onValueChange={setPropertyType} required>
<SelectTrigger>
<SelectValue placeholder="Select property type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Owned">Owned</SelectItem>
<SelectItem value="Leased">Leased</SelectItem>
<SelectItem value="Rented">Rented</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Expected Relocation Date */}
<div className="space-y-2">
<Label htmlFor="expectedDate">Expected Relocation Date</Label>
<Input
id="expectedDate"
type="date"
value={expectedDate}
onChange={(e) => setExpectedDate(e.target.value)}
/>
</div>
{/* Reason */}
<div className="space-y-2">
<Label htmlFor="reason">Reason for Relocation *</Label>
<Textarea
id="reason"
placeholder="Provide detailed reason for relocation request..."
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={4}
required
/>
</div>
{/* Required Documents Info */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-blue-900 mb-2">Documents Required (to be uploaded later)</h4>
<ul className="text-blue-800 text-sm space-y-1">
<li> Property documents for new location</li>
<li> Lease/Rental agreement for new location</li>
<li> NOC from current landlord</li>
<li> Municipal approvals</li>
<li> Fire safety certificate</li>
<li> Pollution clearance</li>
<li> Layout/Floor plan of new location</li>
<li> Photos of new location</li>
<li> Locality map</li>
</ul>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setIsDialogOpen(false)}
>
Cancel
</Button>
<Button
type="submit"
className="bg-amber-600 hover:bg-amber-700"
>
Submit Request
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{stats.map((stat, index) => {
const Icon = stat.icon;
return (
<Card key={index}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm">{stat.title}</CardTitle>
<div className={`${stat.color} p-2 rounded-lg`}>
<Icon className="h-4 w-4 text-white" />
</div>
</CardHeader>
<CardContent>
<div className="text-slate-900 text-2xl">{stat.value}</div>
</CardContent>
</Card>
);
})}
</div>
{/* Requests Table */}
<Card>
<CardHeader>
<CardTitle>My Relocation Requests</CardTitle>
<CardDescription>
View and track all your relocation requests
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Request ID</TableHead>
<TableHead>Current Location</TableHead>
<TableHead>Proposed Location</TableHead>
<TableHead>Distance</TableHead>
<TableHead>Submitted On</TableHead>
<TableHead>Status</TableHead>
<TableHead>Progress</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockDealerRelocations.map((request) => (
<TableRow key={request.id}>
<TableCell>
<span className="text-slate-900">{request.id}</span>
</TableCell>
<TableCell className="text-slate-600">
{request.currentLocation}
</TableCell>
<TableCell className="text-slate-900">
{request.proposedLocation}
</TableCell>
<TableCell className="text-slate-600">
{request.distance}
</TableCell>
<TableCell className="text-slate-600">
{request.submittedOn}
</TableCell>
<TableCell>
<Badge className={`border ${getStatusColor(request.status)}`}>
{request.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2">
<div
className="bg-amber-600 h-2 rounded-full"
style={{ width: `${request.progressPercentage}%` }}
/>
</div>
<span className="text-xs text-slate-600">{request.progressPercentage}%</span>
</div>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetails && onViewDetails(request.id)}
> >
<Eye className="w-4 h-4 mr-1" /> <SelectTrigger>
View <SelectValue placeholder="Select an outlet" />
</SelectTrigger>
<SelectContent>
{outlets.map((outlet) => (
<SelectItem key={outlet.id} value={outlet.id}>
{outlet.name} ({outlet.code})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedOutlet && (
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2">
<h3 className="text-slate-900 text-sm font-medium">Current Location</h3>
<p className="text-slate-600 text-sm">{selectedOutlet.location}</p>
</div>
)}
{/* New Location Details */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="newCity">Proposed City *</Label>
<Input
id="newCity"
placeholder="Enter new city"
value={newCity}
onChange={(e) => setNewCity(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="newState">Proposed State *</Label>
<Input
id="newState"
placeholder="Enter new state"
value={newState}
onChange={(e) => setNewState(e.target.value)}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="newAddress">Proposed Full Address *</Label>
<Textarea
id="newAddress"
placeholder="Enter detailed address of the proposed new location..."
value={newAddress}
onChange={(e) => setNewAddress(e.target.value)}
rows={3}
required
/>
</div>
{/* Reason */}
<div className="space-y-2">
<Label htmlFor="reason">Reason for Relocation *</Label>
<Textarea
id="reason"
placeholder="Why do you want to relocate this outlet?"
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={4}
required
/>
</div>
{/* Important Notes */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-blue-900 mb-2 font-medium">Policy Notes</h4>
<ul className="text-blue-800 text-xs space-y-1">
<li> Relocation is subject to feasibility study of the new location</li>
<li> Maximum allowed distance and other policy criteria apply</li>
<li> Site visit will be conducted by RBM/ASM</li>
<li> New outlet code might be generated upon approval</li>
</ul>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setIsDialogOpen(false)}
>
Cancel
</Button> </Button>
</TableCell> <Button
</TableRow> type="submit"
))} className="bg-amber-600 hover:bg-amber-700 text-white"
</TableBody> disabled={submitting}
</Table> >
</CardContent> {submitting ? (
</Card> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
'Submit Relocation Request'
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{stats.map((stat, index) => {
const Icon = stat.icon;
return (
<Card key={index}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm">{stat.title}</CardTitle>
<div className={`${stat.color} p-2 rounded-lg`}>
<Icon className="h-4 w-4 text-white" />
</div>
</CardHeader>
<CardContent>
<div className="text-slate-900 text-2xl">{stat.value}</div>
</CardContent>
</Card>
);
})}
</div>
{/* Requests Table */}
<Card>
<CardHeader>
<CardTitle>My Relocation Requests</CardTitle>
<CardDescription>
Track the status of your relocation applications
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Request ID</TableHead>
<TableHead>Outlet</TableHead>
<TableHead>Target Location</TableHead>
<TableHead>Submitted On</TableHead>
<TableHead>Current Status</TableHead>
<TableHead>Progress</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{requests.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-4 text-slate-500">
No relocation requests found
</TableCell>
</TableRow>
) : (
requests.map((request) => (
<TableRow key={request.id}>
<TableCell>
<span className="text-slate-900 font-medium">{request.requestId}</span>
</TableCell>
<TableCell>
{request.outlet?.name}
</TableCell>
<TableCell>
<div className="flex items-center gap-1 text-sm">
<span className="text-slate-500">{request.currentCity}</span>
<ArrowRight className="w-3 h-3 text-slate-400" />
<span className="text-slate-900">{request.newCity}</span>
</div>
</TableCell>
<TableCell className="text-slate-600">
{new Date(request.createdAt).toLocaleDateString()}
</TableCell>
<TableCell>
<Badge className={`border ${getStatusColor(request.status)}`}>
{request.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
<div
className="bg-amber-500 h-2 rounded-full"
style={{ width: `${request.progressPercentage || 0}%` }}
/>
</div>
<span className="text-xs text-slate-600">{request.progressPercentage || 0}%</span>
</div>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetails && onViewDetails(request.requestId)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
</>
)}
</div> </div>
); );
} }

View File

@ -1,4 +1,4 @@
import { FileText, Plus, Eye, Calendar, User, Building2, Store, MapPin, CheckCircle, Clock } from 'lucide-react'; import { FileText, Plus, Eye, Calendar, User, Building2, Store, MapPin, CheckCircle, Clock, RefreshCcw, Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
@ -8,107 +8,17 @@ import { Input } from '../ui/input';
import { Label } from '../ui/label'; import { Label } from '../ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data'; import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { dealerService } from '../../services/dealer.service';
import { resignationService } from '../../services/resignation.service';
interface DealerResignationPageProps { interface DealerResignationPageProps {
currentUser: UserType | null; currentUser: UserType | null;
onViewDetails?: (id: string) => void; onViewDetails?: (id: string) => void;
} }
// Mock outlets owned by dealer
interface Outlet {
id: string;
code: string;
name: string;
type: 'Dealership' | 'Studio';
address: string;
city: string;
state: string;
status: 'Active' | 'Pending Resignation' | 'Closed';
establishedDate: string;
hasActiveResignation?: boolean;
resignationId?: string;
}
const mockOutlets: Outlet[] = [
{
id: 'OUT-001',
code: 'DL-MH-001',
name: 'Royal Enfield Mumbai',
type: 'Dealership',
address: 'Plot No. 45, Linking Road, Bandra West',
city: 'Mumbai',
state: 'Maharashtra',
status: 'Active',
establishedDate: '2018-06-15',
hasActiveResignation: true,
resignationId: 'RES-001'
},
{
id: 'OUT-002',
code: 'ST-MH-002',
name: 'Royal Enfield Andheri Studio',
type: 'Studio',
address: 'Shop 12, Phoenix Market City, Kurla',
city: 'Mumbai',
state: 'Maharashtra',
status: 'Active',
establishedDate: '2020-03-20'
},
{
id: 'OUT-003',
code: 'DL-MH-003',
name: 'Royal Enfield Thane Dealership',
type: 'Dealership',
address: 'Eastern Express Highway, Thane West',
city: 'Thane',
state: 'Maharashtra',
status: 'Active',
establishedDate: '2019-09-10'
},
{
id: 'OUT-004',
code: 'ST-MH-004',
name: 'Royal Enfield Pune Studio',
type: 'Studio',
address: 'FC Road, Deccan Gymkhana',
city: 'Pune',
state: 'Maharashtra',
status: 'Active',
establishedDate: '2021-01-05'
}
];
// Mock resignation requests for this dealer
const mockDealerResignations = [
{
id: 'RES-001',
dealerCode: 'DL-MH-001',
dealerName: 'Amit Sharma Motors',
resignationType: 'Voluntary',
lastOperationalDate: '2026-02-28',
reason: 'Personal health issues',
status: 'ASM Review',
submittedOn: '2025-12-20',
currentStage: 'ASM',
progressPercentage: 15
},
{
id: 'RES-005',
dealerCode: 'DL-MH-001',
dealerName: 'Amit Sharma Motors',
resignationType: 'Voluntary',
lastOperationalDate: '2025-06-30',
reason: 'Relocation to different city',
status: 'Completed',
submittedOn: '2025-04-15',
currentStage: 'Closed',
progressPercentage: 100
},
];
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300'; if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300'; if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
@ -118,19 +28,46 @@ const getStatusColor = (status: string) => {
export function DealerResignationPage({ currentUser, onViewDetails }: DealerResignationPageProps) { export function DealerResignationPage({ currentUser, onViewDetails }: DealerResignationPageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedOutlet, setSelectedOutlet] = useState<Outlet | null>(null); const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
const [resignationType, setResignationType] = useState(''); const [resignationType, setResignationType] = useState('');
const [lastOperationalDateSales, setLastOperationalDateSales] = useState(''); const [lastOperationalDateSales, setLastOperationalDateSales] = useState('');
const [lastOperationalDateServices, setLastOperationalDateServices] = useState(''); const [lastOperationalDateServices, setLastOperationalDateServices] = useState('');
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
const [additionalInfo, setAdditionalInfo] = useState(''); const [additionalInfo, setAdditionalInfo] = useState('');
const [outlets, setOutlets] = useState<any[]>([]);
const [resignations, setResignations] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [profile, setProfile] = useState<any>(null);
const handleOpenResignationDialog = (outlet: Outlet) => { useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
setLoading(true);
const dashboard = await dealerService.getDashboardData();
const resignationsRes = await resignationService.getResignations();
setOutlets(dashboard.outlets || []);
setProfile(dashboard.profile);
setResignations(resignationsRes.resignations || []);
} catch (error) {
console.error('Fetch resignation data error:', error);
toast.error('Failed to load outlets and requests');
} finally {
setLoading(false);
}
};
const handleOpenResignationDialog = (outlet: any) => {
setSelectedOutlet(outlet); setSelectedOutlet(outlet);
setIsDialogOpen(true); setIsDialogOpen(true);
}; };
const handleSubmitRequest = (e: React.FormEvent) => { const handleSubmitRequest = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!resignationType) { if (!resignationType) {
@ -148,34 +85,53 @@ export function DealerResignationPage({ currentUser, onViewDetails }: DealerResi
return; return;
} }
toast.success(`Resignation request submitted successfully for ${selectedOutlet?.name}`); try {
setIsDialogOpen(false); setSubmitting(true);
const payload = {
// Reset form outletId: selectedOutlet?.id,
setSelectedOutlet(null); resignationType,
setResignationType(''); lastOperationalDateSales,
setLastOperationalDateSales(''); lastOperationalDateServices,
setLastOperationalDateServices(''); reason,
setReason(''); additionalInfo
setAdditionalInfo(''); };
await resignationService.createResignation(payload);
toast.success(`Resignation request submitted successfully for ${selectedOutlet?.name}`);
setIsDialogOpen(false);
fetchData(); // Refresh data
// Reset form
setSelectedOutlet(null);
setResignationType('');
setLastOperationalDateSales('');
setLastOperationalDateServices('');
setReason('');
setAdditionalInfo('');
} catch (error) {
console.error('Submit resignation error:', error);
toast.error('Failed to submit resignation request');
} finally {
setSubmitting(false);
}
}; };
const stats = [ const stats = [
{ {
title: 'Total Outlets', title: 'Total Outlets',
value: mockOutlets.length, value: outlets.length,
icon: Building2, icon: Building2,
color: 'bg-blue-500', color: 'bg-blue-500',
}, },
{ {
title: 'Active Outlets', title: 'Active Outlets',
value: mockOutlets.filter(o => o.status === 'Active').length, value: outlets.filter(o => o.status === 'Active').length,
icon: CheckCircle, icon: CheckCircle,
color: 'bg-green-500', color: 'bg-green-500',
}, },
{ {
title: 'Pending Resignations', title: 'Pending Resignations',
value: mockOutlets.filter(o => o.hasActiveResignation).length, value: outlets.filter(o => o.status === 'Pending Resignation').length,
icon: Clock, icon: Clock,
color: 'bg-amber-500', color: 'bg-amber-500',
}, },
@ -183,254 +139,338 @@ export function DealerResignationPage({ currentUser, onViewDetails }: DealerResi
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Loading Overlay */}
<div> {loading && (
<h1 className="text-slate-900 mb-2">Dealership Resignation Management</h1> <div className="min-h-[400px] flex items-center justify-center">
<p className="text-slate-600"> <Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
Manage resignation requests for your dealerships and studios </div>
</p> )}
</div>
{/* Stats */} {!loading && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <>
{stats.map((stat, index) => { {/* Header */}
const Icon = stat.icon; <div>
return ( <h1 className="text-slate-900 mb-2">Dealership Resignation Management</h1>
<Card key={index}> <p className="text-slate-600">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> Manage resignation requests for your dealerships and studios
<CardTitle className="text-sm">{stat.title}</CardTitle> </p>
<div className={`${stat.color} p-2 rounded-lg`}> </div>
<Icon className="h-4 w-4 text-white" />
</div>
</CardHeader>
<CardContent>
<div className="text-slate-900 text-2xl">{stat.value}</div>
</CardContent>
</Card>
);
})}
</div>
{/* My Outlets Section */} {/* Stats */}
<Card> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<CardHeader> {stats.map((stat, index) => {
<CardTitle>My Outlets</CardTitle> const Icon = stat.icon;
<CardDescription>
Select an outlet to request resignation
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{mockOutlets.map((outlet) => {
const OutletIcon = outlet.type === 'Dealership' ? Building2 : Store;
return ( return (
<div <Card key={index}>
key={outlet.id} <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
className="border border-slate-200 rounded-lg p-4 hover:shadow-md transition-shadow" <CardTitle className="text-sm">{stat.title}</CardTitle>
> <div className={`${stat.color} p-2 rounded-lg`}>
<div className="flex items-start justify-between mb-3"> <Icon className="h-4 w-4 text-white" />
<div className="flex items-start gap-3">
<div className={`${outlet.type === 'Dealership' ? 'bg-blue-100' : 'bg-purple-100'} p-2 rounded-lg`}>
<OutletIcon className={`w-5 h-5 ${outlet.type === 'Dealership' ? 'text-blue-600' : 'text-purple-600'}`} />
</div>
<div>
<h3 className="text-slate-900">{outlet.name}</h3>
<p className="text-slate-600 text-sm">{outlet.code}</p>
</div>
</div> </div>
<Badge </CardHeader>
className={`border ${ <CardContent>
outlet.status === 'Active' <div className="text-slate-900 text-2xl">{stat.value}</div>
? 'bg-green-100 text-green-700 border-green-300' </CardContent>
: outlet.status === 'Pending Resignation' </Card>
? 'bg-amber-100 text-amber-700 border-amber-300'
: 'bg-slate-100 text-slate-700 border-slate-300'
}`}
>
{outlet.status}
</Badge>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-start gap-2 text-sm">
<MapPin className="w-4 h-4 text-slate-400 mt-0.5 flex-shrink-0" />
<div>
<p className="text-slate-600">{outlet.address}</p>
<p className="text-slate-500">{outlet.city}, {outlet.state}</p>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<Calendar className="w-4 h-4 text-slate-400" />
<span className="text-slate-600">
Established: {new Date(outlet.establishedDate).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric'
})}
</span>
</div>
</div>
{outlet.hasActiveResignation ? (
<div className="bg-amber-50 border border-amber-200 rounded p-3 text-sm">
<p className="text-amber-800">
Resignation in progress - <span className="underline cursor-pointer" onClick={() => onViewDetails && outlet.resignationId && onViewDetails(outlet.resignationId)}>View Request</span>
</p>
</div>
) : (
<Button
className="w-full bg-red-600 hover:bg-red-700"
onClick={() => handleOpenResignationDialog(outlet)}
>
<FileText className="w-4 h-4 mr-2" />
Request Resignation
</Button>
)}
</div>
); );
})} })}
</div> </div>
</CardContent>
</Card>
{/* Resignation Request Dialog */} {/* My Outlets Section */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Card>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> <CardHeader>
<DialogHeader> <CardTitle>My Outlets</CardTitle>
<DialogTitle>Submit Resignation Request</DialogTitle> <CardDescription>
<DialogDescription> Select an outlet to request resignation
Fill in the details for your resignation request. All fields are mandatory. </CardDescription>
</DialogDescription> </CardHeader>
</DialogHeader> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<form onSubmit={handleSubmitRequest} className="space-y-4"> {outlets.map((outlet) => {
{/* Outlet Info */} const OutletIcon = outlet.type === 'Dealership' ? Building2 : Store;
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2"> const hasActiveResignation = outlet.status === 'Pending Resignation';
<h3 className="text-slate-900">Outlet Information</h3> const resignation = resignations.find(r => r.outletId === outlet.id && r.status !== 'Completed' && r.status !== 'Rejected');
<div className="grid grid-cols-2 gap-3 text-sm">
<div> return (
<span className="text-slate-600">Outlet Code:</span> <div
<p className="text-slate-900">{selectedOutlet?.code}</p> key={outlet.id}
</div> className="border border-slate-200 rounded-lg p-4 hover:shadow-md transition-shadow"
<div> >
<span className="text-slate-600">Outlet Name:</span> <div className="flex items-start justify-between mb-3">
<p className="text-slate-900">{selectedOutlet?.name}</p> <div className="flex items-start gap-3">
</div> <div className={`${outlet.type === 'Dealership' ? 'bg-blue-100' : 'bg-purple-100'} p-2 rounded-lg`}>
<div> <OutletIcon className={`w-5 h-5 ${outlet.type === 'Dealership' ? 'text-blue-600' : 'text-purple-600'}`} />
<span className="text-slate-600">Type:</span> </div>
<p className="text-slate-900">{selectedOutlet?.type}</p> <div>
</div> <h3 className="text-slate-900">{outlet.name}</h3>
<div> <p className="text-slate-600 text-sm">{outlet.code}</p>
<span className="text-slate-600">City:</span> </div>
<p className="text-slate-900">{selectedOutlet?.city}, {selectedOutlet?.state}</p> </div>
</div> <Badge
<div className="col-span-2"> className={`border ${
<span className="text-slate-600">Address:</span> outlet.status === 'Active'
<p className="text-slate-900">{selectedOutlet?.address}</p> ? 'bg-green-100 text-green-700 border-green-300'
</div> : outlet.status === 'Pending Resignation'
? 'bg-amber-100 text-amber-700 border-amber-300'
: 'bg-slate-100 text-slate-700 border-slate-300'
}`}
>
{outlet.status}
</Badge>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-start gap-2 text-sm">
<MapPin className="w-4 h-4 text-slate-400 mt-0.5 flex-shrink-0" />
<div>
<p className="text-slate-600">{outlet.location}</p>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<Calendar className="w-4 h-4 text-slate-400" />
<span className="text-slate-600">
Outlet Code: {outlet.code}
</span>
</div>
</div>
{hasActiveResignation ? (
<div className="bg-amber-50 border border-amber-200 rounded p-3 text-sm">
<p className="text-amber-800">
Resignation in progress - <span className="underline cursor-pointer" onClick={() => onViewDetails && resignation?.resignationId && onViewDetails(resignation.resignationId)}>View Request</span>
</p>
</div>
) : (
<Button
className="w-full bg-red-600 hover:bg-red-700"
onClick={() => handleOpenResignationDialog(outlet)}
>
<FileText className="w-4 h-4 mr-2" />
Request Resignation
</Button>
)}
</div>
);
})}
</div> </div>
</div> </CardContent>
</Card>
{/* Resignation Type */} {/* Resignation Request Dialog */}
<div className="space-y-2"> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<Label htmlFor="resignationType">Resignation Type *</Label> <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<Select value={resignationType} onValueChange={setResignationType} required> <DialogHeader>
<SelectTrigger> <DialogTitle>Submit Resignation Request</DialogTitle>
<SelectValue placeholder="Select resignation type" /> <DialogDescription>
</SelectTrigger> Fill in the details for your resignation request. All fields are mandatory.
<SelectContent> </DialogDescription>
<SelectItem value="Voluntary">Voluntary</SelectItem> </DialogHeader>
<SelectItem value="Retirement">Retirement</SelectItem>
<SelectItem value="Health Issues">Health Issues</SelectItem> <form onSubmit={handleSubmitRequest} className="space-y-4">
<SelectItem value="Business Closure">Business Closure</SelectItem> {/* Outlet Info */}
<SelectItem value="Other">Other</SelectItem> <div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2">
</SelectContent> <h3 className="text-slate-900">Outlet Information</h3>
</Select> <div className="grid grid-cols-2 gap-3 text-sm">
</div> <div>
<span className="text-slate-600">Outlet Code:</span>
<p className="text-slate-900">{selectedOutlet?.code}</p>
</div>
<div>
<span className="text-slate-600">Outlet Name:</span>
<p className="text-slate-900">{selectedOutlet?.name}</p>
</div>
<div>
<span className="text-slate-600">Dealer Code:</span>
<p className="text-slate-900">{profile?.dealerCode || 'N/A'}</p>
</div>
<div>
<span className="text-slate-600">City:</span>
<p className="text-slate-900">{selectedOutlet?.location}</p>
</div>
<div className="col-span-2">
<span className="text-slate-600">Location:</span>
<p className="text-slate-900">{selectedOutlet?.location}</p>
</div>
</div>
</div>
{/* Last Operational Dates */} {/* Resignation Type */}
<div className="grid grid-cols-2 gap-3"> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor="resignationType">Resignation Type *</Label>
<Label htmlFor="lastOpDateSales">Last Operational Date - Sales *</Label> <Select value={resignationType} onValueChange={setResignationType} required>
<Input <SelectTrigger>
id="lastOpDateSales" <SelectValue placeholder="Select resignation type" />
type="date" </SelectTrigger>
value={lastOperationalDateSales} <SelectContent>
onChange={(e) => setLastOperationalDateSales(e.target.value)} <SelectItem value="Voluntary">Voluntary</SelectItem>
required <SelectItem value="Retirement">Retirement</SelectItem>
/> <SelectItem value="Health Issues">Health Issues</SelectItem>
</div> <SelectItem value="Business Closure">Business Closure</SelectItem>
<div className="space-y-2"> <SelectItem value="Other">Other</SelectItem>
<Label htmlFor="lastOpDateServices">Last Operational Date - Services *</Label> </SelectContent>
<Input </Select>
id="lastOpDateServices" </div>
type="date"
value={lastOperationalDateServices}
onChange={(e) => setLastOperationalDateServices(e.target.value)}
required
/>
</div>
</div>
{/* Reason */} {/* Last Operational Dates */}
<div className="space-y-2"> <div className="grid grid-cols-2 gap-3">
<Label htmlFor="reason">Reason for Resignation *</Label> <div className="space-y-2">
<Textarea <Label htmlFor="lastOpDateSales">Last Operational Date - Sales *</Label>
id="reason" <Input
placeholder="Please provide detailed reason for resignation..." id="lastOpDateSales"
value={reason} type="date"
onChange={(e) => setReason(e.target.value)} value={lastOperationalDateSales}
rows={4} onChange={(e) => setLastOperationalDateSales(e.target.value)}
required required
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="lastOpDateServices">Last Operational Date - Services *</Label>
<Input
id="lastOpDateServices"
type="date"
value={lastOperationalDateServices}
onChange={(e) => setLastOperationalDateServices(e.target.value)}
required
/>
</div>
</div>
{/* Additional Information */} {/* Reason */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="additionalInfo">Additional Information (Optional)</Label> <Label htmlFor="reason">Reason for Resignation *</Label>
<Textarea <Textarea
id="additionalInfo" id="reason"
placeholder="Any additional details..." placeholder="Please provide detailed reason for resignation..."
value={additionalInfo} value={reason}
onChange={(e) => setAdditionalInfo(e.target.value)} onChange={(e) => setReason(e.target.value)}
rows={3} rows={4}
/> required
</div> />
</div>
{/* Important Info */} {/* Additional Information */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4"> <div className="space-y-2">
<h4 className="text-amber-900 mb-2">Important Information</h4> <Label htmlFor="additionalInfo">Additional Information (Optional)</Label>
<ul className="text-amber-800 text-sm space-y-1"> <Textarea
<li> F&F settlement process will be initiated after submission</li> id="additionalInfo"
<li> All department clearances must be obtained</li> placeholder="Any additional details..."
<li> Final settlement will be processed after closure</li> value={additionalInfo}
<li> Please ensure all documents are ready for submission</li> onChange={(e) => setAdditionalInfo(e.target.value)}
</ul> rows={3}
</div> />
</div>
<DialogFooter> {/* Important Info */}
<Button <div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
type="button" <h4 className="text-amber-900 mb-2">Important Information</h4>
variant="outline" <ul className="text-amber-800 text-sm space-y-1">
onClick={() => { <li> F&F settlement process will be initiated after submission</li>
setIsDialogOpen(false); <li> All department clearances must be obtained</li>
setSelectedOutlet(null); <li> Final settlement will be processed after closure</li>
}} <li> Please ensure all documents are ready for submission</li>
> </ul>
Cancel </div>
</Button>
<Button
type="submit"
className="bg-red-600 hover:bg-red-700"
>
Submit Resignation Request
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Requests Table - Removed */} <DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setIsDialogOpen(false);
setSelectedOutlet(null);
}}
>
Cancel
</Button>
<Button
type="submit"
className="bg-red-600 hover:bg-red-700"
disabled={submitting}
>
{submitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
'Submit Resignation Request'
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Requests Table */}
<Card>
<CardHeader>
<CardTitle>My Resignation Requests</CardTitle>
<CardDescription>
Track the progress of your resignation requests
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Request ID</TableHead>
<TableHead>Outlet</TableHead>
<TableHead>Type</TableHead>
<TableHead>Submitted On</TableHead>
<TableHead>Status</TableHead>
<TableHead>Progress</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{resignations.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-4 text-slate-500">
No resignation requests found
</TableCell>
</TableRow>
) : (
resignations.map((request) => (
<TableRow key={request.id}>
<TableCell className="font-medium text-slate-900">{request.resignationId}</TableCell>
<TableCell>{request.outlet?.name}</TableCell>
<TableCell>{request.resignationType}</TableCell>
<TableCell>{new Date(request.submittedOn).toLocaleDateString()}</TableCell>
<TableCell>
<Badge className={`border ${getStatusColor(request.status)}`}>
{request.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
<div
className="bg-red-600 h-2 rounded-full"
style={{ width: `${request.progressPercentage}%` }}
/>
</div>
<span className="text-xs text-slate-600">{request.progressPercentage}%</span>
</div>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetails && onViewDetails(request.resignationId)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
</>
)}
</div> </div>
); );
} }

View File

@ -112,6 +112,7 @@ export interface Application {
architectureAssignedTo?: string; architectureAssignedTo?: string;
architectureStatus?: string; architectureStatus?: string;
dealerCode?: any; dealerCode?: any;
dealer?: any;
} }
export interface Participant { export interface Participant {

View File

@ -0,0 +1,49 @@
import { API } from '../api/API';
export const dealerService = {
getDashboardData: async () => {
try {
const response: any = await API.getDealerDashboard();
return response.data?.data || response.data;
} catch (error) {
console.error('Get dealer dashboard error:', error);
throw error;
}
},
getConstitutionalChanges: async () => {
try {
const response: any = await API.getConstitutionalChanges();
return response.data;
} catch (error) {
console.error('Get constitutional changes error:', error);
throw error;
}
},
submitConstitutionalChange: async (data: any) => {
try {
const response: any = await API.createConstitutionalChange(data);
return response.data;
} catch (error) {
console.error('Submit constitutional change error:', error);
throw error;
}
},
getRelocationRequests: async () => {
try {
const response: any = await API.getRelocationRequests();
return response.data;
} catch (error) {
console.error('Get relocation requests error:', error);
throw error;
}
},
submitRelocationRequest: async (data: any) => {
try {
const response: any = await API.createRelocationRequest(data);
return response.data;
} catch (error) {
console.error('Submit relocation request error:', error);
throw error;
}
}
};

View File

@ -1,6 +1,24 @@
import { API } from '../api/API'; import { API } from '../api/API';
export const resignationService = { export const resignationService = {
getResignations: async () => {
try {
const response: any = await API.getResignations();
return response.data;
} catch (error) {
console.error('Get resignations error:', error);
throw error;
}
},
createResignation: async (data: any) => {
try {
const response: any = await API.createResignation(data);
return response.data;
} catch (error) {
console.error('Create resignation error:', error);
throw error;
}
},
getResignationById: async (id: string) => { getResignationById: async (id: string) => {
try { try {
const response: any = await API.getResignationById(id); const response: any = await API.getResignationById(id);