From cdca4ad2640edc807395dd3157312af9cc99f509 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sat, 20 Jun 2026 19:21:23 +0330 Subject: [PATCH] Admin: role merge tool + usage list (taxonomy hygiene) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New /Admin/Roles screen lists every role with its shift/job/talent usage and lets an admin merge a duplicate role into another — reassigns all listings (the Restrict FKs) plus preferences/alerts/profiles to the target, then deletes the source — or toggle a role's visibility. Linked from the admin panel nav (🏷️ نقش‌ها). Lets you clean up dynamic-ingestion sprawl («کمک‌یار»→«کمک بهیار») without DB surgery. Improvement 7 of the backlog (data). Co-Authored-By: Claude Opus 4.8 --- src/JobsMedical.Web/Pages/Admin/Roles.cshtml | 84 +++++++++++++++++++ .../Pages/Admin/Roles.cshtml.cs | 75 +++++++++++++++++ .../Pages/Shared/_PanelNav.cshtml | 1 + 3 files changed, 160 insertions(+) create mode 100644 src/JobsMedical.Web/Pages/Admin/Roles.cshtml create mode 100644 src/JobsMedical.Web/Pages/Admin/Roles.cshtml.cs diff --git a/src/JobsMedical.Web/Pages/Admin/Roles.cshtml b/src/JobsMedical.Web/Pages/Admin/Roles.cshtml new file mode 100644 index 0000000..ab210eb --- /dev/null +++ b/src/JobsMedical.Web/Pages/Admin/Roles.cshtml @@ -0,0 +1,84 @@ +@page +@model JobsMedical.Web.Pages.Admin.RolesModel +@{ + ViewData["Title"] = "نقش‌ها و دسته‌بندی"; + string P(int n) => JalaliDate.ToPersianDigits(n.ToString()); +} + +
+
+

نقش‌ها و دسته‌بندی

+

← صف بررسی — ادغام نقش‌های تکراری و مدیریت تاکسونومی.

+
+
+ +
+ @if (Model.Message is not null) + { +
✓ @Model.Message
+ } + +
+

ادغام نقش

+

همهٔ آگهی‌ها و علاقه‌مندی‌های «نقش مبدأ» به «نقش مقصد» منتقل و نقش مبدأ حذف می‌شود. این کار بازگشت‌ناپذیر است.

+
+
+ + +
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + @foreach (var x in Model.Stats) + { + + + + + + + + + + } + +
نقشگروهشیفتاستخدامآماده‌به‌کارجمع
@x.Role.Name @(x.Role.IsActive ? "" : "(غیرفعال)")@x.Role.Category@P(x.Shifts)@P(x.Jobs)@P(x.Talent)@P(x.Total) +
+ +
+
+
diff --git a/src/JobsMedical.Web/Pages/Admin/Roles.cshtml.cs b/src/JobsMedical.Web/Pages/Admin/Roles.cshtml.cs new file mode 100644 index 0000000..fe67ee1 --- /dev/null +++ b/src/JobsMedical.Web/Pages/Admin/Roles.cshtml.cs @@ -0,0 +1,75 @@ +using JobsMedical.Web.Data; +using JobsMedical.Web.Models; +using JobsMedical.Web.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace JobsMedical.Web.Pages.Admin; + +/// Role taxonomy hygiene — dynamic ingestion can mint near-duplicate roles over time +/// («کمک‌یار» vs «کمک بهیار»). This screen lists every role with its usage and lets an admin merge +/// one role into another (reassigning all its listings/preferences) or toggle a role's visibility. +[Authorize(Roles = "Admin")] +public class RolesModel : PageModel +{ + private readonly AppDbContext _db; + public RolesModel(AppDbContext db) => _db = db; + + public record RoleStat(Role Role, int Shifts, int Jobs, int Talent) + { + public int Total => Shifts + Jobs + Talent; + } + + public List Stats { get; private set; } = new(); + [TempData] public string? Message { get; set; } + + public async Task OnGetAsync() => await LoadAsync(); + + private async Task LoadAsync() + { + var roles = await _db.Roles.OrderBy(r => r.Category).ThenBy(r => r.Name).ToListAsync(); + var sc = await _db.Shifts.GroupBy(s => s.RoleId).Select(g => new { g.Key, C = g.Count() }).ToDictionaryAsync(x => x.Key, x => x.C); + var jc = await _db.JobOpenings.GroupBy(j => j.RoleId).Select(g => new { g.Key, C = g.Count() }).ToDictionaryAsync(x => x.Key, x => x.C); + var tc = await _db.TalentListings.GroupBy(t => t.RoleId).Select(g => new { g.Key, C = g.Count() }).ToDictionaryAsync(x => x.Key, x => x.C); + Stats = roles.Select(r => new RoleStat(r, sc.GetValueOrDefault(r.Id), jc.GetValueOrDefault(r.Id), tc.GetValueOrDefault(r.Id))) + .OrderByDescending(x => x.Total).ToList(); + } + + /// Move every reference from to + /// (listings — the Restrict FKs that would otherwise block — plus preferences/alerts/profiles), + /// then delete the now-empty source role. + public async Task OnPostMergeAsync(int sourceId, int targetId) + { + if (sourceId == 0 || targetId == 0 || sourceId == targetId) + { Message = "نقش مبدأ و مقصد را درست انتخاب کن."; return RedirectToPage(); } + + var source = await _db.Roles.FindAsync(sourceId); + var target = await _db.Roles.FindAsync(targetId); + if (source is null || target is null) { Message = "نقش پیدا نشد."; return RedirectToPage(); } + + var s = await _db.Shifts.Where(x => x.RoleId == sourceId).ExecuteUpdateAsync(u => u.SetProperty(x => x.RoleId, targetId)); + var j = await _db.JobOpenings.Where(x => x.RoleId == sourceId).ExecuteUpdateAsync(u => u.SetProperty(x => x.RoleId, targetId)); + var t = await _db.TalentListings.Where(x => x.RoleId == sourceId).ExecuteUpdateAsync(u => u.SetProperty(x => x.RoleId, targetId)); + // Nullable references too, so a saved preference/alert follows the merge instead of dangling. + await _db.UserPreferences.Where(x => x.RoleId == sourceId).ExecuteUpdateAsync(u => u.SetProperty(x => x.RoleId, (int?)targetId)); + await _db.JobAlerts.Where(x => x.RoleId == sourceId).ExecuteUpdateAsync(u => u.SetProperty(x => x.RoleId, (int?)targetId)); + await _db.DoctorProfiles.Where(x => x.RoleId == sourceId).ExecuteUpdateAsync(u => u.SetProperty(x => x.RoleId, (int?)targetId)); + + _db.Roles.Remove(source); + await _db.SaveChangesAsync(); + + string P(int n) => JalaliDate.ToPersianDigits(n.ToString()); + Message = $"«{source.Name}» در «{target.Name}» ادغام شد — منتقل‌شده: {P(s)} شیفت، {P(j)} استخدام، {P(t)} آماده‌به‌کار."; + return RedirectToPage(); + } + + /// Hide a role from filters/forms without deleting it (keeps its listings intact). + public async Task OnPostToggleAsync(int id) + { + var role = await _db.Roles.FindAsync(id); + if (role is not null) { role.IsActive = !role.IsActive; await _db.SaveChangesAsync(); } + return RedirectToPage(); + } +} diff --git a/src/JobsMedical.Web/Pages/Shared/_PanelNav.cshtml b/src/JobsMedical.Web/Pages/Shared/_PanelNav.cshtml index 62f1bcd..e7c1068 100644 --- a/src/JobsMedical.Web/Pages/Shared/_PanelNav.cshtml +++ b/src/JobsMedical.Web/Pages/Shared/_PanelNav.cshtml @@ -16,6 +16,7 @@ 📥 صف آگهی‌ها 📜 نتایج جمع‌آوری 🏥 مراکز + 🏷️ نقش‌ها 👥 کاربران 🛡️ گزارش‌ها 📣 اعلان همگانی