313 lines
14 KiB
TypeScript
313 lines
14 KiB
TypeScript
import React from 'react';
|
|
import { useSelector } from 'react-redux';
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
import { RootState } from '@/store';
|
|
|
|
interface RegionDialogProps {
|
|
isOpen: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
editingRegionId: string | null;
|
|
regionName: string;
|
|
setRegionName: (name: string) => void;
|
|
regionDescription: string;
|
|
setRegionDescription: (desc: string) => void;
|
|
selectedRegionZone: string;
|
|
setSelectedRegionZone: (id: string) => void;
|
|
regionalManagerId: string;
|
|
setRegionalManagerId: (id: string) => void;
|
|
selectedRegionStates: string[]; // District IDs selected
|
|
setSelectedRegionStates: (districts: string[]) => void;
|
|
onSave: () => void;
|
|
userAssignedData: any[];
|
|
}
|
|
|
|
export const RegionDialog: React.FC<RegionDialogProps> = ({
|
|
isOpen, onOpenChange, editingRegionId, regionName, setRegionName,
|
|
regionDescription, setRegionDescription,
|
|
selectedRegionZone, setSelectedRegionZone, regionalManagerId, setRegionalManagerId,
|
|
selectedRegionStates, setSelectedRegionStates, onSave, userAssignedData
|
|
}) => {
|
|
const { zones, allStates, allDistricts, regionalOffices } = useSelector((state: RootState) => state.master);
|
|
|
|
// Internal: which states are checked (for filtering districts)
|
|
const [selectedStateIds, setSelectedStateIds] = React.useState<string[]>([]);
|
|
|
|
// --- Build conflict map: districtId -> region name (for districts in OTHER regions) ---
|
|
const assignedToOtherRegion = React.useMemo(() => {
|
|
const map: Record<string, string> = {};
|
|
(regionalOffices || []).forEach((r: any) => {
|
|
if (r.id !== editingRegionId) {
|
|
(r.districts || []).forEach((d: any) => {
|
|
map[d.id] = r.name;
|
|
});
|
|
}
|
|
});
|
|
return map;
|
|
}, [regionalOffices, editingRegionId]);
|
|
|
|
// --- States visible for the selected zone (only states that have at least 1 district in this zone OR unassigned) ---
|
|
const statesForZone = React.useMemo(() => {
|
|
if (!selectedRegionZone) return allStates;
|
|
const stateIdsWithZoneDistricts = new Set(
|
|
allDistricts
|
|
.filter(d =>
|
|
d.zoneId === selectedRegionZone ||
|
|
!d.zoneId ||
|
|
selectedRegionStates.includes(d.id)
|
|
)
|
|
.map(d => d.stateId)
|
|
.filter(Boolean)
|
|
);
|
|
return allStates.filter((s: any) =>
|
|
s.zoneId === selectedRegionZone ||
|
|
stateIdsWithZoneDistricts.has(s.id) ||
|
|
!s.zoneId
|
|
);
|
|
}, [allStates, allDistricts, selectedRegionZone, selectedRegionStates]);
|
|
|
|
// --- Districts shown = only those in selected states + (in this zone or unassigned) ---
|
|
const availableDistricts = React.useMemo(() => {
|
|
if (selectedStateIds.length === 0) return [];
|
|
return allDistricts.filter(d =>
|
|
selectedStateIds.includes(d.stateId as string) &&
|
|
(!d.zoneId || d.zoneId === selectedRegionZone || d.regionId === editingRegionId)
|
|
);
|
|
}, [allDistricts, selectedStateIds, selectedRegionZone, editingRegionId]);
|
|
|
|
// --- Group districts by state for rendering ---
|
|
const districtsByState = React.useMemo(() => {
|
|
const map: Record<string, { stateName: string; districts: typeof allDistricts }> = {};
|
|
availableDistricts.forEach(d => {
|
|
const state = allStates.find((s: any) => s.id === d.stateId);
|
|
const stateName = state?.name || (d.stateId as string);
|
|
if (!map[d.stateId as string]) map[d.stateId as string] = { stateName, districts: [] };
|
|
map[d.stateId as string].districts.push(d);
|
|
});
|
|
return Object.values(map);
|
|
}, [availableDistricts, allStates]);
|
|
|
|
// --- Prefill selected states and manager when dialog opens in EDIT mode ---
|
|
React.useEffect(() => {
|
|
if (!isOpen) {
|
|
setSelectedStateIds([]);
|
|
return;
|
|
}
|
|
|
|
// Derive states from already-selected districts
|
|
if (selectedRegionStates.length > 0) {
|
|
const derived = Array.from(new Set(
|
|
allDistricts
|
|
.filter(d => selectedRegionStates.includes(d.id))
|
|
.map(d => d.stateId)
|
|
.filter(Boolean)
|
|
)) as string[];
|
|
setSelectedStateIds(derived);
|
|
}
|
|
}, [isOpen]); // Reduced dependencies to avoid re-triggering during active selection
|
|
|
|
// --- When zone changes, reset state & district selections ---
|
|
const handleZoneChange = (zoneId: string) => {
|
|
setSelectedRegionZone(zoneId);
|
|
setSelectedStateIds([]);
|
|
setSelectedRegionStates([]);
|
|
};
|
|
|
|
// --- Toggle state checkbox ---
|
|
const handleStateToggle = (stateId: string, checked: boolean) => {
|
|
if (checked) {
|
|
setSelectedStateIds(prev => [...prev, stateId]);
|
|
} else {
|
|
setSelectedStateIds(prev => prev.filter(id => id !== stateId));
|
|
// Remove districts of this state from selection
|
|
const districtIdsInState = allDistricts.filter(d => d.stateId === stateId).map(d => d.id);
|
|
setSelectedRegionStates(selectedRegionStates.filter(id => !districtIdsInState.includes(id)));
|
|
}
|
|
};
|
|
|
|
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 details and coverage area</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* Region Name */}
|
|
<div>
|
|
<Label>Region Name</Label>
|
|
<Input placeholder="e.g., Delhi NCR Region" className="mt-2 text-slate-900" value={regionName} onChange={(e) => setRegionName(e.target.value)} />
|
|
</div>
|
|
|
|
{/* Zone */}
|
|
<div>
|
|
<Label>Zone</Label>
|
|
<Select value={selectedRegionZone} onValueChange={handleZoneChange}>
|
|
<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>
|
|
|
|
{/* Regional Manager — all users dropdown */}
|
|
<div>
|
|
<Label>Regional Manager</Label>
|
|
<Select value={regionalManagerId} onValueChange={setRegionalManagerId}>
|
|
<SelectTrigger className="mt-2 w-full text-slate-900">
|
|
<SelectValue placeholder="Select from available users" />
|
|
</SelectTrigger>
|
|
<SelectContent className="max-h-60">
|
|
{userAssignedData.map((user) => (
|
|
<SelectItem key={user.id} value={user.id}>
|
|
{user.name || user.fullName}
|
|
{user.email ? ` — ${user.email}` : ''}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div>
|
|
<Label>Description</Label>
|
|
<Textarea placeholder="Describe the region..." className="mt-2 text-slate-900" rows={2} value={regionDescription} onChange={(e) => setRegionDescription(e.target.value)} />
|
|
</div>
|
|
|
|
{/* States Covered — scoped to selected zone */}
|
|
<div>
|
|
<Label>States Covered</Label>
|
|
{!selectedRegionZone && (
|
|
<p className="text-xs text-re-red mt-1">Select a zone first to see available states</p>
|
|
)}
|
|
<div className="mt-2 border rounded-lg p-3 max-h-40 overflow-y-auto bg-slate-50">
|
|
{statesForZone.length === 0 ? (
|
|
<p className="text-xs text-slate-400 italic">
|
|
{selectedRegionZone ? 'No states with available districts in this zone' : 'Select a zone to load states'}
|
|
</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{statesForZone.map((state: any) => (
|
|
<div key={state.id} className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id={`region-state-${state.id}`}
|
|
checked={selectedStateIds.includes(state.id)}
|
|
disabled={!selectedRegionZone}
|
|
onCheckedChange={(checked) => handleStateToggle(state.id, !!checked)}
|
|
/>
|
|
<label
|
|
htmlFor={`region-state-${state.id}`}
|
|
className={`text-sm cursor-pointer ${!selectedRegionZone ? 'text-slate-400' : 'text-slate-900'}`}
|
|
>
|
|
{state.name}
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
{selectedStateIds.length} {selectedStateIds.length === 1 ? 'state' : 'states'} selected
|
|
</p>
|
|
</div>
|
|
|
|
{/* Districts Covered — filtered by zone + selected states */}
|
|
<div>
|
|
<Label>Districts Covered</Label>
|
|
<div className="mt-2 border rounded-lg p-3 max-h-56 overflow-y-auto bg-slate-50">
|
|
{selectedStateIds.length === 0 ? (
|
|
<p className="text-xs text-slate-400 italic">Select one or more states above to see districts</p>
|
|
) : districtsByState.length === 0 ? (
|
|
<p className="text-xs text-slate-400 italic">No available districts in the selected states for this zone</p>
|
|
) : (
|
|
<TooltipProvider>
|
|
{districtsByState.map(({ stateName, districts }) => (
|
|
<div key={stateName} className="mb-4 last:mb-0">
|
|
<h4 className="text-xs font-semibold text-re-red-hover uppercase tracking-wide mb-2 pb-1 border-b border-slate-200">
|
|
{stateName}
|
|
</h4>
|
|
<div className="space-y-2 ml-1">
|
|
{districts.map((district) => {
|
|
const conflictRegion = assignedToOtherRegion[district.id];
|
|
const inDifferentZone = district.zoneId && district.zoneId !== selectedRegionZone && district.regionId !== editingRegionId;
|
|
const isDisabled = !!(conflictRegion || inDifferentZone);
|
|
|
|
const tooltipText = conflictRegion
|
|
? `Already assigned to region: ${conflictRegion}`
|
|
: inDifferentZone
|
|
? `Belongs to a different zone`
|
|
: '';
|
|
|
|
return (
|
|
<div key={district.id} className="flex items-center space-x-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="flex items-center space-x-2 w-full">
|
|
<Checkbox
|
|
id={`region-district-${district.id}`}
|
|
disabled={isDisabled}
|
|
checked={selectedRegionStates.includes(district.id)}
|
|
onCheckedChange={(checked) => {
|
|
if (checked) {
|
|
setSelectedRegionStates([...selectedRegionStates, district.id]);
|
|
} else {
|
|
setSelectedRegionStates(selectedRegionStates.filter(id => id !== district.id));
|
|
}
|
|
}}
|
|
/>
|
|
<label
|
|
htmlFor={`region-district-${district.id}`}
|
|
className={`text-sm flex-1 ${isDisabled ? 'text-slate-400 cursor-not-allowed line-through' : 'text-slate-900 cursor-pointer'}`}
|
|
>
|
|
{district.name}
|
|
{conflictRegion && (
|
|
<span className="ml-2 text-xs text-red-400 font-normal no-underline" style={{ textDecoration: 'none' }}>
|
|
(in {conflictRegion})
|
|
</span>
|
|
)}
|
|
</label>
|
|
</div>
|
|
</TooltipTrigger>
|
|
{isDisabled && tooltipText && (
|
|
<TooltipContent>
|
|
<p className="text-xs">{tooltipText}</p>
|
|
</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</TooltipProvider>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
{selectedRegionStates.length} {selectedRegionStates.length === 1 ? 'district' : 'districts'} selected
|
|
</p>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-3 pt-4">
|
|
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
|
|
<Button className="flex-1 bg-re-red hover:bg-re-red-hover" onClick={onSave}>Save Regional Office</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|