Dealer_Onboard_Frontend/src/components/applications/MasterPage.tsx

418 lines
22 KiB
TypeScript

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import {
Tabs, TabsContent, TabsList, TabsTrigger
} from '../ui/tabs';
import { Globe, Shield, Clock, Mail, MapPin, SlidersHorizontal } from 'lucide-react';
import { Badge } from '../ui/badge';
import { toast } from 'sonner';
// Services & Hooks
import { masterService } from '../../services/master.service';
import { useMasterData } from '../../hooks/useMasterData';
// Sub-components
import { ZonesOverview } from './MasterPage/ZonesOverview';
import { ZoneDetails } from './MasterPage/ZoneDetails';
import { RegionalManagement } from './MasterPage/RegionalManagement';
import { ASMManagement } from './MasterPage/ASMManagement';
import { ZMManagement } from './MasterPage/ZMManagement';
import { UserManagementTable } from './MasterPage/UserManagementTable';
import { SLAConfiguration } from './MasterPage/SLAConfiguration';
import { RolePermissions } from './MasterPage/RolePermissions';
import { EmailTemplates } from './MasterPage/EmailTemplates';
import { LocationManagement } from './MasterPage/LocationManagement';
import { ASMDialog } from './MasterPage/ASMDialog';
import { ZMDialog } from './MasterPage/ZMDialog';
import { ZoneDialog } from './MasterPage/ZoneDialog';
import { RegionDialog } from './MasterPage/RegionDialog';
import { TemplateDialog } from './MasterPage/TemplateDialog';
import { LocationDialog } from './MasterPage/LocationDialog';
import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage';
import { RootState } from '../../store';
export const MasterPage: React.FC = () => {
const { fetchInitialData } = useMasterData();
const {
asms, zonalManagerMappings,
allDistricts,
users,
loading
} = useSelector((state: RootState) => state.master);
// Tab & Selection State
const [activeTab, setActiveTab] = useState('hierarchy');
const [selectedZone, setSelectedZone] = useState('all');
// Dialog Visibility
const [showASMDialog, setShowASMDialog] = useState(false);
const [showZoneDialog, setShowZoneDialog] = useState(false);
const [showRegionDialog, setShowRegionDialog] = useState(false);
const [showTemplateDialog, setShowTemplateDialog] = useState(false);
const [showLocationDialog, setShowLocationDialog] = useState(false);
// Form State (ASM)
const [editingASMId, setEditingASMId] = useState<string | null>(null);
const [asmManagerId, setAsmManagerId] = useState('');
const [asmName, setAsmName] = useState('');
const [asmCode, setAsmCode] = useState('');
const [asmEmployeeId, setAsmEmployeeId] = useState('');
const [asmStatus, setAsmStatus] = useState<'active' | 'inactive'>('active');
const [selectedASMZone, setSelectedASMZone] = useState('');
const [selectedASMRegion, setSelectedASMRegion] = useState('');
const [selectedASMStates, setSelectedASMStates] = useState<string[]>([]);
const [selectedASMDistricts, setSelectedASMDistricts] = useState<string[]>([]);
// ZM Management State
const [showZMDialog, setShowZMDialog] = useState(false);
const [editingZMId, setEditingZMId] = useState<string | null>(null);
const [zmManagerId, setZmManagerId] = useState('');
const [zmName, setZmName] = useState('');
const [zmCode, setZmCode] = useState('');
const [zmEmployeeId, setZmEmployeeId] = useState('');
const [zmStatus, setZmStatus] = useState<'active' | 'inactive'>('active');
const [selectedZMZone, setSelectedZMZone] = useState('');
const [selectedZMStates, setSelectedZMStates] = useState<string[]>([]);
const [selectedZMDistricts, setSelectedZMDistricts] = useState<string[]>([]);
// Form State (Zone)
const [editingZoneId, setEditingZoneId] = useState<string | null>(null);
const [zoneName, setZoneName] = useState('');
const [zoneCode, setZoneCode] = useState('');
const [zoneDescription, setZoneDescription] = useState('');
const [zonalBusinessHeadId, setZonalBusinessHeadId] = useState('');
// Form State (Region)
const [editingRegionId, setEditingRegionId] = useState<string | null>(null);
const [regionName, setRegionName] = useState('');
const [regionCode, setRegionCode] = useState('');
const [regionDescription, setRegionDescription] = useState('');
const [selectedRegionZone, setSelectedRegionZone] = useState('');
const [regionalManagerId, setRegionalManagerId] = useState('');
const [selectedRegionDistricts, setSelectedRegionDistricts] = useState<string[]>([]);
// Form State (Template)
const [editingTemplate, setEditingTemplate] = useState<any>(null);
const [testDataInput, setTestDataInput] = useState('{"applicant_name": "John Doe"}');
const [previewLoading, setPreviewLoading] = useState(false);
const [previewContent, setPreviewContent] = useState<any>(null);
// Form State (Location)
const [editingLocationId] = useState<string | null>(null);
const [locationState, setLocationState] = useState('');
const [locationDistrict, setLocationDistrict] = useState('');
const [locationCity, setLocationCity] = useState('');
const [locationPincode, setLocationPincode] = useState('');
const [locationActiveFrom, setLocationActiveFrom] = useState('');
const [locationActiveTo, setLocationActiveTo] = useState('');
const [locationStatus, setLocationStatus] = useState('active');
// Initial Load
useEffect(() => {
fetchInitialData();
}, [fetchInitialData]);
// Shared Data Helpers
const districtsAssignedToOthers = useMemo(() => {
const map: Record<string, string[]> = {};
[...asms, ...zonalManagerMappings].forEach(m => {
const dists = (m as any).areasManaged || (m as any).districts || [];
dists.forEach((d: any) => {
const id = typeof d === 'string' ? d : d.id;
if (!map[id]) map[id] = [];
if (!map[id].includes(m.name)) map[id].push(m.name);
});
});
return map;
}, [asms, zonalManagerMappings]);
const getDistrictsForSelectedState = useCallback((stateName: string) => {
return allDistricts
.filter(d => d.stateName?.toUpperCase() === stateName?.toUpperCase())
.map(d => ({ id: d.id, name: d.name }));
}, [allDistricts]);
// Handlers
const handleSaveASM = async () => {
if (!asmManagerId) {
toast.error('Please select an ASM user');
return;
}
try {
const payload = { userId: asmManagerId, asmCode, districts: selectedASMDistricts, status: asmStatus };
const res = await masterService.saveASM(payload) as any;
if (res.success) {
toast.success(`ASM ${editingASMId ? 'updated' : 'assigned'} successfully`);
setShowASMDialog(false);
fetchInitialData();
}
} catch (error) { toast.error('Failed to save ASM'); }
};
const handleEditASM = (asm: any) => {
setEditingASMId(asm.id);
setAsmManagerId(asm.id);
setAsmName(asm.name);
setAsmCode(asm.asmCode);
setAsmEmployeeId(asm.employeeId);
setAsmStatus(asm.status.toLowerCase() as 'active' | 'inactive');
setSelectedASMZone(asm.zoneId);
setSelectedASMRegion(asm.regionId);
setSelectedASMStates(asm.stateNames || []);
setSelectedASMDistricts(asm.areasManaged?.map((a: any) => a.id) || []);
setShowASMDialog(true);
};
const handleEditZM = (zm: any) => {
setEditingZMId(zm.id);
setZmManagerId(zm.id);
setZmName(zm.name);
setZmCode(zm.code === 'N/A' ? '' : zm.code);
setZmEmployeeId(zm.code === 'N/A' ? '' : zm.code);
setZmStatus(zm.status.toLowerCase() as 'active' | 'inactive');
setSelectedZMZone(zm.zoneId || '');
setSelectedZMStates(zm.stateNames || []);
setSelectedZMDistricts(zm.districts?.map((d: any) => d.id) || []);
setShowZMDialog(true);
};
const handleSaveZM = async () => {
if (!zmManagerId || !selectedZMZone) {
toast.error('Manager and Zone are required');
return;
}
try {
const payload = {
userId: zmManagerId,
roleCode: 'DD-ZM',
zmCode,
districts: selectedZMDistricts,
status: zmStatus,
locationId: selectedZMZone // ZM primary location is the zone
};
// Use the generic saveASM method which I generalized on the backend
const res = await masterService.saveASM(payload) as any;
if (res.success) {
toast.success(`Zonal Manager ${editingZMId ? 'updated' : 'assigned'} successfully`);
setShowZMDialog(false);
fetchInitialData();
}
} catch (error) { toast.error('Failed to save Zonal Manager'); }
};
const handleSaveZone = async () => {
try {
const payload = { id: editingZoneId, name: zoneName, code: zoneCode, description: zoneDescription, managerId: zonalBusinessHeadId };
const res = await masterService.saveZone(payload) as any;
if (res.success) {
toast.success('Zone saved successfully');
setShowZoneDialog(false);
fetchInitialData();
}
} catch (error) { toast.error('Error saving zone'); }
};
const handleSaveRegion = async () => {
try {
const payload = {
name: regionName,
code: regionCode,
description: regionDescription,
parentId: selectedRegionZone,
managerId: regionalManagerId,
districts: selectedRegionDistricts,
status: 'Active'
};
const res = await masterService.saveRegion(payload) as any;
if (res.success) {
toast.success('Region saved successfully');
setShowRegionDialog(false);
fetchInitialData();
}
} catch (error) { toast.error('Error saving region'); }
};
const handleSaveTemplate = async () => {
try {
const res = await (editingTemplate?.id
? masterService.updateEmailTemplate(editingTemplate.id, editingTemplate)
: masterService.createEmailTemplate(editingTemplate)) as any;
if (res.success) {
toast.success('Template saved');
setShowTemplateDialog(false);
fetchInitialData();
}
} catch (error) { toast.error('Error saving template'); }
};
const handlePreviewTemplate = async () => {
setPreviewLoading(true);
try {
const res = await masterService.previewEmailTemplate({
template: editingTemplate,
testData: JSON.parse(testDataInput)
}) as any;
if (res.success) setPreviewContent(res.data);
} catch (error) { toast.error('Preview failed'); }
finally { setPreviewLoading(false); }
};
const handleSaveLocation = async () => {
try {
const payload = {
id: editingLocationId,
stateId: locationState,
districtId: locationDistrict,
city: locationCity,
pincode: locationPincode,
status: locationStatus,
activeFrom: locationActiveFrom,
activeTo: locationActiveTo
};
const res = await (editingLocationId
? masterService.updateArea(editingLocationId, payload)
: masterService.createArea(payload)) as any;
if (res.success) {
toast.success('Location saved');
setShowLocationDialog(false);
fetchInitialData();
}
} catch (error) { toast.error('Error saving location'); }
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-slate-900 mb-2 font-bold text-2xl">Master Configuration</h1>
<p className="text-slate-600">Centralized governance for locations, roles, and operational policies</p>
</div>
<Badge className="bg-gradient-to-r from-purple-600 to-indigo-600 px-4 py-1">Admin Control Panel</Badge>
</div>
{loading ? (
<div className="flex flex-col items-center justify-center p-20 space-y-4">
<div className="w-12 h-12 border-4 border-amber-600 border-t-transparent rounded-full animate-spin"></div>
<p className="text-slate-600 font-medium animate-pulse">Synchronizing Global Settings...</p>
</div>
) : (
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-6 h-auto sticky top-0 z-10 bg-white/80 backdrop-blur-sm border shadow-sm rounded-xl p-1">
<TabsTrigger value="hierarchy" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
<Globe className="w-4 h-4" /> Organisation
</TabsTrigger>
<TabsTrigger value="roles" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
<Shield className="w-4 h-4" /> Roles
</TabsTrigger>
<TabsTrigger value="sla" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
<Clock className="w-4 h-4" /> SLA Config
</TabsTrigger>
<TabsTrigger value="templates" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
<Mail className="w-4 h-4" /> Emails
</TabsTrigger>
<TabsTrigger value="locations" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
<MapPin className="w-4 h-4" /> Locations
</TabsTrigger>
<TabsTrigger value="approvals" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
<SlidersHorizontal className="w-4 h-4" /> Approvals
</TabsTrigger>
</TabsList>
<TabsContent value="hierarchy" className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300">
<ZonesOverview selectedZone={selectedZone} onZoneClick={(id) => setSelectedZone(selectedZone === id ? 'all' : id)} />
<ZoneDetails selectedZone={selectedZone} onAddZone={() => { setEditingZoneId(null); setZoneName(''); setZoneCode(''); setZoneDescription(''); setZonalBusinessHeadId(''); setShowZoneDialog(true); }}
onEditZone={(z) => { setEditingZoneId(z.id); setZoneName(z.name); setZoneCode(z.code); setZoneDescription(z.description); setZonalBusinessHeadId(z.zonalBusinessHead?.id || ''); setShowZoneDialog(true); }} />
<RegionalManagement selectedZone={selectedZone} onAddRegion={() => { setEditingRegionId(null); setRegionName(''); setRegionCode(''); setSelectedRegionZone(selectedZone === 'all' ? '' : selectedZone); setRegionalManagerId(''); setSelectedRegionDistricts([]); setShowRegionDialog(true); }}
onEditRegion={(r) => {
setEditingRegionId(r.id);
setRegionName(r.name);
setRegionCode(r.code);
setSelectedRegionZone(r.zoneId);
setRegionalManagerId(r.regionalManager?.id || '');
setSelectedRegionDistricts(r.districts?.map((d: any) => d.id) || []);
setShowRegionDialog(true);
}}
onDeleteRegion={() => toast.error('Regional office deletion is restricted via portal')} />
<ZMManagement selectedZone={selectedZone}
onAddZM={() => {
setEditingZMId(null); setZmManagerId(''); setZmName(''); setZmCode(''); setZmEmployeeId('');
setZmStatus('active'); setSelectedZMZone(selectedZone === 'all' ? '' : selectedZone);
setSelectedZMStates([]); setSelectedZMDistricts([]);
setShowZMDialog(true);
}}
onEditZM={handleEditZM}
onDeleteZM={() => toast.error('ZM deletion restricted')} />
<ASMManagement selectedZone={selectedZone} onAddASM={() => { setEditingASMId(null); setAsmManagerId(''); setAsmName(''); setAsmCode(''); setAsmEmployeeId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setShowASMDialog(true); }}
onEditASM={handleEditASM} onDeleteASM={() => toast.error('ASM deletion restricted')} />
<UserManagementTable userAssignedData={users.length > 0 ? users : asms} />
</TabsContent>
<TabsContent value="roles" className="animate-in fade-in duration-300">
<RolePermissions onAddRole={() => toast.info('Unified Role Management interface being updated')}
onEditRole={() => toast.info('Unified Role Management interface being updated')} />
</TabsContent>
<TabsContent value="sla" className="animate-in fade-in duration-300">
<SLAConfiguration onConfigureSLA={() => toast.info('SLA Matrix Configuration interface being updated')} />
</TabsContent>
<TabsContent value="templates" className="animate-in fade-in duration-300">
<EmailTemplates onAddTemplate={() => setShowTemplateDialog(true)}
onEditTemplate={() => toast.info('Template Editor being updated')} onDeleteTemplate={() => toast.error('Delete Template restricted')} />
</TabsContent>
<TabsContent value="locations" className="animate-in fade-in duration-300">
<LocationManagement onAddLocation={() => setShowLocationDialog(true)}
onEditLocation={() => toast.info('Location Editor being updated')} onDeleteLocation={() => toast.error('Delete Location restricted')} />
</TabsContent>
<TabsContent value="approvals" className="animate-in fade-in duration-300">
<ApprovalPoliciesPage />
</TabsContent>
</Tabs>
)}
{/* Main Dialogs */}
<ZoneDialog isOpen={showZoneDialog} onOpenChange={setShowZoneDialog} editingZoneId={editingZoneId} zoneName={zoneName} setZoneName={setZoneName} zoneCode={zoneCode} setZoneCode={setZoneCode} zoneDescription={zoneDescription} setZoneDescription={setZoneDescription} zonalBusinessHeadId={zonalBusinessHeadId} setZonalBusinessHeadId={setZonalBusinessHeadId} onSave={handleSaveZone} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} />
<RegionDialog isOpen={showRegionDialog} onOpenChange={setShowRegionDialog} editingRegionId={editingRegionId} regionName={regionName} setRegionName={setRegionName} regionCode={regionCode} setRegionCode={setRegionCode} regionDescription={regionDescription} setRegionDescription={setRegionDescription} selectedRegionZone={selectedRegionZone} setSelectedRegionZone={setSelectedRegionZone} regionalManagerId={regionalManagerId} setRegionalManagerId={setRegionalManagerId} selectedRegionStates={selectedRegionDistricts} setSelectedRegionStates={setSelectedRegionDistricts} onSave={handleSaveRegion} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} />
<ASMDialog isOpen={showASMDialog} onOpenChange={setShowASMDialog} editingASMId={editingASMId} asmManagerId={asmManagerId} setAsmManagerId={setAsmManagerId} asmName={asmName} setAsmName={setAsmName} asmCode={asmCode} setAsmCode={setAsmCode} asmEmployeeId={asmEmployeeId} setAsmEmployeeId={setAsmEmployeeId} asmStatus={asmStatus} setAsmStatus={setAsmStatus} selectedASMZone={selectedASMZone} setSelectedASMZone={setSelectedASMZone} selectedASMRegion={selectedASMRegion} setSelectedASMRegion={setSelectedASMRegion} selectedASMStates={selectedASMStates} setSelectedASMStates={setSelectedASMStates} selectedASMDistricts={selectedASMDistricts} setSelectedASMDistricts={setSelectedASMDistricts} onSave={handleSaveASM} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} districtsAssignedToOthers={districtsAssignedToOthers} getDistrictsForSelectedState={getDistrictsForSelectedState} />
<ZMDialog
isOpen={showZMDialog}
onOpenChange={setShowZMDialog}
editingZMId={editingZMId}
zmManagerId={zmManagerId}
setZmManagerId={setZmManagerId}
zmName={zmName}
setZmName={setZmName}
zmCode={zmCode}
setZmCode={setZmCode}
zmEmployeeId={zmEmployeeId}
setZmEmployeeId={setZmEmployeeId}
zmStatus={zmStatus}
setZmStatus={setZmStatus}
selectedZMZone={selectedZMZone}
setSelectedZMZone={setSelectedZMZone}
selectedZMStates={selectedZMStates}
setSelectedZMStates={setSelectedZMStates}
selectedZMDistricts={selectedZMDistricts}
setSelectedZMDistricts={setSelectedZMDistricts}
onSave={handleSaveZM}
userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms}
districtsAssignedToOthers={districtsAssignedToOthers}
getDistrictsForSelectedState={getDistrictsForSelectedState}
/>
<TemplateDialog isOpen={showTemplateDialog} onOpenChange={setShowTemplateDialog} editingTemplate={editingTemplate} setEditingTemplate={setEditingTemplate} testDataInput={testDataInput} setTestDataInput={setTestDataInput} previewLoading={previewLoading} handlePreviewTemplate={handlePreviewTemplate} previewContent={previewContent} handleSaveTemplate={handleSaveTemplate} />
<LocationDialog isOpen={showLocationDialog} onOpenChange={setShowLocationDialog} editingLocationId={editingLocationId} locationState={locationState} setLocationState={setLocationState} locationDistrict={locationDistrict} setLocationDistrict={setLocationDistrict} locationCity={locationCity} setLocationCity={setLocationCity} locationPincode={locationPincode} setLocationPincode={setLocationPincode} locationActiveFrom={locationActiveFrom} setLocationActiveFrom={setLocationActiveFrom} locationActiveTo={locationActiveTo} setLocationActiveTo={setLocationActiveTo} locationStatus={locationStatus} setLocationStatus={setLocationStatus} onSave={handleSaveLocation} />
</div>
);
};
// No default export as App.tsx expects named export MasterPage