location hirarchy related changes done

This commit is contained in:
laxman h 2026-03-26 21:23:04 +05:30
parent 73cf4fdfac
commit def289e12b
23 changed files with 2565 additions and 3464 deletions

53
package-lock.json generated
View File

@ -33,6 +33,7 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@reduxjs/toolkit": "^2.11.2",
"apisauce": "^3.2.2",
"axios": "^1.13.3",
@ -3139,6 +3140,58 @@
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",

View File

@ -35,6 +35,7 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@reduxjs/toolkit": "^2.11.2",
"apisauce": "^3.2.2",
"axios": "^1.13.3",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,274 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
import { Button } from '../../ui/button';
import { Label } from '../../ui/label';
import { Input } from '../../ui/input';
import { Checkbox } from '../../ui/checkbox';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip';
import { UserCog, Users } from 'lucide-react';
import { RootState } from '../../../store';
interface ASMDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
editingASMId: string | null;
asmManagerId: string;
setAsmManagerId: (id: string) => void;
asmName: string;
setAsmName: (name: string) => void;
asmCode: string;
setAsmCode: (code: string) => void;
asmEmployeeId: string;
setAsmEmployeeId: (id: string) => void;
asmStatus: 'active' | 'inactive';
setAsmStatus: (status: 'active' | 'inactive') => void;
selectedASMZone: string;
setSelectedASMZone: (id: string) => void;
selectedASMRegion: string;
setSelectedASMRegion: (id: string) => void;
selectedASMStates: string[];
setSelectedASMStates: (states: string[]) => void;
selectedASMDistricts: string[];
setSelectedASMDistricts: (districts: string[]) => void;
onSave: () => void;
userAssignedData: any[];
districtsAssignedToOthers: Record<string, string[]>;
getDistrictsForSelectedState: (state: string) => string[];
}
export const ASMDialog: React.FC<ASMDialogProps> = ({
isOpen, onOpenChange, editingASMId, asmManagerId, setAsmManagerId,
asmName, setAsmName, asmCode, setAsmCode, asmEmployeeId, setAsmEmployeeId,
asmStatus, setAsmStatus, selectedASMZone, setSelectedASMZone,
selectedASMRegion, setSelectedASMRegion, selectedASMStates, setSelectedASMStates,
selectedASMDistricts, setSelectedASMDistricts, onSave,
userAssignedData, districtsAssignedToOthers, getDistrictsForSelectedState
}) => {
const { zones, regionalOffices } = useSelector((state: RootState) => state.master);
const filteredASMUsers = userAssignedData.filter(u => {
const code = (u.roleCode || '').toLowerCase();
const name = (u.role || '').toLowerCase();
return code === 'asm' || name.includes('area sales manager');
});
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingASMId ? 'Edit' : 'Add'} Area Sales Manager</DialogTitle>
<DialogDescription>Configure ASM details and assignment</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Zone</Label>
<Select value={selectedASMZone} onValueChange={(value) => {
setSelectedASMZone(value);
setSelectedASMRegion('');
setSelectedASMStates([]);
setSelectedASMDistricts([]);
}}>
<SelectTrigger className="mt-2 text-slate-900">
<SelectValue placeholder="Select zone" />
</SelectTrigger>
<SelectContent>
{zones.map((zone) => (
<SelectItem key={zone.id} value={zone.id}>{zone.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedASMZone && (
<div>
<Label>Regional Office</Label>
<Select value={selectedASMRegion} onValueChange={(value) => {
setSelectedASMRegion(value);
setSelectedASMStates([]);
setSelectedASMDistricts([]);
}}>
<SelectTrigger className="mt-2 text-slate-900">
<SelectValue placeholder="Select regional office" />
</SelectTrigger>
<SelectContent>
{regionalOffices
.filter((office) => office.zoneId === selectedASMZone)
.map((office) => (
<SelectItem key={office.id} value={office.id}>{office.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{selectedASMRegion && (
<div>
<Label>States Covered</Label>
<div className="mt-2 border rounded-lg p-3 max-h-48 overflow-y-auto bg-slate-50">
{(() => {
const selectedRegion = regionalOffices.find(r => r.id === selectedASMRegion);
const availableStates = selectedRegion?.states || [];
return availableStates.length > 0 ? (
<div className="space-y-2">
{availableStates.map((state) => (
<div key={state} className="flex items-center space-x-2">
<Checkbox
id={`asm-state-${state}`}
checked={selectedASMStates.includes(state)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedASMStates([...selectedASMStates, state]);
} else {
setSelectedASMStates(selectedASMStates.filter(s => s !== state));
const stateDistricts = getDistrictsForSelectedState(state);
setSelectedASMDistricts(selectedASMDistricts.filter(d => !stateDistricts.includes(d)));
}
}}
/>
<label htmlFor={`asm-state-${state}`} className="text-sm cursor-pointer text-slate-900">
{state}
</label>
</div>
))}
</div>
) : (
<p className="text-sm text-slate-500">No states available for this regional office</p>
);
})()}
</div>
</div>
)}
{selectedASMStates.length > 0 && (
<div>
<Label>Districts/Cities Covered</Label>
<div className="mt-2 border rounded-lg p-3 max-h-64 overflow-y-auto bg-slate-50">
<TooltipProvider>
{selectedASMStates.map((state) => {
const districts = getDistrictsForSelectedState(state);
if (districts.length === 0) return null;
return (
<div key={state} className="mb-4 last:mb-0">
<h4 className="text-sm text-amber-700 mb-2 pb-1 border-b border-slate-200">{state}</h4>
<div className="space-y-2 ml-2">
{districts.map((district: string) => (
<div key={district}>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-2 py-0.5">
<Checkbox
id={`asm-district-${district}`}
checked={selectedASMDistricts.includes(district)}
disabled={!!districtsAssignedToOthers[district]}
onCheckedChange={(checked) => {
if (checked) {
setSelectedASMDistricts([...selectedASMDistricts, district]);
} else {
setSelectedASMDistricts(selectedASMDistricts.filter(d => d !== district));
}
}}
/>
<label
htmlFor={`asm-district-${district}`}
className={`text-sm flex items-center gap-1.5 ${districtsAssignedToOthers[district] ? "text-slate-400 cursor-not-allowed" : "cursor-pointer text-slate-900"}`}
>
{district}
</label>
</div>
</TooltipTrigger>
{districtsAssignedToOthers[district] && (
<TooltipContent>
<p>Already managed by: {districtsAssignedToOthers[district].join(', ')}</p>
</TooltipContent>
)}
</Tooltip>
</div>
))}
</div>
</div>
);
})}
</TooltipProvider>
</div>
</div>
)}
<div className="border-t pt-4">
<Label>Area Sales Manager <span className="text-red-500">*</span></Label>
<Select
value={asmManagerId}
onValueChange={(value) => {
setAsmManagerId(value);
const selectedUser = userAssignedData.find(u => u.id === value);
if (selectedUser) {
setAsmName(selectedUser.name);
setAsmCode(selectedUser.asmCode || '');
setAsmEmployeeId(selectedUser.employeeId || '');
setSelectedASMDistricts(selectedUser.areasManaged || []);
setSelectedASMStates(selectedUser.stateNames || []);
}
}}
disabled={!!editingASMId}
>
<SelectTrigger className="mt-2 w-full text-slate-900">
<SelectValue placeholder="Select ASM User" />
</SelectTrigger>
<SelectContent className="max-h-60">
{filteredASMUsers.length > 0 ? (
filteredASMUsers.map((user) => (
<SelectItem key={user.id} value={user.id}>
<div className="flex flex-col text-left">
<span className="font-medium text-slate-900">{user.name}</span>
<span className="text-xs text-slate-500">{user.email}</span>
</div>
</SelectItem>
))
) : (
<div className="p-2 text-sm text-slate-500 text-center">No users available</div>
)}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Employee ID</Label>
<Input readOnly placeholder="Auto-populated" className="mt-2 bg-slate-100" value={asmEmployeeId} />
</div>
<div>
<Label>ASM Code</Label>
<Input placeholder="Enter ASM Code" className="mt-2 text-slate-900" value={asmCode} onChange={(e) => setAsmCode(e.target.value)} />
</div>
</div>
<div>
<Label>Full Name</Label>
<Input placeholder="Enter full name" className="mt-2 text-slate-900" value={asmName} onChange={(e) => setAsmName(e.target.value)} />
</div>
<div>
<Label>Status</Label>
<Select value={asmStatus} onValueChange={(val: 'active' | 'inactive') => setAsmStatus(val)}>
<SelectTrigger className="mt-2 text-slate-900">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save ASM</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,125 @@
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { UserCog, Plus, Edit2, Trash2, Users } from 'lucide-react';
import { RootState } from '../../../store';
interface ASMManagementProps {
selectedZone: string;
onAddASM: () => void;
onEditASM: (asm: any) => void;
onDeleteASM: (id: string, name: string) => void;
}
export const ASMManagement: React.FC<ASMManagementProps> = ({
selectedZone, onAddASM, onEditASM, onDeleteASM
}) => {
const { asms, zonalManagerMappings } = useSelector((state: RootState) => state.master);
const filteredASMs = asms.filter((a: any) => selectedZone === 'all' || a.zoneId === selectedZone);
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: string) => {
if (!map[d]) map[d] = [];
if (!map[d].includes(m.name)) map[d].push(m.name);
});
});
return map;
}, [asms, zonalManagerMappings]);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Area Sales Managers (ASM)</CardTitle>
<CardDescription>Manage ASMs across all regions and zones</CardDescription>
</div>
<Button onClick={onAddASM} className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" />
Add ASM
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>ASM Code</TableHead>
<TableHead>Name</TableHead>
<TableHead>Zone</TableHead>
<TableHead>Region</TableHead>
<TableHead>Districts Managed</TableHead>
<TableHead>Contact</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredASMs.map((asm: any) => (
<TableRow key={asm.id}>
<TableCell>
<div className="flex items-center gap-2">
<UserCog className="w-4 h-4 text-green-600" />
<span className="font-medium">{asm.asmCode || 'N/A'}</span>
</div>
</TableCell>
<TableCell>{asm.name}</TableCell>
<TableCell>
<Badge variant="outline">{asm.zoneName}</Badge>
</TableCell>
<TableCell className="text-sm text-slate-600">{asm.regionName}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{asm.areasManaged.map((area: string, idx: number) => {
const otherManagers = (districtsAssignedToOthers[area] || []).filter((name: string) => name !== asm.name);
const isShared = otherManagers.length > 0;
return (
<Badge
key={idx}
variant={isShared ? "outline" : "secondary"}
className={`text-xs ${isShared ? "border-amber-300 bg-amber-50 text-amber-700 font-medium" : ""}`}
title={isShared ? `Also managed by: ${otherManagers.join(', ')}` : undefined}
>
{area}
{isShared && <Users className="w-2.5 h-2.5 ml-1 inline" />}
</Badge>
);
})}
</div>
</TableCell>
<TableCell>
<div className="text-sm">
<p className="text-slate-900">{asm.email}</p>
<p className="text-slate-500">{asm.phone}</p>
</div>
</TableCell>
<TableCell>
<Badge variant={asm.status === 'Active' ? 'default' : 'secondary'} className={asm.status === 'Active' ? 'bg-emerald-100 text-emerald-700' : ''}>
{asm.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex gap-2 justify-end">
<Button variant="ghost" size="sm" onClick={() => onEditASM(asm)}>
<Edit2 className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteASM(asm.id, asm.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50">
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,96 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { Mail, Plus, Edit2, Trash2, Calendar } from 'lucide-react';
import { RootState } from '../../../store';
interface EmailTemplatesProps {
onAddTemplate: () => void;
onEditTemplate: (template: any) => void;
onDeleteTemplate: (id: string) => void;
}
export const EmailTemplates: React.FC<EmailTemplatesProps> = ({
onAddTemplate, onEditTemplate, onDeleteTemplate
}) => {
const { emailTemplates } = useSelector((state: RootState) => state.master);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Email & Letter Templates</CardTitle>
<CardDescription>Manage automated transactional email and onboarding letter templates</CardDescription>
</div>
<Button onClick={onAddTemplate} className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" />
Add Template
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Template Name</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Trigger Code</TableHead>
<TableHead>Modified Date</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{emailTemplates.map((template) => (
<TableRow key={template.id}>
<TableCell>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-amber-50 rounded-lg flex items-center justify-center">
<Mail className="w-4 h-4 text-amber-600" />
</div>
<span className="font-medium text-slate-900">{template.name || template.templateCode}</span>
</div>
</TableCell>
<TableCell className="text-slate-600 max-w-xs truncate">{template.subject}</TableCell>
<TableCell>
<Badge variant="outline" className="bg-slate-50 text-[10px] font-mono">
{template.templateCode || '-'}
</Badge>
</TableCell>
<TableCell className="text-slate-500 text-sm">
<div className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5" />
{template.updatedAt ? new Date(template.updatedAt).toLocaleDateString() : '-'}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex gap-2 justify-end">
<Button variant="ghost" size="sm" onClick={() => onEditTemplate(template)}>
<Edit2 className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteTemplate(template.id)} className="text-red-500 hover:text-red-600 hover:bg-red-50">
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
{emailTemplates.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-center py-12">
<div className="flex flex-col items-center gap-2">
<Mail className="w-8 h-8 text-slate-200" />
<p className="text-slate-400 text-sm">No templates configured yet</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,139 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
import { Button } from '../../ui/button';
import { Label } from '../../ui/label';
import { Input } from '../../ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
import { RootState } from '../../../store';
interface LocationDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
editingLocationId: string | null;
locationState: string;
setLocationState: (id: string) => void;
locationDistrict: string;
setLocationDistrict: (id: string) => void;
locationCity: string;
setLocationCity: (city: string) => void;
locationPincode: string;
setLocationPincode: (pincode: string) => void;
locationActiveFrom: string;
setLocationActiveFrom: (date: string) => void;
locationActiveTo: string;
setLocationActiveTo: (date: string) => void;
locationStatus: string;
setLocationStatus: (status: string) => void;
onSave: () => void;
}
export const LocationDialog: React.FC<LocationDialogProps> = ({
isOpen, onOpenChange, editingLocationId, locationState, setLocationState,
locationDistrict, setLocationDistrict, locationCity, setLocationCity,
locationPincode, setLocationPincode, locationActiveFrom, setLocationActiveFrom,
locationActiveTo, setLocationActiveTo, locationStatus, setLocationStatus, onSave
}) => {
const { allStates, allDistricts } = useSelector((state: RootState) => state.master);
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingLocationId ? 'Edit Location' : 'Add Location'}</DialogTitle>
<DialogDescription>{editingLocationId ? 'Modify dealership location details' : 'Add a new dealership location (Area/City)'}</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>State</Label>
<Select value={locationState} onValueChange={(val) => {
setLocationState(val);
setLocationDistrict('');
}}>
<SelectTrigger className="mt-2 text-slate-900">
<SelectValue placeholder="Select State" />
</SelectTrigger>
<SelectContent>
{allStates.map((state) => (
<SelectItem key={state.id} value={state.id}>{state.stateName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>District</Label>
<Select value={locationDistrict} onValueChange={setLocationDistrict} disabled={!locationState}>
<SelectTrigger className="mt-2 text-slate-900">
<SelectValue placeholder="Select District" />
</SelectTrigger>
<SelectContent>
{allDistricts
.filter(d => d.stateId === locationState || d.state?.id === locationState)
.map((district) => (
<SelectItem key={district.id} value={district.id}>{district.districtName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Area Name / City</Label>
<Input
placeholder="Enter area name (e.g., Connaught Place)"
className="mt-2 text-slate-900"
value={locationCity}
onChange={(e) => setLocationCity(e.target.value)}
/>
</div>
<div>
<Label>Pincode</Label>
<Input
placeholder="Enter pincode"
className="mt-2 text-slate-900"
value={locationPincode}
onChange={(e) => setLocationPincode(e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Active From</Label>
<Input
type="date"
className="mt-2 text-slate-900"
value={locationActiveFrom}
onChange={(e) => setLocationActiveFrom(e.target.value)}
/>
</div>
<div>
<Label>Active To</Label>
<Input
type="date"
className="mt-2 text-slate-900"
value={locationActiveTo}
onChange={(e) => setLocationActiveTo(e.target.value)}
/>
</div>
</div>
<div>
<Label>Status</Label>
<Select value={locationStatus} onValueChange={setLocationStatus}>
<SelectTrigger className="mt-2 text-slate-900">
<SelectValue placeholder="Select Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Location</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,108 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { MapPin, Plus, Edit2, Trash2, Globe } from 'lucide-react';
import { RootState } from '../../../store';
interface LocationManagementProps {
onAddLocation: () => void;
onEditLocation: (location: any) => void;
onDeleteLocation: (id: string) => void;
}
export const LocationManagement: React.FC<LocationManagementProps> = ({
onAddLocation, onEditLocation, onDeleteLocation
}) => {
const { allAreas } = useSelector((state: RootState) => state.master);
return (
<div className="space-y-4">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Dealership Locations</CardTitle>
<CardDescription>Manage geographical locations and their operational status</CardDescription>
</div>
<Button onClick={onAddLocation} className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" />
Add Location
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>State</TableHead>
<TableHead>Area / City</TableHead>
<TableHead>District</TableHead>
<TableHead>Pincode</TableHead>
<TableHead>Manager</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allAreas.map((area) => (
<TableRow key={area.id}>
<TableCell>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-amber-600" />
<span className="font-medium">{area.district?.state?.stateName || area.state?.stateName || 'N/A'}</span>
</div>
</TableCell>
<TableCell className="font-medium text-slate-900">{area.areaName} ({area.city})</TableCell>
<TableCell className="text-slate-600 text-sm">{area.district?.districtName || 'N/A'}</TableCell>
<TableCell className="text-slate-600 text-sm">{area.pincode}</TableCell>
<TableCell>
{area.manager ? (
<span className="text-slate-700 font-medium">{area.manager.fullName}</span>
) : (
<span className="text-slate-400 italic text-sm">Unassigned</span>
)}
</TableCell>
<TableCell>
<Badge variant={area.isActive ? 'default' : 'secondary'} className={area.isActive ? 'bg-emerald-100 text-emerald-700' : ''}>
{area.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex gap-2 justify-end">
<Button variant="ghost" size="sm" onClick={() => onEditLocation(area)}>
<Edit2 className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteLocation(area.id)} className="text-red-500 hover:text-red-600">
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card className="border-t border-slate-200 mt-6 shadow-none">
<CardContent className="py-6">
<div className="flex flex-col items-center justify-center space-y-4 text-center">
<div className="w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center">
<Globe className="w-6 h-6 text-slate-400" />
</div>
<div>
<h3 className="text-sm font-medium text-slate-900">Bulk Geographical Upload</h3>
<p className="text-xs text-slate-500 mt-1 max-w-xs">Upload your geographical hierarchy in bulk using an Excel template</p>
</div>
<Button variant="outline" size="sm" className="mt-2 text-amber-600 border-amber-200 hover:bg-amber-50">
Download Template
</Button>
</div>
</CardContent>
</Card>
</div>
);
};

View File

@ -0,0 +1,137 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
import { Button } from '../../ui/button';
import { Label } from '../../ui/label';
import { Input } from '../../ui/input';
import { Textarea } from '../../ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
import { Checkbox } from '../../ui/checkbox';
import { RootState } from '../../../store';
interface RegionDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
editingRegionId: string | null;
regionName: string;
setRegionName: (name: string) => void;
regionCode: string;
setRegionCode: (code: string) => void;
regionDescription: string;
setRegionDescription: (desc: string) => void;
selectedRegionZone: string;
setSelectedRegionZone: (id: string) => void;
regionalManagerId: string;
setRegionalManagerId: (id: string) => void;
selectedRegionStates: string[];
setSelectedRegionStates: (states: string[]) => void;
onSave: () => void;
userAssignedData: any[]; // Used for RM selection
}
export const RegionDialog: React.FC<RegionDialogProps> = ({
isOpen, onOpenChange, editingRegionId, regionName, setRegionName,
regionCode, setRegionCode, regionDescription, setRegionDescription,
selectedRegionZone, setSelectedRegionZone, regionalManagerId, setRegionalManagerId,
selectedRegionStates, setSelectedRegionStates, onSave, userAssignedData
}) => {
const { zones, allStates } = useSelector((state: RootState) => state.master);
const filteredRMUsers = userAssignedData.filter(u => {
const code = (u.roleCode || '').toLowerCase();
const name = (u.role || '').toLowerCase();
return code === 'rm' || code === 'rbm' || name.includes('regional manager');
});
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingRegionId ? 'Edit' : 'Add'} Regional Office</DialogTitle>
<DialogDescription>Configure regional office and assign to a zone</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Zone</Label>
<Select value={selectedRegionZone} onValueChange={setSelectedRegionZone}>
<SelectTrigger className="mt-2 text-slate-900">
<SelectValue placeholder="Select zone" />
</SelectTrigger>
<SelectContent>
{zones.map((zone) => (
<SelectItem key={zone.id} value={zone.id}>{zone.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Regional Manager</Label>
<Select value={regionalManagerId} onValueChange={setRegionalManagerId}>
<SelectTrigger className="mt-2 w-full text-slate-900">
<SelectValue placeholder="Select Manager" />
</SelectTrigger>
<SelectContent className="max-h-60">
{filteredRMUsers.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.name} ({user.email})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Region Name</Label>
<Input placeholder="e.g., Delhi Region" className="mt-2 text-slate-900" value={regionName} onChange={(e) => setRegionName(e.target.value)} />
</div>
<div>
<Label>Region Code</Label>
<Input placeholder="e.g., NZ-R1" className="mt-2 text-slate-900" value={regionCode} onChange={(e) => setRegionCode(e.target.value)} />
</div>
</div>
<div>
<Label>Description</Label>
<Textarea placeholder="Describe the region..." className="mt-2 text-slate-900" rows={3} value={regionDescription} onChange={(e) => setRegionDescription(e.target.value)} />
</div>
<div>
<Label>States Covered</Label>
<div className="mt-2 border rounded-lg p-3 max-h-48 overflow-y-auto bg-slate-50">
<div className="grid grid-cols-2 gap-2">
{allStates
.filter(s => !selectedRegionZone || s.zoneId === selectedRegionZone)
.map((state) => (
<div key={state.id} className="flex items-center space-x-2">
<Checkbox
id={`region-state-${state.id}`}
checked={selectedRegionStates.includes(state.stateName)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedRegionStates([...selectedRegionStates, state.stateName]);
} else {
setSelectedRegionStates(selectedRegionStates.filter(s => s !== state.stateName));
}
}}
/>
<label htmlFor={`region-state-${state.id}`} className="text-sm cursor-pointer text-slate-900">
{state.stateName}
</label>
</div>
))}
</div>
</div>
</div>
<div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Regional Office</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,129 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { Building2, Plus, Edit2, Trash2 } from 'lucide-react';
import { RootState } from '../../../store';
interface RegionalManagementProps {
selectedZone: string;
onAddRegion: () => void;
onEditRegion: (region: any) => void;
onDeleteRegion: (id: string, name: string) => void;
}
export const RegionalManagement: React.FC<RegionalManagementProps> = ({
selectedZone, onAddRegion, onEditRegion, onDeleteRegion
}) => {
const { regionalOffices } = useSelector((state: RootState) => state.master);
const filteredRegions = regionalOffices.filter((r: any) => selectedZone === 'all' || r.zoneId === selectedZone);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Regional Offices</CardTitle>
<CardDescription>Manage regional offices within zones</CardDescription>
</div>
<Button onClick={onAddRegion} className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" />
Add Regional Office
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Region Code</TableHead>
<TableHead>Region Name</TableHead>
<TableHead>Zone</TableHead>
<TableHead>Regional Manager</TableHead>
<TableHead>States</TableHead>
<TableHead>Cities</TableHead>
<TableHead>Regional Officers</TableHead>
<TableHead>ASMs</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRegions.map((region: any) => (
<TableRow key={region.id}>
<TableCell>
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-indigo-600" />
<span className="font-medium">{region.code}</span>
</div>
</TableCell>
<TableCell className="font-medium">{region.name}</TableCell>
<TableCell>{region.zoneName}</TableCell>
<TableCell>
{region.regionalManager ? (
<div className="flex flex-col">
<span className="text-sm">{region.regionalManager.name}</span>
<span className="text-xs text-slate-500">{region.regionalManager.email}</span>
</div>
) : (
<span className="text-slate-400 italic">Not Assigned</span>
)}
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{region.states.slice(0, 2).map((state: string, idx: number) => (
<Badge key={idx} variant="secondary" className="text-xs">
{state}
</Badge>
))}
{region.states.length > 2 && (
<Badge variant="secondary" className="text-xs">
+{region.states.length - 2}
</Badge>
)}
</div>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{region.cities.slice(0, 3).map((city: string, idx: number) => (
<span key={idx} className="text-xs text-slate-600">
{city}{idx < Math.min(region.cities.length, 3) - 1 ? ',' : ''}
</span>
))}
{region.cities.length > 3 && (
<span className="text-xs text-slate-500">+{region.cities.length - 3}</span>
)}
</div>
</TableCell>
<TableCell>
<Badge className="bg-purple-600">{region.regionalOfficerCount}</Badge>
</TableCell>
<TableCell>
<Badge className="bg-green-600">{region.asmCount}</Badge>
</TableCell>
<TableCell>
<Badge variant={region.status === 'Active' ? 'default' : 'secondary'} className={region.status === 'Active' ? 'bg-emerald-100 text-emerald-700' : ''}>
{region.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={() => onEditRegion(region)}>
<Edit2 className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteRegion(region.id, region.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50">
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,72 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
import { Shield, Plus, Edit2, Users } from 'lucide-react';
import { RootState } from '../../../store';
interface RolePermissionsProps {
onAddRole: () => void;
onEditRole: (role: any) => void;
}
export const RolePermissions: React.FC<RolePermissionsProps> = ({ onAddRole, onEditRole }) => {
const { roles } = useSelector((state: RootState) => state.master);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Roles & Access Controls</CardTitle>
<CardDescription>Manage user roles and their associated system permissions</CardDescription>
</div>
<Button onClick={onAddRole} className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" />
Add Role
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{roles.map((role) => (
<div key={role.id} className="border-2 rounded-xl p-6 bg-white hover:border-amber-200 transition-all group shadow-sm">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center group-hover:bg-purple-100 transition-colors">
<Shield className="w-6 h-6 text-purple-700" />
</div>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => onEditRole(role)} className="h-8 w-8 p-0">
<Edit2 className="w-4 h-4" />
</Button>
</div>
</div>
<h3 className="text-lg font-bold text-slate-900 mb-1">{role.name}</h3>
<div className="flex items-center gap-2 text-sm text-slate-500 mb-4">
<Users className="w-4 h-4" />
<span>{role.userCount} Active Users</span>
</div>
<div className="space-y-3">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Key Permissions</p>
<div className="flex flex-wrap gap-1.5">
{role.permissions?.slice(0, 6).map((perm: string, idx: number) => (
<Badge key={idx} variant="secondary" className="text-[10px] bg-slate-50 border-slate-200 font-medium">
{perm.replace(/_/g, ' ')}
</Badge>
))}
{role.permissions?.length > 6 && (
<Badge variant="outline" className="text-[10px] border-slate-200">
+{role.permissions.length - 6} more
</Badge>
)}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,91 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
import { Clock, Plus, Edit2, AlertTriangle, Bell, Save } from 'lucide-react';
import { RootState } from '../../../store';
interface SLAConfigurationProps {
onConfigureSLA: (sla: any) => void;
}
export const SLAConfiguration: React.FC<SLAConfigurationProps> = ({ onConfigureSLA }) => {
const { slaConfigs } = useSelector((state: RootState) => state.master);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>SLA & Escalation Matrix</CardTitle>
<CardDescription>Configure Turn Around Time (TAT) and escalation rules for each process stage</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{slaConfigs.map((sla) => (
<div key={sla.id} className="border rounded-xl p-5 space-y-4 bg-white shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-50 rounded-lg flex items-center justify-center border border-amber-100">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<h4 className="text-slate-900 font-medium">{sla.stage}</h4>
<p className="text-xs text-slate-500">Target TAT: <span className="text-amber-600 font-bold">{sla.days} Days</span></p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant={sla.enabled ? "default" : "secondary"} className={sla.enabled ? "bg-emerald-500" : ""}>
{sla.enabled ? 'Enabled' : 'Disabled'}
</Badge>
<Button variant="ghost" size="sm" onClick={() => onConfigureSLA(sla)} className="text-slate-400 hover:text-amber-600">
<Edit2 className="w-4 h-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-3">
<div className="border-l-2 border-blue-400 pl-3">
<div className="flex items-center gap-2 mb-2">
<Bell className="w-4 h-4 text-blue-600" />
<span className="text-xs text-slate-700 font-medium">Reminders ({sla.reminders.length})</span>
</div>
<div className="space-y-1">
{sla.reminders.map((reminder: any, idx: number) => (
<div key={idx} className="text-xs text-slate-600 flex items-center gap-1">
<Badge variant="outline" className="text-[10px] h-4 px-1 bg-blue-50 border-blue-200">
{reminder.time} {reminder.unit}
</Badge>
<span>before</span>
</div>
))}
</div>
</div>
<div className="border-l-2 border-red-400 pl-3">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-red-600" />
<span className="text-xs text-slate-700 font-medium">Escalations ({sla.escalations.length})</span>
</div>
<div className="space-y-1">
{sla.escalations.map((escalation: any, idx: number) => (
<div key={idx} className="text-xs">
<Badge variant="outline" className="text-[10px] h-4 px-1 bg-red-50 border-red-200">
L{escalation.level}
</Badge>
<span className="text-slate-400 ml-1"> {escalation.userEmail.split('@')[0]}</span>
</div>
))}
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,132 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
import { Button } from '../../ui/button';
import { Label } from '../../ui/label';
import { Input } from '../../ui/input';
import { Textarea } from '../../ui/textarea';
import { Switch } from '../../ui/switch';
import { Loader2, Play } from 'lucide-react';
interface TemplateDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
editingTemplate: any;
setEditingTemplate: (template: any) => void;
testDataInput: string;
setTestDataInput: (data: string) => void;
previewLoading: boolean;
handlePreviewTemplate: () => void;
previewContent: any;
handleSaveTemplate: () => void;
}
export const TemplateDialog: React.FC<TemplateDialogProps> = ({
isOpen, onOpenChange, editingTemplate, setEditingTemplate,
testDataInput, setTestDataInput, previewLoading, handlePreviewTemplate,
previewContent, handleSaveTemplate
}) => {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-7xl w-full max-h-[95vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingTemplate?.id ? 'Edit Email Template' : 'Add Email Template'}</DialogTitle>
<DialogDescription>Configure automated email template with dynamic content</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<Label>Template Name</Label>
<Input
placeholder="e.g., Application Received"
className="mt-2 text-slate-900"
value={editingTemplate?.name || ''}
onChange={(e) => setEditingTemplate({ ...editingTemplate!, name: e.target.value })}
/>
</div>
<div>
<Label>Template Code (Unique)</Label>
<Input
placeholder="e.g., APPLICATION_RECEIVED"
className="mt-2 text-slate-900"
value={editingTemplate?.templateCode || ''}
onChange={(e) => setEditingTemplate({ ...editingTemplate!, templateCode: e.target.value })}
/>
</div>
<div>
<Label>Email Subject</Label>
<Input
placeholder="Subject line with {{variables}}"
className="mt-2 text-slate-900"
value={editingTemplate?.subject || ''}
onChange={(e) => setEditingTemplate({ ...editingTemplate!, subject: e.target.value })}
/>
</div>
<div>
<Label>Email Body (Handlebars supported)</Label>
<Textarea
placeholder="Hello {{applicant_name}}, ..."
className="mt-2 font-mono text-sm text-slate-900"
rows={12}
value={editingTemplate?.body || ''}
onChange={(e) => setEditingTemplate({ ...editingTemplate!, body: e.target.value })}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
id="active"
checked={editingTemplate?.isActive ?? true}
onCheckedChange={(checked) => setEditingTemplate({ ...editingTemplate!, isActive: checked })}
/>
<Label htmlFor="active">Active template</Label>
</div>
</div>
<div className="space-y-4 lg:border-l lg:pl-6">
<h3 className="font-semibold text-slate-900">Preview & Test</h3>
<div>
<Label>Test Data (JSON)</Label>
<Textarea
placeholder='{"applicant_name": "John Doe"}'
className="mt-2 font-mono text-xs text-slate-900"
rows={6}
value={testDataInput}
onChange={(e) => setTestDataInput(e.target.value)}
/>
</div>
<Button
variant="secondary"
className="w-full"
onClick={handlePreviewTemplate}
disabled={previewLoading}
>
{previewLoading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Play className="w-4 h-4 mr-2" />}
Generate Preview
</Button>
{previewContent && (
<div className="mt-4 border rounded-lg overflow-hidden flex flex-col max-h-[500px]">
<div className="bg-slate-100 p-2 border-b text-xs font-mono text-slate-500 shrink-0">
Subject: {previewContent.subject}
</div>
<div className="p-4 bg-white prose prose-sm max-w-none overflow-auto flex-1">
<div dangerouslySetInnerHTML={{ __html: previewContent.html }} />
</div>
</div>
)}
</div>
</div>
<div className="flex gap-3 pt-4 border-t mt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={handleSaveTemplate}>
Save Template
</Button>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,78 @@
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { Shield, User, Mail, MapPin } from 'lucide-react';
interface UserManagementTableProps {
userAssignedData: any[];
}
export const UserManagementTable: React.FC<UserManagementTableProps> = ({ userAssignedData }) => {
return (
<Card>
<CardHeader>
<CardTitle className="text-xl">System Users & Territory Assignments</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>User Details</TableHead>
<TableHead>Role</TableHead>
<TableHead>Assigned Zone</TableHead>
<TableHead>Assigned Region</TableHead>
<TableHead>Location Type</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{userAssignedData.map((user: any) => (
<TableRow key={user.id}>
<TableCell>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center">
<User className="w-4 h-4 text-slate-500" />
</div>
<div>
<p className="font-medium text-slate-900">{user.name}</p>
<div className="flex items-center gap-2 text-xs text-slate-500">
<Mail className="w-3 h-3" />
{user.email}
</div>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Shield className="w-3 h-3 text-amber-600" />
<span className="text-sm font-medium">{user.role}</span>
</div>
</TableCell>
<TableCell>
<Badge variant="outline">{user.zone}</Badge>
</TableCell>
<TableCell>
<Badge variant="outline" className="bg-indigo-50 text-indigo-700 border-indigo-200">
{user.region}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-1.5 text-slate-600">
<MapPin className="w-3.5 h-3.5" />
<span className="text-sm capitalize">{user.locationType || 'N/A'}</span>
</div>
</TableCell>
<TableCell>
<Badge variant={user.status === 'Active' ? 'default' : 'secondary'} className={user.status === 'Active' ? 'bg-emerald-100 text-emerald-700' : ''}>
{user.status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,108 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { Users, Plus, Edit2, Trash2 } from 'lucide-react';
import { RootState } from '../../../store';
interface ZMManagementProps {
selectedZone: string;
onAddZM: () => void;
onEditZM: (zm: any) => void;
onDeleteZM: (id: string, name: string) => void;
}
export const ZMManagement: React.FC<ZMManagementProps> = ({
selectedZone, onAddZM, onEditZM, onDeleteZM
}) => {
const { zonalManagerMappings } = useSelector((state: RootState) => state.master);
const filteredZMs = zonalManagerMappings.filter((zm: any) => selectedZone === 'all' || zm.zoneId === selectedZone);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Zonal Managers (ZM)</CardTitle>
<CardDescription>Manage Zonal Managers and their district assignments</CardDescription>
</div>
<Button onClick={onAddZM} className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" />
Add ZM
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>ZM Code</TableHead>
<TableHead>Name</TableHead>
<TableHead>Zone</TableHead>
<TableHead>Region</TableHead>
<TableHead>Districts Managed</TableHead>
<TableHead>Contact</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredZMs.map((zm: any) => (
<TableRow key={zm.id}>
<TableCell>
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-blue-600" />
<span className="font-medium">{zm.code}</span>
</div>
</TableCell>
<TableCell>{zm.name}</TableCell>
<TableCell>
<Badge variant="outline">{zm.zoneName}</Badge>
</TableCell>
<TableCell className="text-sm text-slate-600">{zm.regionName}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{zm.districts.slice(0, 3).map((district: string, idx: number) => (
<Badge key={idx} variant="secondary" className="text-xs">
{district}
</Badge>
))}
{zm.districts.length > 3 && (
<Badge variant="secondary" className="text-xs">
+{zm.districts.length - 3}
</Badge>
)}
</div>
</TableCell>
<TableCell>
<div className="text-sm">
<p className="text-slate-900">{zm.email}</p>
<p className="text-slate-500">{zm.phone}</p>
</div>
</TableCell>
<TableCell>
<Badge variant={zm.status === 'Active' ? 'default' : 'secondary'} className={zm.status === 'Active' ? 'bg-emerald-100 text-emerald-700' : ''}>
{zm.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button variant="ghost" size="sm" onClick={() => onEditZM(zm)}>
<Edit2 className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteZM(zm.id, zm.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50">
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,149 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
import { ScrollArea } from '../../ui/scroll-area';
import { Label } from '../../ui/label';
import { Globe, Plus, Edit2, UserCog, Mail, Users, MapPin } from 'lucide-react';
import { RootState } from '../../../store';
interface ZoneDetailsProps {
selectedZone: string;
onAddZone: () => void;
onEditZone: (zone: any) => void;
}
export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZone, onEditZone }) => {
const { zones } = useSelector((state: RootState) => state.master);
const filteredZones = zones.filter(z => selectedZone === 'all' || z.id === selectedZone);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Zone Details</CardTitle>
<CardDescription>Geographical coverage and state mappings for each zone</CardDescription>
</div>
<Button onClick={onAddZone} className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" />
Add Zone
</Button>
</div>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px]">
<div className="space-y-4">
{filteredZones.map((zone) => (
<div key={zone.id} className="border rounded-lg p-5 space-y-4 bg-gradient-to-br from-white to-slate-50">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-lg flex items-center justify-center shadow-md">
<Globe className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="text-slate-900">{zone.name}</h3>
<p className="text-slate-500 text-sm">{zone.code}</p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => onEditZone(zone)}>
<Edit2 className="w-4 h-4" />
</Button>
</div>
</div>
{zone.description && (
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-sm text-slate-600">{zone.description}</p>
</div>
)}
<div>
<Label className="text-xs text-slate-600 mb-2 block">States Covered ({zone.states.length})</Label>
<div className="flex flex-wrap gap-1">
{zone.states.map((state: string, idx: number) => (
<Badge key={idx} variant="secondary" className="text-xs">
{state}
</Badge>
))}
</div>
</div>
{zone.zbh && zone.zbh.name && (
<div className="border-t pt-3">
<Label className="text-xs text-slate-600 mb-2 block">Zone Business Head (ZBH)</Label>
<div className="bg-amber-50 rounded-lg p-3 space-y-1">
<div className="flex items-center gap-2">
<UserCog className="w-4 h-4 text-amber-600" />
<span className="text-sm text-slate-900">{zone.zbh.name}</span>
</div>
{zone.zbh.email && (
<div className="flex items-center gap-2 ml-6">
<Mail className="w-3 h-3 text-slate-400" />
<span className="text-xs text-slate-600">{zone.zbh.email}</span>
</div>
)}
{zone.zbh.phone && (
<div className="flex items-center gap-2 ml-6">
<span className="text-xs text-slate-600">{zone.zbh.phone}</span>
</div>
)}
</div>
</div>
)}
{zone.zonalManagers && zone.zonalManagers.length > 0 && (
<div className="border-t pt-3">
<Label className="text-xs text-slate-600 mb-2 block">
Zonal Managers ({zone.zonalManagers.length})
</Label>
<div className="space-y-2">
{zone.zonalManagers.map((zm: any, idx: number) => (
<div key={idx} className="bg-slate-50 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-slate-600" />
<span className="text-sm text-slate-900">{zm.name}</span>
<Badge variant="outline" className="text-xs ml-auto">ZM-{idx + 1}</Badge>
</div>
{zm.email && (
<div className="flex items-center gap-2 ml-6">
<Mail className="w-3 h-3 text-slate-400" />
<span className="text-xs text-slate-600">{zm.email}</span>
</div>
)}
{zm.phone && (
<div className="flex items-center gap-2 ml-6">
<span className="text-xs text-slate-600">{zm.phone}</span>
</div>
)}
{zm.districts && zm.districts.length > 0 && (
<div className="ml-6 mt-2">
<Label className="text-xs text-slate-500 mb-1 block">
Assigned Districts ({zm.districts.length})
</Label>
<div className="flex flex-wrap gap-1">
{zm.districts.map((district: string, dIdx: number) => (
<Badge key={dIdx} variant="outline" className="text-xs bg-white">
<MapPin className="w-2.5 h-2.5 mr-1" />
{district}
</Badge>
))}
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,69 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
import { Button } from '../../ui/button';
import { Label } from '../../ui/label';
import { Input } from '../../ui/input';
import { Textarea } from '../../ui/textarea';
interface ZoneDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
editingZoneId: string | null;
zoneName: string;
setZoneName: (name: string) => void;
zoneCode: string;
setZoneCode: (code: string) => void;
zoneDescription: string;
setZoneDescription: (desc: string) => void;
onSave: () => void;
}
export const ZoneDialog: React.FC<ZoneDialogProps> = ({
isOpen, onOpenChange, editingZoneId, zoneName, setZoneName,
zoneCode, setZoneCode, zoneDescription, setZoneDescription, onSave
}) => {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{editingZoneId ? 'Edit' : 'Add'} Zone</DialogTitle>
<DialogDescription>Configure zonal details and geographical boundaries</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Zone Name</Label>
<Input
placeholder="e.g., North Zone"
className="mt-2 text-slate-900"
value={zoneName}
onChange={(e) => setZoneName(e.target.value)}
/>
</div>
<div>
<Label>Zone Code</Label>
<Input
placeholder="e.g., NZ"
className="mt-2 text-slate-900"
value={zoneCode}
onChange={(e) => setZoneCode(e.target.value)}
/>
</div>
<div>
<Label>Description</Label>
<Textarea
placeholder="Describe the zone's coverage..."
className="mt-2 text-slate-900"
rows={3}
value={zoneDescription}
onChange={(e) => setZoneDescription(e.target.value)}
/>
</div>
<div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Zone</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,69 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
import { Badge } from '../../ui/badge';
import { Globe } from 'lucide-react';
import { RootState } from '../../../store';
interface ZonesOverviewProps {
selectedZone: string;
onZoneClick: (zoneId: string) => void;
}
export const ZonesOverview: React.FC<ZonesOverviewProps> = ({ selectedZone, onZoneClick }) => {
const { zones, regionalOffices, asms } = useSelector((state: RootState) => state.master);
return (
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{zones.map((zone: any) => {
const zoneRegions = regionalOffices.filter((r: any) => r.zoneId === zone.id);
const zoneASMs = asms.filter((a: any) => a.zoneId === zone.id);
const totalRegionalOfficers = zoneRegions.reduce((sum: number, region: any) => sum + region.regionalOfficerCount, 0);
return (
<Card
key={zone.id}
className={`border-2 transition-all cursor-pointer ${selectedZone === zone.id ? 'border-amber-600 shadow-lg' : 'hover:border-amber-400'
}`}
onClick={() => onZoneClick(zone.id)}
>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Globe className="w-5 h-5 text-amber-600" />
<CardTitle className="text-lg">{zone.name} Zone</CardTitle>
</div>
<Badge variant="outline" className="text-xs">{zone.code}</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<p className="text-slate-600 text-xs leading-relaxed mb-3">{zone.description}</p>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-slate-500">States</span>
<Badge variant="outline">{zone.states.length}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500">Regions</span>
<Badge className="bg-indigo-600">{zoneRegions.length}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500">Regional Officers</span>
<Badge className="bg-purple-600">{totalRegionalOfficers}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500">ASMs</span>
<Badge className="bg-green-600">{zoneASMs.length}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500">ZMs</span>
<Badge className="bg-blue-600">{zone.zmCount}</Badge>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
);
};

View File

@ -0,0 +1,40 @@
export const getAncestorByType = (node: any, type: string): any => {
if (!node) return null;
if (node.type === type) return node;
if (node.ancestors && Array.isArray(node.ancestors)) {
return node.ancestors.find((a: any) => a.type === type);
}
if (node.parent) {
if (node.parent.type === type) return node.parent;
return getAncestorByType(node.parent, type);
}
return null;
};
export const getRegionId = (loc: any, stateToRegionId: Record<string, string>, parsedDistricts: any[], parsedStates: any[]) => {
if (!loc) return null;
const regAncestor = getAncestorByType(loc, 'region');
if (regAncestor?.id) return regAncestor.id;
if (loc.type === 'region') return loc.id;
const sName = loc.stateName || loc.state?.name || loc.state?.stateName;
if (sName && stateToRegionId[sName]) return stateToRegionId[sName];
if (loc.type === 'district' || loc.districtName) {
const dName = loc.districtName || loc.name;
const dObj = parsedDistricts.find((d: any) => d.districtName === dName || d.id === loc.id);
if (dObj?.stateId) {
const sObj = parsedStates.find((s: any) => s.id === dObj.stateId);
if (sObj?.stateName && stateToRegionId[sObj.stateName]) return stateToRegionId[sObj.stateName];
}
}
return null;
};
export const getZoneId = (loc: any, regionIdToZoneId: Record<string, string>, stateToRegionId: Record<string, string>, parsedDistricts: any[], parsedStates: any[]) => {
if (!loc) return null;
const zon = getAncestorByType(loc, 'zone');
if (zon?.id) return zon.id;
if (loc.type === 'zone') return loc.id;
const rid = getRegionId(loc, stateToRegionId, parsedDistricts, parsedStates);
if (rid && regionIdToZoneId[rid]) return regionIdToZoneId[rid];
return loc.zoneId;
};

304
src/hooks/useMasterData.ts Normal file
View File

@ -0,0 +1,304 @@
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { masterService } from '../services/master.service';
import {
setMasterData, setLoading, setError
} from '../store/slices/masterSlice';
import { getAncestorByType, getRegionId, getZoneId } from './useLocationHelpers';
import { toast } from 'sonner';
export const useMasterData = () => {
const dispatch = useDispatch();
const fetchInitialData = useCallback(async () => {
try {
dispatch(setLoading(true));
const [
rolesRes, zonesRes, permsRes, regionsRes, usersRes,
statesRes, emailTemplatesRes, districtsRes, areasRes, slaRes
] = await Promise.all([
masterService.getRoles().catch(() => ({ success: false })),
masterService.getZones().catch(() => ({ success: false })),
masterService.getPermissions().catch(() => ({ success: false })),
masterService.getRegions().catch(() => ({ success: false })),
masterService.getUsers().catch(() => ({ success: false })),
masterService.getStates().catch(() => ({ success: false })),
masterService.getEmailTemplates().catch(() => ({ success: false })),
masterService.getDistricts().catch(() => ({ success: false })),
masterService.getAreas().catch(() => ({ success: false })),
masterService.getSlaConfigs().catch(() => ({ success: false }))
]);
const getBody = (res: any) => res.success ? res : (res.data ? res.data : res);
const bodyRoles = getBody(rolesRes);
const bodyZones = getBody(zonesRes);
const bodyPerms = getBody(permsRes);
const bodyRegions = getBody(regionsRes);
const bodyUsers = getBody(usersRes);
const bodyStates = getBody(statesRes);
const bodyEmail = getBody(emailTemplatesRes);
const bodyDistricts = getBody(districtsRes);
const bodyAreas = getBody(areasRes);
const bodySla = getBody(slaRes);
const users = bodyUsers?.users || bodyUsers?.data || [];
const usersByLocation: Record<string, any[]> = {};
users.forEach((u: any) => {
const locId = u.location?.id || u.locationId;
if (locId) {
if (!usersByLocation[locId]) usersByLocation[locId] = [];
usersByLocation[locId].push(u);
}
});
const rawZones = bodyZones?.zones || bodyZones?.data || [];
const rawRegions = bodyRegions?.regions || bodyRegions?.data || [];
const rawStates = bodyStates?.states || bodyStates?.data || [];
const rawDistricts = bodyDistricts?.districts || bodyDistricts?.data || [];
const allStates = rawStates.map((s: any) => ({
...s,
stateName: s.name,
zoneId: getAncestorByType(s, 'zone')?.id || s.zoneId
}));
const allDistricts = rawDistricts.map((d: any) => ({
...d,
districtName: d.name,
stateId: getAncestorByType(d, 'state')?.id || d.stateId
}));
const stateToRegionId: Record<string, string> = {};
const regionIdToZoneId: Record<string, string> = {};
rawRegions.forEach((r: any) => {
const zAncestor = getAncestorByType(r, 'zone');
if (zAncestor?.id) regionIdToZoneId[r.id] = zAncestor.id;
else if (r.zoneId) regionIdToZoneId[r.id] = r.zoneId;
});
const isAsmRole = (role: string) => role === 'ASM' || role === 'AREA SALES MANAGER';
const isRmRole = (role: string) => role === 'RM' || role === 'REGIONAL MANAGER';
const isZmRole = (role: string) => (role === 'ZM' || role.includes('ZONAL MANAGER')) && !role.includes('HEAD');
const parsedRegions = rawRegions.map((r: any) => {
const zoneId = getZoneId(r, regionIdToZoneId, stateToRegionId, allDistricts, allStates);
const states = Array.isArray(r.children) ? r.children.filter((c: any) => c.type === 'state').map((c: any) => c.name) : [];
states.forEach((sName: string) => { if (sName) stateToRegionId[sName] = r.id; });
return {
id: r.id,
code: r.name ? r.name.substring(0, 3).toUpperCase() : 'REG',
name: r.name || r.regionName,
zoneId: zoneId,
zoneName: r.zone?.name || r.zone?.zoneName || 'Unknown',
states: states,
cities: [],
status: (r.isActive !== false) ? 'Active' : 'Inactive',
regionalOfficerCount: 0,
asmCount: 0
};
});
const regionAsmIds: Record<string, string[]> = {};
const regionRmIds: Record<string, string[]> = {};
const zoneZmIds: Record<string, string[]> = {};
const zoneAsmIds: Record<string, string[]> = {};
const zoneRmIds: Record<string, string[]> = {};
users.forEach((u: any) => {
const asmRegionIds = new Set<string>();
const rmRegionIds = new Set<string>();
const zmZoneIds = new Set<string>();
const pRole = (u.roleCode || u.role?.roleCode || '').toUpperCase();
const pRoleName = (u.role?.roleName || '').toUpperCase();
const loc = u.location;
if (isAsmRole(pRole) || isAsmRole(pRoleName)) {
const rid = getRegionId(loc, stateToRegionId, allDistricts, allStates);
if (rid) asmRegionIds.add(rid);
}
if (isRmRole(pRole) || isRmRole(pRoleName)) {
const rid = getRegionId(loc, stateToRegionId, allDistricts, allStates);
if (rid) rmRegionIds.add(rid);
}
if (isZmRole(pRole) || isZmRole(pRoleName)) {
const zid = getZoneId(loc, regionIdToZoneId, stateToRegionId, allDistricts, allStates);
if (zid) zmZoneIds.add(zid);
}
if (Array.isArray(u.userRoles)) {
u.userRoles.forEach((ur: any) => {
const urRole = (ur.role?.roleCode || '').toUpperCase();
const urRoleName = (ur.role?.roleName || '').toUpperCase();
const rid = getRegionId(ur.location, stateToRegionId, allDistricts, allStates);
const zid = getZoneId(ur.location, regionIdToZoneId, stateToRegionId, allDistricts, allStates);
if (isAsmRole(urRole) || isAsmRole(urRoleName)) { if (rid) asmRegionIds.add(rid); }
if (isRmRole(urRole) || isRmRole(urRoleName)) { if (rid) rmRegionIds.add(rid); }
if (isZmRole(urRole) || isZmRole(urRoleName)) { if (zid) zmZoneIds.add(zid); }
});
}
if (Array.isArray(u.areaManagers)) {
u.areaManagers.forEach((am: any) => {
const rid = getRegionId(am.area || am.area?.district, stateToRegionId, allDistricts, allStates);
if (rid) asmRegionIds.add(rid);
});
}
asmRegionIds.forEach((rid: string) => {
if (!regionAsmIds[rid]) regionAsmIds[rid] = [];
if (!regionAsmIds[rid].includes(u.id)) regionAsmIds[rid].push(u.id);
const zid = regionIdToZoneId[rid];
if (zid) {
if (!zoneAsmIds[zid]) zoneAsmIds[zid] = [];
if (!zoneAsmIds[zid].includes(u.id)) zoneAsmIds[zid].push(u.id);
}
});
rmRegionIds.forEach((rid: string) => {
if (!regionRmIds[rid]) regionRmIds[rid] = [];
if (!regionRmIds[rid].includes(u.id)) regionRmIds[rid].push(u.id);
const zid = regionIdToZoneId[rid];
if (zid) {
if (!zoneRmIds[zid]) zoneRmIds[zid] = [];
if (!zoneRmIds[zid].includes(u.id)) zoneRmIds[zid].push(u.id);
}
});
zmZoneIds.forEach((zid: string) => {
if (!zoneZmIds[zid]) zoneZmIds[zid] = [];
if (!zoneZmIds[zid].includes(u.id)) zoneZmIds[zid].push(u.id);
});
});
const roles = (bodyRoles?.roles || bodyRoles?.data || []).map((r: any) => ({
id: r.id, name: r.roleName, permissions: r.permissions?.map((p: any) => p.permissionCode) || [], userCount: r.userCount || 0
}));
const zones = rawZones.map((z: any) => {
const hierarchyZmIds = zoneZmIds[z.id] || [];
const hAsmIds = zoneAsmIds[z.id] || [];
const hRmIds = zoneRmIds[z.id] || [];
const zoneUsersMatched = users.filter((u: any) => getZoneId(u.location, regionIdToZoneId, stateToRegionId, allDistricts, allStates) === z.id || (Array.isArray(u.userRoles) && u.userRoles.some((ur: any) => getZoneId(ur.location, regionIdToZoneId, stateToRegionId, allDistricts, allStates) === z.id)));
const zoneZmUsers = zoneUsersMatched.filter((u: any) => {
const c = (u.roleCode || u.role?.roleCode || '').toUpperCase();
const n = (u.role?.roleName || '').toUpperCase();
return isZmRole(c) || isZmRole(n);
});
const zbhUser = zoneUsersMatched.find((u: any) => {
const c = (u.roleCode || u.role?.roleCode || '').toLowerCase();
const n = (u.role?.roleName || '').toLowerCase();
return c === 'zbh' || n.includes('zonal business head') || c.includes('business head');
}) || ({} as any);
return {
id: z.id, name: z.name || z.zoneName, description: z.description || '',
code: z.name ? z.name.substring(0, 3).toUpperCase() : 'ZON',
states: Array.isArray(z.children) ? z.children.filter((child: any) => child.type === 'state').map((c: any) => c.name) : [],
zmCount: hierarchyZmIds.length,
asmCount: hAsmIds.length,
regionalOfficerCount: hRmIds.length,
zbh: { name: zbhUser.fullName || 'Not Assigned', email: zbhUser.email || '', phone: zbhUser.mobileNumber || '' },
zonalManagers: zoneZmUsers.map((m: any) => ({ name: m.fullName || 'Unknown', email: m.email || '', phone: m.mobileNumber || '', districts: [] }))
};
});
const regionalOffices = parsedRegions.map((r: any) => {
const hRmIds = regionRmIds[r.id] || [];
const hAsmIds = regionAsmIds[r.id] || [];
const rmUser = users.find((u: any) => hRmIds.includes(u.id)) || ({} as any);
return {
...r, regionalOfficerCount: hRmIds.length, asmCount: hAsmIds.length,
regionalManager: rmUser.id ? { id: rmUser.id, name: rmUser.fullName, email: rmUser.email, phone: rmUser.mobileNumber } : undefined
};
});
const asms = users.filter((u: any) => {
const c = (u.roleCode || u.role?.roleCode || '').toUpperCase();
const n = (u.role?.roleName || '').toUpperCase();
return isAsmRole(c) || isAsmRole(n);
}).map((u: any) => {
const zid = getZoneId(u.location, regionIdToZoneId, stateToRegionId, allDistricts, allStates);
const rid = getRegionId(u.location, stateToRegionId, allDistricts, allStates);
const regObj = rid ? parsedRegions.find((reg: any) => reg.id === rid) : null;
const zonObj = zid ? rawZones.find((z: any) => z.id === zid) : null;
const district = getAncestorByType(u.location, 'district');
const state = getAncestorByType(u.location, 'state');
const dFromAM = Array.isArray(u.areaManagers) ? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.district?.districtName || am.area?.district?.name).filter(Boolean))) : [];
const dFromUR = Array.isArray(u.userRoles) ? Array.from(new Set(u.userRoles.filter((ur: any) => ur?.role?.roleCode === 'ASM' && ur?.location?.type === 'district').map((ur: any) => ur?.location?.name).filter(Boolean))) : [];
const mDists = Array.from(new Set([...dFromAM, ...dFromUR]));
const sFromAM = Array.isArray(u.areaManagers) ? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.state?.stateName || am.area?.district?.state?.stateName).filter(Boolean))) : [];
const sFromUR = Array.isArray(u.userRoles) ? Array.from(new Set(u.userRoles.filter((ur: any) => ur?.role?.roleCode === 'ASM' && (ur?.location?.type === 'district' || ur?.location?.type === 'state')).map((ur: any) => ur?.location?.stateName || ur?.location?.name).filter(Boolean))) : [];
const mStates = Array.from(new Set([...sFromAM, ...sFromUR]));
const aAsmCode = Array.isArray(u.userRoles) ? (u.userRoles.find((ur: any) => ur?.role?.roleCode === 'ASM' && ur?.managerCode)?.managerCode || '') : '';
return {
id: u.id, code: u.employeeId || 'N/A', asmCode: u.asmCode || aAsmCode || '', employeeId: u.employeeId || '',
name: u.fullName, zoneId: zid || '', regionId: rid || '',
zoneName: zonObj?.name || zonObj?.zoneName || 'Unassigned', regionName: regObj?.name || regObj?.regionName || 'Unassigned',
areasManaged: mDists.length > 0 ? mDists : (district?.name ? [district?.name] : []),
districtNames: mDists.length > 0 ? mDists : (district?.name ? [district?.name] : []),
stateNames: mStates.length > 0 ? mStates : (state?.name ? [state?.name] : []),
email: u.email, phone: u.mobileNumber, status: (u.isActive !== false) ? 'Active' : 'Inactive'
};
});
const zonalManagerMappings = users.filter((u: any) => {
const c = (u.roleCode || u.role?.roleCode || '').toUpperCase();
const n = (u.role?.roleName || '').toUpperCase();
return (isZmRole(c) || isZmRole(n)) && !n.includes('HEAD');
}).map((u: any) => {
const zid = getZoneId(u.location, regionIdToZoneId, stateToRegionId, allDistricts, allStates);
const rid = getRegionId(u.location, stateToRegionId, allDistricts, allStates);
return {
id: u.id, name: u.fullName, code: u.employeeId || 'N/A', email: u.email, phone: u.mobileNumber || 'N/A',
zoneId: zid || '',
zoneName: (zid ? rawZones.find((z: any) => z.id === zid)?.name : 'Not Assigned') || 'Not Assigned',
regionId: rid || '',
regionName: (rid ? parsedRegions.find((r: any) => r.id === rid)?.name : 'Not Assigned') || 'Not Assigned',
districts: u.districts || [], status: (u.isActive !== false) ? 'Active' : 'Inactive'
};
});
const allAreas = (bodyAreas.areas || bodyAreas.data || []).map((a: any) => {
const dId = getAncestorByType(a, 'district')?.id || a.districtId;
const dObj = allDistricts.find((d: any) => d.id === dId);
const sObj = allStates.find((s: any) => s.id === dObj?.stateId);
return {
...a, areaName: a.name, districtId: dId,
district: { districtName: dObj?.districtName || 'Unknown', stateId: dObj?.stateId, state: { stateName: sObj?.stateName || 'Unknown' } }
};
});
const slaConfigs = (bodySla.data || []).map((s: any) => ({
id: s.id, stage: s.stageCode || 'Unknown', days: s.tatValue || 0, enabled: s.isActive !== false,
reminders: s.reminderConfig?.reminders || [], escalations: s.escalationConfig?.escalations || []
}));
dispatch(setMasterData({
zones, regionalOffices, asms, zonalManagerMappings,
roles, allStates, allDistricts, allAreas,
availablePermissions: bodyPerms?.permissions || bodyPerms?.data || [],
emailTemplates: bodyEmail?.data || [],
slaConfigs,
loading: false
}));
} catch (error) {
console.error('[useMasterData] Error fetching data:', error);
dispatch(setError('Could not load configuration data'));
toast.error('Could not load configuration data');
} finally {
dispatch(setLoading(false));
}
}, [dispatch]);
return { fetchInitialData };
};

View File

@ -105,5 +105,17 @@ export const masterService = {
getSlaConfigs: async () => {
const response = await API.getSlaConfigs();
return response.data;
},
// Semantic wrappers for consistent API
saveZone: async (data: any) => {
return data.id ? masterService.updateZone(data.id, data) : masterService.createZone(data);
},
saveRegion: async (data: any) => {
return data.id ? masterService.updateRegion(data.id, data) : masterService.createRegion(data);
},
saveASM: async (data: any) => {
// ASMs are handled via user update in this system
return API.updateUser(data.userId, data).then(res => res.data);
}
};

View File

@ -1,9 +1,11 @@
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/authSlice';
import masterReducer from './slices/masterSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
master: masterReducer,
},
});

View File

@ -0,0 +1,127 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface Zone {
id: string;
name: string;
description: string;
code: string;
states: string[];
zmCount: number;
asmCount?: number;
regionalOfficerCount?: number;
zbh: { name: string; email: string; phone: string };
zonalManagers: any[];
}
export interface Region {
id: string;
code: string;
name: string;
zoneId: string;
zoneName: string;
states: string[];
cities: string[];
status: string;
regionalOfficerCount: number;
asmCount: number;
regionalManager?: { id: string; name: string; email: string; phone: string };
}
export interface ASM {
id: string;
code: string;
asmCode: string;
employeeId: string;
name: string;
zoneId: string;
regionId: string;
zoneName: string;
regionName: string;
areasManaged: string[];
districtNames: string[];
stateNames: string[];
email: string;
phone: string;
status: string;
}
export interface ZonalManagerMapping {
id: string;
name: string;
code: string;
email: string;
phone: string;
zoneId: string;
zoneName: string;
regionId: string;
regionName: string;
districts: string[];
status: string;
}
export interface MasterState {
zones: Zone[];
regionalOffices: Region[];
asms: ASM[];
zonalManagerMappings: ZonalManagerMapping[];
roles: any[];
allStates: any[];
allDistricts: any[];
allAreas: any[];
availablePermissions: any[];
emailTemplates: any[];
slaConfigs: any[];
loading: boolean;
error: string | null;
}
const initialState: MasterState = {
zones: [],
regionalOffices: [],
asms: [],
zonalManagerMappings: [],
roles: [],
allStates: [],
allDistricts: [],
allAreas: [],
availablePermissions: [],
emailTemplates: [],
slaConfigs: [],
loading: false,
error: null,
};
const masterSlice = createSlice({
name: 'master',
initialState,
reducers: {
setMasterData: (state, action: PayloadAction<Partial<MasterState>>) => {
return { ...state, ...action.payload };
},
setZones: (state, action: PayloadAction<Zone[]>) => {
state.zones = action.payload;
},
setRegionalOffices: (state, action: PayloadAction<Region[]>) => {
state.regionalOffices = action.payload;
},
setAsms: (state, action: PayloadAction<ASM[]>) => {
state.asms = action.payload;
},
setZonalManagerMappings: (state, action: PayloadAction<ZonalManagerMapping[]>) => {
state.zonalManagerMappings = action.payload;
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
},
});
export const {
setMasterData, setZones, setRegionalOffices, setAsms,
setZonalManagerMappings, setLoading, setError
} = masterSlice.actions;
export default masterSlice.reducer;