master page modified and trying to cover all models necessary flow
This commit is contained in:
parent
ad33de7e26
commit
2cf919a0dc
@ -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;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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))) &&
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user