From 3538bb5b0e67ac061bac0226403fa6a0809edb13 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 6 Oct 2026 15:40:43 +0530 Subject: [PATCH] Assign a region manager for the route --- .../Services/RegionServiceTests.cs | 89 +++++++++++ .../_components/region-manager-dialog.tsx | 138 ++++++++++++++++++ .../regions/_components/regions-client.tsx | 13 ++ distribution_fe/lib/api/master.ts | 10 ++ distribution_fe/types/api.ts | 7 + 5 files changed, 257 insertions(+) create mode 100644 distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx diff --git a/Distribution.Tests/Services/RegionServiceTests.cs b/Distribution.Tests/Services/RegionServiceTests.cs index 07159f0..cb9453d 100644 --- a/Distribution.Tests/Services/RegionServiceTests.cs +++ b/Distribution.Tests/Services/RegionServiceTests.cs @@ -313,4 +313,93 @@ public class RegionServiceTests _uow.Verify(u => u.RollbackAsync(default), Times.Once); } + + // Region manager + + private static User ManagerUser(string role = "RegionalManager", bool active = true) => new() + { + Id = Guid.NewGuid(), Username = "mgr", FullName = "Mgr One", IsActive = active, + Role = new Role { Name = role, Level = 50 } + }; + + [Fact] + public async Task SetManagerAsync_ReturnsNull_WhenRegionMissing() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync((Region?)null); + + Assert.Null(await _sut.SetManagerAsync(Guid.NewGuid(), new SetRegionManagerRequest(Guid.NewGuid()))); + } + + [Fact] + public async Task SetManagerAsync_Throws_WhenUserNotRegionalManager() + { + var region = ActiveRegion(); + var user = ManagerUser("SalesRep"); + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetUserWithRoleAsync(user.Id)).ReturnsAsync(user); + + await Assert.ThrowsAsync(() => + _sut.SetManagerAsync(region.Id, new SetRegionManagerRequest(user.Id))); + _uow.Verify(u => u.CommitAsync(default), Times.Never); + } + + [Fact] + public async Task SetManagerAsync_ReplacesExistingManager_AndAddsScope() + { + var region = ActiveRegion(); + var user = ManagerUser(); + var old = new UserRegionScope { FK_UserId = Guid.NewGuid(), FK_RegionId = region.Id }; + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetUserWithRoleAsync(user.Id)).ReturnsAsync(user); + _repo.Setup(r => r.GetManagerScopesAsync(region.Id)).ReturnsAsync([old]); + _repo.Setup(r => r.GetRegionScopeAsync(user.Id, region.Id)).ReturnsAsync((UserRegionScope?)null); + + var result = await _sut.SetManagerAsync(region.Id, new SetRegionManagerRequest(user.Id)); + + Assert.Equal(user.Id, result!.UserId); + _repo.Verify(r => r.RemoveRegionScopes(It.Is>(s => s.Single() == old)), Times.Once); + _repo.Verify(r => r.AddRegionScopeAsync(It.Is(s => s.FK_UserId == user.Id && s.FK_RegionId == region.Id)), Times.Once); + _uow.Verify(u => u.CommitAsync(default), Times.Once); + } + + [Fact] + public async Task SetManagerAsync_DoesNotDuplicateScope_WhenAlreadyManager() + { + var region = ActiveRegion(); + var user = ManagerUser(); + var existing = new UserRegionScope { FK_UserId = user.Id, FK_RegionId = region.Id }; + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetUserWithRoleAsync(user.Id)).ReturnsAsync(user); + _repo.Setup(r => r.GetManagerScopesAsync(region.Id)).ReturnsAsync([existing]); + _repo.Setup(r => r.GetRegionScopeAsync(user.Id, region.Id)).ReturnsAsync(existing); + + await _sut.SetManagerAsync(region.Id, new SetRegionManagerRequest(user.Id)); + + _repo.Verify(r => r.AddRegionScopeAsync(It.IsAny()), Times.Never); + _repo.Verify(r => r.RemoveRegionScopes(It.Is>(s => !s.Any())), Times.Once); + } + + [Fact] + public async Task RemoveManagerAsync_ReturnsFalse_WhenNoManager() + { + var region = ActiveRegion(); + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetManagerScopesAsync(region.Id)).ReturnsAsync([]); + + Assert.False(await _sut.RemoveManagerAsync(region.Id)); + } + + [Fact] + public async Task GetManagerAsync_ReturnsManager() + { + var region = ActiveRegion(); + var user = ManagerUser(); + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetManagerScopesAsync(region.Id)) + .ReturnsAsync([new UserRegionScope { FK_UserId = user.Id, FK_RegionId = region.Id, User = user }]); + + var result = await _sut.GetManagerAsync(region.Id); + + Assert.Equal("mgr", result!.Username); + } } diff --git a/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx b/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx new file mode 100644 index 0000000..4013883 --- /dev/null +++ b/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx @@ -0,0 +1,138 @@ +'use client' + +import { useEffect, useState } from 'react' + +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { toast } from '@/components/ui/toast' +import { regions as regionsApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { RegionResponse, RegionManagerResponse, UserResponse } from '@/types/api' + +const MANAGER_ROLE = 'RegionalManager' + +interface RegionManagerDialogProps { + open: boolean + onClose: () => void + region: RegionResponse | null +} + +export function RegionManagerDialog({ open, onClose, region }: RegionManagerDialogProps) { + const [current, setCurrent] = useState(null) + const [candidates, setCandidates] = useState([]) + const [selected, setSelected] = useState('') + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (!open || !region) return + let cancelled = false + setLoading(true) + setSelected('') + setCurrent(null) + Promise.all([ + regionsApi.getManager(region.id).catch(() => null), // 404 = no manager yet + usersApi.list({ pageSize: 500 }), + ]) + .then(([manager, userRes]) => { + if (cancelled) return + setCurrent(manager) + setSelected(manager?.userId ?? '') + setCandidates(getPaginatedItems(userRes).filter((u) => u.isActive && u.roleName === MANAGER_ROLE)) + }) + .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) + .finally(() => !cancelled && setLoading(false)) + return () => { cancelled = true } + }, [open, region]) + + async function save() { + if (!region || !selected) return + setSaving(true) + try { + await regionsApi.setManager(region.id, selected) + toast.add({ title: 'Region manager assigned', type: 'success' }) + onClose() + } catch (err) { + toast.add({ title: 'Failed to assign manager', description: getApiError(err), type: 'error' }) + } finally { + setSaving(false) + } + } + + async function remove() { + if (!region) return + setSaving(true) + try { + await regionsApi.removeManager(region.id) + toast.add({ title: 'Region manager removed', type: 'success' }) + onClose() + } catch (err) { + toast.add({ title: 'Failed to remove manager', description: getApiError(err), type: 'error' }) + } finally { + setSaving(false) + } + } + + return ( + !v && !saving && onClose()}> + + + Region Manager + + {region ? `${region.code} – ${region.name}. ` : ''} + Each region has one manager, chosen from users with the RegionalManager role. + + + +
+

+ Current manager:{' '} + + {loading ? 'Loading…' : current ? `${current.fullName} (${current.username})` : 'None'} + +

+ + +
+ + + {current && ( + + )} + + + +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx b/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx index cff5519..2d0b8f2 100644 --- a/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx +++ b/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx @@ -14,6 +14,7 @@ import { regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api import type { RegionResponse } from '@/types/api' import { RegionSheet } from './region-sheet' +import { RegionManagerDialog } from './region-manager-dialog' export function RegionsClient() { const [data, setData] = useState([]) @@ -27,6 +28,7 @@ export function RegionsClient() { const [viewOnly, setViewOnly] = useState(false) const [deleting, setDeleting] = useState(null) const [deleteBusy, setDeleteBusy] = useState(false) + const [managerRegion, setManagerRegion] = useState(null) const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) @@ -124,6 +126,11 @@ export function RegionsClient() { ], rowActions: { actions: () => [ + { + action: 'confirm', + label: 'Manage region manager', + onClick: (region: RegionResponse) => setManagerRegion(region), + }, { action: 'view', label: 'View region', @@ -176,6 +183,12 @@ export function RegionsClient() { readOnly={viewOnly} onSuccess={() => fetchData(page, pageSize)} /> + setManagerRegion(null)} + region={managerRegion} + /> + !v && !deleteBusy && setDeleting(null)}> diff --git a/distribution_fe/lib/api/master.ts b/distribution_fe/lib/api/master.ts index 0cf844e..a13bf59 100644 --- a/distribution_fe/lib/api/master.ts +++ b/distribution_fe/lib/api/master.ts @@ -2,6 +2,7 @@ import { api } from '@/lib/api' import type { PaginatedResponse, RegionResponse, + RegionManagerResponse, ManageRegionResponse, RegionHierarchyResponse, RegionDashboardResponse, @@ -92,6 +93,15 @@ export const regions = { list: (params?: PageParams) => api.get>('/regions', { params }).then(r => r.data), + getManager: (id: string) => + api.get(`/regions/${id}/manager`).then(r => r.data), + + setManager: (id: string, userId: string) => + api.put(`/regions/${id}/manager`, { userId }).then(r => r.data), + + removeManager: (id: string) => + api.delete(`/regions/${id}/manager`), + nextCode: () => api.get<{ code: string }>('/regions/next-code').then(r => r.data), diff --git a/distribution_fe/types/api.ts b/distribution_fe/types/api.ts index 7af0b3b..c027417 100644 --- a/distribution_fe/types/api.ts +++ b/distribution_fe/types/api.ts @@ -729,3 +729,10 @@ export interface SysConfigResponse { export interface ApiError { error: string } + +export interface RegionManagerResponse { + regionId: string + userId: string + username: string + fullName: string +}