master page modified and trying to cover all models necessary flow

This commit is contained in:
laxmanhalaki 2026-03-18 19:50:32 +05:30
parent ad33de7e26
commit 2cf919a0dc
17 changed files with 2765 additions and 3557 deletions

View File

@ -17,6 +17,8 @@ export const API = {
createRegion: (data: any) => client.post('/master/regions', data),
updateRegion: (id: string, data: any) => client.put(`/master/regions/${id}`, data),
getRegions: () => client.get('/master/regions'),
getOutlets: () => client.get('/master/outlets'),
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
getStates: (zoneId?: string) => client.get('/master/states', { zoneId }),
getDistricts: (stateId?: string) => client.get('/master/districts', { stateId }),
getAreas: (districtId?: string) => client.get('/master/areas', { districtId }),
@ -36,6 +38,8 @@ export const API = {
getQuestionnaireById: (id: string) => client.get(`/onboarding/questionnaires/${id}`),
assignArchitectureTeam: (applicationId: string, assignedTo: string) => client.post(`/onboarding/applications/${applicationId}/assign-architecture`, { assignedTo }),
updateArchitectureStatus: (applicationId: string, status: string, remarks?: string) => client.post(`/onboarding/applications/${applicationId}/architecture-status`, { status, remarks }),
generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`),
updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data),
// Documents
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {
@ -71,6 +75,12 @@ export const API = {
updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data),
deleteUser: (id: string) => client.delete(`/admin/users/${id}`),
// Dealer & Outlets
getDealers: () => client.get('/dealer'),
createDealer: (data: any) => client.post('/dealer', data),
getDealerById: (id: string) => client.get(`/dealer/${id}`),
updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data),
// Email Templates
getEmailTemplates: () => client.get('/admin/email-templates'),
getEmailTemplate: (id: string) => client.get(`/admin/email-templates/${id}`),
@ -91,6 +101,7 @@ export const API = {
// Resignation
getResignationById: (id: string) => client.get(`/resignation/${id}`),
updateClearance: (id: string, data: any) => client.post(`/resignation/${id}/clearance`, data),
updateResignationStatus: (id: string, data: any) => client.post(`/resignation/${id}/status`, data),
// Termination
getTerminationById: (id: string) => client.get(`/termination/${id}`),
@ -100,6 +111,39 @@ export const API = {
headers: { 'Content-Type': 'multipart/form-data' }
}),
finalizeTermination: (id: string, data: any) => client.post(`/termination/${id}/finalize`, data),
// Lifecycle Modules (Self-Service)
getResignations: (params?: any) => client.get('/resignation', { params }),
createResignation: (data: any) => client.post('/resignation', data),
approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data),
rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, data),
getTerminations: () => client.get('/termination'),
createTermination: (data: any) => client.post('/termination', data),
updateTermination: (id: string, data: any) => client.post(`/termination/${id}/status`, data),
getFnFSettlements: () => client.get('/settlement/fnf'),
getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`),
calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`),
updateFnF: (id: string, data: any) => client.put(`/settlement/fnf/${id}`, data),
// Line items
addLineItem: (fnfId: string, data: any) => client.post(`/settlement/fnf/${fnfId}/line-items`, data),
updateLineItem: (itemId: string, data: any) => client.put(`/settlement/fnf/line-items/${itemId}`, data),
deleteLineItem: (itemId: string) => client.delete(`/settlement/fnf/line-items/${itemId}`),
getRelocationRequests: () => client.get('/relocation'),
getRelocationRequestById: (id: string) => client.get(`/relocation/${id}`),
createRelocationRequest: (data: any) => client.post('/relocation', data),
updateRelocationRequest: (id: string, action: string, data?: any) => client.post(`/relocation/${id}/action`, { action, ...data }),
getConstitutionalChanges: () => client.get('/constitutional-change'),
getConstitutionalChangeById: (id: string) => client.get(`/constitutional-change/${id}`),
createConstitutionalChange: (data: any) => client.post('/constitutional-change', data),
updateConstitutionalChange: (id: string, action: string, data?: any) => client.post(`/constitutional-change/${id}/action`, { action, ...data }),
// SLA
getSlaConfigs: () => client.get('/sla/configs'),
};
export default API;

View File

@ -29,6 +29,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
export function UserManagementPage() {
const [users, setUsers] = useState<any[]>([]);
const [roles, setRoles] = useState<any[]>([]);
const [zones, setZones] = useState<any[]>([]);
const [regions, setRegions] = useState<any[]>([]);
const [states, setStates] = useState<any[]>([]);
const [districts, setDistricts] = useState<any[]>([]);
const [areas, setAreas] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [roleFilter, setRoleFilter] = useState('all');
@ -46,7 +52,12 @@ export function UserManagementPage() {
mobileNumber: '',
department: '',
designation: '',
employeeId: ''
employeeId: '',
zoneId: '',
regionId: '',
stateId: '',
districtId: '',
areaId: ''
});
useEffect(() => {
@ -56,11 +67,17 @@ export function UserManagementPage() {
const fetchData = async () => {
setLoading(true);
try {
const usersRes = await adminService.getAllUsers() as any;
const rolesRes = await masterService.getRoles() as any;
const [usersRes, rolesRes, zonesRes, regionsRes] = await Promise.all([
adminService.getAllUsers(),
masterService.getRoles(),
masterService.getZones(),
masterService.getRegions()
]) as any[];
if (usersRes.success) setUsers(usersRes.data);
if (rolesRes.success) setRoles(rolesRes.data);
if (zonesRes.success) setZones(zonesRes.data);
if (regionsRes.success) setRegions(regionsRes.data);
} catch (error) {
toast.error('Failed to load user management data');
} finally {
@ -68,6 +85,39 @@ export function UserManagementPage() {
}
};
// Load states when zone changes (or on edit)
useEffect(() => {
if (formData.zoneId) {
masterService.getStates(formData.zoneId).then((res: any) => {
if (res.success) setStates(res.states);
});
} else {
setStates([]);
}
}, [formData.zoneId]);
// Load districts when state changes
useEffect(() => {
if (formData.stateId) {
masterService.getDistricts(formData.stateId).then((res: any) => {
if (res.success) setDistricts(res.districts);
});
} else {
setDistricts([]);
}
}, [formData.stateId]);
// Load areas when district changes
useEffect(() => {
if (formData.districtId) {
masterService.getAreas(formData.districtId).then((res: any) => {
if (res.success) setAreas(res.areas);
});
} else {
setAreas([]);
}
}, [formData.districtId]);
const handleEditUser = (user: any) => {
setEditingUser(user);
setFormData({
@ -79,7 +129,12 @@ export function UserManagementPage() {
mobileNumber: user.mobileNumber || '',
department: user.department || '',
designation: user.designation || '',
employeeId: user.employeeId || ''
employeeId: user.employeeId || '',
zoneId: user.zoneId || '',
regionId: user.regionId || '',
stateId: user.stateId || '',
districtId: user.districtId || '',
areaId: user.areaId || ''
});
setShowUserModal(true);
};
@ -110,7 +165,12 @@ export function UserManagementPage() {
mobileNumber: '',
department: '',
designation: '',
employeeId: ''
employeeId: '',
zoneId: '',
regionId: '',
stateId: '',
districtId: '',
areaId: ''
});
setShowUserModal(false);
fetchData();
@ -157,9 +217,10 @@ export function UserManagementPage() {
<Button
onClick={() => {
setEditingUser(null); setFormData({
fullName: '', email: '', roleCode: '', status: 'active', isActive: true,
mobileNumber: '', department: '', designation: '', employeeId: ''
}); setShowUserModal(true);
fullName: '', email: '', roleCode: '', status: 'active', isActive: true,
mobileNumber: '', department: '', designation: '', employeeId: '',
zoneId: '', regionId: '', stateId: '', districtId: '', areaId: ''
}); setShowUserModal(true);
}}
className="bg-amber-600 hover:bg-amber-700 text-white shrink-0"
>
@ -218,6 +279,7 @@ export function UserManagementPage() {
<TableHeader className="bg-slate-50">
<TableRow>
<TableHead>User Information</TableHead>
<TableHead>Geographical Mapping</TableHead>
<TableHead>Account Details</TableHead>
<TableHead>Role & Department</TableHead>
<TableHead>Status</TableHead>
@ -258,6 +320,18 @@ export function UserManagementPage() {
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
Zone: {user.zone?.zoneName || 'N/A'}
</Badge>
</div>
<div className="text-xs text-slate-500">
Region: {user.region?.regionName || 'N/A'}
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="text-sm font-medium">ID: {user.employeeId || 'N/A'}</div>
@ -406,6 +480,97 @@ export function UserManagementPage() {
</SelectContent>
</Select>
</div>
{/* Geographical Assignments */}
<div className="col-span-2 border-t pt-4 mt-2">
<h3 className="text-sm font-semibold text-slate-900 mb-4">Geographical Assignments</h3>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="zoneId">Zone</Label>
<Select
value={formData.zoneId}
onValueChange={(val) => setFormData({ ...formData, zoneId: val, regionId: '', stateId: '', districtId: '', areaId: '' })}
>
<SelectTrigger id="zoneId">
<SelectValue placeholder="Select Zone" />
</SelectTrigger>
<SelectContent>
{zones.map(zone => (
<SelectItem key={zone.id} value={zone.id}>{zone.zoneName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="regionId">Region</Label>
<Select
value={formData.regionId}
onValueChange={(val) => setFormData({ ...formData, regionId: val })}
disabled={!formData.zoneId}
>
<SelectTrigger id="regionId">
<SelectValue placeholder="Select Region" />
</SelectTrigger>
<SelectContent>
{regions.filter(r => r.zoneId === formData.zoneId).map(region => (
<SelectItem key={region.id} value={region.id}>{region.regionName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="stateId">State</Label>
<Select
value={formData.stateId}
onValueChange={(val) => setFormData({ ...formData, stateId: val, districtId: '', areaId: '' })}
disabled={!formData.zoneId}
>
<SelectTrigger id="stateId">
<SelectValue placeholder="Select State" />
</SelectTrigger>
<SelectContent>
{states.filter(s => s.zoneId === formData.zoneId).map(state => (
<SelectItem key={state.id} value={state.id}>{state.stateName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="districtId">District</Label>
<Select
value={formData.districtId}
onValueChange={(val) => setFormData({ ...formData, districtId: val, areaId: '' })}
disabled={!formData.stateId}
>
<SelectTrigger id="districtId">
<SelectValue placeholder="Select District" />
</SelectTrigger>
<SelectContent>
{districts.map(district => (
<SelectItem key={district.id} value={district.id}>{district.districtName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="areaId">Area</Label>
<Select
value={formData.areaId}
onValueChange={(val) => setFormData({ ...formData, areaId: val })}
disabled={!formData.districtId}
>
<SelectTrigger id="areaId">
<SelectValue placeholder="Select Area" />
</SelectTrigger>
<SelectContent>
{areas.map(area => (
<SelectItem key={area.id} value={area.id}>{area.areaName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
<DialogFooter className="bg-slate-50 -mx-6 -mb-6 p-4 rounded-b-lg">

View File

@ -1,10 +1,9 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { mockDocuments, mockWorkNotes, Application, ApplicationStatus } from '../../lib/mock-data';
import { mockWorkNotes, Application, ApplicationStatus } from '../../lib/mock-data';
import { onboardingService } from '../../services/onboarding.service';
import { auditService } from '../../services/audit.service';
import { WorkNotesPage } from './WorkNotesPage';
import QuestionnaireResponseView from './QuestionnaireResponseView';
import { useSelector } from 'react-redux';
import { RootState } from '../../store';
@ -34,7 +33,8 @@ import {
ChevronDown,
ChevronRight,
GitBranch,
Star
Star,
Zap
} from 'lucide-react';
import { Progress } from '../ui/progress';
import { Textarea } from '../ui/textarea';
@ -348,7 +348,6 @@ export function ApplicationDetails() {
const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
const [showWorkNoteModal, setShowWorkNoteModal] = useState(false);
const [showWorkNotesPage, setShowWorkNotesPage] = useState(false);
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [showKTMatrixModal, setShowKTMatrixModal] = useState(false);
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
@ -372,7 +371,6 @@ export function ApplicationDetails() {
const [meetingLink, setMeetingLink] = useState('');
const [location, setLocation] = useState('');
const [documents, setDocuments] = useState<any[]>([]);
const [showUploadModal, setShowUploadModal] = useState(false);
const [showUploadForm, setShowUploadForm] = useState(false); // Toggle for upload view
const [uploadFile, setUploadFile] = useState<File | null>(null);
const [uploadDocType, setUploadDocType] = useState('');
@ -733,7 +731,7 @@ export function ApplicationDetails() {
await onboardingService.uploadDocument(applicationId!, formData);
alert('Document uploaded successfully');
setShowUploadModal(false);
setShowUploadForm(false);
setUploadFile(null);
setUploadDocType('');
@ -1085,11 +1083,39 @@ export function ApplicationDetails() {
alert('Please enter a remark');
return;
}
// Application level approval (mock for now)
alert(`Application ${application?.registrationNumber} approved!\nRemark: ${approvalRemark}`);
setShowApproveModal(false);
setApprovalRemark('');
setApprovalFile(null); // Reset file
try {
// Application level approval
const newStatus = application.status === 'Inauguration' ? 'Approved' :
application.status === 'EOR Complete' ? 'Inauguration' :
application.status === 'LOA Pending' ? 'EOR Complete' :
application.status === 'Statutory LOI Ack' ? 'LOA Pending' : 'Approved'; // Default fallback
await onboardingService.updateApplicationStatus(applicationId!, {
status: newStatus,
remarks: approvalRemark
});
// Special case: If final approval, create Dealer record
if (newStatus === 'Approved') {
// In a real scenario, we'd have the dealerCodeId from the application's associated DealerCode record
await onboardingService.createDealer({
applicationId: applicationId,
// dealerCodeId is handled by backend if not provided, or we can fetch it
});
toast.success('Application approved and Dealer profile created!');
} else {
toast.success(`Application moved to ${newStatus}`);
}
setShowApproveModal(false);
setApprovalRemark('');
setApprovalFile(null);
fetchApplication();
} catch (error) {
console.error('Approval error:', error);
toast.error('Failed to process approval');
}
};
const handleReject = async () => {
@ -1125,9 +1151,31 @@ export function ApplicationDetails() {
alert('Please enter a reason for rejection');
return;
}
alert(`Application ${application?.registrationNumber} rejected!\nReason: ${rejectionReason}`);
setShowRejectModal(false);
setRejectionReason('');
try {
await onboardingService.updateApplicationStatus(applicationId!, {
status: 'Rejected',
remarks: rejectionReason
});
toast.success('Application rejected');
setShowRejectModal(false);
setRejectionReason('');
fetchApplication();
} catch (error) {
console.error('Rejection error:', error);
toast.error('Failed to reject application');
}
};
const handleGenerateDealerCodes = async () => {
try {
await onboardingService.generateDealerCodes(applicationId!);
toast.success('Dealer codes generated successfully');
fetchApplication();
} catch (error) {
console.error('Generate codes error:', error);
toast.error('Failed to generate dealer codes');
}
};
const handleAssignArchitecture = async () => {
@ -1635,7 +1683,7 @@ export function ApplicationDetails() {
<h3 className="text-slate-900">Uploaded Documents</h3>
<Button size="sm" className="bg-amber-600 hover:bg-amber-700" onClick={() => {
setSelectedStage(null);
setShowDocumentsModal(true);
setShowUploadForm(true);
}}>
<Upload className="w-4 h-4 mr-2" />
Upload Document
@ -2042,14 +2090,24 @@ export function ApplicationDetails() {
)}
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && (
<Button
variant="outline"
className="w-full border-blue-200 hover:bg-blue-50 text-blue-700"
onClick={() => setShowAssignArchitectureModal(true)}
>
<GitBranch className="w-4 h-4 mr-2" />
Assign Architecture Team
</Button>
<>
<Button
className="w-full bg-blue-600 hover:bg-blue-700"
onClick={handleGenerateDealerCodes}
>
<Zap className="w-4 h-4 mr-2" />
Generate Dealer Codes
</Button>
<Button
variant="outline"
className="w-full border-blue-200 hover:bg-blue-50 text-blue-700"
onClick={() => setShowAssignArchitectureModal(true)}
>
<GitBranch className="w-4 h-4 mr-2" />
Assign Architecture Team
</Button>
</>
)}
{((currentUser && currentUser.id === application.architectureAssignedTo) || (currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role))) &&

View File

@ -1,6 +1,6 @@
import { ArrowLeft, FileText, Calendar, User, Building2, CheckCircle2, Clock, AlertCircle, Upload, Download, Eye, ArrowRight, Shield, MessageSquare } from 'lucide-react';
import { ArrowLeft, CheckCircle2, Clock, AlertCircle, Upload, Download, Eye, ArrowRight, MessageSquare, Loader2 } from 'lucide-react';
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
@ -8,10 +8,10 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Textarea } from '../ui/textarea';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner';
import { mockConstitutionalChangeRequests } from './ConstitutionalChangePage';
import { API } from '../../api/API';
interface ConstitutionalChangeDetailsProps {
requestId: string;
@ -64,78 +64,8 @@ const documentNames: Record<number, string> = {
16: 'Declaration / Authorization Letter'
};
// Mock uploaded documents
const mockUploadedDocuments = [
{ docNumber: 1, fileName: 'GST_Certificate.pdf', uploadedOn: '2025-12-15', uploadedBy: 'Dealer', status: 'Verified' },
{ docNumber: 2, fileName: 'Firm_PAN.pdf', uploadedOn: '2025-12-15', uploadedBy: 'Dealer', status: 'Verified' },
{ docNumber: 3, fileName: 'KYC_Documents.pdf', uploadedOn: '2025-12-15', uploadedBy: 'Dealer', status: 'Pending Verification' },
{ docNumber: 4, fileName: 'Partnership_Agreement_Notarised.pdf', uploadedOn: '2025-12-16', uploadedBy: 'Dealer', status: 'Verified' },
];
// Mock workflow history
const mockWorkflowHistory = [
{
stage: 'Request Created',
actor: 'Amit Sharma (Dealer)',
action: 'Created',
date: '2025-12-15 10:30 AM',
comments: 'Submitted constitutional change request from Proprietorship to Partnership',
status: 'Completed'
},
{
stage: 'ASM Review',
actor: 'Rajesh Kumar (ASM)',
action: 'Approved',
date: '2025-12-16 02:15 PM',
comments: 'Verified dealer credentials and approved for next stage',
status: 'Completed'
},
{
stage: 'RBM Review',
actor: 'Priya Sharma (RBM)',
action: 'Under Review',
date: '2025-12-17 09:00 AM',
comments: 'Documents under verification',
status: 'In Progress'
},
];
// Mock worknotes - Discussion platform for this request
const initialWorknotes = [
{
id: 1,
user: 'Rajesh Kumar',
role: 'ASM',
message: 'I have reviewed the partnership agreement. All partners have proper KYC documentation. Looks good to proceed.',
timestamp: '2025-12-16 11:30 AM',
avatar: 'RK'
},
{
id: 2,
user: 'Priya Sharma',
role: 'RBM',
message: 'Can we get clarification on the profit sharing ratio mentioned in the partnership deed? It seems different from what was discussed.',
timestamp: '2025-12-17 02:45 PM',
avatar: 'PS'
},
{
id: 3,
user: 'Amit Sharma',
role: 'Dealer',
message: 'The profit sharing ratio is 60:40 as per the partnership deed. This was agreed upon by all partners and is correctly reflected in the document.',
timestamp: '2025-12-17 04:15 PM',
avatar: 'AS'
},
{
id: 4,
user: 'Priya Sharma',
role: 'RBM',
message: 'Thank you for the clarification. I have verified the BPA and other statutory documents. Everything appears to be in order.',
timestamp: '2025-12-18 10:00 AM',
avatar: 'PS'
}
];
// Helper functions moved above component to avoid lint errors
const getTypeColor = (type: string) => {
switch(type) {
case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300';
@ -159,11 +89,40 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
const [comments, setComments] = useState('');
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [isWorknoteDialogOpen, setIsWorknoteDialogOpen] = useState(false);
const [worknotes, setWorknotes] = useState(initialWorknotes);
const [request, setRequest] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [isActionLoading, setIsActionLoading] = useState(false);
const [newWorknote, setNewWorknote] = useState('');
// Find the request
const request = mockConstitutionalChangeRequests.find(r => r.id === requestId);
useEffect(() => {
fetchRequestDetails();
}, [requestId]);
const fetchRequestDetails = async () => {
try {
setIsLoading(true);
const response = await API.getConstitutionalChangeById(requestId) as any;
if (response.data.success) {
setRequest(response.data.request);
} else {
toast.error('Failed to fetch request details');
}
} catch (error) {
console.error('Fetch request details error:', error);
toast.error('Error loading request details');
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] space-y-4">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
<p className="text-slate-600">Loading request details...</p>
</div>
);
}
if (!request) {
return (
@ -176,7 +135,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
}
// Get required documents for this request
const requiredDocs = documentRequirements[request.targetType] || [];
const requiredDocs = documentRequirements[request.changeType] || [];
// Calculate current stage index
const getCurrentStageIndex = () => {
@ -203,14 +162,29 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
setIsActionDialogOpen(true);
};
const handleSubmitAction = (e: React.FormEvent) => {
const handleSubmitAction = async (e: React.FormEvent) => {
e.preventDefault();
const actionText = actionType === 'approve' ? 'approved' : actionType === 'reject' ? 'rejected' : 'put on hold';
toast.success(`Request ${actionText} successfully`);
setIsActionDialogOpen(false);
setComments('');
try {
setIsActionLoading(true);
const action = actionType === 'approve' ? 'approve' : actionType === 'reject' ? 'reject' : 'hold';
const response = await API.updateConstitutionalChange(requestId, action, {
comments
}) as any;
if (response.data.success) {
const actionText = actionType === 'approve' ? 'approved' : actionType === 'reject' ? 'rejected' : 'put on hold';
toast.success(`Request ${actionText} successfully`);
setIsActionDialogOpen(false);
setComments('');
fetchRequestDetails();
}
} catch (error) {
console.error('Submit action error:', error);
toast.error('Failed to submit action');
} finally {
setIsActionLoading(false);
}
};
const handleUploadDocument = () => {
@ -218,19 +192,23 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
setIsUploadDialogOpen(false);
};
const handleAddWorknote = () => {
const handleAddWorknote = async () => {
if (newWorknote.trim()) {
const newNote = {
id: worknotes.length + 1,
user: currentUser?.name || 'Anonymous',
role: currentUser?.role || 'User',
message: newWorknote,
timestamp: new Date().toLocaleString(),
avatar: currentUser?.name?.slice(0, 2).toUpperCase() || 'AN'
};
setWorknotes([...worknotes, newNote]);
setNewWorknote('');
toast.success('Worknote added successfully');
try {
const response = await API.addWorknote({
requestId,
requestType: 'constitutional-change',
message: newWorknote
}) as any;
if (response.data.success) {
setNewWorknote('');
toast.success('Worknote added successfully');
fetchRequestDetails();
}
} catch (error) {
toast.error('Failed to add worknote');
}
}
};
@ -248,9 +226,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
Back
</Button>
<div>
<h1 className="text-slate-900">{request.id} - Constitutional Change Details</h1>
<h1 className="text-slate-900">{request.requestId} - Constitutional Change Details</h1>
<p className="text-slate-600">
{request.dealerName} ({request.dealerCode})
{request.outlet?.name || 'N/A'} ({request.outlet?.code || 'N/A'})
</p>
</div>
</div>
@ -268,33 +246,33 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p className="text-slate-600 text-sm mb-1">Dealer Details</p>
<p className="text-slate-900">{request.dealerName}</p>
<p className="text-slate-600 text-sm">{request.dealerCode}</p>
<p className="text-slate-600 text-sm">{request.location}</p>
<p className="text-slate-900">{request.outlet?.name || 'N/A'}</p>
<p className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</p>
<p className="text-slate-600 text-sm">{request.outlet?.city || request.outlet?.address || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600 text-sm mb-2">Constitutional Change</p>
<div className="flex items-center gap-2">
<Badge className={getTypeColor(request.currentType)}>
{request.currentType}
<Badge className={getTypeColor(request.outlet?.type || 'Proprietorship')}>
{request.outlet?.type || 'Proprietorship'}
</Badge>
<ArrowRight className="w-4 h-4 text-slate-400" />
<Badge className={getTypeColor(request.targetType)}>
{request.targetType}
<Badge className={getTypeColor(request.changeType)}>
{request.changeType}
</Badge>
</div>
</div>
<div>
<p className="text-slate-600 text-sm mb-1">Request Information</p>
<p className="text-slate-900 text-sm">Submitted: {request.submittedOn}</p>
<p className="text-slate-600 text-sm">By: {request.submittedBy}</p>
<p className="text-slate-900 text-sm">Submitted: {new Date(request.createdAt).toLocaleDateString()}</p>
<p className="text-slate-600 text-sm">By: {request.dealer?.fullName || 'Dealer'}</p>
<p className="text-slate-900 text-sm mt-2">Current Stage: {request.currentStage}</p>
</div>
</div>
<div className="mt-6">
<p className="text-slate-600 text-sm mb-2">Reason for Change</p>
<p className="text-slate-900">{request.reason}</p>
<p className="text-slate-900">{request.description}</p>
</div>
</CardContent>
</Card>
@ -336,7 +314,6 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
{workflowStages.map((stage, index) => {
const isCompleted = index < currentStageIndex - 1;
const isCurrent = index === currentStageIndex - 1;
const isPending = index > currentStageIndex - 1;
return (
<div key={stage.id} className="flex items-start gap-4">
@ -447,7 +424,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
</div>
<div className="space-y-2">
{requiredDocs.map((docNum) => {
const uploaded = mockUploadedDocuments.find(d => d.docNumber === docNum);
const uploaded = (request.documents || []).find((d: any) => d.docNumber === docNum || d.name?.includes(documentNames[docNum]));
return (
<div
key={docNum}
@ -466,7 +443,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
{documentNames[docNum]}
</p>
{uploaded && (
<p className="text-green-700 text-sm">{uploaded.fileName}</p>
<p className="text-green-700 text-sm">{uploaded.fileName || uploaded.name}</p>
)}
</div>
</div>
@ -488,7 +465,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
{/* Existing Documents Sub-tab */}
<TabsContent value="existing" className="mt-0">
{mockUploadedDocuments.length > 0 ? (
{(request.documents || []).length > 0 ? (
<div>
<h4 className="text-slate-900 mb-3">All Uploaded Documents</h4>
<div className="border border-slate-200 rounded-lg overflow-hidden">
@ -504,19 +481,19 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
</TableRow>
</TableHeader>
<TableBody>
{mockUploadedDocuments.map((doc) => (
<TableRow key={doc.docNumber}>
{(request.documents || []).map((doc: any, index: number) => (
<TableRow key={index}>
<TableCell className="text-slate-900">
{documentNames[doc.docNumber]}
{doc.docNumber ? documentNames[doc.docNumber] : doc.name}
</TableCell>
<TableCell className="text-slate-600">
{doc.fileName}
{doc.fileName || doc.name}
</TableCell>
<TableCell className="text-slate-600">
{doc.uploadedOn}
{new Date(doc.uploadedOn || doc.createdAt).toLocaleDateString()}
</TableCell>
<TableCell className="text-slate-600">
{doc.uploadedBy}
{doc.uploadedBy || 'Dealer'}
</TableCell>
<TableCell>
<Badge className={getStatusColor(doc.status)}>
@ -550,39 +527,42 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
</Tabs>
</TabsContent>
{/* History Tab */}
{/* History Tab */}
<TabsContent value="history" className="mt-0">
<div className="space-y-4">
{mockWorkflowHistory.map((entry, index) => (
{(request.timeline || request.history || []).map((entry: any, index: number) => (
<div key={index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
entry.status === 'Completed' ? 'bg-green-100' :
entry.status === 'In Progress' ? 'bg-amber-100' :
(entry.status || entry.action)?.toLowerCase().includes('approve') || (entry.status || entry.action)?.toLowerCase().includes('complete') ? 'bg-green-100' :
(entry.status || entry.action)?.toLowerCase().includes('pending') || (entry.status || entry.action)?.toLowerCase().includes('progress') ? 'bg-amber-100' :
'bg-slate-100'
}`}>
{entry.status === 'Completed' ? (
{(entry.status || entry.action)?.toLowerCase().includes('approve') || (entry.status || entry.action)?.toLowerCase().includes('complete') ? (
<CheckCircle2 className="w-5 h-5 text-green-600" />
) : entry.status === 'In Progress' ? (
<Clock className="w-5 h-5 text-amber-600" />
) : (
<User className="w-5 h-5 text-slate-600" />
<Clock className="w-5 h-5 text-amber-600" />
)}
</div>
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h4 className="text-slate-900">{entry.stage}</h4>
<p className="text-slate-600 text-sm">{entry.actor}</p>
<h4 className="text-slate-900">{entry.stage || entry.entityType || 'Update'}</h4>
<p className="text-slate-600 text-sm">{entry.actor || entry.user?.fullName || entry.user}</p>
</div>
<Badge className={getStatusColor(entry.status)}>
{entry.action}
<Badge className={getStatusColor(entry.status || entry.action)}>
{entry.action || entry.status}
</Badge>
</div>
<p className="text-slate-600 text-sm mt-2">{entry.comments}</p>
<p className="text-slate-500 text-sm mt-1">{entry.date}</p>
<p className="text-slate-600 text-sm mt-2">{entry.comments || entry.remarks || 'No remarks provided'}</p>
<p className="text-slate-500 text-sm mt-1">{new Date(entry.date || entry.createdAt || entry.timestamp).toLocaleString()}</p>
</div>
</div>
))}
{(request.timeline || request.history || []).length === 0 && (
<div className="text-center py-8 text-slate-500">
No history found
</div>
)}
</div>
</TabsContent>
</CardContent>
@ -614,8 +594,13 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={() => handleAction('approve')}
disabled={isActionLoading}
>
<CheckCircle2 className="w-4 h-4 mr-2" />
{isActionLoading && actionType === 'approve' ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<CheckCircle2 className="w-4 h-4 mr-2" />
)}
Approve Request
</Button>
@ -623,25 +608,30 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
variant="destructive"
className="w-full"
onClick={() => handleAction('reject')}
disabled={isActionLoading}
>
<AlertCircle className="w-4 h-4 mr-2" />
{isActionLoading && actionType === 'reject' ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<AlertCircle className="w-4 h-4 mr-2" />
)}
Reject Request
</Button>
<div className="border-t border-slate-200 pt-3 mt-3">
<div className="border-t border-slate-200 pt-3 mt-3">
<Button
variant="outline"
className="w-full border-blue-300 text-blue-700 hover:bg-blue-50"
className="w-full border-blue- blue-700 hover:bg-blue-50"
onClick={() => {
if (onOpenWorknote) {
onOpenWorknote(requestId, 'constitutional-change', `${request.dealerName} (${request.dealerCode}) - Constitutional Change Request`);
onOpenWorknote(requestId, 'constitutional-change', `${request.outlet?.name || 'N/A'} (${request.outlet?.code || 'N/A'}) - Constitutional Change Request`);
} else {
setIsWorknoteDialogOpen(true);
}
}}
>
<MessageSquare className="w-4 h-4 mr-2" />
Worknotes ({worknotes.length})
Worknotes ({(request.worknotes || []).length})
</Button>
</div>
</CardContent>
@ -684,18 +674,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
>
Cancel
</Button>
<Button
type="submit"
className={
actionType === 'approve' ? 'bg-green-600 hover:bg-green-700' :
actionType === 'reject' ? 'bg-red-600 hover:bg-red-700' :
'bg-amber-600 hover:bg-amber-700'
}
>
{actionType === 'approve' ? 'Approve' :
actionType === 'reject' ? 'Reject' :
'Put on Hold'}
</Button>
<Button
type="submit"
className={
actionType === 'approve' ? 'bg-green-600 hover:bg-green-700' :
actionType === 'reject' ? 'bg-red-600 hover:bg-red-700' :
'bg-amber-600 hover:bg-amber-700'
}
disabled={isActionLoading}
>
{isActionLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Processing...
</>
) : (
actionType === 'approve' ? 'Approve' :
actionType === 'reject' ? 'Reject' :
'Put on Hold'
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
@ -714,27 +712,27 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
<div className="space-y-4">
{/* Discussion Thread */}
<div className="space-y-2">
<Label>Discussion History ({worknotes.length} messages)</Label>
<Label>Discussion History ({(request.worknotes || []).length} messages)</Label>
<div className="border border-slate-200 rounded-lg p-4 max-h-96 overflow-y-auto bg-slate-50">
<div className="space-y-4">
{worknotes.map((note) => (
{(request.worknotes || []).map((note: any) => (
<div key={note.id} className="flex items-start gap-3">
{/* Avatar */}
<div className="w-10 h-10 rounded-full bg-amber-600 flex items-center justify-center text-white flex-shrink-0">
{note.avatar}
{note.author?.fullName?.slice(0, 2).toUpperCase() || note.user?.fullName?.slice(0, 2).toUpperCase() || 'UN'}
</div>
{/* Message Content */}
<div className="flex-1 bg-white rounded-lg p-3 border border-slate-200">
<div className="flex items-start justify-between mb-1">
<div>
<h5 className="text-slate-900">{note.user}</h5>
<h5 className="text-slate-900">{note.author?.fullName || note.user?.fullName || 'Unknown User'}</h5>
<Badge variant="outline" className="border-slate-300 text-xs">
{note.role}
{note.author?.role || note.user?.role?.name || 'User'}
</Badge>
</div>
<span className="text-slate-500 text-xs">{note.timestamp}</span>
<span className="text-slate-500 text-xs">{new Date(note.createdAt).toLocaleString()}</span>
</div>
<p className="text-slate-700 text-sm mt-2">{note.message}</p>
<p className="text-slate-700 text-sm mt-2">{note.noteText || note.message}</p>
</div>
</div>
))}

View File

@ -1,4 +1,4 @@
import { FileText, Calendar, Building, Plus, Eye, ArrowRight, Shield } from 'lucide-react';
import { FileText, Calendar, Building, Plus, Eye, ArrowRight, Shield, Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@ -9,126 +9,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 { User } from '../../lib/mock-data';
import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner';
import { API } from '../../api/API';
interface ConstitutionalChangePageProps {
currentUser: User | null;
currentUser: UserType | null;
onViewDetails: (id: string) => void;
}
// Mock dealer data for auto-fetch
const mockDealerData: Record<string, any> = {
'DL-MH-001': {
dealerName: 'Amit Sharma Motors',
address: '123, MG Road, Bandra West',
cityCategory: 'Tier 1',
domainName: 'Mumbai Central',
dealershipName: 'Royal Enfield Mumbai',
gst: '27AABCU9603R1ZX',
currentType: 'Proprietorship',
region: 'West',
zone: 'Maharashtra'
},
'DL-KA-045': {
dealerName: 'Priya Automobiles',
address: '456, Brigade Road, Whitefield',
cityCategory: 'Tier 1',
domainName: 'Bangalore South',
dealershipName: 'Royal Enfield Bangalore',
gst: '29AABCU9603R1ZX',
currentType: 'Partnership',
region: 'South',
zone: 'Karnataka'
},
'DL-TN-028': {
dealerName: 'Rahul Motors',
address: '789, Anna Salai, T Nagar',
cityCategory: 'Tier 1',
domainName: 'Chennai East',
dealershipName: 'Royal Enfield Chennai',
gst: '33AABCU9603R1ZX',
currentType: 'LLP',
region: 'South',
zone: 'Tamil Nadu'
}
};
// Mock constitutional change requests
export const mockConstitutionalChangeRequests = [
{
id: 'CC-001',
dealerCode: 'DL-MH-001',
dealerName: 'Amit Sharma Motors',
location: 'Mumbai, Maharashtra',
currentType: 'Proprietorship',
targetType: 'Partnership',
reason: 'Adding new partner to expand business operations',
status: 'RBM Review',
currentStage: 'RBM',
submittedOn: '2025-12-15',
submittedBy: 'Dealer',
progressPercentage: 23
},
{
id: 'CC-002',
dealerCode: 'DL-KA-045',
dealerName: 'Priya Automobiles',
location: 'Bangalore, Karnataka',
currentType: 'Partnership',
targetType: 'Pvt Ltd',
reason: 'Converting to Pvt Ltd for better business structure',
status: 'DD Lead Review',
currentStage: 'DD Lead',
submittedOn: '2025-12-10',
submittedBy: 'Dealer',
progressPercentage: 46
},
{
id: 'CC-003',
dealerCode: 'DL-TN-028',
dealerName: 'Rahul Motors',
location: 'Chennai, Tamil Nadu',
currentType: 'LLP',
targetType: 'Pvt Ltd',
reason: 'Upgrading to Pvt Ltd for investment opportunities',
status: 'NBH Review',
currentStage: 'NBH',
submittedOn: '2025-12-05',
submittedBy: 'Dealer',
progressPercentage: 69
},
{
id: 'CC-004',
dealerCode: 'DL-DL-012',
dealerName: 'Suresh Auto Pvt Ltd',
location: 'Delhi, Delhi',
currentType: 'Pvt Ltd',
targetType: 'Partnership',
reason: 'Removing one partner from the business',
status: 'Docs Collection',
currentStage: 'DD H.O',
submittedOn: '2025-11-28',
submittedBy: 'Dealer',
progressPercentage: 77
},
{
id: 'CC-005',
dealerCode: 'DL-GJ-089',
dealerName: 'Gujarat Motors',
location: 'Ahmedabad, Gujarat',
currentType: 'Partnership',
targetType: 'LLP',
reason: 'Converting to LLP for limited liability protection',
status: 'Completed',
currentStage: 'Closed',
submittedOn: '2025-11-20',
submittedBy: 'Dealer',
progressPercentage: 100
}
];
// Document requirements mapping
const documentRequirements: Record<string, number[]> = {
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
@ -175,24 +65,62 @@ const getTypeColor = (type: string) => {
}
};
export function ConstitutionalChangePage({ currentUser, onViewDetails }: ConstitutionalChangePageProps) {
export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChangePageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dealerCode, setDealerCode] = useState('');
const [dealerData, setDealerData] = useState<any>(null);
const [targetType, setTargetType] = useState('');
const [reason, setReason] = useState('');
const [requiredDocs, setRequiredDocs] = useState<number[]>([]);
const [requests, setRequests] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleDealerCodeChange = (code: string) => {
useEffect(() => {
fetchRequests();
}, []);
const fetchRequests = async () => {
try {
setIsLoading(true);
const response = await API.getConstitutionalChanges() as any;
if (response.data.success) {
setRequests(response.data.requests || []);
}
} catch (error) {
console.error('Fetch requests error:', error);
toast.error('Failed to fetch requests');
} finally {
setIsLoading(false);
}
};
const handleDealerCodeChange = async (code: string) => {
setDealerCode(code);
if (mockDealerData[code]) {
setDealerData(mockDealerData[code]);
toast.success('Dealer details loaded successfully');
if (code.length >= 5) {
try {
const response = await API.getOutletByCode(code) as any;
if (response.data.success && response.data.outlet) {
const outlet = response.data.outlet;
setDealerData({
id: outlet.id,
dealerName: outlet.name,
address: outlet.address,
dealershipName: outlet.name,
gst: outlet.gstNumber || 'N/A',
currentType: outlet.type || 'Proprietorship',
region: outlet.region || 'N/A',
zone: outlet.zone || 'N/A'
});
toast.success('Dealer details loaded successfully');
} else {
setDealerData(null);
}
} catch (error) {
setDealerData(null);
}
} else {
setDealerData(null);
if (code.trim()) {
toast.error('Dealer code not found');
}
}
};
@ -201,7 +129,7 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
setRequiredDocs(documentRequirements[type] || []);
};
const handleSubmitRequest = (e: React.FormEvent) => {
const handleSubmitRequest = async (e: React.FormEvent) => {
e.preventDefault();
if (!dealerData) {
@ -219,54 +147,64 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
return;
}
// Validate that target type is different from current type
if (dealerData.currentType === targetType) {
toast.error('Target type cannot be same as current type');
return;
}
toast.success('Constitutional change request submitted successfully');
setIsDialogOpen(false);
// Reset form
setDealerCode('');
setDealerData(null);
setTargetType('');
setReason('');
setRequiredDocs([]);
try {
setIsSubmitting(true);
const payload = {
outletId: dealerData.id,
changeType: targetType,
description: reason,
newEntityDetails: {}
};
const response = await API.createConstitutionalChange(payload) as any;
if (response.data.success) {
toast.success('Constitutional change request submitted successfully');
setIsDialogOpen(false);
fetchRequests();
// Reset form
setDealerCode('');
setDealerData(null);
setTargetType('');
setReason('');
setRequiredDocs([]);
}
} catch (error) {
console.error('Submit request error:', error);
toast.error('Failed to submit request');
} finally {
setIsSubmitting(false);
}
};
// Filter requests based on user role
const getFilteredRequests = () => {
// For now, showing all requests. In real implementation, filter by role permissions
return mockConstitutionalChangeRequests;
};
const filteredRequests = getFilteredRequests();
// Statistics
const stats = [
{
title: 'Total Requests',
value: filteredRequests.length,
value: requests.length,
icon: FileText,
color: 'bg-blue-500',
},
{
title: 'In Progress',
value: filteredRequests.filter(r => r.status !== 'Completed' && !r.status.includes('Rejected')).length,
value: requests.filter(r => r.status !== 'Completed' && !r.status.includes('Rejected')).length,
icon: Calendar,
color: 'bg-yellow-500',
},
{
title: 'Completed',
value: filteredRequests.filter(r => r.status === 'Completed').length,
value: requests.filter(r => r.status === 'Completed').length,
icon: Shield,
color: 'bg-green-500',
},
{
title: 'Pending Action',
value: filteredRequests.filter(r => r.status.includes('Review') || r.status.includes('Pending')).length,
value: requests.filter(r => r.status.includes('Review') || r.status.includes('Pending')).length,
icon: Building,
color: 'bg-amber-500',
},
@ -274,6 +212,13 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
return (
<div className="space-y-6">
{/* Loading Overlay */}
{isLoading && (
<div className="fixed inset-0 bg-slate-900/20 backdrop-blur-sm z-50 flex items-center justify-center">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
</div>
)}
{/* Header */}
<div className="flex items-center justify-between">
<div>
@ -404,9 +349,16 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
<Button
type="submit"
className="bg-amber-600 hover:bg-amber-700"
disabled={!dealerData || !targetType || (dealerData && dealerData.currentType === targetType)}
disabled={!dealerData || !targetType || (dealerData && dealerData.currentType === targetType) || isSubmitting}
>
Submit Request
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
'Submit Request'
)}
</Button>
</DialogFooter>
</form>
@ -468,59 +420,67 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
</TableRow>
</TableHeader>
<TableBody>
{filteredRequests.map((request) => (
<TableRow key={request.id}>
<TableCell>
<div className="font-medium text-slate-900">{request.id}</div>
<div className="text-slate-600 text-sm">{request.dealerCode}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.dealerName}</div>
<div className="text-slate-600 text-sm">{request.location}</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Badge className={getTypeColor(request.currentType)}>
{request.currentType}
</Badge>
<ArrowRight className="w-4 h-4 text-slate-400" />
<Badge className={getTypeColor(request.targetType)}>
{request.targetType}
</Badge>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.currentStage}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-amber-600 transition-all duration-300"
style={{ width: `${request.progressPercentage}%` }}
/>
</div>
<span className="text-slate-600 text-sm">{request.progressPercentage}%</span>
</div>
</TableCell>
<TableCell>
<div className="text-slate-900">{request.submittedOn}</div>
<div className="text-slate-600 text-sm">By {request.submittedBy}</div>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
{requests.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-slate-500">
No constitutional change requests found
</TableCell>
</TableRow>
))}
) : (
requests.map((request: any) => (
<TableRow key={request.requestId}>
<TableCell>
<div className="font-medium text-slate-900">{request.requestId}</div>
<div className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
<div className="text-slate-600 text-sm">{request.outlet?.city || request.outlet?.address || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Badge className={getTypeColor(request.outlet?.type || 'Proprietorship')}>
{request.outlet?.type || 'Proprietorship'}
</Badge>
<ArrowRight className="w-4 h-4 text-slate-400" />
<Badge className={getTypeColor(request.changeType)}>
{request.changeType}
</Badge>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.currentStage}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-amber-600 transition-all duration-300"
style={{ width: `${request.progressPercentage || 0}%` }}
/>
</div>
<span className="text-slate-600 text-sm">{request.progressPercentage || 0}%</span>
</div>
</TableCell>
<TableCell>
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div>
<div className="text-slate-600 text-sm">By {request.dealer?.fullName || 'Dealer'}</div>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.requestId)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
@ -540,26 +500,26 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
</TableRow>
</TableHeader>
<TableBody>
{filteredRequests
.filter(r => r.status.includes('Review') || r.status.includes('Pending'))
.map((request) => (
<TableRow key={request.id}>
{requests
.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending'))
.map((request: any) => (
<TableRow key={request.requestId}>
<TableCell>
<div className="font-medium text-slate-900">{request.id}</div>
<div className="text-slate-600 text-sm">{request.dealerCode}</div>
<div className="font-medium text-slate-900">{request.requestId}</div>
<div className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.dealerName}</div>
<div className="text-slate-600 text-sm">{request.location}</div>
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
<div className="text-slate-600 text-sm">{request.outlet?.city || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Badge className={getTypeColor(request.currentType)}>
{request.currentType}
<Badge className={getTypeColor(request.outlet?.type || 'Proprietorship')}>
{request.outlet?.type || 'Proprietorship'}
</Badge>
<ArrowRight className="w-4 h-4 text-slate-400" />
<Badge className={getTypeColor(request.targetType)}>
{request.targetType}
<Badge className={getTypeColor(request.changeType)}>
{request.changeType}
</Badge>
</div>
</TableCell>
@ -577,7 +537,7 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
onClick={() => onViewDetails(request.requestId)}
>
<Eye className="w-4 h-4 mr-1" />
View
@ -585,6 +545,13 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
</TableCell>
</TableRow>
))}
{requests.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending')).length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-slate-500">
No pending requests found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
@ -604,26 +571,26 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
</TableRow>
</TableHeader>
<TableBody>
{filteredRequests
.filter(r => r.status !== 'Completed' && !r.status.includes('Rejected'))
.map((request) => (
<TableRow key={request.id}>
{requests
.filter((r: any) => r.status !== 'Completed' && !r.status.includes('Rejected'))
.map((request: any) => (
<TableRow key={request.requestId}>
<TableCell>
<div className="font-medium text-slate-900">{request.id}</div>
<div className="text-slate-600 text-sm">{request.dealerCode}</div>
<div className="font-medium text-slate-900">{request.requestId}</div>
<div className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.dealerName}</div>
<div className="text-slate-600 text-sm">{request.location}</div>
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
<div className="text-slate-600 text-sm">{request.outlet?.city || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Badge className={getTypeColor(request.currentType)}>
{request.currentType}
<Badge className={getTypeColor(request.outlet?.type || 'Proprietorship')}>
{request.outlet?.type || 'Proprietorship'}
</Badge>
<ArrowRight className="w-4 h-4 text-slate-400" />
<Badge className={getTypeColor(request.targetType)}>
{request.targetType}
<Badge className={getTypeColor(request.changeType)}>
{request.changeType}
</Badge>
</div>
</TableCell>
@ -632,10 +599,10 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-amber-600 transition-all duration-300"
style={{ width: `${request.progressPercentage}%` }}
style={{ width: `${request.progressPercentage || 0}%` }}
/>
</div>
<span className="text-slate-600 text-sm">{request.progressPercentage}%</span>
<span className="text-slate-600 text-sm">{request.progressPercentage || 0}%</span>
</div>
</TableCell>
<TableCell>
@ -647,7 +614,7 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
onClick={() => onViewDetails(request.requestId)}
>
<Eye className="w-4 h-4 mr-1" />
View
@ -655,6 +622,13 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
</TableCell>
</TableRow>
))}
{requests.filter((r: any) => r.status !== 'Completed' && !r.status.includes('Rejected')).length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-slate-500">
No in-progress requests found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
@ -674,26 +648,26 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
</TableRow>
</TableHeader>
<TableBody>
{filteredRequests
.filter(r => r.status === 'Completed')
.map((request) => (
<TableRow key={request.id}>
{requests
.filter((r: any) => r.status === 'Completed' || r.status === 'Closed')
.map((request: any) => (
<TableRow key={request.requestId}>
<TableCell>
<div className="font-medium text-slate-900">{request.id}</div>
<div className="text-slate-600 text-sm">{request.dealerCode}</div>
<div className="font-medium text-slate-900">{request.requestId}</div>
<div className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.dealerName}</div>
<div className="text-slate-600 text-sm">{request.location}</div>
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
<div className="text-slate-600 text-sm">{request.outlet?.city || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Badge className={getTypeColor(request.currentType)}>
{request.currentType}
<Badge className={getTypeColor(request.outlet?.type || 'Proprietorship')}>
{request.outlet?.type || 'Proprietorship'}
</Badge>
<ArrowRight className="w-4 h-4 text-slate-400" />
<Badge className={getTypeColor(request.targetType)}>
{request.targetType}
<Badge className={getTypeColor(request.changeType)}>
{request.changeType}
</Badge>
</div>
</TableCell>
@ -703,13 +677,13 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
</Badge>
</TableCell>
<TableCell>
<div className="text-slate-900">{request.submittedOn}</div>
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
onClick={() => onViewDetails(request.requestId)}
>
<Eye className="w-4 h-4 mr-1" />
View
@ -717,6 +691,13 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
</TableCell>
</TableRow>
))}
{requests.filter((r: any) => r.status === 'Completed' || r.status === 'Closed').length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-slate-500">
No completed requests found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>

View File

@ -1,4 +1,6 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { API } from '../../api/API';
import { Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@ -15,9 +17,7 @@ import {
XCircle,
Upload,
FileText,
Calendar,
User,
MapPin,
AlertCircle,
Wallet,
Receipt,
@ -25,99 +25,22 @@ import {
TrendingDown,
Building,
CreditCard,
Hash,
Send,
Users,
Plus,
Edit2,
Trash2,
Save,
Calculator
Save
} from 'lucide-react';
import { toast } from 'sonner';
import { departments } from '../../lib/mock-data';
interface FinanceFnFDetailsPageProps {
fnfId: string;
onBack: () => void;
}
// Mock data - in real app this would come from API
const getFnFData = (id: string) => {
return {
id: id,
caseNumber: 'FNF-2024-001',
dealerName: 'Rajesh Kumar',
dealerCode: 'DLR-001',
location: 'Mumbai, Maharashtra',
terminationType: 'Resignation',
submittedDate: '2025-09-15',
dueDate: '2025-10-20',
status: 'Pending Finance Review',
financialData: {
// Payables (Company owes dealer)
securityDeposit: 500000,
inventoryValue: 1200000,
equipmentValue: 300000,
// Receivables (Dealer owes company)
outstandingInvoices: 450000,
serviceDues: 75000,
partsDues: 125000,
advancesGiven: 200000,
penalties: 50000,
otherCharges: 25000,
// Deductions
warrantyPending: 100000,
},
bankDetails: {
accountName: 'Rajesh Kumar',
accountNumber: '1234567890',
ifscCode: 'HDFC0001234',
bankName: 'HDFC Bank',
branch: 'Mumbai Central'
},
documents: [
{ name: 'Resignation Letter.pdf', size: '245 KB', uploadedOn: '2025-09-15', type: 'Resignation' },
{ name: 'Asset Handover Receipt.pdf', size: '312 KB', uploadedOn: '2025-09-16', type: 'Asset' },
{ name: 'Inventory Report.xlsx', size: '856 KB', uploadedOn: '2025-09-17', type: 'Inventory' },
{ name: 'Bank Statement.pdf', size: '1.2 MB', uploadedOn: '2025-09-15', type: 'Financial' }
],
departmentResponses: departments.map((dept, index) => ({
id: `dept-${index + 1}`,
departmentName: dept,
status: index < 8 ? 'NOC Submitted' : index < 12 ? 'Dues Pending' : 'Pending',
remarks: index < 8 ? 'No outstanding dues, clearance provided' : index < 12 ? 'Outstanding amount to be recovered' : 'Awaiting department response',
amountType: index === 8 ? 'Recovery Amount' : index === 9 ? 'Payable Amount' : index === 10 ? 'Recovery Amount' : undefined,
amount: index === 8 ? 75000 : index === 9 ? 12000 : index === 10 ? 125000 : undefined,
submittedDate: index < 12 ? '2025-10-05' : undefined,
submittedBy: index < 12 ? `${dept} Head` : undefined
}))
};
};
// Removing mock data functions as we use live API
const calculateSettlement = (financialData: any) => {
const payables = financialData.securityDeposit + financialData.inventoryValue + financialData.equipmentValue;
const receivables =
financialData.outstandingInvoices +
financialData.serviceDues +
financialData.partsDues +
financialData.advancesGiven +
financialData.penalties +
financialData.otherCharges;
const deductions = financialData.warrantyPending;
const netSettlement = payables - receivables - deductions;
return {
payables,
receivables,
deductions,
netSettlement,
settlementAmount: Math.abs(netSettlement),
settlementType: netSettlement > 0 ? 'Payable to Dealer' : 'Recovery from Dealer'
};
};
const getDepartmentStatusColor = (status: string) => {
switch (status) {
@ -142,27 +65,92 @@ interface FinancialLineItem {
}
export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPageProps) {
const fnfCase = getFnFData(fnfId);
const [fnfCase, setFnfCase] = useState<any>(null);
const [loading, setLoading] = useState(true);
// Initialize editable line items from mock data
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([
{ id: '1', department: 'Security Deposit', description: 'Refundable security deposit', amount: fnfCase.financialData.securityDeposit },
{ id: '2', department: 'Inventory', description: 'Vehicle inventory value', amount: fnfCase.financialData.inventoryValue },
{ id: '3', department: 'Equipment', description: 'Equipment and fixtures value', amount: fnfCase.financialData.equipmentValue }
]);
// Initialize editable line items
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]);
const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]);
const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]);
const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([
{ id: '1', department: 'Sales', description: 'Outstanding invoices', amount: fnfCase.financialData.outstandingInvoices },
{ id: '2', department: 'Service', description: 'Service dues', amount: fnfCase.financialData.serviceDues },
{ id: '3', department: 'Parts', description: 'Parts dues', amount: fnfCase.financialData.partsDues },
{ id: '4', department: 'Finance', description: 'Advances given to dealer', amount: fnfCase.financialData.advancesGiven },
{ id: '5', department: 'Compliance', description: 'Penalties and fines', amount: fnfCase.financialData.penalties },
{ id: '6', department: 'Other', description: 'Miscellaneous charges', amount: fnfCase.financialData.otherCharges }
]);
useEffect(() => {
fetchFnFDetails();
}, [fnfId]);
const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([
{ id: '1', department: 'Warranty', description: 'Pending warranty claims', amount: fnfCase.financialData.warrantyPending }
]);
const fetchFnFDetails = async () => {
try {
setLoading(true);
const response = await API.getFnFSettlementById(fnfId);
const data = response.data as any;
if (data.success) {
const s = data.settlement;
setFnfCase({
id: s.id,
caseNumber: s.id.substring(0, 8).toUpperCase(),
dealerName: s.outlet?.dealer?.name || 'N/A',
dealerCode: s.outlet?.code || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A',
terminationType: s.resignationId ? 'Resignation' : 'Termination',
submittedDate: new Date(s.createdAt).toLocaleDateString(),
dueDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : 'TBD',
status: s.status,
bankDetails: {
accountName: s.outlet?.dealer?.name || 'N/A',
accountNumber: 'N/A', // These should come from dealer model in a real app
ifscCode: 'N/A',
bankName: 'N/A',
branch: 'N/A'
},
departmentResponses: (s.lineItems || []).map((li: any) => ({
id: li.id,
departmentName: li.department,
status: 'Submitted',
remarks: li.remarks,
amount: Math.abs(li.amount),
amountType: li.amount < 0 ? 'Payable Amount' : 'Recovery Amount'
})),
documents: [
{ name: 'Resignation Letter.pdf', size: '245 KB', uploadedOn: new Date(s.createdAt).toLocaleDateString(), type: 'Resignation' },
{ name: 'Inventory Report.xlsx', size: '856 KB', uploadedOn: new Date(s.createdAt).toLocaleDateString(), type: 'Inventory' }
]
});
// Split line items into categories
const pItems: FinancialLineItem[] = [];
const rItems: FinancialLineItem[] = [];
const dItems: FinancialLineItem[] = [];
(s.lineItems || []).forEach((li: any) => {
const item: FinancialLineItem = {
id: li.id,
department: li.department,
description: li.remarks || '',
amount: Math.abs(li.amount)
};
if (li.amount < 0) {
pItems.push(item);
} else {
// Check if it's a deduction (usually Warranty related in this UI)
if (li.department.toLowerCase().includes('warranty')) {
dItems.push(item);
} else {
rItems.push(item);
}
}
});
setPayableItems(pItems);
setReceivableItems(rItems);
setDeductionItems(dItems);
}
} catch (error) {
console.error('Fetch F&F error:', error);
toast.error('Failed to fetch settlement details');
} finally {
setLoading(false);
}
};
// Form states for adding new items
const [newPayable, setNewPayable] = useState({ department: '', description: '', amount: '' });
@ -206,87 +194,186 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const [uploadedDocuments, setUploadedDocuments] = useState<any[]>([]);
// Handlers for Payables
const handleAddPayable = () => {
const handleAddPayable = async () => {
if (!newPayable.department || !newPayable.description || !newPayable.amount) {
toast.error('Please fill in all fields');
return;
}
const item: FinancialLineItem = {
id: Date.now().toString(),
department: newPayable.department,
description: newPayable.description,
amount: parseFloat(newPayable.amount)
};
setPayableItems([...payableItems, item]);
setNewPayable({ department: '', description: '', amount: '' });
toast.success('Payable item added');
try {
const amount = -Math.abs(parseFloat(newPayable.amount)); // Payable is negative
const response = await API.addLineItem(fnfId, {
department: newPayable.department,
remarks: newPayable.description,
amount: amount
});
const data = response.data as any;
if (data.success) {
setPayableItems([...payableItems, {
id: data.lineItem.id,
department: data.lineItem.department,
description: data.lineItem.remarks,
amount: Math.abs(data.lineItem.amount)
}]);
setNewPayable({ department: '', description: '', amount: '' });
toast.success('Payable item added');
}
} catch (error) {
toast.error('Failed to add payable item');
}
};
const handleUpdatePayable = (id: string, field: keyof FinancialLineItem, value: string | number) => {
setPayableItems(payableItems.map(item =>
const handleUpdatePayable = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
// Optimistic update
const updatedItems = payableItems.map(item =>
item.id === id ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) : value } : item
));
);
setPayableItems(updatedItems);
// API update
try {
const item = updatedItems.find(i => i.id === id);
if (item) {
await API.updateLineItem(id, {
department: item.department,
remarks: item.description,
amount: -Math.abs(item.amount)
});
}
} catch (error) {
toast.error('Failed to update item');
fetchFnFDetails(); // Rollback
}
};
const handleDeletePayable = (id: string) => {
setPayableItems(payableItems.filter(item => item.id !== id));
toast.info('Payable item removed');
const handleDeletePayable = async (id: string) => {
try {
const response = await API.deleteLineItem(id);
const data = response.data as any;
if (data.success) {
setPayableItems(payableItems.filter(item => item.id !== id));
toast.info('Payable item removed');
}
} catch (error) {
toast.error('Failed to delete item');
}
};
// Handlers for Receivables
const handleAddReceivable = () => {
const handleAddReceivable = async () => {
if (!newReceivable.department || !newReceivable.description || !newReceivable.amount) {
toast.error('Please fill in all fields');
return;
}
const item: FinancialLineItem = {
id: Date.now().toString(),
department: newReceivable.department,
description: newReceivable.description,
amount: parseFloat(newReceivable.amount)
};
setReceivableItems([...receivableItems, item]);
setNewReceivable({ department: '', description: '', amount: '' });
toast.success('Receivable item added');
try {
const response = await API.addLineItem(fnfId, {
department: newReceivable.department,
remarks: newReceivable.description,
amount: Math.abs(parseFloat(newReceivable.amount)) // Receivable is positive
});
const data = response.data as any;
if (data.success) {
setReceivableItems([...receivableItems, {
id: data.lineItem.id,
department: data.lineItem.department,
description: data.lineItem.remarks,
amount: data.lineItem.amount
}]);
setNewReceivable({ department: '', description: '', amount: '' });
toast.success('Receivable item added');
}
} catch (error) {
toast.error('Failed to add receivable item');
}
};
const handleUpdateReceivable = (id: string, field: keyof FinancialLineItem, value: string | number) => {
setReceivableItems(receivableItems.map(item =>
const handleUpdateReceivable = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
const updatedItems = receivableItems.map(item =>
item.id === id ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) : value } : item
));
);
setReceivableItems(updatedItems);
try {
const item = updatedItems.find(i => i.id === id);
if (item) {
await API.updateLineItem(id, {
department: item.department,
remarks: item.description,
amount: Math.abs(item.amount)
});
}
} catch (error) {
toast.error('Failed to update item');
fetchFnFDetails();
}
};
const handleDeleteReceivable = (id: string) => {
setReceivableItems(receivableItems.filter(item => item.id !== id));
toast.info('Receivable item removed');
const handleDeleteReceivable = async (id: string) => {
try {
await API.deleteLineItem(id);
setReceivableItems(receivableItems.filter(item => item.id !== id));
toast.info('Receivable item removed');
} catch (error) {
toast.error('Failed to delete item');
}
};
// Handlers for Deductions
const handleAddDeduction = () => {
const handleAddDeduction = async () => {
if (!newDeduction.department || !newDeduction.description || !newDeduction.amount) {
toast.error('Please fill in all fields');
return;
}
const item: FinancialLineItem = {
id: Date.now().toString(),
department: newDeduction.department,
description: newDeduction.description,
amount: parseFloat(newDeduction.amount)
};
setDeductionItems([...deductionItems, item]);
setNewDeduction({ department: '', description: '', amount: '' });
toast.success('Deduction item added');
try {
const response = await API.addLineItem(fnfId, {
department: newDeduction.department,
remarks: newDeduction.description,
amount: Math.abs(parseFloat(newDeduction.amount)) // Deductions are positive (act as receivables)
});
const data = response.data as any;
if (data.success) {
setDeductionItems([...deductionItems, {
id: data.lineItem.id,
department: data.lineItem.department,
description: data.lineItem.remarks,
amount: data.lineItem.amount
}]);
setNewDeduction({ department: '', description: '', amount: '' });
toast.success('Deduction item added');
}
} catch (error) {
toast.error('Failed to add deduction item');
}
};
const handleUpdateDeduction = (id: string, field: keyof FinancialLineItem, value: string | number) => {
setDeductionItems(deductionItems.map(item =>
const handleUpdateDeduction = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
const updatedItems = deductionItems.map(item =>
item.id === id ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) : value } : item
));
);
setDeductionItems(updatedItems);
try {
const item = updatedItems.find(i => i.id === id);
if (item) {
await API.updateLineItem(id, {
department: item.department,
remarks: item.description,
amount: Math.abs(item.amount)
});
}
} catch (error) {
toast.error('Failed to update item');
fetchFnFDetails();
}
};
const handleDeleteDeduction = (id: string) => {
setDeductionItems(deductionItems.filter(item => item.id !== id));
toast.info('Deduction item removed');
const handleDeleteDeduction = async (id: string) => {
try {
await API.deleteLineItem(id);
setDeductionItems(deductionItems.filter(item => item.id !== id));
toast.info('Deduction item removed');
} catch (error) {
toast.error('Failed to delete item');
}
};
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -338,6 +425,23 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
setTimeout(() => onBack(), 1500);
};
if (loading) {
return (
<div className="flex items-center justify-center p-12">
<Loader2 className="w-8 h-8 animate-spin text-amber-600" />
</div>
);
}
if (!fnfCase) {
return (
<div className="text-center py-12 text-slate-500">
<p>Settlement case not found</p>
<Button onClick={onBack} className="mt-4">Go Back</Button>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
@ -1012,7 +1116,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<Card className="border-2 border-blue-300 bg-blue-50">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calculator className="w-5 h-5 text-blue-600" />
<CheckCircle className="w-5 h-5 text-blue-600" />
Final Settlement Summary
</CardTitle>
</CardHeader>
@ -1204,7 +1308,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</CardHeader>
<CardContent>
<div className="space-y-2">
{fnfCase.documents.map((doc, index) => (
{fnfCase.documents.map((doc: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border border-slate-200">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-slate-400" />
@ -1257,7 +1361,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
{uploadedDocuments.length > 0 && (
<div className="space-y-2">
<Label>Uploaded Documents</Label>
{uploadedDocuments.map((doc, index) => (
{uploadedDocuments.map((doc: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-green-600" />

View File

@ -1,4 +1,6 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { API } from '../../api/API';
import { Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@ -23,99 +25,28 @@ import {
} from '../ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import {
DollarSign,
CheckCircle,
XCircle,
AlertCircle,
TrendingUp,
TrendingDown,
Calculator,
FileText,
User,
MapPin,
Calendar,
IndianRupee,
Wallet,
CreditCard,
Receipt
Receipt,
Calculator,
TrendingUp,
TrendingDown,
MapPin
} from 'lucide-react';
import { toast } from 'sonner';
// Mock F&F cases data
const mockFnFCases = [
{
id: 'FNF-2025-001',
dealerCode: 'RE-MUM-001',
dealerName: 'Rajesh Motors',
location: 'Mumbai, Maharashtra',
terminationType: 'Resignation',
submittedDate: '2025-10-01',
status: 'Pending Finance Review',
financialData: {
securityDeposit: 500000,
inventoryValue: 2500000,
equipmentValue: 800000,
outstandingInvoices: 350000,
warrantyPending: 125000,
serviceDues: 80000,
partsDues: 150000,
advancesGiven: 0,
penalties: 50000,
otherCharges: 25000,
},
},
{
id: 'FNF-2025-002',
dealerCode: 'RE-DEL-002',
dealerName: 'Capital Enfield',
location: 'Delhi, NCR',
terminationType: 'Termination',
submittedDate: '2025-10-03',
status: 'Pending Finance Review',
financialData: {
securityDeposit: 750000,
inventoryValue: 1800000,
equipmentValue: 600000,
outstandingInvoices: 520000,
warrantyPending: 95000,
serviceDues: 120000,
partsDues: 200000,
advancesGiven: 100000,
penalties: 150000,
otherCharges: 75000,
},
},
{
id: 'FNF-2025-003',
dealerCode: 'RE-BLR-003',
dealerName: 'Bangalore Bikes',
location: 'Bangalore, Karnataka',
terminationType: 'Resignation',
submittedDate: '2025-09-28',
status: 'Settlement Approved',
settlementAmount: 425000,
settlementType: 'Payable to Dealer',
approvedDate: '2025-10-05',
financialData: {
securityDeposit: 600000,
inventoryValue: 3000000,
equipmentValue: 900000,
outstandingInvoices: 280000,
warrantyPending: 150000,
serviceDues: 95000,
partsDues: 180000,
advancesGiven: 50000,
penalties: 0,
otherCharges: 20000,
},
},
];
// Using live data from API instead of mockFnFCases
interface FinanceFnFPageProps {
onViewFnFDetails?: (fnfId: string) => void;
}
export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
const [settlements, setSettlements] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCase, setSelectedCase] = useState<any>(null);
const [showReviewDialog, setShowReviewDialog] = useState(false);
const [showDetailsDialog, setShowDetailsDialog] = useState(false);
@ -123,42 +54,62 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
const [finalNotes, setFinalNotes] = useState('');
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'approved'>('all');
const filteredCases = mockFnFCases.filter(fnf => {
useEffect(() => {
fetchSettlements();
}, []);
const fetchSettlements = async () => {
try {
setLoading(true);
const response = await API.getFnFSettlements();
const data = response.data as any;
if (data.success) {
setSettlements(data.settlements || []);
}
} catch (error) {
console.error('Fetch settlements error:', error);
toast.error('Failed to fetch settlement cases');
} finally {
setLoading(false);
}
};
const getMappedData = (s: any) => ({
id: s.id,
dealerCode: s.outlet?.code || 'N/A',
dealerName: s.outlet?.dealer?.name || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A',
terminationType: s.resignationId ? 'Resignation' : 'Termination',
submittedDate: new Date(s.createdAt).toLocaleDateString(),
status: s.status === 'Calculated' ? 'Pending Finance Review' : (s.status === 'Settled' ? 'Settled' : s.status),
financialData: {
totalPayables: parseFloat(s.totalPayables) || 0,
totalReceivables: parseFloat(s.totalReceivables) || 0,
netAmount: parseFloat(s.netAmount) || 0,
},
settlementAmount: Math.abs(parseFloat(s.netAmount) || 0),
settlementType: parseFloat(s.netAmount) > 0 ? 'Payable to Dealer' : 'Receivable from Dealer',
approvedDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : null
});
const displaySettlements = settlements.map(getMappedData);
const filteredCases = displaySettlements.filter(fnf => {
if (filterStatus === 'all') return true;
if (filterStatus === 'pending') return fnf.status === 'Pending Finance Review';
if (filterStatus === 'approved') return fnf.status === 'Settlement Approved';
if (filterStatus === 'pending') return fnf.status === 'Pending Finance Review' || fnf.status === 'Calculated' || fnf.status === 'Initiated' || fnf.status === 'Under Review';
if (filterStatus === 'approved') return fnf.status === 'Settled' || fnf.status === 'Completed';
return true;
});
const calculateSettlement = (data: any) => {
// Amounts dealer needs to pay back (Receivables from dealer)
const receivables =
data.outstandingInvoices +
data.serviceDues +
data.partsDues +
data.advancesGiven +
data.penalties +
data.otherCharges;
// Amounts company needs to pay back (Payables to dealer)
const payables =
data.securityDeposit +
data.inventoryValue +
data.equipmentValue;
// Pending warranty claims (to be deducted)
const deductions = data.warrantyPending;
// Net settlement = Payables - Receivables - Deductions
const netSettlement = payables - receivables - deductions;
// Backend already provides these, but keep for UI consistency
return {
receivables,
payables,
deductions,
netSettlement,
settlementType: netSettlement > 0 ? 'Payable to Dealer' : 'Receivable from Dealer',
settlementAmount: Math.abs(netSettlement),
receivables: data.totalReceivables || 0,
payables: data.totalPayables || 0,
deductions: 0, // Backend sums deductions into receivables or payables
netSettlement: data.netAmount || 0,
settlementType: (data.netAmount || 0) > 0 ? 'Payable to Dealer' : 'Receivable from Dealer',
settlementAmount: Math.abs(data.netAmount || 0),
};
};
@ -191,8 +142,16 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
setSelectedCase(null);
};
const pendingCount = mockFnFCases.filter(fnf => fnf.status === 'Pending Finance Review').length;
const approvedCount = mockFnFCases.filter(fnf => fnf.status === 'Settlement Approved').length;
const pendingCount = displaySettlements.filter(fnf => fnf.status === 'Pending Finance Review' || fnf.status === 'Calculated' || fnf.status === 'Initiated' || fnf.status === 'Under Review').length;
const approvedCount = displaySettlements.filter(fnf => fnf.status === 'Settled' || fnf.status === 'Completed').length;
if (loading) {
return (
<div className="flex items-center justify-center p-12">
<Loader2 className="w-8 h-8 animate-spin text-amber-600" />
</div>
);
}
return (
<div className="p-6 space-y-6">
@ -234,7 +193,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-slate-900 text-2xl">{mockFnFCases.length}</div>
<div className="text-slate-900 text-2xl">{displaySettlements.length}</div>
<FileText className="w-8 h-8 text-blue-600" />
</div>
</CardContent>
@ -260,7 +219,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
onClick={() => setFilterStatus('all')}
className={filterStatus === 'all' ? 'bg-amber-600 hover:bg-amber-700' : ''}
>
All Cases ({mockFnFCases.length})
All Cases ({displaySettlements.length})
</Button>
<Button
variant={filterStatus === 'pending' ? 'default' : 'outline'}

View File

@ -1,17 +1,15 @@
import { ArrowLeft, Check, FileText, Calendar, DollarSign, AlertCircle, Upload, Send, Clock, Users, FileCheck, MessageSquare, Handshake, CheckCircle2 } from 'lucide-react';
import { ArrowLeft, Check, FileText, Send, Clock, Users, FileCheck, MessageSquare, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { Badge } from '../ui/badge';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
import { Input } from '../ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { Progress } from '../ui/progress';
import { useState } from 'react';
import { User, mockFnFCases, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
import { useState, useEffect } from 'react';
import { User, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
import { API } from '../../api/API';
import { WorkNotesPage } from './WorkNotesPage';
import { toast } from 'sonner';
@ -22,10 +20,67 @@ interface FnFDetailsProps {
}
export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const [fnfCase, setFnfCase] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
// Find the F&F case
const fnfCase = mockFnFCases.find(c => c.id === fnfId);
useEffect(() => {
fetchFnFDetails();
}, [fnfId]);
const fetchFnFDetails = async () => {
try {
setLoading(true);
const response = await API.getFnFSettlementById(fnfId);
const data = response.data as any;
if (data.success) {
const s = data.settlement;
// Map backend data to UI format
const mappedCase = {
id: s.id,
caseNumber: s.id.substring(0, 8).toUpperCase(),
status: s.status,
requestType: s.resignationId ? 'Resignation' : 'Termination',
dealerName: s.outlet?.dealer?.name || 'N/A',
dealerCode: s.outlet?.code || 'N/A',
dealershipName: s.outlet?.name || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A',
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.id || 'N/A',
submittedOn: new Date(s.createdAt).toLocaleDateString(),
lastOperationalDateSales: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A',
lastOperationalDateServices: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A',
typeOfClosure: s.resignationId ? 'Voluntary' : 'Involuntary',
gst: s.outlet?.dealer?.pan || 'N/A', // Using PAN as placeholder if GST not available
financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending',
totalPayableAmount: parseFloat(s.totalPayables) || 0,
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
departmentResponses: (s.lineItems || []).map((li: any) => ({
id: li.id,
departmentName: li.department,
status: li.remarks && li.remarks.toLowerCase().includes('no dues') ? 'No Dues' : (li.amount > 0 ? 'Dues' : 'Pending'),
amountType: li.amount > 0 ? 'Recovery Amount' : null,
amount: Math.abs(parseFloat(li.amount)) || 0,
submittedDate: li.updatedAt ? new Date(li.updatedAt).toLocaleDateString() : null,
remarks: li.remarks
}))
};
setFnfCase(mappedCase);
}
} catch (error) {
console.error('Fetch F&F details error:', error);
toast.error('Failed to fetch settlement details');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center p-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
if (!fnfCase) {
return (
@ -83,7 +138,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
}
};
const responsesReceived = fnfCase.departmentResponses.filter(d => d.status !== 'Pending').length;
const responsesReceived = fnfCase.departmentResponses.filter((d: any) => d.status !== 'Pending').length;
const totalDepartments = fnfCase.departmentResponses.length;
const progressPercentage = (responsesReceived / totalDepartments) * 100;
@ -131,19 +186,19 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div>
<p className="text-slate-600 text-sm">No Dues</p>
<p className="text-2xl text-green-600">
{fnfCase.departmentResponses.filter(d => d.status === 'No Dues').length}
{fnfCase.departmentResponses.filter((d: any) => d.status === 'No Dues').length}
</p>
</div>
<div>
<p className="text-slate-600 text-sm">Dues</p>
<p className="text-2xl text-red-600">
{fnfCase.departmentResponses.filter(d => d.status === 'Dues').length}
{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues').length}
</p>
</div>
<div>
<p className="text-slate-600 text-sm">Pending</p>
<p className="text-2xl text-slate-600">
{fnfCase.departmentResponses.filter(d => d.status === 'Pending').length}
{fnfCase.departmentResponses.filter((d: any) => d.status === 'Pending').length}
</p>
</div>
<div>
@ -281,15 +336,15 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="grid grid-cols-3 gap-3 text-sm">
<div className="text-center p-2 bg-green-100 rounded">
<p className="text-green-700">No Dues</p>
<p className="text-green-900">{fnfCase.departmentResponses.filter(d => d.status === 'No Dues').length}</p>
<p className="text-green-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'No Dues').length}</p>
</div>
<div className="text-center p-2 bg-red-100 rounded">
<p className="text-red-700">Dues</p>
<p className="text-red-900">{fnfCase.departmentResponses.filter(d => d.status === 'Dues').length}</p>
<p className="text-red-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues').length}</p>
</div>
<div className="text-center p-2 bg-slate-100 rounded">
<p className="text-slate-700">Pending</p>
<p className="text-slate-900">{fnfCase.departmentResponses.filter(d => d.status === 'Pending').length}</p>
<p className="text-slate-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'Pending').length}</p>
</div>
</div>
</div>
@ -686,7 +741,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</TableRow>
</TableHeader>
<TableBody>
{fnfCase.departmentResponses.map((dept) => (
{fnfCase.departmentResponses.map((dept: any) => (
<TableRow key={dept.id}>
<TableCell>{dept.departmentName}</TableCell>
<TableCell>
@ -819,7 +874,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</TableRow>
</TableHeader>
<TableBody>
{mockDocuments.map((doc) => (
{mockDocuments.map((doc: any) => (
<TableRow key={doc.id}>
<TableCell>
<div className="flex items-center gap-2">
@ -859,7 +914,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</CardHeader>
<CardContent>
<div className="space-y-4">
{mockAuditLogs.map((log) => (
{mockAuditLogs.map((log: any) => (
<div key={log.id} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className="w-2 h-2 rounded-full bg-blue-600 mt-2" />
<div className="flex-1">

View File

@ -1,9 +1,11 @@
import { DollarSign, Calendar, Building, Eye, Send, FileCheck } from 'lucide-react';
import { useState, useEffect } from 'react';
import { DollarSign, Calendar, Eye, Send, FileCheck, Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { User, mockFnFCases } from '../../lib/mock-data';
import { User } from '../../lib/mock-data';
import { API } from '../../api/API';
import { toast } from 'sonner';
interface FnFPageProps {
@ -33,14 +35,66 @@ const getTypeColor = (type: string) => {
};
export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
// Check if user can send to stakeholders (DD Lead and above, not Finance)
const [settlements, setSettlements] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const canSendToStakeholders = currentUser &&
['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
useEffect(() => {
fetchSettlements();
}, []);
const fetchSettlements = async () => {
try {
setLoading(true);
const response = await API.getFnFSettlements();
const data = response.data as any;
if (data.success) {
setSettlements(data.settlements || []);
}
} catch (error) {
console.error('Fetch settlements error:', error);
toast.error('Failed to fetch settlement cases');
} finally {
setLoading(false);
}
};
const handleSendToStakeholders = (caseId: string) => {
console.log('Sending to stakeholders for case:', caseId);
toast.success('Notifications sent to all stakeholders');
};
if (loading) {
return (
<div className="flex items-center justify-center p-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
// Helper to map backend data to UI-friendly format
const getMappedData = (s: any) => ({
id: s.id,
caseNumber: s.id.substring(0, 8).toUpperCase(),
status: s.status,
requestType: s.resignationId ? 'Resignation' : 'Termination',
dealerName: s.outlet?.dealer?.name || 'N/A',
dealerCode: s.outlet?.code || 'N/A',
dealershipName: s.outlet?.name || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A',
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.id || 'N/A',
submittedOn: new Date(s.createdAt).toLocaleDateString(),
financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending',
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
totalPayableAmount: parseFloat(s.totalPayables) || 0,
completedOn: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : null,
departmentResponses: s.lineItems || []
});
const displaySettlements: any[] = settlements.map(getMappedData);
return (
<div className="space-y-6">
{/* Header Stats */}
@ -49,7 +103,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardHeader className="pb-3">
<CardDescription>New Cases</CardDescription>
<CardTitle className="text-3xl text-blue-600">
{mockFnFCases.filter(c => c.status === 'New').length}
{displaySettlements.filter(c => c.status === 'Initiated' || c.status === 'New').length}
</CardTitle>
</CardHeader>
<CardContent>
@ -61,7 +115,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardHeader className="pb-3">
<CardDescription>In Progress</CardDescription>
<CardTitle className="text-3xl text-yellow-600">
{mockFnFCases.filter(c => c.status === 'In Progress').length}
{displaySettlements.filter(c => c.status === 'In Progress').length}
</CardTitle>
</CardHeader>
<CardContent>
@ -73,7 +127,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardHeader className="pb-3">
<CardDescription>Under Review</CardDescription>
<CardTitle className="text-3xl text-orange-600">
{mockFnFCases.filter(c => c.status === 'Under Review').length}
{displaySettlements.filter(c => c.status === 'Under Review' || c.status === 'Calculated').length}
</CardTitle>
</CardHeader>
<CardContent>
@ -85,18 +139,18 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardHeader className="pb-3">
<CardDescription>Completed</CardDescription>
<CardTitle className="text-3xl text-green-600">
{mockFnFCases.filter(c => c.status === 'Completed').length}
{displaySettlements.filter(c => c.status === 'Completed' || c.status === 'Settled').length}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">Finalized</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>All Cases</CardDescription>
<CardTitle className="text-3xl">{mockFnFCases.length}</CardTitle>
<CardTitle className="text-3xl">{displaySettlements.length}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">Total</p>
@ -126,8 +180,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
{/* New Cases Tab */}
<TabsContent value="new" className="mt-6">
<div className="space-y-4">
{mockFnFCases
.filter(c => c.status === 'New')
{displaySettlements
.filter(c => c.status === 'New' || c.status === 'Initiated')
.map((fnfCase) => (
<Card key={fnfCase.id} className="border-slate-200">
<CardContent className="pt-6">
@ -206,7 +260,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</CardContent>
</Card>
))}
{mockFnFCases.filter(c => c.status === 'New').length === 0 && (
{displaySettlements.filter((c: any) => c.status === 'New' || c.status === 'Initiated').length === 0 && (
<div className="text-center py-12 text-slate-500">
<FileCheck className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p>No new cases to display</p>
@ -218,7 +272,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
{/* All Cases Tab */}
<TabsContent value="all" className="mt-6">
<div className="space-y-4">
{mockFnFCases.map((fnfCase) => (
{displaySettlements.map((fnfCase) => (
<Card key={fnfCase.id} className="border-slate-200">
<CardContent className="pt-6">
<div className="flex items-start justify-between">
@ -297,7 +351,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
{/* In Progress Tab */}
<TabsContent value="progress" className="mt-6">
<div className="space-y-4">
{mockFnFCases
{displaySettlements
.filter(c => c.status === 'In Progress')
.map((fnfCase) => (
<Card key={fnfCase.id} className="border-slate-200">
@ -325,7 +379,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<div>
<p className="text-slate-600">Departments Responded</p>
<p>
{fnfCase.departmentResponses.filter(d => d.status !== 'Pending').length} / {fnfCase.departmentResponses.length}
{fnfCase.departmentResponses.filter((d: any) => d.status !== 'Pending').length} / {fnfCase.departmentResponses.length}
</p>
</div>
<div>
@ -348,7 +402,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</CardContent>
</Card>
))}
{mockFnFCases.filter(c => c.status === 'In Progress').length === 0 && (
{displaySettlements.filter((c: any) => c.status === 'In Progress').length === 0 && (
<div className="text-center py-12 text-slate-500">
<DollarSign className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p>No cases in progress</p>
@ -360,8 +414,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
{/* Under Review Tab */}
<TabsContent value="review" className="mt-6">
<div className="space-y-4">
{mockFnFCases
.filter(c => c.status === 'Under Review')
{displaySettlements
.filter(c => c.status === 'Under Review' || c.status === 'Calculated')
.map((fnfCase) => (
<Card key={fnfCase.id} className="border-slate-200">
<CardContent className="pt-6">
@ -413,7 +467,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</CardContent>
</Card>
))}
{mockFnFCases.filter(c => c.status === 'Under Review').length === 0 && (
{displaySettlements.filter(c => c.status === 'Under Review' || c.status === 'Calculated').length === 0 && (
<div className="text-center py-12 text-slate-500">
<DollarSign className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p>No cases under review</p>
@ -425,8 +479,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
{/* Completed Tab */}
<TabsContent value="completed" className="mt-6">
<div className="space-y-4">
{mockFnFCases
.filter(c => c.status === 'Completed')
{displaySettlements
.filter(c => c.status === 'Completed' || c.status === 'Settled')
.map((fnfCase) => (
<Card key={fnfCase.id} className="border-slate-200">
<CardContent className="pt-6">
@ -474,7 +528,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</CardContent>
</Card>
))}
{mockFnFCases.filter(c => c.status === 'Completed').length === 0 && (
{displaySettlements.filter(c => c.status === 'Completed' || c.status === 'Settled').length === 0 && (
<div className="text-center py-12 text-slate-500">
<FileCheck className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p>No completed cases</p>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { ArrowLeft, FileText, Calendar, User, Building2, CheckCircle2, Clock, AlertCircle, Upload, Download, Eye, Navigation, MapPin, GitBranch, MessageSquare } from 'lucide-react';
import { ArrowLeft, CheckCircle2, Clock, AlertCircle, Upload, Download, Eye, Navigation, MapPin, GitBranch, MessageSquare, Loader2 } from 'lucide-react';
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
@ -8,10 +8,10 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Textarea } from '../ui/textarea';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner';
import { mockRelocationRequests } from './RelocationRequestPage';
import { API } from '../../api/API';
interface RelocationRequestDetailsProps {
requestId: string;
@ -20,7 +20,7 @@ interface RelocationRequestDetailsProps {
onOpenWorknote?: (requestId: string, requestType: 'relocation' | 'constitutional-change' | 'fnf' | 'resignation' | 'termination', requestTitle: string) => void;
}
// Workflow stages as per the process flow (12 stages with parallel branches)
// Workflow stages configuration
const workflowStages = [
{ id: 1, name: 'Request Created', key: 'created', role: 'Dealer' },
{ id: 2, name: 'ASM Review', key: 'asm', role: 'ASM' },
@ -59,7 +59,7 @@ const workflowStages = [
{ id: 11, name: 'Relocation Complete', key: 'complete', role: 'System' }
];
// Required documents list
// Required documents configuration
const requiredDocuments = [
'Property documents for new location',
'Lease/Rental agreement for new location',
@ -75,127 +75,49 @@ const requiredDocuments = [
'Water supply documents'
];
// Mock uploaded documents
const mockUploadedDocuments = [
{ id: 1, name: 'Property_Documents.pdf', uploadedOn: '2025-12-20', uploadedBy: 'Dealer', status: 'Verified', category: 'Property' },
{ id: 2, name: 'Lease_Agreement.pdf', uploadedOn: '2025-12-20', uploadedBy: 'Dealer', status: 'Verified', category: 'Property' },
{ id: 3, name: 'NOC_Current_Landlord.pdf', uploadedOn: '2025-12-21', uploadedBy: 'Dealer', status: 'Pending Verification', category: 'Legal' },
{ id: 4, name: 'Municipal_Approval.pdf', uploadedOn: '2025-12-21', uploadedBy: 'Dealer', status: 'Verified', category: 'Statutory' },
{ id: 5, name: 'Floor_Plan.pdf', uploadedOn: '2025-12-22', uploadedBy: 'Dealer', status: 'Verified', category: 'Infrastructure' }
];
// Mock workflow history
const mockWorkflowHistory = [
{
stage: 'Request Created',
actor: 'Amit Sharma (Dealer)',
action: 'Created',
date: '2025-12-20 10:30 AM',
comments: 'Submitted relocation request to move from Bandra West to Andheri East',
status: 'Completed'
},
{
stage: 'ASM Review',
actor: 'Rajesh Kumar (ASM)',
action: 'Approved',
date: '2025-12-21 02:15 PM',
comments: 'Verified proposed location and approved for next stage',
status: 'Completed'
},
{
stage: 'RBM Review',
actor: 'Priya Sharma (RBM)',
action: 'Approved',
date: '2025-12-22 11:00 AM',
comments: 'Location feasibility checked and approved',
status: 'Completed'
},
{
stage: 'DD ZM Review',
actor: 'Suresh Patel (DD-ZM)',
action: 'Under Review',
date: '2025-12-23 09:00 AM',
comments: 'Reviewing market potential of new location',
status: 'In Progress'
}
];
// Mock worknotes - Discussion platform for this request
const initialWorknotes = [
{
id: 1,
user: 'Rajesh Kumar',
role: 'ASM',
message: 'I have visited the proposed location. The area has good visibility and footfall. However, parking might be a concern during peak hours.',
timestamp: '2025-12-21 10:30 AM',
avatar: 'RK'
},
{
id: 2,
user: 'Priya Sharma',
role: 'RBM',
message: 'Thanks for the site visit update. Can we get clarity on the parking arrangements from the dealer?',
timestamp: '2025-12-21 03:45 PM',
avatar: 'PS'
},
{
id: 3,
user: 'Amit Sharma',
role: 'Dealer',
message: 'We have secured dedicated parking for 15 bikes in the basement. Additionally, there\'s street parking available during non-peak hours.',
timestamp: '2025-12-22 09:15 AM',
avatar: 'AS'
},
{
id: 4,
user: 'Suresh Patel',
role: 'DD-ZM',
message: 'Good to know about parking. What about the competition analysis in the new area? Any other Royal Enfield dealers nearby?',
timestamp: '2025-12-23 11:00 AM',
avatar: 'SP'
},
{
id: 5,
user: 'Amit Sharma',
role: 'Dealer',
message: 'Nearest RE dealer is 8km away in Powai. This location will help us tap into the Andheri East market which is currently underserved.',
timestamp: '2025-12-23 02:20 PM',
avatar: 'AS'
}
];
const getStatusColor = (status: string) => {
if (status === 'Completed' || status === 'Verified') return 'bg-green-100 text-green-700 border-green-300';
if (status === 'Completed' || status === 'Verified' || status === 'Closed') return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300';
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
if (status.includes('Collection') || status.includes('Completion')) return 'bg-blue-100 text-blue-700 border-blue-300';
if (status.includes('Collection') || status.includes('Completion') || status.includes('Infra')) return 'bg-blue-100 text-blue-700 border-blue-300';
return 'bg-slate-100 text-slate-700 border-slate-300';
};
export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpenWorknote }: RelocationRequestDetailsProps) {
const [request, setRequest] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
const [comments, setComments] = useState('');
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [isWorknoteDialogOpen, setIsWorknoteDialogOpen] = useState(false);
const [worknotes, setWorknotes] = useState(initialWorknotes);
const [worknotes, setWorknotes] = useState<any[]>([]);
const [newWorknote, setNewWorknote] = useState('');
// Find the request
const request = mockRelocationRequests.find(r => r.id === requestId);
useEffect(() => {
fetchRequestDetails();
}, [requestId]);
if (!request) {
return (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Request Not Found</h2>
<p className="text-slate-600 mb-4">The relocation request you're looking for doesn't exist.</p>
<Button onClick={onBack}>Go Back</Button>
</div>
);
}
const fetchRequestDetails = async () => {
try {
setIsLoading(true);
const response = await API.getRelocationRequestById(requestId) as any;
if (response.data.success) {
setRequest(response.data.request);
setWorknotes(response.data.request.worknotes || []);
}
} catch (error) {
console.error('Fetch relocation request details error:', error);
toast.error('Failed to fetch request details');
} finally {
setIsLoading(false);
}
};
// Calculate current stage index
// Calculate current stage index based on request data
const getCurrentStageIndex = () => {
if (!request) return 1;
const stageMap: Record<string, number> = {
'Dealer': 1,
'ASM': 2,
@ -207,49 +129,97 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
'NBH': 8,
'DD H.O': 9, // Parallel branch A
'Architect': 9, // Parallel branch B
'Closed': 11
'Closed': 11,
'Completed': 11
};
return stageMap[request.currentStage] || 1;
};
const currentStageIndex = getCurrentStageIndex();
if (!request) {
return (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Request Not Found</h2>
<p className="text-slate-600 mb-4">The relocation request you're looking for doesn't exist.</p>
<Button onClick={onBack}>Go Back</Button>
</div>
);
}
const handleAction = (type: 'approve' | 'reject' | 'hold') => {
setActionType(type);
setIsActionDialogOpen(true);
};
const handleSubmitAction = (e: React.FormEvent) => {
const handleSubmitAction = async (e: React.FormEvent) => {
e.preventDefault();
const actionText = actionType === 'approve' ? 'approved' : actionType === 'reject' ? 'rejected' : 'put on hold';
toast.success(`Request ${actionText} successfully`);
setIsActionDialogOpen(false);
setComments('');
};
const handleUploadDocument = () => {
toast.success('Document uploaded successfully');
setIsUploadDialogOpen(false);
};
const handleAddWorknote = () => {
if (newWorknote.trim()) {
const newNote = {
id: worknotes.length + 1,
user: currentUser?.name || 'Anonymous',
role: currentUser?.role || 'User',
message: newWorknote,
timestamp: new Date().toLocaleString(),
avatar: currentUser?.name?.slice(0, 2).toUpperCase() || 'AN'
};
setWorknotes([...worknotes, newNote]);
setNewWorknote('');
toast.success('Worknote added successfully');
try {
setIsSubmitting(true);
const action = actionType === 'approve' ? 'APPROVE' : actionType === 'reject' ? 'REJECT' : 'HOLD';
const response = await API.updateRelocationRequest(requestId, action, { remarks: comments }) as any;
if (response.data.success) {
toast.success(`Request ${actionType}d successfully`);
setIsActionDialogOpen(false);
setComments('');
fetchRequestDetails();
}
} catch (error) {
console.error('Submit action error:', error);
toast.error('Failed to submit action');
} finally {
setIsSubmitting(false);
}
};
const handleAddWorknote = async () => {
if (newWorknote.trim()) {
try {
const response = await API.addWorknote({
requestId,
requestType: 'relocation',
message: newWorknote
}) as any;
if (response.data.success) {
setNewWorknote('');
fetchRequestDetails();
toast.success('Worknote added successfully');
}
} catch (error) {
console.error('Add worknote error:', error);
toast.error('Failed to add worknote');
}
}
};
const handleUploadDocument = async () => {
// In a real app, this would use API.uploadDocument
toast.success('Document uploaded successfully');
setIsUploadDialogOpen(false);
fetchRequestDetails();
};
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] space-y-4">
<Loader2 className="w-10 h-10 text-amber-600 animate-spin" />
<p className="text-slate-500 font-medium">Loading request details...</p>
</div>
);
}
if (!request) {
return (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<h2 className="text-slate-900 mb-2">Request Not Found</h2>
<p className="text-slate-600 mb-4">The relocation request you're looking for doesn't exist.</p>
<Button onClick={onBack}>Go Back</Button>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
@ -264,9 +234,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
Back
</Button>
<div>
<h1 className="text-slate-900">{request.id} - Relocation Request Details</h1>
<h1 className="text-slate-900">{request.requestId} - Relocation Request Details</h1>
<p className="text-slate-600">
{request.dealerName} ({request.dealerCode})
{request.outlet?.name} ({request.outlet?.code})
</p>
</div>
</div>
@ -284,8 +254,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p className="text-slate-600 text-sm mb-1">Dealer Details</p>
<p className="text-slate-900">{request.dealerName}</p>
<p className="text-slate-600 text-sm">{request.dealerCode}</p>
<p className="text-slate-900">{request.outlet?.name}</p>
<p className="text-slate-600 text-sm">{request.outlet?.code}</p>
<p className="text-slate-600 text-xs mt-1">{request.dealer?.fullName}</p>
</div>
<div>
<p className="text-slate-600 text-sm mb-2">Relocation Route</p>
@ -293,27 +264,27 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-slate-400" />
<div>
<p className="text-slate-600 text-xs">From</p>
<p className="text-slate-900 text-sm">{request.currentLocation}</p>
<p className="text-slate-600 text-xs">From (Current)</p>
<p className="text-slate-900 text-sm">{request.outlet?.address}, {request.outlet?.city}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Navigation className="w-4 h-4 text-amber-600" />
<div>
<p className="text-slate-600 text-xs">To</p>
<p className="text-slate-900 text-sm">{request.proposedLocation}</p>
<p className="text-slate-600 text-xs">To (Proposed)</p>
<p className="text-slate-900 text-sm">{request.newAddress}, {request.newCity}</p>
</div>
</div>
<Badge variant="outline" className="border-slate-300 text-slate-700">
Distance: {request.distance}
Type: {request.relocationType}
</Badge>
</div>
</div>
<div>
<p className="text-slate-600 text-sm mb-1">Request Information</p>
<p className="text-slate-900 text-sm">Submitted: {request.submittedOn}</p>
<p className="text-slate-600 text-sm">By: {request.submittedBy}</p>
<p className="text-slate-900 text-sm mt-2">Current Stage: {request.currentStage}</p>
<p className="text-slate-900 text-sm">Submitted: {new Date(request.createdAt).toLocaleDateString()}</p>
<p className="text-slate-600 text-sm">By: {request.dealer?.fullName}</p>
<p className="text-slate-900 text-sm mt-2">Current Stage: {request.currentStage.replace(/_/g, ' ')}</p>
</div>
</div>
@ -358,10 +329,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{/* Workflow Stages */}
<div className="space-y-4">
{workflowStages.map((stage, index) => {
{workflowStages.map((stage: any, index: number) => {
const isCompleted = index < currentStageIndex - 1;
const isCurrent = index === currentStageIndex - 1;
const isPending = index > currentStageIndex - 1;
// Handle parallel branches
if (stage.isParallel) {
@ -391,7 +361,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{/* Parallel branches */}
<div className="ml-14 grid grid-cols-2 gap-4">
{stage.branches?.map((branch, branchIndex) => (
{stage.branches?.map((branch: any, branchIndex: number) => (
<div key={branchIndex} className={`border-2 rounded-lg p-4 ${
branch.color === 'blue' ? 'border-blue-200 bg-blue-50' : 'border-green-200 bg-green-50'
}`}>
@ -404,11 +374,11 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</h5>
</div>
<div className="space-y-3">
{branch.stages.map((subStage) => {
{branch.stages.map((subStage: any) => {
const subIsCompleted = currentStageIndex > 9;
const subIsCurrent = currentStageIndex === 9 &&
((branch.color === 'blue' && request.currentStage === 'DD H.O') ||
(branch.color === 'green' && request.currentStage === 'Architect'));
((branch.color === 'blue' && request.currentStage.includes('H.O')) ||
(branch.color === 'green' && request.currentStage.includes('Arch')));
return (
<div key={subStage.id} className="flex items-start gap-3">
@ -558,8 +528,8 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{/* Required Documents Checklist */}
<div className="grid grid-cols-2 gap-2">
{requiredDocuments.map((doc, index) => {
const uploaded = mockUploadedDocuments.find(d => d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0]));
{requiredDocuments.map((doc: string, index: number) => {
const uploaded = request.documents?.find((d: any) => d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0]));
return (
<div
key={index}
@ -584,7 +554,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{/* Existing Documents Sub-tab */}
<TabsContent value="existing" className="mt-0">
{mockUploadedDocuments.length > 0 ? (
{request.documents && request.documents.length > 0 ? (
<div>
<h4 className="text-slate-900 mb-3">All Uploaded Documents</h4>
<div className="border border-slate-200 rounded-lg overflow-hidden">
@ -600,18 +570,18 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</TableRow>
</TableHeader>
<TableBody>
{mockUploadedDocuments.map((doc) => (
{request.documents.map((doc: any) => (
<TableRow key={doc.id}>
<TableCell className="text-slate-900">
{doc.name}
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300">
{doc.category}
{doc.category || 'General'}
</Badge>
</TableCell>
<TableCell className="text-slate-600">
{doc.uploadedOn}
{new Date(doc.uploadedOn).toLocaleDateString()}
</TableCell>
<TableCell className="text-slate-600">
{doc.uploadedBy}
@ -651,36 +621,39 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{/* History Tab */}
<TabsContent value="history" className="mt-0">
<div className="space-y-4">
{mockWorkflowHistory.map((entry, index) => (
{request.timeline && request.timeline.length > 0 ? request.timeline.map((entry: any, index: number) => (
<div key={index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
entry.status === 'Completed' ? 'bg-green-100' :
entry.status === 'In Progress' ? 'bg-amber-100' :
'bg-slate-100'
entry.action.toLowerCase().includes('approve') || entry.action.toLowerCase().includes('submit') ? 'bg-green-100' :
'bg-amber-100'
}`}>
{entry.status === 'Completed' ? (
{entry.action.toLowerCase().includes('approve') ? (
<CheckCircle2 className="w-5 h-5 text-green-600" />
) : entry.action.toLowerCase().includes('submit') ? (
<CheckCircle2 className="w-5 h-5 text-green-600" />
) : entry.status === 'In Progress' ? (
<Clock className="w-5 h-5 text-amber-600" />
) : (
<User className="w-5 h-5 text-slate-600" />
<Clock className="w-5 h-5 text-amber-600" />
)}
</div>
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h4 className="text-slate-900">{entry.stage}</h4>
<p className="text-slate-600 text-sm">{entry.actor}</p>
<h4 className="text-slate-900">{entry.stage || 'Update'}</h4>
<p className="text-slate-600 text-sm">{entry.user}</p>
</div>
<Badge className={getStatusColor(entry.status)}>
<Badge className={getStatusColor(entry.action)}>
{entry.action}
</Badge>
</div>
<p className="text-slate-600 text-sm mt-2">{entry.comments}</p>
<p className="text-slate-500 text-sm mt-1">{entry.date}</p>
<p className="text-slate-600 text-sm mt-2">{entry.remarks || entry.remarks || 'No remarks provided'}</p>
<p className="text-slate-500 text-sm mt-1">{new Date(entry.timestamp).toLocaleString()}</p>
</div>
</div>
))}
)) : (
<div className="text-center py-8 text-slate-500">
No history records available
</div>
)}
</div>
</TabsContent>
</CardContent>
@ -730,8 +703,13 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={() => handleAction('approve')}
disabled={isSubmitting}
>
<CheckCircle2 className="w-4 h-4 mr-2" />
{isSubmitting && actionType === 'approve' ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<CheckCircle2 className="w-4 h-4 mr-2" />
)}
Approve Request
</Button>
@ -739,8 +717,13 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
variant="destructive"
className="w-full"
onClick={() => handleAction('reject')}
disabled={isSubmitting}
>
<AlertCircle className="w-4 h-4 mr-2" />
{isSubmitting && actionType === 'reject' ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<AlertCircle className="w-4 h-4 mr-2" />
)}
Reject Request
</Button>
@ -753,7 +736,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
className="w-full border-blue-300 text-blue-700 hover:bg-blue-50"
onClick={() => {
if (onOpenWorknote) {
onOpenWorknote(requestId, 'relocation', `${request.dealerName} (${request.dealerCode}) - Relocation Request`);
onOpenWorknote(requestId, 'relocation', `${request.outlet?.name} (${request.outlet?.code}) - Relocation Request`);
} else {
setIsWorknoteDialogOpen(true);
}
@ -799,6 +782,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
type="button"
variant="outline"
onClick={() => setIsActionDialogOpen(false)}
disabled={isSubmitting}
>
Cancel
</Button>
@ -809,7 +793,11 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
actionType === 'reject' ? 'bg-red-600 hover:bg-red-700' :
'bg-amber-600 hover:bg-amber-700'
}
disabled={isSubmitting}
>
{isSubmitting ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : null}
{actionType === 'approve' ? 'Approve' :
actionType === 'reject' ? 'Reject' :
'Put on Hold'}
@ -819,6 +807,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</DialogContent>
</Dialog>
{/* Worknotes Dialog */}
<Dialog open={isWorknoteDialogOpen} onOpenChange={setIsWorknoteDialogOpen}>
<DialogContent className="max-w-3xl max-h-[80vh]">
@ -835,27 +824,31 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<Label>Discussion History ({worknotes.length} messages)</Label>
<div className="border border-slate-200 rounded-lg p-4 max-h-96 overflow-y-auto bg-slate-50">
<div className="space-y-4">
{worknotes.map((note) => (
{worknotes.length > 0 ? worknotes.map((note: any) => (
<div key={note.id} className="flex items-start gap-3">
{/* Avatar */}
<div className="w-10 h-10 rounded-full bg-amber-600 flex items-center justify-center text-white flex-shrink-0">
{note.avatar}
{note.author?.fullName?.slice(0, 2).toUpperCase() || 'AN'}
</div>
{/* Message Content */}
<div className="flex-1 bg-white rounded-lg p-3 border border-slate-200">
<div className="flex items-start justify-between mb-1">
<div>
<h5 className="text-slate-900">{note.user}</h5>
<h5 className="text-slate-900">{note.author?.fullName}</h5>
<Badge variant="outline" className="border-slate-300 text-xs">
{note.role}
{note.author?.role || 'User'}
</Badge>
</div>
<span className="text-slate-500 text-xs">{note.timestamp}</span>
<span className="text-slate-500 text-xs">{new Date(note.createdAt).toLocaleString()}</span>
</div>
<p className="text-slate-700 text-sm mt-2">{note.message}</p>
<p className="text-slate-700 text-sm mt-2">{note.noteText}</p>
</div>
</div>
))}
)) : (
<div className="text-center py-4 text-slate-500">
No worknotes yet
</div>
)}
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { FileText, Calendar, Building, Plus, Eye, MapPin, Navigation } from 'lucide-react';
import { FileText, Calendar, Building, Plus, Eye, MapPin, Navigation, Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@ -9,125 +9,29 @@ 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 } from '../../lib/mock-data';
import { toast } from 'sonner';
import { API } from '../../api/API';
interface RelocationRequestPageProps {
currentUser: User | null;
onViewDetails: (id: string) => void;
}
// Mock dealer data for auto-fetch
const mockDealerData: Record<string, any> = {
'DL-MH-001': {
dealerName: 'Amit Sharma Motors',
dealerCode: 'DL-MH-001',
currentAddress: '123, MG Road, Bandra West, Mumbai, Maharashtra - 400050',
city: 'Mumbai',
state: 'Maharashtra',
pincode: '400050',
dealershipName: 'Royal Enfield Mumbai',
gst: '27AABCU9603R1ZX',
region: 'West',
zone: 'Maharashtra'
},
'DL-KA-045': {
dealerName: 'Priya Automobiles',
dealerCode: 'DL-KA-045',
currentAddress: '456, Brigade Road, Whitefield, Bangalore, Karnataka - 560066',
city: 'Bangalore',
state: 'Karnataka',
pincode: '560066',
dealershipName: 'Royal Enfield Bangalore',
gst: '29AABCU9603R1ZX',
region: 'South',
zone: 'Karnataka'
},
'DL-TN-028': {
dealerName: 'Rahul Motors',
dealerCode: 'DL-TN-028',
currentAddress: '789, Anna Salai, T Nagar, Chennai, Tamil Nadu - 600017',
city: 'Chennai',
state: 'Tamil Nadu',
pincode: '600017',
dealershipName: 'Royal Enfield Chennai',
gst: '33AABCU9603R1ZX',
region: 'South',
zone: 'Tamil Nadu'
}
};
// Mock relocation requests
export const mockRelocationRequests = [
{
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',
currentStage: 'DD-ZM',
submittedOn: '2025-12-20',
submittedBy: 'Dealer',
progressPercentage: 25
},
{
id: 'RLO-002',
dealerCode: 'DL-KA-045',
dealerName: 'Priya Automobiles',
currentLocation: 'Whitefield, Bangalore',
proposedLocation: 'Koramangala, Bangalore',
distance: '18 km',
reason: 'Expansion to premium market segment',
status: 'DD Lead Review',
currentStage: 'DD Lead',
submittedOn: '2025-12-15',
submittedBy: 'Dealer',
progressPercentage: 50
},
{
id: 'RLO-003',
dealerCode: 'DL-TN-028',
dealerName: 'Rahul Motors',
currentLocation: 'T Nagar, Chennai',
proposedLocation: 'OMR, Chennai',
distance: '22 km',
reason: 'Moving to IT corridor for better business prospects',
status: 'Infra Completion',
currentStage: 'Architect',
submittedOn: '2025-11-28',
submittedBy: 'Dealer',
progressPercentage: 83
},
{
id: 'RLO-004',
dealerCode: 'DL-DL-012',
dealerName: 'Suresh Auto Pvt Ltd',
currentLocation: 'Connaught Place, Delhi',
proposedLocation: 'Dwarka, Delhi',
distance: '15 km',
reason: 'Lower rental costs and larger space availability',
status: 'Completed',
currentStage: 'Closed',
submittedOn: '2025-11-10',
submittedBy: 'Dealer',
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';
if (status === 'Completed' || status === 'Closed') return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300';
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
if (status.includes('Collection') || status.includes('Completion')) return 'bg-blue-100 text-blue-700 border-blue-300';
if (status.includes('Collection') || status.includes('Completion') || status.includes('Infra')) return 'bg-blue-100 text-blue-700 border-blue-300';
return 'bg-slate-100 text-slate-700 border-slate-300';
};
export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [requests, setRequests] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [dealerCode, setDealerCode] = useState('');
const [dealerData, setDealerData] = useState<any>(null);
const [proposedAddress, setProposedAddress] = useState('');
@ -139,19 +43,56 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
const [propertyType, setPropertyType] = useState('');
const [expectedDate, setExpectedDate] = useState('');
const [locationMode, setLocationMode] = useState<'manual' | 'map'>('manual');
const [mapCoordinates, setMapCoordinates] = useState({ lat: 19.0760, lng: 72.8777 }); // Default to Mumbai
const [mapCoordinates] = useState({ lat: 19.0760, lng: 72.8777 }); // Default to Mumbai
const [selectedLocation, setSelectedLocation] = useState<{ lat: number; lng: number } | null>(null);
const handleDealerCodeChange = (code: string) => {
useEffect(() => {
fetchRequests();
}, []);
const fetchRequests = async () => {
try {
setIsLoading(true);
const response = await API.getRelocationRequests() as any;
if (response.data.success) {
setRequests(response.data.requests);
}
} catch (error) {
console.error('Fetch relocation requests error:', error);
toast.error('Failed to fetch relocation requests');
} finally {
setIsLoading(false);
}
};
const handleDealerCodeChange = async (code: string) => {
setDealerCode(code);
if (mockDealerData[code]) {
setDealerData(mockDealerData[code]);
toast.success('Dealer details loaded successfully');
if (code.length >= 4) {
try {
const response = await API.getOutletByCode(code) as any;
if (response.data.success && response.data.outlet) {
const outlet = response.data.outlet;
setDealerData({
dealerName: outlet.name,
dealerCode: outlet.code,
currentAddress: outlet.address || 'N/A',
city: outlet.city || 'N/A',
state: outlet.state || 'N/A',
pincode: outlet.pincode || 'N/A',
dealershipName: outlet.name,
gst: outlet.gst || 'N/A',
region: outlet.region?.name || 'N/A',
zone: outlet.zone?.name || 'N/A'
});
toast.success('Dealer details loaded successfully');
} else {
setDealerData(null);
}
} catch (error) {
setDealerData(null);
}
} else {
setDealerData(null);
if (code.trim()) {
toast.error('Dealer code not found');
}
}
};
@ -199,7 +140,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
setSelectedLocation(null);
};
const handleSubmitRequest = (e: React.FormEvent) => {
const handleSubmitRequest = async (e: React.FormEvent) => {
e.preventDefault();
if (!dealerData) {
@ -212,59 +153,58 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
return;
}
if (!distance.trim()) {
toast.error('Please enter distance from current location');
return;
}
try {
setIsSubmitting(true);
const payload = {
dealerCode,
currentLocation: dealerData.currentAddress,
proposedLocation: `${proposedAddress}, ${proposedCity}, ${proposedState} - ${proposedPincode}`,
distance,
reason,
propertyType,
expectedDate: expectedDate || null,
coordinates: selectedLocation ? `${selectedLocation.lat},${selectedLocation.lng}` : null
};
if (!reason.trim()) {
toast.error('Please provide a reason for relocation');
return;
}
const response = await API.createRelocationRequest(payload) as any;
if (!propertyType) {
toast.error('Please select property type');
return;
if (response.data.success) {
toast.success('Relocation request submitted successfully');
setIsDialogOpen(false);
handleResetForm();
fetchRequests();
}
} catch (error) {
console.error('Submit relocation request error:', error);
toast.error('Failed to submit relocation request');
} finally {
setIsSubmitting(false);
}
toast.success('Relocation request submitted successfully');
setIsDialogOpen(false);
// Reset form
handleResetForm();
};
// Filter requests based on user role
const getFilteredRequests = () => {
// For now, showing all requests. In real implementation, filter by role permissions
return mockRelocationRequests;
};
const filteredRequests = getFilteredRequests();
// Statistics
const stats = [
{
title: 'Total Requests',
value: filteredRequests.length,
value: requests.length,
icon: FileText,
color: 'bg-blue-500',
},
{
title: 'In Progress',
value: filteredRequests.filter(r => r.status !== 'Completed' && !r.status.includes('Rejected')).length,
value: requests.filter((r: any) => r.status !== 'Completed' && r.status !== 'Closed' && !r.status.includes('Rejected')).length,
icon: Calendar,
color: 'bg-yellow-500',
},
{
title: 'Completed',
value: filteredRequests.filter(r => r.status === 'Completed').length,
value: requests.filter((r: any) => r.status === 'Completed' || r.status === 'Closed').length,
icon: MapPin,
color: 'bg-green-500',
},
{
title: 'Pending Action',
value: filteredRequests.filter(r => r.status.includes('Review') || r.status.includes('Pending')).length,
value: requests.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending')).length,
icon: Building,
color: 'bg-amber-500',
},
@ -576,9 +516,16 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
<Button
type="submit"
className="bg-amber-600 hover:bg-amber-700"
disabled={!dealerData}
disabled={!dealerData || isSubmitting}
>
Submit Request
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
'Submit Request'
)}
</Button>
</DialogFooter>
</form>
@ -641,65 +588,82 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</TableRow>
</TableHeader>
<TableBody>
{filteredRequests.map((request) => (
<TableRow key={request.id}>
<TableCell>
<div className="font-medium text-slate-900">{request.id}</div>
<div className="text-slate-600 text-sm">{request.dealerCode}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.dealerName}</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center gap-1 text-slate-600 text-sm">
<span className="text-slate-500">From:</span>
<span>{request.currentLocation}</span>
</div>
<div className="flex items-center gap-1 text-slate-900 text-sm">
<Navigation className="w-3 h-3 text-amber-600" />
<span className="text-slate-500">To:</span>
<span>{request.proposedLocation}</span>
</div>
{isLoading ? (
<TableRow>
<TableCell colSpan={8} className="h-32 text-center">
<div className="flex flex-col items-center justify-center space-y-2">
<Loader2 className="w-6 h-6 text-amber-600 animate-spin" />
<p className="text-slate-500 text-sm">Loading requests...</p>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.distance}
</Badge>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.currentStage}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-amber-600 transition-all duration-300"
style={{ width: `${request.progressPercentage}%` }}
/>
</div>
<span className="text-slate-600 text-sm">{request.progressPercentage}%</span>
</div>
</TableCell>
<TableCell>
<div className="text-slate-900">{request.submittedOn}</div>
<div className="text-slate-600 text-sm">By {request.submittedBy}</div>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))}
) : requests.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="h-32 text-center text-slate-500">
No relocation requests found
</TableCell>
</TableRow>
) : (
requests.map((request: any) => (
<TableRow key={request.id}>
<TableCell>
<div className="font-medium text-slate-900">{request.requestId || request.id}</div>
<div className="text-slate-600 text-sm">{request.outlet?.code || request.dealerCode}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.outlet?.name || request.dealerName}</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center gap-1 text-slate-600 text-sm">
<span className="text-slate-500">From:</span>
<span>{request.currentLocation}</span>
</div>
<div className="flex items-center gap-1 text-slate-900 text-sm">
<Navigation className="w-3 h-3 text-amber-600" />
<span className="text-slate-500">To:</span>
<span>{request.proposedLocation}</span>
</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.distance}
</Badge>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.currentStage}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-amber-600 transition-all duration-300"
style={{ width: `${request.progressPercentage || 0}%` }}
/>
</div>
<span className="text-slate-600 text-sm">{request.progressPercentage || 0}%</span>
</div>
</TableCell>
<TableCell>
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div>
<div className="text-slate-600 text-sm">By {request.dealer?.fullName || 'Dealer'}</div>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
@ -718,40 +682,48 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</TableRow>
</TableHeader>
<TableBody>
{filteredRequests
.filter(r => r.status.includes('Review') || r.status.includes('Pending'))
.map((request) => (
<TableRow key={request.id}>
<TableCell>
<div className="font-medium text-slate-900">{request.id}</div>
<div className="text-slate-600 text-sm">{request.dealerCode}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.dealerName}</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="text-slate-600 text-sm">From: {request.currentLocation}</div>
<div className="text-slate-900 text-sm">To: {request.proposedLocation}</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.currentStage}
</Badge>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))}
{isLoading ? (
<TableRow>
<TableCell colSpan={5} className="h-32 text-center">
<Loader2 className="w-6 h-6 text-amber-600 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : (
requests
.filter((r: any) => r.status.includes('Review') || r.status.includes('Pending'))
.map((request: any) => (
<TableRow key={request.id}>
<TableCell>
<div className="font-medium text-slate-900">{request.requestId || request.id}</div>
<div className="text-slate-600 text-sm">{request.outlet?.code || request.dealerCode}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.outlet?.name || request.dealerName}</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="text-slate-600 text-sm">From: {request.currentLocation}</div>
<div className="text-slate-900 text-sm">To: {request.proposedLocation}</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.currentStage}
</Badge>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
@ -771,51 +743,59 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</TableRow>
</TableHeader>
<TableBody>
{filteredRequests
.filter(r => r.status !== 'Completed' && !r.status.includes('Rejected'))
.map((request) => (
<TableRow key={request.id}>
<TableCell>
<div className="font-medium text-slate-900">{request.id}</div>
<div className="text-slate-600 text-sm">{request.dealerCode}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.dealerName}</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="text-slate-600 text-sm">From: {request.currentLocation}</div>
<div className="text-slate-900 text-sm">To: {request.proposedLocation}</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-amber-600 transition-all duration-300"
style={{ width: `${request.progressPercentage}%` }}
/>
{isLoading ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center">
<Loader2 className="w-6 h-6 text-amber-600 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : (
requests
.filter((r: any) => r.status !== 'Completed' && r.status !== 'Closed' && !r.status.includes('Rejected'))
.map((request: any) => (
<TableRow key={request.id}>
<TableCell>
<div className="font-medium text-slate-900">{request.requestId || request.id}</div>
<div className="text-slate-600 text-sm">{request.outlet?.code || request.dealerCode}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.outlet?.name || request.dealerName}</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="text-slate-600 text-sm">From: {request.currentLocation}</div>
<div className="text-slate-900 text-sm">To: {request.proposedLocation}</div>
</div>
<span className="text-slate-600 text-sm">{request.progressPercentage}%</span>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.currentStage}
</Badge>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-amber-600 transition-all duration-300"
style={{ width: `${request.progressPercentage || 0}%` }}
/>
</div>
<span className="text-slate-600 text-sm">{request.progressPercentage || 0}%</span>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
{request.currentStage}
</Badge>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
@ -834,38 +814,46 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</TableRow>
</TableHeader>
<TableBody>
{filteredRequests
.filter(r => r.status === 'Completed')
.map((request) => (
<TableRow key={request.id}>
<TableCell>
<div className="font-medium text-slate-900">{request.id}</div>
<div className="text-slate-600 text-sm">{request.dealerCode}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.dealerName}</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="text-slate-600 text-sm">From: {request.currentLocation}</div>
<div className="text-slate-900 text-sm">To: {request.proposedLocation}</div>
</div>
</TableCell>
<TableCell>
<div className="text-slate-900">{request.submittedOn}</div>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))}
{isLoading ? (
<TableRow>
<TableCell colSpan={5} className="h-32 text-center">
<Loader2 className="w-6 h-6 text-amber-600 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : (
requests
.filter((r: any) => r.status === 'Completed' || r.status === 'Closed')
.map((request: any) => (
<TableRow key={request.id}>
<TableCell>
<div className="font-medium text-slate-900">{request.requestId || request.id}</div>
<div className="text-slate-600 text-sm">{request.outlet?.code || request.dealerCode}</div>
</TableCell>
<TableCell>
<div className="font-medium text-slate-900">{request.outlet?.name || request.dealerName}</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="text-slate-600 text-sm">From: {request.currentLocation}</div>
<div className="text-slate-900 text-sm">To: {request.proposedLocation}</div>
</div>
</TableCell>
<TableCell>
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
>
<Eye className="w-4 h-4 mr-1" />
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>

View File

@ -9,16 +9,17 @@ import { Textarea } from '../ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { useState, useEffect } from 'react';
import { User, mockWorkNotes, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
import { User as UserType } from '../../lib/mock-data';
import { WorkNotesPage } from './WorkNotesPage';
import { toast } from 'sonner';
import { resignationService } from '../../services/resignation.service';
import { ShieldCheck, Info } from 'lucide-react';
import { Loader2 } from 'lucide-react';
import { API } from '../../api/API';
interface ResignationDetailsProps {
resignationId: string;
onBack: () => void;
currentUser: User | null;
currentUser: UserType | null;
}
export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) {
@ -34,6 +35,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
const [resignationData, setResignationData] = useState<any>(null); // Real data from API
const [isLoading, setIsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const fetchResignation = async () => {
try {
@ -54,134 +56,42 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
// Check if user can push to F&F (DD Lead and above)
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
// Mock data - would come from API
const request = {
id: resignationId,
dealerCode: 'DL-MH-001',
dealerName: 'Amit Sharma Motors',
address: '123, MG Road, Bandra West, Mumbai',
cityCategory: 'Tier 1',
domainName: 'Mumbai Central',
dealershipName: 'Royal Enfield Mumbai',
gst: '27AABCU9603R1ZX',
salesCode: 'SAL-MH-001',
serviceCode: 'SRV-MH-001',
accessoriesCode: 'ACC-MH-001',
gmaCode: 'GMA-MH-001',
location: 'Mumbai, Maharashtra',
inauguration: 'March 2020',
loa: 'February 2020',
loi: 'January 2020',
lastSixMonthsSales: '₹85,00,000',
numberOfDealerships: '2',
numberOfStudios: '1',
constitution: 'PVT. LTD.',
dealershipType: 'Main Dealer',
typeOfClosure: 'Complete',
formatCategory: 'A+',
dealerScoreCardBand: 'Gold',
resignationReason: 'Personal Health Reasons',
customerDescription: 'Due to ongoing health issues and family commitments, I am unable to continue managing the dealership operations effectively. After careful consideration, I have decided to resign from my position.',
status: 'ASM Review',
currentStage: 'ASM',
submittedOn: '2025-10-08',
submittedBy: 'DD Lead'
};
// Mock documents by stage
const stageDocuments: Record<string, any[]> = {
'Request Submitted': [
{ id: 1, name: 'Resignation Application Form.pdf', type: 'Application', uploadDate: '2025-10-08', uploader: 'DD Lead' },
{ id: 2, name: 'Dealer Profile.pdf', type: 'Profile', uploadDate: '2025-10-08', uploader: 'DD Lead' }
],
'ASM Review': [
{ id: 3, name: 'ASM Review Report.pdf', type: 'Review', uploadDate: '2025-10-09', uploader: 'ASM - Mumbai' },
{ id: 4, name: 'Sales Performance Report.xlsx', type: 'Report', uploadDate: '2025-10-09', uploader: 'ASM - Mumbai' },
{ id: 5, name: 'Customer Feedback Summary.pdf', type: 'Feedback', uploadDate: '2025-10-09', uploader: 'ASM - Mumbai' }
],
'RBM + DD ZM Review': [
{ id: 6, name: 'RBM Evaluation.pdf', type: 'Evaluation', uploadDate: '2025-10-10', uploader: 'RBM - West Zone' },
{ id: 7, name: 'DD ZM Assessment.pdf', type: 'Assessment', uploadDate: '2025-10-10', uploader: 'DD ZM - West' }
],
'ZBH Review': [
{ id: 8, name: 'ZBH Approval Document.pdf', type: 'Approval', uploadDate: '2025-10-11', uploader: 'ZBH - West Zone' }
],
'DD Lead Review': [],
'NBH Approval': [],
'Legal - Resignation Letter': []
};
// Progress stages logic based on live data
const progressStages = [
{
id: 1,
name: 'Request Submitted',
status: 'completed',
date: '2025-10-08',
description: 'Resignation request created by DD Lead',
actionType: 'approved',
actionBy: 'DD Lead',
remarks: 'Initial resignation request submitted with all required documentation.',
feedback: 'Request is complete and ready for ASM review.'
},
{
id: 2,
name: 'ASM Review',
status: request.currentStage === 'ASM' ? 'active' : request.currentStage === 'ASM' ? 'pending' : 'completed',
date: request.currentStage === 'ASM' ? '2025-10-09' : undefined,
description: 'Area Sales Manager review',
actionType: request.currentStage === 'ASM' ? undefined : 'approved',
actionBy: request.currentStage === 'ASM' ? undefined : 'ASM - Mumbai',
remarks: request.currentStage === 'ASM' ? undefined : 'Reviewed dealer performance and resignation request. All documentation verified.',
feedback: request.currentStage === 'ASM' ? undefined : 'Dealer has maintained good performance. Recommended for approval at next level.'
},
{
id: 3,
name: 'Departmental Clearances',
status: request.currentStage === 'ASM' ? 'pending' : request.currentStage === 'Clearance' ? 'active' : 'completed',
description: 'Clearance from all relevant departments'
},
{
id: 4,
name: 'RBM + DD ZM Review',
status: request.currentStage === 'RBM' || request.currentStage === 'DD ZM' ? 'active' : ['Legal', 'NBH', 'DD Lead', 'ZBH'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'Regional Business Manager and DD ZM evaluation'
},
{
id: 4,
name: 'ZBH Review',
status: request.currentStage === 'ZBH' ? 'active' : ['Legal', 'NBH', 'DD Lead'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'Zonal Business Head approval'
},
{
id: 5,
name: 'DD Lead Review',
status: request.currentStage === 'DD Lead' ? 'active' : ['Legal', 'NBH'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'DD Lead final review'
},
{
id: 6,
name: 'NBH Approval',
status: request.currentStage === 'NBH' ? 'active' : request.currentStage === 'Legal' ? 'completed' : 'pending',
description: 'National Business Head approval'
},
{
id: 7,
name: 'Legal - Resignation Letter',
status: request.currentStage === 'Legal' ? 'active' : 'pending',
description: 'Legal team issues resignation approval letter'
}
{ id: 1, name: 'Request Submitted', key: 'Submitted', description: 'Resignation request created' },
{ id: 2, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
{ id: 3, name: 'Departmental Clearances', key: 'Clearance', description: 'Clearance from departments' },
{ id: 4, name: 'RBM + DD ZM Review', key: 'RBM', description: 'Regional Business Manager and DD ZM evaluation' },
{ id: 5, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
{ id: 6, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
{ id: 7, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
{ id: 8, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' }
];
const handleViewStageDocuments = (stageName: string) => {
const documents = stageDocuments[stageName] || [];
setStageDocumentsDialog({ open: true, stageName, documents });
const getStageStatus = (stageKey: string) => {
if (!resignationData) return 'pending';
const currentStage = resignationData.currentStage;
// Simple logic for simulation - in real app, this would be more complex
const stagesOrdered = ['Submitted', 'ASM', 'Clearance', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'Legal'];
const currentIndex = stagesOrdered.indexOf(currentStage);
const stageIndex = stagesOrdered.indexOf(stageKey);
if (stageIndex < currentIndex) return 'completed';
if (stageIndex === currentIndex) return 'active';
return 'pending';
};
const handleAction = (type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf') => {
setActionDialog({ open: true, type });
};
const handleSubmitAction = () => {
const handleViewStageDocuments = (stageName: string) => {
const documents = resignationData?.documents?.filter((d: any) => d.stage === stageName) || [];
setStageDocumentsDialog({ open: true, stageName, documents });
};
const handleSubmitAction = async () => {
if (!remarks && actionDialog.type !== 'assign') {
toast.error('Please provide remarks');
return;
@ -191,18 +101,28 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
return;
}
const actionMessages: Record<string, string> = {
approve: 'Request approved successfully',
withdrawal: 'Request withdrawn successfully',
sendback: 'Request sent back for clarification',
assign: `Request assigned to ${assignToUser}`,
pushfnf: 'Request pushed to F&F successfully'
};
toast.success(actionMessages[actionDialog.type!]);
setActionDialog({ open: false, type: null });
setRemarks('');
setAssignToUser('');
try {
setIsSubmitting(true);
const payload = {
action: actionDialog.type,
remarks,
assignTo: assignToUser
};
const response: any = await API.updateResignationStatus(resignationId, payload);
if (response.data?.success) {
toast.success(response.data?.message || 'Action completed successfully');
setActionDialog({ open: false, type: null });
setRemarks('');
setAssignToUser('');
fetchResignation();
}
} catch (error: any) {
console.error('Error submitting action:', error);
toast.error(error.response?.data?.message || 'Failed to submit action');
} finally {
setIsSubmitting(false);
}
};
const handleClearanceUpdate = async () => {
@ -216,7 +136,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
});
toast.success(`${selectedDept} clearance updated`);
setShowClearanceDialog(false);
// fetchResignation(); // Refresh data
fetchResignation();
} catch (error) {
toast.error('Failed to update clearance');
} finally {
@ -224,19 +144,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
}
};
const departments = ['Sales', 'Service', 'Spares', 'Fin-Accounts', 'GSA', 'Legal'];
// Mock clearance data if not available from API yet
const departmentalClearances = resignationData?.departmentalClearances || [
{ department: 'Sales', status: 'Cleared', remarks: 'All units settled' },
{ department: 'Service', status: 'Pending', remarks: '' },
{ department: 'Spares', status: 'Cleared', remarks: 'Inventory returned' },
{ department: 'Fin-Accounts', status: 'Pending', remarks: '' },
{ department: 'GSA', status: 'Pending', remarks: '' },
{ department: 'Legal', status: 'Pending', remarks: '' }
];
const workNotesCount = mockWorkNotes.length;
if (isLoading && !resignationData) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
return (
<div className="space-y-6">
@ -248,10 +162,10 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</Button>
<div>
<h1 className="text-2xl">{resignationId}</h1>
<p className="text-slate-600">{request.dealerName}</p>
<p className="text-slate-600">{resignationData?.outlet?.name}</p>
</div>
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
{request.status}
{resignationData?.status}
</Badge>
</div>
</div>
@ -268,19 +182,21 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<>
<Button
size="sm"
disabled={isSubmitting}
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
onClick={() => handleAction('approve')}
>
<Check className="w-4 h-4 mr-2" />
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Approve
</Button>
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('sendback')}
>
<RotateCcw className="w-4 h-4 mr-2" />
{isSubmitting && actionDialog.type === 'sendback' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RotateCcw className="w-4 h-4 mr-2" />}
Send Back
</Button>
</>
@ -288,10 +204,11 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-red-600 border-red-300 hover:bg-red-50 transition-all"
onClick={() => handleAction('withdrawal')}
>
<X className="w-4 h-4 mr-2" />
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
Withdrawal
</Button>
</div>
@ -303,20 +220,22 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-blue-600 border-blue-300 hover:bg-blue-50 transition-all"
onClick={() => handleAction('pushfnf')}
>
<Send className="w-4 h-4 mr-2" />
{isSubmitting && actionDialog.type === 'pushfnf' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Send className="w-4 h-4 mr-2" />}
Push to F&F
</Button>
)}
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('assign')}
>
<UserPlus className="w-4 h-4 mr-2" />
{isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />}
Assign User
</Button>
</div>
@ -338,9 +257,9 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
{workNotesCount > 0 && (
{resignationData?.worknotes?.length > 0 && (
<Badge className="ml-2 bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
{workNotesCount}
{resignationData.worknotes.length}
</Badge>
)}
</Button>
@ -385,47 +304,31 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div>
<Label className="text-slate-600">Dealer Code</Label>
<p>{request.dealerCode}</p>
<p>{resignationData?.outlet?.code}</p>
</div>
<div>
<Label className="text-slate-600">Dealer Name</Label>
<p>{request.dealerName}</p>
<p>{resignationData?.outlet?.name}</p>
</div>
<div>
<Label className="text-slate-600">GST</Label>
<p>{request.gst}</p>
<p>{resignationData?.outlet?.gstNumber || 'N/A'}</p>
</div>
<div className="col-span-2">
<Label className="text-slate-600">Address</Label>
<p>{request.address}</p>
<p>{resignationData?.outlet?.address}</p>
</div>
<div>
<Label className="text-slate-600">City Category</Label>
<p>{request.cityCategory}</p>
<Label className="text-slate-600">City</Label>
<p>{resignationData?.outlet?.city}</p>
</div>
<div>
<Label className="text-slate-600">Domain Name</Label>
<p>{request.domainName}</p>
<Label className="text-slate-600">State</Label>
<p>{resignationData?.outlet?.state}</p>
</div>
<div>
<Label className="text-slate-600">Dealership Name</Label>
<p>{request.dealershipName}</p>
</div>
<div>
<Label className="text-slate-600">Sales Code</Label>
<p>{request.salesCode}</p>
</div>
<div>
<Label className="text-slate-600">Service Code</Label>
<p>{request.serviceCode}</p>
</div>
<div>
<Label className="text-slate-600">Accessories Code</Label>
<p>{request.accessoriesCode}</p>
</div>
<div>
<Label className="text-slate-600">GMA Code</Label>
<p>{request.gmaCode}</p>
<Label className="text-slate-600">Region</Label>
<p>{resignationData?.outlet?.region}</p>
</div>
</div>
</CardContent>
@ -439,74 +342,59 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div>
<Label className="text-slate-600">Inauguration</Label>
<p>{request.inauguration}</p>
</div>
<div>
<Label className="text-slate-600">LOA</Label>
<p>{request.loa}</p>
</div>
<div>
<Label className="text-slate-600">LOI</Label>
<p>{request.loi}</p>
</div>
<div>
<Label className="text-slate-600">Last 6 Months Sales</Label>
<p>{request.lastSixMonthsSales}</p>
</div>
<div>
<Label className="text-slate-600">Number of Dealerships</Label>
<p>{request.numberOfDealerships}</p>
</div>
<div>
<Label className="text-slate-600">Number of Studios</Label>
<p>{request.numberOfStudios}</p>
</div>
<div>
<Label className="text-slate-600">Constitution</Label>
<p>{request.constitution}</p>
<p>{resignationData?.outlet?.inaugurationDate ? new Date(resignationData.outlet.inaugurationDate).toLocaleDateString() : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Dealership Type</Label>
<p>{request.dealershipType}</p>
<p>{resignationData?.outlet?.type || 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Type of Closure</Label>
<p>{request.typeOfClosure}</p>
</div>
<div>
<Label className="text-slate-600">Format Category</Label>
<p>{request.formatCategory}</p>
</div>
<div>
<Label className="text-slate-600">Dealer Score Card Band</Label>
<p>{request.dealerScoreCardBand}</p>
<Label className="text-slate-600">City Category</Label>
<p>{resignationData?.outlet?.cityCategory || 'N/A'}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Resignation Details</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<Label className="text-slate-600">Resignation Reason</Label>
<p>{request.resignationReason}</p>
</div>
<div>
<Label className="text-slate-600">Customer Description</Label>
<p>{request.customerDescription}</p>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Resignation Type</Label>
<p>{resignationData?.resignationType}</p>
</div>
<div>
<Label className="text-slate-600">Reason</Label>
<p>{resignationData?.reason}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Submitted By</Label>
<p>{request.submittedBy}</p>
<Label className="text-slate-600">Last Operational Date (Sales)</Label>
<p>{resignationData?.lastOperationalDateSales ? new Date(resignationData.lastOperationalDateSales).toLocaleDateString() : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Last Operational Date (Services)</Label>
<p>{resignationData?.lastOperationalDateServices ? new Date(resignationData.lastOperationalDateServices).toLocaleDateString() : 'N/A'}</p>
</div>
</div>
<div>
<Label className="text-slate-600">Additional Info / Dealer Voice</Label>
<p>{resignationData?.additionalInfo || 'No additional info provided'}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Submitted On</Label>
<p>{request.submittedOn}</p>
<p>{resignationData?.submittedOn ? new Date(resignationData.submittedOn).toLocaleDateString() : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Current Stage</Label>
<p>{resignationData?.currentStage}</p>
</div>
</div>
</div>
@ -524,87 +412,57 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<CardContent>
<div className="space-y-4">
{progressStages.map((stage, index) => {
const documentCount = stageDocuments[stage.name]?.length || 0;
const status = getStageStatus(stage.key);
const timelineEntry = resignationData?.timeline?.find((t: any) => t.stage === stage.key || t.stage === stage.name);
return (
<div key={stage.id} className="flex gap-4">
<div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${stage.status === 'completed' ? 'bg-green-100 text-green-600' :
stage.status === 'active' ? 'bg-blue-100 text-blue-600' :
'bg-slate-100 text-slate-400'
}`}>
{stage.status === 'completed' ? (
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
status === 'completed' ? 'bg-green-100 text-green-600' :
status === 'active' ? 'bg-blue-100 text-blue-600' :
'bg-slate-100 text-slate-400'
}`}>
{status === 'completed' ? (
<Check className="w-5 h-5" />
) : (
<span>{stage.id}</span>
)}
</div>
{index < progressStages.length - 1 && (
<div className={`w-0.5 ${stage.remarks ? 'h-32' : 'h-16'
} ${stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
}`} />
<div className={`w-0.5 h-16 ${
status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
}`} />
)}
</div>
<div className="flex-1 pb-8">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<h3 className={
stage.status === 'completed' ? 'text-green-600' :
stage.status === 'active' ? 'text-blue-600' :
'text-slate-400'
}>{stage.name}</h3>
{documentCount > 0 && (
<button
onClick={() => handleViewStageDocuments(stage.name)}
className="flex items-center gap-1 px-2 py-1 rounded-full bg-blue-100 hover:bg-blue-200 text-blue-700 text-xs transition-colors cursor-pointer"
>
<FileText className="w-3 h-3" />
<span>{documentCount} {documentCount === 1 ? 'doc' : 'docs'}</span>
</button>
)}
</div>
{stage.date && (
<h3 className={
status === 'completed' ? 'text-green-600' :
status === 'active' ? 'text-blue-600' :
'text-slate-400'
}>{stage.name}</h3>
{timelineEntry && (
<div className="flex items-center gap-1 text-sm text-slate-600">
<Calendar className="w-4 h-4" />
<span>{stage.date}</span>
<span>{new Date(timelineEntry.timestamp || timelineEntry.createdAt).toLocaleDateString()}</span>
</div>
)}
</div>
<p className="text-slate-600 text-sm">{stage.description}</p>
{/* Action Badge and Remarks */}
{stage.actionType && stage.remarks && (
<div className="mt-3 space-y-2">
<div className="flex items-center gap-2">
<Badge className={
stage.actionType === 'approved' ? 'bg-green-100 text-green-700 border-green-300' :
stage.actionType === 'sendback' ? 'bg-orange-100 text-orange-700 border-orange-300' :
stage.actionType === 'withdrawal' ? 'bg-red-100 text-red-700 border-red-300' :
'bg-blue-100 text-blue-700 border-blue-300'
}>
{stage.actionType === 'approved' && '✓ Approved'}
{stage.actionType === 'sendback' && '↩ Sent Back'}
{stage.actionType === 'withdrawal' && '✗ Withdrawn'}
</Badge>
{stage.actionBy && (
<span className="text-xs text-slate-500">by {stage.actionBy}</span>
)}
</div>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3">
<div className="space-y-2">
<div>
<Label className="text-xs text-slate-600">Remarks:</Label>
<p className="text-sm text-slate-700 mt-1">{stage.remarks}</p>
</div>
{stage.feedback && (
<div>
<Label className="text-xs text-slate-600">Feedback:</Label>
<p className="text-sm text-slate-700 mt-1">{stage.feedback}</p>
</div>
)}
</div>
</div>
{timelineEntry && (
<div className="mt-2 bg-slate-50 p-2 rounded border border-slate-100 text-sm text-slate-600">
{timelineEntry.comments || timelineEntry.remarks}
</div>
)}
<Button
variant="ghost"
size="sm"
className="mt-2 text-blue-600"
onClick={() => handleViewStageDocuments(stage.name)}
>
View Stage Documents
</Button>
</div>
</div>
);
@ -625,14 +483,14 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{departmentalClearances.map((clearance: any) => (
{(resignationData?.clearances || []).map((clearance: any) => (
<Card key={clearance.department} className="border border-slate-200">
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium">{clearance.department}</CardTitle>
<Badge className={
clearance.status === 'Cleared' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
clearance.status === 'Rejected' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
'bg-yellow-100 text-yellow-700 hover:bg-yellow-100'
clearance.status === 'Cleared' || clearance.status === 'Approved' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
clearance.status === 'Rejected' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
'bg-yellow-100 text-yellow-700 hover:bg-yellow-100'
}>
{clearance.status}
</Badge>
@ -641,7 +499,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<p className="text-sm text-slate-600 line-clamp-2 min-h-[2.5rem]">
{clearance.remarks || 'No remarks provided'}
</p>
{currentUser && (currentUser.role === 'Super Admin' || currentUser.role === 'DD Admin' || (currentUser.role.includes(clearance.department) && request.currentStage === 'Clearance')) && (
{currentUser && (currentUser.role === 'Super Admin' || currentUser.role === 'DD Admin' || (currentUser.role.includes(clearance.department) && resignationData?.currentStage === 'Clearance')) && (
<Button
variant="ghost"
size="sm"
@ -683,22 +541,30 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</TableRow>
</TableHeader>
<TableBody>
{mockDocuments.map((doc) => (
<TableRow key={doc.id}>
<TableCell>
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-slate-500" />
<span>{doc.name}</span>
</div>
</TableCell>
<TableCell>{doc.type}</TableCell>
<TableCell>{doc.uploadDate}</TableCell>
<TableCell>{doc.uploader || '-'}</TableCell>
<TableCell>
<Button size="sm" variant="outline">View</Button>
{(resignationData?.documents || []).length > 0 ? (
(resignationData.documents || []).map((doc: any, index: number) => (
<TableRow key={index}>
<TableCell>
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-slate-500" />
<span>{doc.name || doc.fileName}</span>
</div>
</TableCell>
<TableCell>{doc.type || 'Document'}</TableCell>
<TableCell>{doc.createdAt ? new Date(doc.createdAt).toLocaleDateString() : 'N/A'}</TableCell>
<TableCell>{doc.uploadedBy || 'Dealer'}</TableCell>
<TableCell>
<Button size="sm" variant="outline">View</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-4 text-slate-500">
No documents found
</TableCell>
</TableRow>
))}
)}
</TableBody>
</Table>
</CardContent>
@ -714,19 +580,25 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</CardHeader>
<CardContent>
<div className="space-y-4">
{mockAuditLogs.map((log) => (
<div key={log.id} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className="w-2 h-2 rounded-full bg-blue-600 mt-2" />
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<p>{log.action}</p>
<span className="text-sm text-slate-600">{log.timestamp}</span>
{(resignationData?.timeline || []).length > 0 ? (
(resignationData.timeline || []).map((log: any, index: number) => (
<div key={index} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className="w-2 h-2 rounded-full bg-blue-600 mt-2" />
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<p className="font-medium text-slate-900">{log.action || log.status}</p>
<span className="text-sm text-slate-600">{new Date(log.timestamp || log.createdAt).toLocaleString()}</span>
</div>
<p className="text-sm text-slate-600">{log.user || log.actor}</p>
{log.comments && <p className="text-sm text-slate-500 mt-1">{log.comments}</p>}
</div>
<p className="text-sm text-slate-600">{log.user}</p>
{log.details && <p className="text-sm text-slate-500 mt-1">{log.details}</p>}
</div>
))
) : (
<div className="text-center py-8 text-slate-500">
No audit logs found
</div>
))}
)}
</div>
</CardContent>
</Card>
@ -795,18 +667,32 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setActionDialog({ open: false, type: null })}>
<Button variant="outline" onClick={() => setActionDialog({ open: false, type: null })} disabled={isSubmitting}>
Cancel
</Button>
<Button
onClick={handleSubmitAction}
className={
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
'bg-blue-600 hover:bg-blue-700'
}
<Button
onClick={handleSubmitAction}
disabled={isSubmitting}
className={
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
'bg-amber-600 hover:bg-amber-700'
}
>
Confirm
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
{actionDialog.type === 'approve' && 'Approve'}
{actionDialog.type === 'withdrawal' && 'Withdraw'}
{actionDialog.type === 'sendback' && 'Send Back'}
{actionDialog.type === 'assign' && 'Assign'}
{actionDialog.type === 'pushfnf' && 'Push to F&F'}
</>
)}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -1,96 +1,25 @@
import { FileText, Calendar, Building, Plus, Eye } from 'lucide-react';
import { FileText, Calendar, Plus, Eye } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
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 { User } from '../../lib/mock-data';
import { useState, useEffect } from 'react';
import { API } from '../../api/API';
import { toast } from 'sonner';
import { User as UserType } from '../../lib/mock-data';
interface ResignationPageProps {
currentUser: User | null;
currentUser: UserType | null;
onViewDetails: (id: string) => void;
}
// Mock dealer data for auto-fetch
const mockDealerData: Record<string, any> = {
'DL-MH-001': {
dealerName: 'Amit Sharma Motors',
address: '123, MG Road, Bandra West',
cityCategory: 'Tier 1',
domainName: 'Mumbai Central',
dealershipName: 'Royal Enfield Mumbai',
gst: '27AABCU9603R1ZX',
salesCode: 'SAL-MH-001',
serviceCode: 'SRV-MH-001',
accessoriesCode: 'ACC-MH-001',
gmaCode: 'GMA-MH-001'
},
'DL-KA-045': {
dealerName: 'Priya Automobiles',
address: '456, Brigade Road, Whitefield',
cityCategory: 'Tier 1',
domainName: 'Bangalore South',
dealershipName: 'Royal Enfield Bangalore',
gst: '29AABCU9603R1ZX',
salesCode: 'SAL-KA-045',
serviceCode: 'SRV-KA-045',
accessoriesCode: 'ACC-KA-045',
gmaCode: 'GMA-KA-045'
}
};
// Mock resignation requests
export const mockResignationRequests = [
{
id: 'RES-001',
dealerCode: 'DL-MH-001',
dealerName: 'Amit Sharma Motors',
location: 'Mumbai, Maharashtra',
dealershipType: 'Main Dealer',
formatCategory: 'A+',
resignationReason: 'Personal Health Reasons',
status: 'ASM Review',
currentStage: 'ASM',
submittedOn: '2025-10-08',
submittedBy: 'DD Lead'
},
{
id: 'RES-002',
dealerCode: 'DL-KA-045',
dealerName: 'Priya Automobiles',
location: 'Bangalore, Karnataka',
dealershipType: 'Studio',
formatCategory: 'A',
resignationReason: 'Relocating to Different City',
status: 'DD Lead Review',
currentStage: 'DD Lead',
submittedOn: '2025-10-03',
submittedBy: 'DD Lead'
},
{
id: 'RES-003',
dealerCode: 'DL-TN-028',
dealerName: 'Rahul Motors',
location: 'Chennai, Tamil Nadu',
dealershipType: 'Main Dealer',
formatCategory: 'B',
resignationReason: 'Starting Own Venture',
status: 'NBH Approved',
currentStage: 'Legal',
submittedOn: '2025-10-05',
submittedBy: 'DD Lead'
}
];
const getStatusColor = (status: string) => {
if (status.includes('Approved')) return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Approved') || status.includes('Completed')) return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
return 'bg-blue-100 text-blue-700 border-blue-300';
@ -100,69 +29,96 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dealerCode, setDealerCode] = useState('');
const [autoFilledData, setAutoFilledData] = useState<any>(null);
const [resignations, setResignations] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState({
inaugurationMonth: '',
inaugurationYear: '',
loaMonth: '',
loaYear: '',
loiMonth: '',
loiYear: '',
lastSixMonthsSales: '',
numberOfDealerships: '',
numberOfStudios: '',
constitution: '',
dealershipType: '',
typeOfClosure: '',
formatCategory: '',
dealerScoreCardBand: '',
resignationType: 'Voluntary',
lastOperationalDateSales: '',
lastOperationalDateServices: '',
resignationReason: '',
customerDescription: '',
document: null as File | null
});
const handleDealerCodeChange = (code: string) => {
setDealerCode(code);
if (mockDealerData[code]) {
setAutoFilledData(mockDealerData[code]);
toast.success('Dealer details loaded successfully');
} else {
setAutoFilledData(null);
if (code) {
toast.error('Dealer code not found');
const fetchResignations = async () => {
setLoading(true);
try {
const response = await API.getResignations();
const data = response.data as any;
if (data?.success) {
setResignations(data.resignations.rows || data.resignations);
}
} catch (error) {
console.error('Error fetching resignations:', error);
toast.error('Failed to fetch resignation requests');
} finally {
setLoading(false);
}
};
const handleSubmit = (e: React.FormEvent) => {
useEffect(() => {
fetchResignations();
}, []);
const handleDealerCodeChange = async (code: string) => {
setDealerCode(code);
if (code.length >= 5) {
try {
const response = await API.getOutletByCode(code);
const data = response.data as any;
if (data?.success) {
setAutoFilledData(data.outlet);
toast.success('Dealer details loaded');
} else {
setAutoFilledData(null);
}
} catch (error) {
setAutoFilledData(null);
}
} else {
setAutoFilledData(null);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!autoFilledData) {
toast.error('Please enter a valid dealer code');
return;
}
toast.success('Resignation request submitted successfully');
setIsDialogOpen(false);
// Reset form
setDealerCode('');
setAutoFilledData(null);
setFormData({
inaugurationMonth: '',
inaugurationYear: '',
loaMonth: '',
loaYear: '',
loiMonth: '',
loiYear: '',
lastSixMonthsSales: '',
numberOfDealerships: '',
numberOfStudios: '',
constitution: '',
dealershipType: '',
typeOfClosure: '',
formatCategory: '',
dealerScoreCardBand: '',
resignationReason: '',
customerDescription: '',
document: null
});
try {
const payload = {
outletId: autoFilledData.id,
resignationType: formData.resignationType,
lastOperationalDateSales: formData.lastOperationalDateSales,
lastOperationalDateServices: formData.lastOperationalDateServices,
reason: formData.resignationReason,
additionalInfo: formData.customerDescription
};
const response = await API.createResignation(payload);
const data = response.data as any;
if (data?.success) {
toast.success('Resignation request submitted successfully');
setIsDialogOpen(false);
fetchResignations();
// Reset form
setDealerCode('');
setAutoFilledData(null);
setFormData({
resignationType: 'Voluntary',
lastOperationalDateSales: '',
lastOperationalDateServices: '',
resignationReason: '',
customerDescription: '',
document: null
});
}
} catch (error: any) {
console.error('Error submitting resignation:', error);
toast.error(error.response?.data?.message || 'Failed to submit resignation request');
}
};
const isDDLead = currentUser?.role === 'DD Lead';
@ -175,7 +131,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
'DD Lead': ['DD Lead'],
'DD-ZM': ['DD-ZM'],
'RBM': ['RBM'],
'DD AM': ['ASM', 'DD AM'],
'DD AM': ['ASM'],
'ZBH': ['ZBH'],
'NBH': ['NBH'],
'Legal Admin': ['Legal'],
@ -185,21 +141,21 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
const userStages = roleToStageMapping[currentUser.role] || [];
return userStages.some(stage =>
request.currentStage.includes(stage) ||
request.status.includes(stage)
(request.currentStage && request.currentStage.includes(stage)) ||
(request.status && request.status.includes(stage))
);
};
const openRequests = mockResignationRequests.filter(req =>
const openRequests = resignations.filter(req =>
!req.status.includes('Completed') &&
!req.status.includes('Closed') &&
!req.status.includes('Rejected') &&
isRequestAtMyLevel(req)
);
const completedRequests = mockResignationRequests.filter(req =>
const completedRequests = resignations.filter(req =>
req.status.includes('Completed') ||
req.status.includes('Closed') ||
req.status.includes('Final Approval')
req.status.includes('Closed')
);
return (
@ -209,7 +165,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
<Card>
<CardHeader className="pb-3">
<CardDescription>All Requests</CardDescription>
<CardTitle className="text-3xl">{mockResignationRequests.length}</CardTitle>
<CardTitle className="text-3xl">{resignations.length}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">Total Requests</p>
@ -285,43 +241,27 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
<div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg">
<div>
<Label className="text-slate-600">Dealership Name</Label>
<p>{autoFilledData.dealerName}</p>
<p>{autoFilledData.name}</p>
</div>
<div>
<Label className="text-slate-600">GST</Label>
<p>{autoFilledData.gst}</p>
<Label className="text-slate-600">Code</Label>
<p>{autoFilledData.code}</p>
</div>
<div>
<Label className="text-slate-600">Address</Label>
<p>{autoFilledData.address}</p>
</div>
<div>
<Label className="text-slate-600">City Category</Label>
<p>{autoFilledData.cityCategory}</p>
<Label className="text-slate-600">City</Label>
<p>{autoFilledData.city}</p>
</div>
<div>
<Label className="text-slate-600">Domain Name</Label>
<p>{autoFilledData.domainName}</p>
<Label className="text-slate-600">State</Label>
<p>{autoFilledData.state}</p>
</div>
<div>
<Label className="text-slate-600">Dealership Principal Name</Label>
<p>{autoFilledData.dealershipName}</p>
</div>
<div>
<Label className="text-slate-600">Sales Code</Label>
<p>{autoFilledData.salesCode}</p>
</div>
<div>
<Label className="text-slate-600">Service Code</Label>
<p>{autoFilledData.serviceCode}</p>
</div>
<div>
<Label className="text-slate-600">Accessories Code</Label>
<p>{autoFilledData.accessoriesCode}</p>
</div>
<div>
<Label className="text-slate-600">GMA Code</Label>
<p>{autoFilledData.gmaCode}</p>
<Label className="text-slate-600">Type</Label>
<p>{autoFilledData.type}</p>
</div>
</div>
)}
@ -329,186 +269,37 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
{/* Date fields */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Inauguration *</Label>
<div className="flex gap-2">
<Input
type="number"
placeholder="Month (1-12)"
min="1"
max="12"
value={formData.inaugurationMonth}
onChange={(e) => setFormData({...formData, inaugurationMonth: e.target.value})}
required
/>
<Input
type="number"
placeholder="Year"
min="2000"
max="2025"
value={formData.inaugurationYear}
onChange={(e) => setFormData({...formData, inaugurationYear: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label>LOA *</Label>
<div className="flex gap-2">
<Input
type="number"
placeholder="Month (1-12)"
min="1"
max="12"
value={formData.loaMonth}
onChange={(e) => setFormData({...formData, loaMonth: e.target.value})}
required
/>
<Input
type="number"
placeholder="Year"
min="2000"
max="2025"
value={formData.loaYear}
onChange={(e) => setFormData({...formData, loaYear: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label>LOI *</Label>
<div className="flex gap-2">
<Input
type="number"
placeholder="Month (1-12)"
min="1"
max="12"
value={formData.loiMonth}
onChange={(e) => setFormData({...formData, loiMonth: e.target.value})}
required
/>
<Input
type="number"
placeholder="Year"
min="2000"
max="2025"
value={formData.loiYear}
onChange={(e) => setFormData({...formData, loiYear: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="sales">Last 6 Months Sales *</Label>
<Input
id="sales"
type="number"
placeholder="Enter sales figure"
value={formData.lastSixMonthsSales}
onChange={(e) => setFormData({...formData, lastSixMonthsSales: e.target.value})}
required
/>
</div>
</div>
{/* Number fields */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="dealerships">Number of Dealerships *</Label>
<Input
id="dealerships"
type="number"
value={formData.numberOfDealerships}
onChange={(e) => setFormData({...formData, numberOfDealerships: e.target.value})}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="studios">Number of Studios *</Label>
<Input
id="studios"
type="number"
value={formData.numberOfStudios}
onChange={(e) => setFormData({...formData, numberOfStudios: e.target.value})}
required
/>
</div>
</div>
{/* Dropdown fields */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Constitution *</Label>
<Select value={formData.constitution} onValueChange={(value) => setFormData({...formData, constitution: value})}>
<SelectTrigger>
<SelectValue placeholder="Select constitution" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pvt-ltd">PVT. LTD.</SelectItem>
<SelectItem value="partnership">Partnership</SelectItem>
<SelectItem value="proprietorship">Proprietorship</SelectItem>
<SelectItem value="public-limited">Public Limited</SelectItem>
<SelectItem value="llp">LLP</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Dealership Type *</Label>
<Select value={formData.dealershipType} onValueChange={(value) => setFormData({...formData, dealershipType: value})}>
<Label>Resignation Type *</Label>
<Select value={formData.resignationType} onValueChange={(value) => setFormData({...formData, resignationType: value})}>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="main-dealer">Main Dealer</SelectItem>
<SelectItem value="studio">Studio</SelectItem>
<SelectItem value="asp">ASP</SelectItem>
<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>
<div className="space-y-2">
<Label>Type of Closure *</Label>
<Select value={formData.typeOfClosure} onValueChange={(value) => setFormData({...formData, typeOfClosure: value})}>
<SelectTrigger>
<SelectValue placeholder="Select closure type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="complete">Complete</SelectItem>
<SelectItem value="partial">Partial</SelectItem>
</SelectContent>
</Select>
<Label>LWD Sales *</Label>
<Input
type="date"
value={formData.lastOperationalDateSales}
onChange={(e) => setFormData({...formData, lastOperationalDateSales: e.target.value})}
required
/>
</div>
{formData.dealershipType !== 'studio' && (
<div className="space-y-2">
<Label>Format Category *</Label>
<Select value={formData.formatCategory} onValueChange={(value) => setFormData({...formData, formatCategory: value})}>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="a-plus">A+</SelectItem>
<SelectItem value="a">A</SelectItem>
<SelectItem value="b">B</SelectItem>
<SelectItem value="c">C</SelectItem>
<SelectItem value="d">D</SelectItem>
<SelectItem value="e">E</SelectItem>
<SelectItem value="r">R</SelectItem>
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<Label>Dealer Score Card Band *</Label>
<Select value={formData.dealerScoreCardBand} onValueChange={(value) => setFormData({...formData, dealerScoreCardBand: value})}>
<SelectTrigger>
<SelectValue placeholder="Select band" />
</SelectTrigger>
<SelectContent>
<SelectItem value="platinum">Platinum</SelectItem>
<SelectItem value="gold">Gold</SelectItem>
<SelectItem value="silver">Silver</SelectItem>
<SelectItem value="bronze">Bronze</SelectItem>
<SelectItem value="no-band">No Band</SelectItem>
</SelectContent>
</Select>
<Label>LWD Services *</Label>
<Input
type="date"
value={formData.lastOperationalDateServices}
onChange={(e) => setFormData({...formData, lastOperationalDateServices: e.target.value})}
required
/>
</div>
</div>
@ -569,73 +360,77 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
<TabsContent value="all" className="mt-6">
<div className="space-y-4">
{mockResignationRequests.map((request) => (
<Card key={request.id} className="border-slate-200">
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<div className="p-3 bg-amber-100 rounded-lg">
<FileText className="w-6 h-6 text-amber-600" />
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id}</h3>
<Badge className={getStatusColor(request.status)}>
{request.status}
</Badge>
{loading ? (
<div className="text-center py-12">Loading requests...</div>
) : resignations.length > 0 ? (
resignations.map((request) => (
<Card key={request.id} className="border-slate-200">
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<div className="p-3 bg-amber-100 rounded-lg">
<FileText className="w-6 h-6 text-amber-600" />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-slate-600">Dealer Name</p>
<p>{request.dealerName}</p>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.resignationId}</h3>
<Badge className={getStatusColor(request.status)}>
{request.status}
</Badge>
</div>
<div>
<p className="text-slate-600">Dealer Code</p>
<p>{request.dealerCode}</p>
</div>
<div>
<p className="text-slate-600">Location</p>
<p>{request.location}</p>
</div>
<div>
<p className="text-slate-600">Dealership Type</p>
<p>{request.dealershipType}</p>
</div>
<div>
<p className="text-slate-600">Format Category</p>
<p>{request.formatCategory}</p>
</div>
<div>
<p className="text-slate-600">Reason</p>
<p>{request.resignationReason}</p>
</div>
<div>
<p className="text-slate-600">Current Stage</p>
<p>{request.currentStage}</p>
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4 text-slate-500" />
<p>{request.submittedOn}</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-slate-600">Dealer Name</p>
<p>{request.outlet?.name || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600">Dealer Code</p>
<p>{request.outlet?.code || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600">Location</p>
<p>{request.outlet?.city}, {request.outlet?.state}</p>
</div>
<div>
<p className="text-slate-600">Type</p>
<p>{request.resignationType}</p>
</div>
<div>
<p className="text-slate-600">Reason</p>
<p className="truncate max-w-[200px]">{request.reason}</p>
</div>
<div>
<p className="text-slate-600">Current Stage</p>
<p>{request.currentStage}</p>
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4 text-slate-500" />
<p>{new Date(request.submittedOn).toLocaleDateString()}</p>
</div>
</div>
</div>
</div>
</div>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
className="ml-4"
>
<Eye className="w-4 h-4 mr-2" />
View Details
</Button>
</div>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
className="ml-4"
>
<Eye className="w-4 h-4 mr-2" />
View Details
</Button>
</div>
</CardContent>
</Card>
))}
</CardContent>
</Card>
))
) : (
<div className="text-center py-12 text-slate-500">
<p>No resignation requests found</p>
</div>
)}
</div>
</TabsContent>
@ -653,7 +448,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id}</h3>
<h3 className="text-lg">{request.resignationId}</h3>
<Badge className={getStatusColor(request.status)}>
{request.status}
</Badge>
@ -661,11 +456,11 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-slate-600">Dealer Name</p>
<p>{request.dealerName}</p>
<p>{request.outlet?.name || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600">Location</p>
<p>{request.location}</p>
<p>{request.outlet?.city}, {request.outlet?.state}</p>
</div>
<div>
<p className="text-slate-600">Current Stage</p>
@ -673,7 +468,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<p>{request.submittedOn}</p>
<p>{new Date(request.submittedOn).toLocaleDateString()}</p>
</div>
</div>
</div>
@ -714,7 +509,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id}</h3>
<h3 className="text-lg">{request.resignationId}</h3>
<Badge className={getStatusColor(request.status)}>
{request.status}
</Badge>
@ -722,11 +517,11 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-slate-600">Dealer Name</p>
<p>{request.dealerName}</p>
<p>{request.outlet?.name}</p>
</div>
<div>
<p className="text-slate-600">Location</p>
<p>{request.location}</p>
<p>{request.outlet?.city}</p>
</div>
<div>
<p className="text-slate-600">Final Stage</p>
@ -734,7 +529,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<p>{request.submittedOn}</p>
<p>{new Date(request.submittedOn).toLocaleDateString()}</p>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { AlertTriangle, Calendar, Building, Plus, Eye, XCircle } from 'lucide-react';
import { AlertTriangle, Calendar, Plus, Eye, XCircle } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@ -9,7 +9,8 @@ import { Label } from '../ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Textarea } from '../ui/textarea';
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { API } from '../../api/API';
import { User } from '../../lib/mock-data';
import { toast } from 'sonner';
@ -18,80 +19,6 @@ interface TerminationPageProps {
onViewDetails: (id: string) => void;
}
// Mock dealer data for auto-fetch
const mockDealerData: Record<string, any> = {
'DL-MH-025': {
dealerName: 'Vikram Patil Motors',
address: '789, FC Road, Shivaji Nagar',
cityCategory: 'Tier 2',
domainName: 'Pune West',
dealershipName: 'Royal Enfield Pune',
gst: '27AABCU9604R1ZX',
salesCode: 'SAL-MH-025',
serviceCode: 'SRV-MH-025',
accessoriesCode: 'ACC-MH-025',
gmaCode: 'GMA-MH-025'
},
'DL-TG-033': {
dealerName: 'Sanjay Enterprises',
address: '321, Hitec City, Gachibowli',
cityCategory: 'Tier 1',
domainName: 'Hyderabad Central',
dealershipName: 'Royal Enfield Hyderabad',
gst: '36AABCU9604R1ZX',
salesCode: 'SAL-TG-033',
serviceCode: 'SRV-TG-033',
accessoriesCode: 'ACC-TG-033',
gmaCode: 'GMA-TG-033'
}
};
// Mock termination requests
export const mockTerminationRequests = [
{
id: 'TERM-001',
dealerCode: 'DL-MH-025',
dealerName: 'Vikram Patil Motors',
location: 'Pune, Maharashtra',
dealershipType: 'Main Dealer',
formatCategory: 'B',
terminationCategory: 'Breach of Agreement',
severity: 'High',
status: 'RBM Review',
currentStage: 'RBM',
submittedOn: '2025-10-15',
submittedBy: 'DD Lead'
},
{
id: 'TERM-002',
dealerCode: 'DL-TG-033',
dealerName: 'Sanjay Enterprises',
location: 'Hyderabad, Telangana',
dealershipType: 'Studio',
formatCategory: 'C',
terminationCategory: 'Financial Irregularities',
severity: 'Critical',
status: 'Legal Review',
currentStage: 'Legal',
submittedOn: '2025-09-20',
submittedBy: 'DD Lead'
},
{
id: 'TERM-003',
dealerCode: 'DL-KA-052',
dealerName: 'Anil Motors',
location: 'Mysore, Karnataka',
dealershipType: 'Main Dealer',
formatCategory: 'B',
terminationCategory: 'Non-compliance',
severity: 'Medium',
status: 'CEO Approved',
currentStage: 'Terminated',
submittedOn: '2025-08-01',
submittedBy: 'DD Lead'
}
];
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'Critical':
@ -118,71 +45,102 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dealerCode, setDealerCode] = useState('');
const [autoFilledData, setAutoFilledData] = useState<any>(null);
const [terminations, setTerminations] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState({
inaugurationMonth: '',
inaugurationYear: '',
loaMonth: '',
loaYear: '',
loiMonth: '',
loiYear: '',
lastSixMonthsSales: '',
numberOfDealerships: '',
numberOfStudios: '',
constitution: '',
dealershipType: '',
typeOfClosure: '',
formatCategory: '',
dealerScoreCardBand: '',
terminationCategory: '',
subCategory: '',
description: '',
reason: '',
proposedLwd: '',
comments: '',
document: null as File | null
});
const handleDealerCodeChange = (code: string) => {
setDealerCode(code);
if (mockDealerData[code]) {
setAutoFilledData(mockDealerData[code]);
toast.success('Dealer details loaded successfully');
} else {
setAutoFilledData(null);
if (code) {
toast.error('Dealer code not found');
const fetchTerminations = async () => {
setLoading(true);
try {
const response = await API.getTerminations();
const data = response.data as any;
if (data?.success) {
setTerminations(data.terminations);
}
} catch (error) {
console.error('Error fetching terminations:', error);
toast.error('Failed to fetch termination requests');
} finally {
setLoading(false);
}
};
const handleSubmit = (e: React.FormEvent) => {
useEffect(() => {
fetchTerminations();
}, []);
const handleDealerCodeChange = async (code: string) => {
setDealerCode(code);
if (code.length >= 5) {
try {
const response = await API.getOutletByCode(code);
const data = response.data as any;
if (data?.success) {
setAutoFilledData(data.outlet);
toast.success('Dealer details loaded');
} else {
setAutoFilledData(null);
}
} catch (error) {
setAutoFilledData(null);
}
} else {
setAutoFilledData(null);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!autoFilledData) {
toast.error('Please enter a valid dealer code');
return;
}
toast.success('Termination request submitted successfully');
setIsDialogOpen(false);
// Reset form
setDealerCode('');
setAutoFilledData(null);
setFormData({
inaugurationMonth: '',
inaugurationYear: '',
loaMonth: '',
loaYear: '',
loiMonth: '',
loiYear: '',
lastSixMonthsSales: '',
numberOfDealerships: '',
numberOfStudios: '',
constitution: '',
dealershipType: '',
typeOfClosure: '',
formatCategory: '',
dealerScoreCardBand: '',
terminationCategory: '',
subCategory: '',
description: '',
document: null
});
try {
// Backend expects: { dealerId, category, reason, proposedLwd, comments }
// Note: dealerId in TerminationRequest refers to 'Dealer' model ID.
// Outlet model has associate dealer? Let's check.
// In my outlet.controller.ts, I included 'dealer'.
const payload = {
dealerId: autoFilledData.Dealer?.id || autoFilledData.dealerId, // Map from outlet's dealer association
category: formData.terminationCategory,
reason: formData.reason,
proposedLwd: formData.proposedLwd,
comments: formData.comments
};
if (!payload.dealerId) {
toast.error('Dealer record not found for this code');
return;
}
const response = await API.createTermination(payload);
const data = response.data as any;
if (data?.success) {
toast.success('Termination request submitted successfully');
setIsDialogOpen(false);
fetchTerminations();
// Reset form
setDealerCode('');
setAutoFilledData(null);
setFormData({
terminationCategory: '',
reason: '',
proposedLwd: '',
comments: '',
document: null
});
}
} catch (error: any) {
console.error('Error submitting termination:', error);
toast.error(error.response?.data?.message || 'Failed to submit termination request');
}
};
const isDDLead = currentUser?.role === 'DD Lead';
@ -203,23 +161,23 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
const userStages = roleToStageMapping[currentUser.role] || [];
return userStages.some(stage =>
request.currentStage.includes(stage) ||
request.status.includes(stage)
(request.currentStage && request.currentStage.includes(stage)) ||
(request.status && request.status.includes(stage))
);
};
const openRequests = mockTerminationRequests.filter(req =>
const openRequests = terminations.filter(req =>
!req.status.includes('Terminated') &&
!req.status.includes('Completed') &&
!req.status.includes('Closed') &&
!req.status.includes('Rejected') &&
isRequestAtMyLevel(req)
);
const completedRequests = mockTerminationRequests.filter(req =>
const completedRequests = terminations.filter(req =>
req.status.includes('Terminated') ||
req.status.includes('Completed') ||
req.status.includes('Closed') ||
req.currentStage === 'Terminated'
req.status.includes('Closed')
);
return (
@ -238,7 +196,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<Card>
<CardHeader className="pb-3">
<CardDescription>All Cases</CardDescription>
<CardTitle className="text-3xl">{mockTerminationRequests.length}</CardTitle>
<CardTitle className="text-3xl">{terminations.length}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">Total Cases</p>
@ -313,276 +271,88 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
{autoFilledData && (
<div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg">
<div>
<Label className="text-slate-600">Dealer Name</Label>
<p>{autoFilledData.dealerName}</p>
<Label className="text-slate-600">Dealer Name (Legal)</Label>
<p>{autoFilledData.Dealer?.legalName || 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Business Name</Label>
<p>{autoFilledData.Dealer?.businessName || 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">GST</Label>
<p>{autoFilledData.gst}</p>
<p>{autoFilledData.Dealer?.gstNumber || 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Address</Label>
<p>{autoFilledData.address}</p>
</div>
<div>
<Label className="text-slate-600">City Category</Label>
<p>{autoFilledData.cityCategory}</p>
<Label className="text-slate-600">City/State</Label>
<p>{autoFilledData.city}, {autoFilledData.state}</p>
</div>
<div>
<Label className="text-slate-600">Domain Name</Label>
<p>{autoFilledData.domainName}</p>
<Label className="text-slate-600">Outlet Name</Label>
<p>{autoFilledData.name}</p>
</div>
<div>
<Label className="text-slate-600">Dealership Name</Label>
<p>{autoFilledData.dealershipName}</p>
</div>
<div>
<Label className="text-slate-600">Sales Code</Label>
<p>{autoFilledData.salesCode}</p>
</div>
<div>
<Label className="text-slate-600">Service Code</Label>
<p>{autoFilledData.serviceCode}</p>
</div>
<div>
<Label className="text-slate-600">Accessories Code</Label>
<p>{autoFilledData.accessoriesCode}</p>
</div>
<div>
<Label className="text-slate-600">GMA Code</Label>
<p>{autoFilledData.gmaCode}</p>
<Label className="text-slate-600">Contact</Label>
<p>{autoFilledData.email} / {autoFilledData.phoneNumber}</p>
</div>
</div>
)}
{/* Date fields */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Inauguration *</Label>
<div className="flex gap-2">
<Input
type="number"
placeholder="Month (1-12)"
min="1"
max="12"
value={formData.inaugurationMonth}
onChange={(e) => setFormData({...formData, inaugurationMonth: e.target.value})}
required
/>
<Input
type="number"
placeholder="Year"
min="2000"
max="2025"
value={formData.inaugurationYear}
onChange={(e) => setFormData({...formData, inaugurationYear: e.target.value})}
required
/>
</div>
<Label>Termination Category *</Label>
<Select value={formData.terminationCategory} onValueChange={(value) => setFormData({...formData, terminationCategory: value})}>
<SelectTrigger>
<SelectValue placeholder="Select termination category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Working Capital">Working Capital</SelectItem>
<SelectItem value="Performance Issues">Performance Issues</SelectItem>
<SelectItem value="Unethical Practical">Unethical Practical</SelectItem>
<SelectItem value="Unforeseen Circumstances">Unforeseen Circumstances</SelectItem>
<SelectItem value="Others">Others</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>LOA *</Label>
<div className="flex gap-2">
<Input
type="number"
placeholder="Month (1-12)"
min="1"
max="12"
value={formData.loaMonth}
onChange={(e) => setFormData({...formData, loaMonth: e.target.value})}
required
/>
<Input
type="number"
placeholder="Year"
min="2000"
max="2025"
value={formData.loaYear}
onChange={(e) => setFormData({...formData, loaYear: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label>LOI *</Label>
<div className="flex gap-2">
<Input
type="number"
placeholder="Month (1-12)"
min="1"
max="12"
value={formData.loiMonth}
onChange={(e) => setFormData({...formData, loiMonth: e.target.value})}
required
/>
<Input
type="number"
placeholder="Year"
min="2000"
max="2025"
value={formData.loiYear}
onChange={(e) => setFormData({...formData, loiYear: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="sales">Last 6 Months Sales *</Label>
<Label>Proposed LWD *</Label>
<Input
id="sales"
type="number"
placeholder="Enter sales figure"
value={formData.lastSixMonthsSales}
onChange={(e) => setFormData({...formData, lastSixMonthsSales: e.target.value})}
type="date"
value={formData.proposedLwd}
onChange={(e) => setFormData({...formData, proposedLwd: e.target.value})}
required
/>
</div>
</div>
{/* Number fields */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="dealerships">Number of Dealerships *</Label>
<Input
id="dealerships"
type="number"
value={formData.numberOfDealerships}
onChange={(e) => setFormData({...formData, numberOfDealerships: e.target.value})}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="studios">Number of Studios *</Label>
<Input
id="studios"
type="number"
value={formData.numberOfStudios}
onChange={(e) => setFormData({...formData, numberOfStudios: e.target.value})}
required
/>
</div>
</div>
{/* Dropdown fields */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Constitution *</Label>
<Select value={formData.constitution} onValueChange={(value) => setFormData({...formData, constitution: value})}>
<SelectTrigger>
<SelectValue placeholder="Select constitution" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pvt-ltd">PVT. LTD.</SelectItem>
<SelectItem value="partnership">Partnership</SelectItem>
<SelectItem value="proprietorship">Proprietorship</SelectItem>
<SelectItem value="public-limited">Public Limited</SelectItem>
<SelectItem value="llp">LLP</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Dealership Type *</Label>
<Select value={formData.dealershipType} onValueChange={(value) => setFormData({...formData, dealershipType: value})}>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="main-dealer">Main Dealer</SelectItem>
<SelectItem value="studio">Studio</SelectItem>
<SelectItem value="asp">ASP</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Type of Closure *</Label>
<Select value={formData.typeOfClosure} onValueChange={(value) => setFormData({...formData, typeOfClosure: value})}>
<SelectTrigger>
<SelectValue placeholder="Select closure type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="complete">Complete</SelectItem>
<SelectItem value="partial">Partial</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Format Category *</Label>
<Select value={formData.formatCategory} onValueChange={(value) => setFormData({...formData, formatCategory: value})}>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="a-plus">A+</SelectItem>
<SelectItem value="a">A</SelectItem>
<SelectItem value="b">B</SelectItem>
<SelectItem value="c">C</SelectItem>
<SelectItem value="d">D</SelectItem>
<SelectItem value="e">E</SelectItem>
<SelectItem value="r">R</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Dealer Score Card Band *</Label>
<Select value={formData.dealerScoreCardBand} onValueChange={(value) => setFormData({...formData, dealerScoreCardBand: value})}>
<SelectTrigger>
<SelectValue placeholder="Select band" />
</SelectTrigger>
<SelectContent>
<SelectItem value="platinum">Platinum</SelectItem>
<SelectItem value="gold">Gold</SelectItem>
<SelectItem value="silver">Silver</SelectItem>
<SelectItem value="bronze">Bronze</SelectItem>
<SelectItem value="no-band">No Band</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Termination specific fields */}
<div className="space-y-2">
<Label>Termination Category *</Label>
<Select value={formData.terminationCategory} onValueChange={(value) => setFormData({...formData, terminationCategory: value})}>
<SelectTrigger>
<SelectValue placeholder="Select termination category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="working-capital">Working Capital</SelectItem>
<SelectItem value="performance-issues">Performance Issues</SelectItem>
<SelectItem value="unethical-practical">Unethical Practical</SelectItem>
<SelectItem value="unforeseen-circumstances">Unforeseen Circumstances</SelectItem>
<SelectItem value="others">Others</SelectItem>
</SelectContent>
</Select>
</div>
{formData.terminationCategory && (
<div className="space-y-2">
<Label htmlFor="subCategory">Sub Category *</Label>
<Input
id="subCategory"
value={formData.subCategory}
onChange={(e) => setFormData({...formData, subCategory: e.target.value})}
placeholder="Provide more details about the termination category"
required
/>
</div>
)}
<div className="space-y-2">
<Label htmlFor="description">Description *</Label>
<Label htmlFor="reason">Termination Reason *</Label>
<Input
id="reason"
value={formData.reason}
onChange={(e) => setFormData({...formData, reason: e.target.value})}
placeholder="Primary reason for termination"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="comments">Additional Comments *</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
placeholder="Detailed description of the termination reason"
id="comments"
value={formData.comments}
onChange={(e) => setFormData({...formData, comments: e.target.value})}
placeholder="Detailed observations and justification"
rows={4}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="document">Upload Document</Label>
<Label htmlFor="document">Upload Supporting Document</Label>
<Input
id="document"
type="file"
@ -614,76 +384,76 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<TabsContent value="all" className="mt-6">
<div className="space-y-4">
{mockTerminationRequests.map((request) => (
<Card key={request.id} className="border-slate-200">
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<div className="p-3 bg-red-100 rounded-lg">
<XCircle className="w-6 h-6 text-red-600" />
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id}</h3>
<Badge className={getSeverityColor(request.severity)}>
{request.severity}
</Badge>
<Badge className={getStatusColor(request.status)}>
{request.status}
</Badge>
{loading ? (
<div className="text-center py-12">Loading requests...</div>
) : terminations.length > 0 ? (
terminations.map((request: any) => (
<Card key={request.id} className="border-slate-200">
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<div className="p-3 bg-red-100 rounded-lg">
<XCircle className="w-6 h-6 text-red-600" />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-slate-600">Dealer Name</p>
<p>{request.dealerName}</p>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id.substring(0, 8)}</h3>
<Badge className={getSeverityColor(request.severity || 'Medium')}>
{request.severity || 'Normal'}
</Badge>
<Badge className={getStatusColor(request.status)}>
{request.status}
</Badge>
</div>
<div>
<p className="text-slate-600">Dealer Code</p>
<p>{request.dealerCode}</p>
</div>
<div>
<p className="text-slate-600">Location</p>
<p>{request.location}</p>
</div>
<div>
<p className="text-slate-600">Dealership Type</p>
<p>{request.dealershipType}</p>
</div>
<div>
<p className="text-slate-600">Format Category</p>
<p>{request.formatCategory}</p>
</div>
<div>
<p className="text-slate-600">Termination Category</p>
<p>{request.terminationCategory}</p>
</div>
<div>
<p className="text-slate-600">Current Stage</p>
<p>{request.currentStage}</p>
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4 text-slate-500" />
<p>{request.submittedOn}</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-slate-600">Dealer Name</p>
<p>{request.dealer?.businessName || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600">Location</p>
<p>{request.dealer?.registeredAddress || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600">Category</p>
<p>{request.category}</p>
</div>
<div>
<p className="text-slate-600">Current Stage</p>
<p>{request.currentStage}</p>
</div>
<div>
<p className="text-slate-600">Proposed LWD</p>
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4 text-slate-500" />
<p>{request.proposedLwd}</p>
</div>
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<p>{new Date(request.createdAt).toLocaleDateString()}</p>
</div>
</div>
</div>
</div>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
className="ml-4"
>
<Eye className="w-4 h-4 mr-2" />
View Details
</Button>
</div>
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(request.id)}
className="ml-4"
>
<Eye className="w-4 h-4 mr-2" />
View Details
</Button>
</div>
</CardContent>
</Card>
))}
</CardContent>
</Card>
))
) : (
<div className="text-center py-12 text-slate-500">
<p>No termination requests found</p>
</div>
)}
</div>
</TabsContent>
@ -691,7 +461,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<TabsContent value="open" className="mt-6">
<div className="space-y-4">
{openRequests.length > 0 ? (
openRequests.map((request) => (
openRequests.map((request: any) => (
<Card key={request.id} className="border-slate-200">
<CardContent className="pt-6">
<div className="flex items-start justify-between">
@ -701,10 +471,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id}</h3>
<Badge className={getSeverityColor(request.severity)}>
{request.severity}
</Badge>
<h3 className="text-lg">{request.id.substring(0, 8)}</h3>
<Badge className={getStatusColor(request.status)}>
{request.status}
</Badge>
@ -712,11 +479,11 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-slate-600">Dealer Name</p>
<p>{request.dealerName}</p>
<p>{request.dealer?.businessName}</p>
</div>
<div>
<p className="text-slate-600">Location</p>
<p>{request.location}</p>
<p className="text-slate-600">Reason</p>
<p className="truncate">{request.reason}</p>
</div>
<div>
<p className="text-slate-600">Current Stage</p>
@ -724,7 +491,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<p>{request.submittedOn}</p>
<p>{new Date(request.createdAt).toLocaleDateString()}</p>
</div>
</div>
</div>
@ -755,7 +522,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<TabsContent value="completed" className="mt-6">
<div className="space-y-4">
{completedRequests.length > 0 ? (
completedRequests.map((request) => (
completedRequests.map((request: any) => (
<Card key={request.id} className="border-slate-200">
<CardContent className="pt-6">
<div className="flex items-start justify-between">
@ -765,7 +532,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id}</h3>
<h3 className="text-lg">{request.id.substring(0, 8)}</h3>
<Badge className={getStatusColor(request.status)}>
{request.status}
</Badge>
@ -773,19 +540,19 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-slate-600">Dealer Name</p>
<p>{request.dealerName}</p>
<p>{request.dealer?.businessName}</p>
</div>
<div>
<p className="text-slate-600">Location</p>
<p>{request.location}</p>
<p className="text-slate-600">Closed On</p>
<p>{new Date(request.updatedAt).toLocaleDateString()}</p>
</div>
<div>
<p className="text-slate-600">Termination Category</p>
<p>{request.terminationCategory}</p>
<p>{request.category}</p>
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<p>{request.submittedOn}</p>
<p className="text-slate-600">LWD</p>
<p>{request.proposedLwd}</p>
</div>
</div>
</div>
@ -806,7 +573,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
) : (
<div className="text-center py-12 text-slate-500">
<XCircle className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p>No completed termination cases</p>
<p>No completed terminations to display</p>
</div>
)}
</div>

View File

@ -95,5 +95,11 @@ export const masterService = {
previewEmailTemplate: async (data: any) => {
const response = await API.previewEmailTemplate(data);
return response.data;
},
// SLA
getSlaConfigs: async () => {
const response = await API.getSlaConfigs();
return response.data;
}
};

View File

@ -161,5 +161,32 @@ export const onboardingService = {
console.error('Update architecture status error:', error);
throw error;
}
},
generateDealerCodes: async (applicationId: string) => {
try {
const response: any = await API.generateDealerCodes(applicationId);
return response.data;
} catch (error) {
console.error('Generate dealer codes error:', error);
throw error;
}
},
updateApplicationStatus: async (id: string, data: any) => {
try {
const response: any = await API.updateApplicationStatus(id, data);
return response.data;
} catch (error) {
console.error('Update application status error:', error);
throw error;
}
},
createDealer: async (data: any) => {
try {
const response: any = await API.createDealer(data);
return response.data;
} catch (error) {
console.error('Create dealer error:', error);
throw error;
}
}
};