location hirarchy related changes done
This commit is contained in:
parent
73cf4fdfac
commit
def289e12b
53
package-lock.json
generated
53
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
274
src/components/applications/MasterPage/ASMDialog.tsx
Normal file
274
src/components/applications/MasterPage/ASMDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
125
src/components/applications/MasterPage/ASMManagement.tsx
Normal file
125
src/components/applications/MasterPage/ASMManagement.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
96
src/components/applications/MasterPage/EmailTemplates.tsx
Normal file
96
src/components/applications/MasterPage/EmailTemplates.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
139
src/components/applications/MasterPage/LocationDialog.tsx
Normal file
139
src/components/applications/MasterPage/LocationDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
108
src/components/applications/MasterPage/LocationManagement.tsx
Normal file
108
src/components/applications/MasterPage/LocationManagement.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
137
src/components/applications/MasterPage/RegionDialog.tsx
Normal file
137
src/components/applications/MasterPage/RegionDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
129
src/components/applications/MasterPage/RegionalManagement.tsx
Normal file
129
src/components/applications/MasterPage/RegionalManagement.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
72
src/components/applications/MasterPage/RolePermissions.tsx
Normal file
72
src/components/applications/MasterPage/RolePermissions.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
91
src/components/applications/MasterPage/SLAConfiguration.tsx
Normal file
91
src/components/applications/MasterPage/SLAConfiguration.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
132
src/components/applications/MasterPage/TemplateDialog.tsx
Normal file
132
src/components/applications/MasterPage/TemplateDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
108
src/components/applications/MasterPage/ZMManagement.tsx
Normal file
108
src/components/applications/MasterPage/ZMManagement.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
149
src/components/applications/MasterPage/ZoneDetails.tsx
Normal file
149
src/components/applications/MasterPage/ZoneDetails.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
69
src/components/applications/MasterPage/ZoneDialog.tsx
Normal file
69
src/components/applications/MasterPage/ZoneDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
69
src/components/applications/MasterPage/ZonesOverview.tsx
Normal file
69
src/components/applications/MasterPage/ZonesOverview.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
40
src/hooks/useLocationHelpers.ts
Normal file
40
src/hooks/useLocationHelpers.ts
Normal 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
304
src/hooks/useMasterData.ts
Normal 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 };
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
127
src/store/slices/masterSlice.ts
Normal file
127
src/store/slices/masterSlice.ts
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user