From 2cf919a0dc07118ace4b32e6c5dc640452400acc Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Wed, 18 Mar 2026 19:50:32 +0530 Subject: [PATCH] master page modified and trying to cover all models necessary flow --- src/api/API.ts | 44 + src/components/admin/UserManagementPage.tsx | 181 +- .../applications/ApplicationDetails.tsx | 104 +- .../ConstitutionalChangeDetails.tsx | 314 ++-- .../applications/ConstitutionalChangePage.tsx | 463 +++-- .../applications/FinanceFnFDetailsPage.tsx | 406 ++-- .../applications/FinanceFnFPage.tsx | 187 +- src/components/applications/FnFDetails.tsx | 91 +- src/components/applications/FnFPage.tsx | 98 +- src/components/applications/MasterPage.tsx | 1634 +++++------------ .../applications/RelocationRequestDetails.tsx | 363 ++-- .../applications/RelocationRequestPage.tsx | 610 +++--- .../applications/ResignationDetails.tsx | 540 +++--- .../applications/ResignationPage.tsx | 587 ++---- .../applications/TerminationPage.tsx | 667 +++---- src/services/master.service.ts | 6 + src/services/onboarding.service.ts | 27 + 17 files changed, 2765 insertions(+), 3557 deletions(-) diff --git a/src/api/API.ts b/src/api/API.ts index 90cff94..87126e8 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -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; diff --git a/src/components/admin/UserManagementPage.tsx b/src/components/admin/UserManagementPage.tsx index f70baec..146efa1 100644 --- a/src/components/admin/UserManagementPage.tsx +++ b/src/components/admin/UserManagementPage.tsx @@ -29,6 +29,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '. export function UserManagementPage() { const [users, setUsers] = useState([]); const [roles, setRoles] = useState([]); + const [zones, setZones] = useState([]); + const [regions, setRegions] = useState([]); + const [states, setStates] = useState([]); + const [districts, setDistricts] = useState([]); + const [areas, setAreas] = useState([]); + 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() { + <> + + + + )} {((currentUser && currentUser.id === application.architectureAssignedTo) || (currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role))) && diff --git a/src/components/applications/ConstitutionalChangeDetails.tsx b/src/components/applications/ConstitutionalChangeDetails.tsx index 446f90d..0df566c 100644 --- a/src/components/applications/ConstitutionalChangeDetails.tsx +++ b/src/components/applications/ConstitutionalChangeDetails.tsx @@ -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 = { 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(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 ( +
+ +

Loading request details...

+
+ ); + } 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
-

{request.id} - Constitutional Change Details

+

{request.requestId} - Constitutional Change Details

- {request.dealerName} ({request.dealerCode}) + {request.outlet?.name || 'N/A'} ({request.outlet?.code || 'N/A'})

@@ -268,33 +246,33 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on

Dealer Details

-

{request.dealerName}

-

{request.dealerCode}

-

{request.location}

+

{request.outlet?.name || 'N/A'}

+

{request.outlet?.code || 'N/A'}

+

{request.outlet?.city || request.outlet?.address || 'N/A'}

Constitutional Change

- - {request.currentType} + + {request.outlet?.type || 'Proprietorship'} - - {request.targetType} + + {request.changeType}

Request Information

-

Submitted: {request.submittedOn}

-

By: {request.submittedBy}

+

Submitted: {new Date(request.createdAt).toLocaleDateString()}

+

By: {request.dealer?.fullName || 'Dealer'}

Current Stage: {request.currentStage}

Reason for Change

-

{request.reason}

+

{request.description}

@@ -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 (
@@ -447,7 +424,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
{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 (
{uploaded && ( -

{uploaded.fileName}

+

{uploaded.fileName || uploaded.name}

)}
@@ -488,7 +465,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on {/* Existing Documents Sub-tab */} - {mockUploadedDocuments.length > 0 ? ( + {(request.documents || []).length > 0 ? (

All Uploaded Documents

@@ -504,19 +481,19 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on - {mockUploadedDocuments.map((doc) => ( - + {(request.documents || []).map((doc: any, index: number) => ( + - {documentNames[doc.docNumber]} + {doc.docNumber ? documentNames[doc.docNumber] : doc.name} - {doc.fileName} + {doc.fileName || doc.name} - {doc.uploadedOn} + {new Date(doc.uploadedOn || doc.createdAt).toLocaleDateString()} - {doc.uploadedBy} + {doc.uploadedBy || 'Dealer'} @@ -550,39 +527,42 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on - {/* History Tab */} + {/* History Tab */}
- {mockWorkflowHistory.map((entry, index) => ( + {(request.timeline || request.history || []).map((entry: any, index: number) => (
- {entry.status === 'Completed' ? ( + {(entry.status || entry.action)?.toLowerCase().includes('approve') || (entry.status || entry.action)?.toLowerCase().includes('complete') ? ( - ) : entry.status === 'In Progress' ? ( - ) : ( - + )}
-

{entry.stage}

-

{entry.actor}

+

{entry.stage || entry.entityType || 'Update'}

+

{entry.actor || entry.user?.fullName || entry.user}

- - {entry.action} + + {entry.action || entry.status}
-

{entry.comments}

-

{entry.date}

+

{entry.comments || entry.remarks || 'No remarks provided'}

+

{new Date(entry.date || entry.createdAt || entry.timestamp).toLocaleString()}

))} + {(request.timeline || request.history || []).length === 0 && ( +
+ No history found +
+ )}
@@ -614,8 +594,13 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on @@ -623,25 +608,30 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on variant="destructive" className="w-full" onClick={() => handleAction('reject')} + disabled={isActionLoading} > - + {isActionLoading && actionType === 'reject' ? ( + + ) : ( + + )} Reject Request -
+
@@ -684,18 +674,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on > Cancel - + @@ -714,27 +712,27 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser, on
{/* Discussion Thread */}
- +
- {worknotes.map((note) => ( + {(request.worknotes || []).map((note: any) => (
{/* Avatar */}
- {note.avatar} + {note.author?.fullName?.slice(0, 2).toUpperCase() || note.user?.fullName?.slice(0, 2).toUpperCase() || 'UN'}
{/* Message Content */}
-
{note.user}
+
{note.author?.fullName || note.user?.fullName || 'Unknown User'}
- {note.role} + {note.author?.role || note.user?.role?.name || 'User'}
- {note.timestamp} + {new Date(note.createdAt).toLocaleString()}
-

{note.message}

+

{note.noteText || note.message}

))} diff --git a/src/components/applications/ConstitutionalChangePage.tsx b/src/components/applications/ConstitutionalChangePage.tsx index b7e6f81..e6191fb 100644 --- a/src/components/applications/ConstitutionalChangePage.tsx +++ b/src/components/applications/ConstitutionalChangePage.tsx @@ -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 = { - '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 = { '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(null); const [targetType, setTargetType] = useState(''); const [reason, setReason] = useState(''); const [requiredDocs, setRequiredDocs] = useState([]); + const [requests, setRequests] = useState([]); + 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 (
+ {/* Loading Overlay */} + {isLoading && ( +
+ +
+ )} + {/* Header */}
@@ -404,9 +349,16 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit @@ -468,59 +420,67 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit - {filteredRequests.map((request) => ( - - -
{request.id}
-
{request.dealerCode}
-
- -
{request.dealerName}
-
{request.location}
-
- -
- - {request.currentType} - - - - {request.targetType} - -
-
- - - {request.currentStage} - - - -
-
-
-
- {request.progressPercentage}% -
- - -
{request.submittedOn}
-
By {request.submittedBy}
-
- - + {requests.length === 0 ? ( + + + No constitutional change requests found - ))} + ) : ( + requests.map((request: any) => ( + + +
{request.requestId}
+
{request.outlet?.code || 'N/A'}
+
+ +
{request.outlet?.name || 'N/A'}
+
{request.outlet?.city || request.outlet?.address || 'N/A'}
+
+ +
+ + {request.outlet?.type || 'Proprietorship'} + + + + {request.changeType} + +
+
+ + + {request.currentStage} + + + +
+
+
+
+ {request.progressPercentage || 0}% +
+ + +
{new Date(request.createdAt).toLocaleDateString()}
+
By {request.dealer?.fullName || 'Dealer'}
+
+ + + + + )) + )}
@@ -540,26 +500,26 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
- {filteredRequests - .filter(r => r.status.includes('Review') || r.status.includes('Pending')) - .map((request) => ( - + {requests + .filter((r: any) => r.status.includes('Review') || r.status.includes('Pending')) + .map((request: any) => ( + -
{request.id}
-
{request.dealerCode}
+
{request.requestId}
+
{request.outlet?.code || 'N/A'}
-
{request.dealerName}
-
{request.location}
+
{request.outlet?.name || 'N/A'}
+
{request.outlet?.city || 'N/A'}
- - {request.currentType} + + {request.outlet?.type || 'Proprietorship'} - - {request.targetType} + + {request.changeType}
@@ -577,7 +537,7 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
@@ -604,26 +571,26 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
- {filteredRequests - .filter(r => r.status !== 'Completed' && !r.status.includes('Rejected')) - .map((request) => ( - + {requests + .filter((r: any) => r.status !== 'Completed' && !r.status.includes('Rejected')) + .map((request: any) => ( + -
{request.id}
-
{request.dealerCode}
+
{request.requestId}
+
{request.outlet?.code || 'N/A'}
-
{request.dealerName}
-
{request.location}
+
{request.outlet?.name || 'N/A'}
+
{request.outlet?.city || 'N/A'}
- - {request.currentType} + + {request.outlet?.type || 'Proprietorship'} - - {request.targetType} + + {request.changeType}
@@ -632,10 +599,10 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
- {request.progressPercentage}% + {request.progressPercentage || 0}%
@@ -647,7 +614,7 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit
@@ -674,26 +648,26 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit - {filteredRequests - .filter(r => r.status === 'Completed') - .map((request) => ( - + {requests + .filter((r: any) => r.status === 'Completed' || r.status === 'Closed') + .map((request: any) => ( + -
{request.id}
-
{request.dealerCode}
+
{request.requestId}
+
{request.outlet?.code || 'N/A'}
-
{request.dealerName}
-
{request.location}
+
{request.outlet?.name || 'N/A'}
+
{request.outlet?.city || 'N/A'}
- - {request.currentType} + + {request.outlet?.type || 'Proprietorship'} - - {request.targetType} + + {request.changeType}
@@ -703,13 +677,13 @@ export function ConstitutionalChangePage({ currentUser, onViewDetails }: Constit -
{request.submittedOn}
+
{new Date(request.createdAt).toLocaleDateString()}
diff --git a/src/components/applications/FinanceFnFDetailsPage.tsx b/src/components/applications/FinanceFnFDetailsPage.tsx index ff1e0c8..c3bddc5 100644 --- a/src/components/applications/FinanceFnFDetailsPage.tsx +++ b/src/components/applications/FinanceFnFDetailsPage.tsx @@ -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(null); + const [loading, setLoading] = useState(true); - // Initialize editable line items from mock data - const [payableItems, setPayableItems] = useState([ - { 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([]); + const [receivableItems, setReceivableItems] = useState([]); + const [deductionItems, setDeductionItems] = useState([]); - const [receivableItems, setReceivableItems] = useState([ - { 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([ - { 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([]); // 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) => { @@ -338,6 +425,23 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr setTimeout(() => onBack(), 1500); }; + if (loading) { + return ( +
+ +
+ ); + } + + if (!fnfCase) { + return ( +
+

Settlement case not found

+ +
+ ); + } + return (
{/* Header */} @@ -1012,7 +1116,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr - + Final Settlement Summary @@ -1204,7 +1308,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
- {fnfCase.documents.map((doc, index) => ( + {fnfCase.documents.map((doc: any, index: number) => (
@@ -1257,7 +1361,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr {uploadedDocuments.length > 0 && (
- {uploadedDocuments.map((doc, index) => ( + {uploadedDocuments.map((doc: any, index: number) => (
diff --git a/src/components/applications/FinanceFnFPage.tsx b/src/components/applications/FinanceFnFPage.tsx index e3e3a90..af5be86 100644 --- a/src/components/applications/FinanceFnFPage.tsx +++ b/src/components/applications/FinanceFnFPage.tsx @@ -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([]); + const [loading, setLoading] = useState(true); const [selectedCase, setSelectedCase] = useState(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 ( +
+ +
+ ); + } return (
@@ -234,7 +193,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
-
{mockFnFCases.length}
+
{displaySettlements.length}
@@ -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})
- - - - Name - Role - Zone - Region - Permissions Configured - Contact - Status - Actions - - - - {userAssignedData.map((user) => ( - - -
- - {user.name} -
-
- - - {user.role} - - - - {user.zone} - - {user.region} - -
- - {user.permissions?.filter(p => availablePermissions.find(ap => ap.permissionCode === p && ap.permissionCategory === 'ACTION'))?.length || 0} Actions - - - {user.permissions?.filter(p => availablePermissions.find(ap => ap.permissionCode === p && ap.permissionCategory === 'VIEW'))?.length || 0} Views - - - {user.permissions?.filter(p => availablePermissions.find(ap => ap.permissionCode === p && ap.permissionCategory === 'STAGE'))?.length || 0} Stages - -
-
- -
-

{user.email}

-

{user.phone}

-
-
- - {user.status === 'Active' ? ( - Active - ) : ( - Inactive - )} - - -
- - -
-
-
- ))} -
-
+
+
+

+ To ensure consistency and a single source of truth, user assignments to Zones, Regions, and Areas are now managed directly through the centralized User Management module. +

+
+
+ + Assign users to Zones & Regions +
+
+ + Configure Role access levels +
+
+ + Manage District & Area mappings +
+
+ + Update Account & Contact info +
+
+
+
@@ -2085,7 +1747,7 @@ export function MasterPage() { Reminders ({sla.reminders.length})
- {sla.reminders.map((reminder, index) => ( + {sla.reminders.map((reminder) => (
{reminder.time} {reminder.unit} @@ -2196,70 +1858,7 @@ export function MasterPage() { - {/* Template Scenarios Reference */} - - - Template Reference Guide - - Standard codes and available variables for system scenarios. - Use these codes to ensure the system sends the correct email. - - - -
- {TEMPLATE_SCENARIOS.map((category) => ( -
-

- {category.category} -

-
- {category.scenarios.map((scenario) => ( -
-
-
-

{scenario.name}

-
- - {scenario.code} - - -
-
-
-

{scenario.description}

-
- {scenario.variables.map((v) => ( - { - navigator.clipboard.writeText(v); - toast.success("Variable copied!"); - }} - > - {v} - - ))} -
-
- ))} -
-
- ))} -
-
-
+ {/* Template Scenarios Reference removed */} {/* Locations Tab */} @@ -2316,7 +1915,7 @@ export function MasterPage() {
+
+ + @@ -2489,24 +2104,11 @@ export function MasterPage() {
- {[ - 'View Applications', - 'Review Applications', - 'Approve Applications', - 'Reject Applications', - 'Schedule Interviews', - 'Upload Documents', - 'View Reports', - 'Manage Users', - 'Configure SLA', - 'Manage Templates', - 'View Payments', - 'Verify Payments' - ].map((permission) => ( -
- -