flow created upto outlet creation
This commit is contained in:
parent
9f13f8056e
commit
2d0a5fce22
@ -80,6 +80,7 @@ export const API = {
|
||||
createDealer: (data: any) => client.post('/dealer', data),
|
||||
getDealerById: (id: string) => client.get(`/dealer/${id}`),
|
||||
updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data),
|
||||
getDealerDashboard: () => client.get('/dealer/dashboard'),
|
||||
|
||||
// Email Templates
|
||||
getEmailTemplates: () => client.get('/admin/email-templates'),
|
||||
@ -91,13 +92,14 @@ export const API = {
|
||||
|
||||
// Audit Trail
|
||||
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) =>
|
||||
client.get('/audit/summary', { entityType, entityId }),
|
||||
client.get('/audit/summary', { params: { entityType, entityId } }),
|
||||
|
||||
// Prospective Login
|
||||
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
|
||||
verifyOtp: (phone: string, otp: string) => client.post('/prospective-login/verify-otp', { phone, otp }),
|
||||
|
||||
// Resignation
|
||||
getResignationById: (id: string) => client.get(`/resignation/${id}`),
|
||||
updateClearance: (id: string, data: any) => client.post(`/resignation/${id}/clearance`, data),
|
||||
|
||||
@ -2409,14 +2409,14 @@ export function ApplicationDetails() {
|
||||
)}
|
||||
|
||||
{/* 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
|
||||
className="w-full bg-amber-600 hover:bg-amber-700 font-bold"
|
||||
onClick={async () => {
|
||||
if (window.confirm('Do you want to finalize and onboard this dealer?')) {
|
||||
try {
|
||||
await onboardingService.createDealer({ applicationId });
|
||||
toast.success('Dealer record created successfully!');
|
||||
toast.success('Dealer profile and login account created successfully!');
|
||||
fetchApplication();
|
||||
} catch (error) {
|
||||
toast.error('Failed to create dealer profile');
|
||||
@ -2429,6 +2429,32 @@ export function ApplicationDetails() {
|
||||
</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) && (
|
||||
<Dialog open={showAssignModal} onOpenChange={setShowAssignModal}>
|
||||
<DialogTrigger asChild>
|
||||
|
||||
@ -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 { Button } from '../ui/button';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { User } from '../../lib/mock-data';
|
||||
import { dealerService } from '../../services/dealer.service';
|
||||
|
||||
interface DealerDashboardProps {
|
||||
currentUser: User | null;
|
||||
@ -10,27 +12,67 @@ interface 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 = [
|
||||
{
|
||||
title: 'Constitutional Changes',
|
||||
value: 1,
|
||||
value: statsSummary.constitutional,
|
||||
icon: RefreshCcw,
|
||||
color: 'bg-blue-500',
|
||||
change: 'In Review',
|
||||
change: 'Active Requests',
|
||||
onClick: () => onNavigate('dealer-constitutional')
|
||||
},
|
||||
{
|
||||
title: 'Relocation Requests',
|
||||
value: 1,
|
||||
value: statsSummary.relocation,
|
||||
icon: MapPin,
|
||||
color: 'bg-amber-500',
|
||||
change: 'Pending Approval',
|
||||
change: 'Active Requests',
|
||||
onClick: () => onNavigate('dealer-relocation')
|
||||
},
|
||||
{
|
||||
title: 'My Outlets',
|
||||
value: dashboardData.outlets?.length || 0,
|
||||
icon: ShoppingBag,
|
||||
color: 'bg-purple-500',
|
||||
change: 'Registered',
|
||||
onClick: () => {}
|
||||
},
|
||||
{
|
||||
title: 'Total Requests',
|
||||
value: 2,
|
||||
value: statsSummary.total,
|
||||
icon: TrendingUp,
|
||||
color: 'bg-green-500',
|
||||
change: 'All time',
|
||||
@ -39,24 +81,6 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
|
||||
];
|
||||
|
||||
// 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
|
||||
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="flex items-center justify-between">
|
||||
<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">
|
||||
Dealer Code: DL-MH-001 • Royal Enfield Mumbai
|
||||
Dealer Code: {profile.dealerCode} • {profile.businessName}
|
||||
</p>
|
||||
<p className="text-amber-100 text-sm mt-1">
|
||||
Bandra West, Mumbai, Maharashtra
|
||||
{primaryOutlet.name} • {primaryOutlet.location}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
@ -167,7 +191,7 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{recentRequests.map((request) => (
|
||||
{recentRequests.map((request: any) => (
|
||||
<div
|
||||
key={request.id}
|
||||
className="flex items-center justify-between p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"
|
||||
|
||||
@ -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 { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
@ -8,31 +8,16 @@ import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { User as UserType } from '../../lib/mock-data';
|
||||
import { toast } from 'sonner';
|
||||
import { dealerService } from '../../services/dealer.service';
|
||||
|
||||
interface DealerConstitutionalChangePageProps {
|
||||
currentUser: UserType | null;
|
||||
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) => {
|
||||
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';
|
||||
@ -44,13 +29,39 @@ const constitutionTypes = ['Proprietorship', 'Partnership', 'LLP', 'Pvt Ltd'];
|
||||
|
||||
export function DealerConstitutionalChangePage({ currentUser, onViewDetails }: DealerConstitutionalChangePageProps) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [currentConstitution, setCurrentConstitution] = useState('Proprietorship'); // Pre-filled
|
||||
const [currentConstitution, setCurrentConstitution] = useState('');
|
||||
const [proposedConstitution, setProposedConstitution] = useState('');
|
||||
const [reason, setReason] = useState('');
|
||||
const [newPartners, setNewPartners] = useState('');
|
||||
const [shareholdingPattern, setShareholdingPattern] = useState('');
|
||||
|
||||
const handleSubmitRequest = (e: React.FormEvent) => {
|
||||
const [requests, setRequests] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [profile, setProfile] = useState<any>(null);
|
||||
|
||||
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();
|
||||
|
||||
if (!proposedConstitution) {
|
||||
@ -68,32 +79,50 @@ export function DealerConstitutionalChangePage({ currentUser, onViewDetails }: D
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('Constitutional change request submitted successfully');
|
||||
setIsDialogOpen(false);
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const payload = {
|
||||
currentConstitution,
|
||||
changeType: proposedConstitution,
|
||||
reason,
|
||||
newPartnersDetails: newPartners,
|
||||
shareholdingPattern
|
||||
};
|
||||
|
||||
// Reset form
|
||||
setProposedConstitution('');
|
||||
setReason('');
|
||||
setNewPartners('');
|
||||
setShareholdingPattern('');
|
||||
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 = [
|
||||
{
|
||||
title: 'Total Requests',
|
||||
value: mockDealerConstitutionalChanges.length,
|
||||
value: requests.length,
|
||||
icon: RefreshCcw,
|
||||
color: 'bg-blue-500',
|
||||
},
|
||||
{
|
||||
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,
|
||||
color: 'bg-yellow-500',
|
||||
},
|
||||
{
|
||||
title: 'Completed',
|
||||
value: mockDealerConstitutionalChanges.filter(r => r.status === 'Completed').length,
|
||||
value: requests.filter(r => r.status === 'Completed').length,
|
||||
icon: FileText,
|
||||
color: 'bg-green-500',
|
||||
},
|
||||
@ -101,244 +130,271 @@ export function DealerConstitutionalChangePage({ currentUser, onViewDetails }: D
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-slate-900 mb-2">My Constitutional Change Requests</h1>
|
||||
<p className="text-slate-600">
|
||||
Submit and track requests for changing your business constitution
|
||||
</p>
|
||||
{/* Loading Overlay */}
|
||||
{loading && (
|
||||
<div className="min-h-[400px] flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Constitutional Change
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submit Constitutional Change Request</DialogTitle>
|
||||
<DialogDescription>
|
||||
Request to change your dealership's business constitution structure
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{!loading && (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-slate-900 mb-2">My Constitutional Change Requests</h1>
|
||||
<p className="text-slate-600">
|
||||
Submit and track requests for changing your business constitution
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmitRequest} className="space-y-4">
|
||||
{/* Dealer Info */}
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2">
|
||||
<h3 className="text-slate-900">Current Dealership Information</h3>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<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
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Constitutional Change
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Submit Request
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submit Constitutional Change Request</DialogTitle>
|
||||
<DialogDescription>
|
||||
Request to change your dealership's business constitution structure
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 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>
|
||||
{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}%` }}
|
||||
/>
|
||||
<form onSubmit={handleSubmitRequest} className="space-y-4">
|
||||
{/* Dealer Info */}
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2">
|
||||
<h3 className="text-slate-900">Current Dealership Information</h3>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<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">Dealer Name:</span>
|
||||
<p className="text-slate-900">{profile?.businessName || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-slate-600">Current Constitution:</span>
|
||||
<p className="text-slate-900">{currentConstitution}</p>
|
||||
</div>
|
||||
<span className="text-xs text-slate-600">{request.progressPercentage}%</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
</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
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDetails && onViewDetails(request.id)}
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
View
|
||||
Cancel
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
@ -8,32 +8,16 @@ import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { User as UserType } from '../../lib/mock-data';
|
||||
import { toast } from 'sonner';
|
||||
import { dealerService } from '../../services/dealer.service';
|
||||
|
||||
interface DealerRelocationPageProps {
|
||||
currentUser: UserType | null;
|
||||
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) => {
|
||||
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';
|
||||
@ -43,58 +27,54 @@ const getStatusColor = (status: string) => {
|
||||
|
||||
export function DealerRelocationPage({ currentUser, onViewDetails }: DealerRelocationPageProps) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [proposedAddress, setProposedAddress] = useState('');
|
||||
const [proposedCity, setProposedCity] = useState('');
|
||||
const [proposedState, setProposedState] = useState('');
|
||||
const [proposedPincode, setProposedPincode] = useState('');
|
||||
const [distance, setDistance] = useState('');
|
||||
const [propertyType, setPropertyType] = useState('');
|
||||
const [expectedDate, setExpectedDate] = useState('');
|
||||
const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
|
||||
const [newCity, setNewCity] = useState('');
|
||||
const [newState, setNewState] = useState('');
|
||||
const [newAddress, setNewAddress] = useState('');
|
||||
const [reason, setReason] = useState('');
|
||||
const [locationMode, setLocationMode] = useState<'manual' | 'map'>('manual');
|
||||
const [mapCoordinates, setMapCoordinates] = useState({ lat: 19.0760, lng: 72.8777 });
|
||||
const [selectedLocation, setSelectedLocation] = useState<{ lat: number; lng: number } | null>(null);
|
||||
|
||||
const handleMapClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const [outlets, setOutlets] = useState<any[]>([]);
|
||||
const [requests, setRequests] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [profile, setProfile] = useState<any>(null);
|
||||
|
||||
const lat = mapCoordinates.lat + (y - rect.height / 2) / 1000;
|
||||
const lng = mapCoordinates.lng + (x - rect.width / 2) / 1000;
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
setSelectedLocation({ lat, lng });
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const dashboard = await dealerService.getDashboardData();
|
||||
const relocationRes = await dealerService.getRelocationRequests();
|
||||
|
||||
const mockLocations = [
|
||||
{ city: 'Mumbai', state: 'Maharashtra', pincode: '400001', address: 'Nariman Point, South Mumbai' },
|
||||
{ city: 'Mumbai', state: 'Maharashtra', pincode: '400051', address: 'Andheri East, Mumbai' },
|
||||
{ city: 'Mumbai', state: 'Maharashtra', pincode: '400070', address: 'Powai, Mumbai' },
|
||||
];
|
||||
|
||||
const randomLocation = mockLocations[Math.floor(Math.random() * mockLocations.length)];
|
||||
setProposedAddress(randomLocation.address);
|
||||
setProposedCity(randomLocation.city);
|
||||
setProposedState(randomLocation.state);
|
||||
setProposedPincode(randomLocation.pincode);
|
||||
|
||||
toast.success('Location selected from map');
|
||||
setOutlets(dashboard.outlets || []);
|
||||
setProfile(dashboard.profile);
|
||||
setRequests(relocationRes.requests || []);
|
||||
} catch (error) {
|
||||
console.error('Fetch relocation data error:', error);
|
||||
toast.error('Failed to load outlets and requests');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitRequest = (e: React.FormEvent) => {
|
||||
const handleOpenRelocationDialog = (outlet: any) => {
|
||||
setSelectedOutlet(outlet);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmitRequest = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!proposedAddress.trim() || !proposedCity.trim() || !proposedState.trim() || !proposedPincode.trim()) {
|
||||
toast.error('Please enter complete proposed location details');
|
||||
if (!selectedOutlet) {
|
||||
toast.error('Please select an outlet');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distance.trim()) {
|
||||
toast.error('Please enter distance from current location');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!propertyType) {
|
||||
toast.error('Please select property type');
|
||||
if (!newCity.trim() || !newState.trim() || !newAddress.trim()) {
|
||||
toast.error('Please provide complete relocation details');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -103,422 +83,311 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('Relocation request submitted successfully');
|
||||
setIsDialogOpen(false);
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const payload = {
|
||||
outletId: selectedOutlet.id,
|
||||
currentLocation: selectedOutlet.location,
|
||||
newCity,
|
||||
newState,
|
||||
newAddress,
|
||||
reason
|
||||
};
|
||||
|
||||
// Reset form
|
||||
setProposedAddress('');
|
||||
setProposedCity('');
|
||||
setProposedState('');
|
||||
setProposedPincode('');
|
||||
setDistance('');
|
||||
setPropertyType('');
|
||||
setExpectedDate('');
|
||||
setReason('');
|
||||
setLocationMode('manual');
|
||||
setSelectedLocation(null);
|
||||
await dealerService.submitRelocationRequest(payload);
|
||||
toast.success(`Relocation request submitted successfully for ${selectedOutlet.name}`);
|
||||
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 = [
|
||||
{
|
||||
title: 'Total Requests',
|
||||
value: mockDealerRelocations.length,
|
||||
value: requests.length,
|
||||
icon: MapPin,
|
||||
color: 'bg-amber-500',
|
||||
color: 'bg-blue-500',
|
||||
},
|
||||
{
|
||||
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,
|
||||
color: 'bg-yellow-500',
|
||||
},
|
||||
{
|
||||
title: 'Completed',
|
||||
value: mockDealerRelocations.filter(r => r.status === 'Completed').length,
|
||||
icon: FileText,
|
||||
title: 'Approved',
|
||||
value: requests.filter(r => r.status === 'Completed').length,
|
||||
icon: Building2,
|
||||
color: 'bg-green-500',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-slate-900 mb-2">My Relocation Requests</h1>
|
||||
<p className="text-slate-600">
|
||||
Submit and track requests for relocating your dealership
|
||||
</p>
|
||||
{/* Loading Overlay */}
|
||||
{loading && (
|
||||
<div className="min-h-[400px] flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-amber-600 hover:bg-amber-700">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Relocation Request
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submit Relocation Request</DialogTitle>
|
||||
<DialogDescription>
|
||||
Request to relocate your dealership to a new location
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{!loading && (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-slate-900 mb-2">Relocation Requests</h1>
|
||||
<p className="text-slate-600">
|
||||
Request to relocate your existing dealership or studio to a new location
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmitRequest} className="space-y-4">
|
||||
{/* Current Dealer Info */}
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-3">
|
||||
<h3 className="text-slate-900">Current Dealership Details</h3>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<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 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 */}
|
||||
<div className="space-y-2">
|
||||
<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">
|
||||
<Label htmlFor="proposedCity">City *</Label>
|
||||
<Input
|
||||
id="proposedCity"
|
||||
placeholder="City"
|
||||
value={proposedCity}
|
||||
onChange={(e) => setProposedCity(e.target.value)}
|
||||
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
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-amber-600 hover:bg-amber-700 text-white">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Relocation Request
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
>
|
||||
Submit Request
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submit Relocation Request</DialogTitle>
|
||||
<DialogDescription>
|
||||
Provide details about the outlet you want to relocate and its proposed new location
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 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>
|
||||
<form onSubmit={handleSubmitRequest} className="space-y-4">
|
||||
{/* Select Outlet */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="outlet">Select Outlet to Relocate *</Label>
|
||||
<Select
|
||||
value={selectedOutlet?.id}
|
||||
onValueChange={(val) => setSelectedOutlet(outlets.find(o => o.id === val))}
|
||||
required
|
||||
>
|
||||
<SelectTrigger>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{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>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
)}
|
||||
|
||||
{/* 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
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDetails && onViewDetails(request.id)}
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
View
|
||||
Cancel
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-amber-600 hover:bg-amber-700 text-white"
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
@ -8,107 +8,17 @@ import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { User as UserType } from '../../lib/mock-data';
|
||||
import { toast } from 'sonner';
|
||||
import { dealerService } from '../../services/dealer.service';
|
||||
import { resignationService } from '../../services/resignation.service';
|
||||
|
||||
interface DealerResignationPageProps {
|
||||
currentUser: UserType | null;
|
||||
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) => {
|
||||
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';
|
||||
@ -118,19 +28,46 @@ const getStatusColor = (status: string) => {
|
||||
|
||||
export function DealerResignationPage({ currentUser, onViewDetails }: DealerResignationPageProps) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [selectedOutlet, setSelectedOutlet] = useState<Outlet | null>(null);
|
||||
const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
|
||||
const [resignationType, setResignationType] = useState('');
|
||||
const [lastOperationalDateSales, setLastOperationalDateSales] = useState('');
|
||||
const [lastOperationalDateServices, setLastOperationalDateServices] = useState('');
|
||||
const [reason, setReason] = useState('');
|
||||
const [additionalInfo, setAdditionalInfo] = useState('');
|
||||
|
||||
const handleOpenResignationDialog = (outlet: Outlet) => {
|
||||
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);
|
||||
|
||||
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);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmitRequest = (e: React.FormEvent) => {
|
||||
const handleSubmitRequest = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!resignationType) {
|
||||
@ -148,34 +85,53 @@ export function DealerResignationPage({ currentUser, onViewDetails }: DealerResi
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(`Resignation request submitted successfully for ${selectedOutlet?.name}`);
|
||||
setIsDialogOpen(false);
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const payload = {
|
||||
outletId: selectedOutlet?.id,
|
||||
resignationType,
|
||||
lastOperationalDateSales,
|
||||
lastOperationalDateServices,
|
||||
reason,
|
||||
additionalInfo
|
||||
};
|
||||
|
||||
// Reset form
|
||||
setSelectedOutlet(null);
|
||||
setResignationType('');
|
||||
setLastOperationalDateSales('');
|
||||
setLastOperationalDateServices('');
|
||||
setReason('');
|
||||
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 = [
|
||||
{
|
||||
title: 'Total Outlets',
|
||||
value: mockOutlets.length,
|
||||
value: outlets.length,
|
||||
icon: Building2,
|
||||
color: 'bg-blue-500',
|
||||
},
|
||||
{
|
||||
title: 'Active Outlets',
|
||||
value: mockOutlets.filter(o => o.status === 'Active').length,
|
||||
value: outlets.filter(o => o.status === 'Active').length,
|
||||
icon: CheckCircle,
|
||||
color: 'bg-green-500',
|
||||
},
|
||||
{
|
||||
title: 'Pending Resignations',
|
||||
value: mockOutlets.filter(o => o.hasActiveResignation).length,
|
||||
value: outlets.filter(o => o.status === 'Pending Resignation').length,
|
||||
icon: Clock,
|
||||
color: 'bg-amber-500',
|
||||
},
|
||||
@ -183,254 +139,338 @@ export function DealerResignationPage({ currentUser, onViewDetails }: DealerResi
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-slate-900 mb-2">Dealership Resignation Management</h1>
|
||||
<p className="text-slate-600">
|
||||
Manage resignation requests for your dealerships and studios
|
||||
</p>
|
||||
</div>
|
||||
{/* Loading Overlay */}
|
||||
{loading && (
|
||||
<div className="min-h-[400px] flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
|
||||
</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>
|
||||
{!loading && (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-slate-900 mb-2">Dealership Resignation Management</h1>
|
||||
<p className="text-slate-600">
|
||||
Manage resignation requests for your dealerships and studios
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* My Outlets Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>My Outlets</CardTitle>
|
||||
<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;
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{stats.map((stat, index) => {
|
||||
const Icon = stat.icon;
|
||||
return (
|
||||
<div
|
||||
key={outlet.id}
|
||||
className="border border-slate-200 rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<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>
|
||||
<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>
|
||||
<Badge
|
||||
className={`border ${
|
||||
outlet.status === 'Active'
|
||||
? 'bg-green-100 text-green-700 border-green-300'
|
||||
: 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.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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-slate-900 text-2xl">{stat.value}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Resignation Request Dialog */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submit Resignation Request</DialogTitle>
|
||||
<DialogDescription>
|
||||
Fill in the details for your resignation request. All fields are mandatory.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* My Outlets Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>My Outlets</CardTitle>
|
||||
<CardDescription>
|
||||
Select an outlet to request resignation
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{outlets.map((outlet) => {
|
||||
const OutletIcon = outlet.type === 'Dealership' ? Building2 : Store;
|
||||
const hasActiveResignation = outlet.status === 'Pending Resignation';
|
||||
const resignation = resignations.find(r => r.outletId === outlet.id && r.status !== 'Completed' && r.status !== 'Rejected');
|
||||
|
||||
<form onSubmit={handleSubmitRequest} className="space-y-4">
|
||||
{/* Outlet Info */}
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2">
|
||||
<h3 className="text-slate-900">Outlet Information</h3>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<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">Type:</span>
|
||||
<p className="text-slate-900">{selectedOutlet?.type}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-600">City:</span>
|
||||
<p className="text-slate-900">{selectedOutlet?.city}, {selectedOutlet?.state}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-slate-600">Address:</span>
|
||||
<p className="text-slate-900">{selectedOutlet?.address}</p>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key={outlet.id}
|
||||
className="border border-slate-200 rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<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>
|
||||
<Badge
|
||||
className={`border ${
|
||||
outlet.status === 'Active'
|
||||
? 'bg-green-100 text-green-700 border-green-300'
|
||||
: 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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Resignation Type */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="resignationType">Resignation Type *</Label>
|
||||
<Select value={resignationType} onValueChange={setResignationType} required>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select resignation type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Voluntary">Voluntary</SelectItem>
|
||||
<SelectItem value="Retirement">Retirement</SelectItem>
|
||||
<SelectItem value="Health Issues">Health Issues</SelectItem>
|
||||
<SelectItem value="Business Closure">Business Closure</SelectItem>
|
||||
<SelectItem value="Other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Resignation Request Dialog */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submit Resignation Request</DialogTitle>
|
||||
<DialogDescription>
|
||||
Fill in the details for your resignation request. All fields are mandatory.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Last Operational Dates */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastOpDateSales">Last Operational Date - Sales *</Label>
|
||||
<Input
|
||||
id="lastOpDateSales"
|
||||
type="date"
|
||||
value={lastOperationalDateSales}
|
||||
onChange={(e) => setLastOperationalDateSales(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
<form onSubmit={handleSubmitRequest} className="space-y-4">
|
||||
{/* Outlet Info */}
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2">
|
||||
<h3 className="text-slate-900">Outlet Information</h3>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<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>
|
||||
|
||||
{/* Reason */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reason">Reason for Resignation *</Label>
|
||||
<Textarea
|
||||
id="reason"
|
||||
placeholder="Please provide detailed reason for resignation..."
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/* Resignation Type */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="resignationType">Resignation Type *</Label>
|
||||
<Select value={resignationType} onValueChange={setResignationType} required>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select resignation type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Voluntary">Voluntary</SelectItem>
|
||||
<SelectItem value="Retirement">Retirement</SelectItem>
|
||||
<SelectItem value="Health Issues">Health Issues</SelectItem>
|
||||
<SelectItem value="Business Closure">Business Closure</SelectItem>
|
||||
<SelectItem value="Other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Additional Information */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="additionalInfo">Additional Information (Optional)</Label>
|
||||
<Textarea
|
||||
id="additionalInfo"
|
||||
placeholder="Any additional details..."
|
||||
value={additionalInfo}
|
||||
onChange={(e) => setAdditionalInfo(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
{/* Last Operational Dates */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastOpDateSales">Last Operational Date - Sales *</Label>
|
||||
<Input
|
||||
id="lastOpDateSales"
|
||||
type="date"
|
||||
value={lastOperationalDateSales}
|
||||
onChange={(e) => setLastOperationalDateSales(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* Important Info */}
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
||||
<h4 className="text-amber-900 mb-2">Important Information</h4>
|
||||
<ul className="text-amber-800 text-sm space-y-1">
|
||||
<li>• F&F settlement process will be initiated after submission</li>
|
||||
<li>• All department clearances must be obtained</li>
|
||||
<li>• Final settlement will be processed after closure</li>
|
||||
<li>• Please ensure all documents are ready for submission</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/* Reason */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reason">Reason for Resignation *</Label>
|
||||
<Textarea
|
||||
id="reason"
|
||||
placeholder="Please provide detailed reason for resignation..."
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsDialogOpen(false);
|
||||
setSelectedOutlet(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Submit Resignation Request
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* Additional Information */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="additionalInfo">Additional Information (Optional)</Label>
|
||||
<Textarea
|
||||
id="additionalInfo"
|
||||
placeholder="Any additional details..."
|
||||
value={additionalInfo}
|
||||
onChange={(e) => setAdditionalInfo(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Requests Table - Removed */}
|
||||
{/* Important Info */}
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
||||
<h4 className="text-amber-900 mb-2">Important Information</h4>
|
||||
<ul className="text-amber-800 text-sm space-y-1">
|
||||
<li>• F&F settlement process will be initiated after submission</li>
|
||||
<li>• All department clearances must be obtained</li>
|
||||
<li>• Final settlement will be processed after closure</li>
|
||||
<li>• Please ensure all documents are ready for submission</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -112,6 +112,7 @@ export interface Application {
|
||||
architectureAssignedTo?: string;
|
||||
architectureStatus?: string;
|
||||
dealerCode?: any;
|
||||
dealer?: any;
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
|
||||
49
src/services/dealer.service.ts
Normal file
49
src/services/dealer.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,6 +1,24 @@
|
||||
import { API } from '../api/API';
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const response: any = await API.getResignationById(id);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user