Admin suite: monitoring dashboard, user management/ban, broadcast, reports, SMS test
- /Admin/Overview: platform monitoring stats (users by role, facilities, listings, applies, push subs, queue, reports, bans) - /Admin/Users: search/filter + ban/unban (User.IsBanned + reason); banned users blocked at login - /Admin/Broadcast: send announcement (in-app + web push) to all / staff / employers via NotificationService - Reports: report button on shift/job detail → /report endpoint → /Admin/Reports (resolve/dismiss) - Settings: 'send test SMS' button; admin cross-nav links; SMS API config already in place - migration AdminBanReports; verified overview/users/broadcast/report persist Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Admin.BroadcastModel
|
||||
@{
|
||||
ViewData["Title"] = "ارسال اعلان همگانی";
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>ارسال اعلان همگانی</h1>
|
||||
<p class="muted"><a asp-page="/Admin/Overview">← داشبورد</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section" style="max-width:560px;">
|
||||
@if (Model.Result is not null) { <div class="alert alert-success">✓ @Model.Result</div> }
|
||||
<form method="post" class="card card-pad">
|
||||
<div class="filter-group">
|
||||
<label>مخاطب</label>
|
||||
<select name="Audience">
|
||||
<option value="all" selected="@(Model.Audience == "all")">همه کاربران</option>
|
||||
<option value="staff" selected="@(Model.Audience == "staff")">فقط کادر درمان</option>
|
||||
<option value="employers" selected="@(Model.Audience == "employers")">فقط کارفرمایان</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>عنوان</label>
|
||||
<input type="text" name="Title" value="@Model.Title" placeholder="مثلاً: شیفتهای جدید این هفته" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>متن</label>
|
||||
<textarea name="Body" rows="3">@Model.Body</textarea>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>لینک (اختیاری)</label>
|
||||
<input type="text" name="Url" value="@Model.Url" dir="ltr" placeholder="/Shifts" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-accent btn-block btn-lg">ارسال اعلان</button>
|
||||
<p class="muted" style="font-size:12px; margin-bottom:0;">اعلان در زنگولهی کاربران ثبت میشود و اگر پوش فعال باشد بهصورت اعلان مرورگری هم ارسال میشود.</p>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class BroadcastModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly NotificationService _notify;
|
||||
|
||||
public BroadcastModel(AppDbContext db, NotificationService notify)
|
||||
{
|
||||
_db = db;
|
||||
_notify = notify;
|
||||
}
|
||||
|
||||
[BindProperty] public string Title { get; set; } = "";
|
||||
[BindProperty] public string? Body { get; set; }
|
||||
[BindProperty] public string? Url { get; set; }
|
||||
[BindProperty] public string Audience { get; set; } = "all"; // all | staff | employers
|
||||
[TempData] public string? Result { get; set; }
|
||||
|
||||
public void OnGet() { }
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Title)) { Result = "عنوان لازم است."; return Page(); }
|
||||
|
||||
IQueryable<User> q = _db.Users.Where(u => !u.IsBanned);
|
||||
q = Audience switch
|
||||
{
|
||||
"staff" => q.Where(u => u.Role == UserRole.Doctor),
|
||||
"employers" => q.Where(u => u.Role == UserRole.FacilityAdmin),
|
||||
_ => q,
|
||||
};
|
||||
var ids = await q.Select(u => u.Id).ToListAsync();
|
||||
await _notify.BroadcastAsync(ids, Title.Trim(), Body?.Trim(), string.IsNullOrWhiteSpace(Url) ? "/" : Url.Trim());
|
||||
|
||||
Result = $"اعلان برای {ids.Count} کاربر ارسال شد (دراپ + پوش در صورت فعالبودن).";
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,12 @@
|
||||
آگهیهای جمعآوریشده از منابع را بررسی، ساختارمند و منتشر کن.
|
||||
(@JalaliDate.ToPersianDigits(Model.Queue.Count.ToString()) در صف،
|
||||
@JalaliDate.ToPersianDigits(Model.Flagged.Count.ToString()) پرچمخورده)
|
||||
· <a asp-page="/Admin/Facilities">تأیید مراکز درمانی</a>
|
||||
· <a asp-page="/Admin/Settings">تنظیمات جمعآوری و AI</a>
|
||||
· <a asp-page="/Admin/Overview">داشبورد</a>
|
||||
· <a asp-page="/Admin/Users">کاربران</a>
|
||||
· <a asp-page="/Admin/Facilities">مراکز</a>
|
||||
· <a asp-page="/Admin/Reports">گزارشها</a>
|
||||
· <a asp-page="/Admin/Broadcast">ارسال اعلان</a>
|
||||
· <a asp-page="/Admin/Settings">تنظیمات</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Admin.OverviewModel
|
||||
@{
|
||||
ViewData["Title"] = "داشبورد مدیریت";
|
||||
string P(int n) => JalaliDate.ToPersianDigits(n.ToString());
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>داشبورد مدیریت</h1>
|
||||
<p class="muted">
|
||||
<a asp-page="/Admin/Index">صف آگهیها</a> ·
|
||||
<a asp-page="/Admin/Users">کاربران</a> ·
|
||||
<a asp-page="/Admin/Facilities">مراکز</a> ·
|
||||
<a asp-page="/Admin/Reports">گزارشها</a> ·
|
||||
<a asp-page="/Admin/Broadcast">ارسال اعلان</a> ·
|
||||
<a asp-page="/Admin/Settings">تنظیمات</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="grid grid-4">
|
||||
<div class="card card-pad"><div class="stat-pill" style="background:none;padding:0;"><span class="n" style="color:var(--primary-dark);font-size:26px;">@P(Model.Users)</span><span class="l">کاربر (@P(Model.Staff) کادر / @P(Model.Employers) کارفرما)</span></div></div>
|
||||
<div class="card card-pad"><span style="font-size:26px;font-weight:900;color:var(--primary-dark)">@P(Model.Facilities)</span><div class="muted">مرکز (@P(Model.VerifiedFacilities) تأییدشده، @P(Model.PendingFacilities) در انتظار)</div></div>
|
||||
<div class="card card-pad"><span style="font-size:26px;font-weight:900;color:var(--primary-dark)">@P(Model.OpenShifts)</span><div class="muted">شیفت باز</div></div>
|
||||
<div class="card card-pad"><span style="font-size:26px;font-weight:900;color:var(--primary-dark)">@P(Model.OpenJobs)</span><div class="muted">استخدام باز</div></div>
|
||||
<div class="card card-pad"><span style="font-size:26px;font-weight:900;color:var(--accent)">@P(Model.Applies)</span><div class="muted">اعلام تمایل</div></div>
|
||||
<div class="card card-pad"><span style="font-size:26px;font-weight:900;color:var(--accent)">@P(Model.PushSubs)</span><div class="muted">اشتراک اعلان</div></div>
|
||||
<div class="card card-pad"><span style="font-size:26px;font-weight:900;">@P(Model.QueueNew + Model.QueueFlagged)</span><div class="muted">در صف بررسی (@P(Model.QueueFlagged) پرچمخورده) · <a asp-page="/Admin/Index">باز کن</a></div></div>
|
||||
<div class="card card-pad"><span style="font-size:26px;font-weight:900;color:@(Model.OpenReports>0?"var(--danger)":"inherit")">@P(Model.OpenReports)</span><div class="muted">گزارش باز · <a asp-page="/Admin/Reports">رسیدگی</a></div></div>
|
||||
<div class="card card-pad"><span style="font-size:26px;font-weight:900;color:@(Model.Banned>0?"var(--danger)":"inherit")">@P(Model.Banned)</span><div class="muted">کاربر مسدود · <a asp-page="/Admin/Users">مدیریت</a></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Admin;
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class OverviewModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public OverviewModel(AppDbContext db) => _db = db;
|
||||
|
||||
public int Users, Employers, Staff, Banned;
|
||||
public int Facilities, VerifiedFacilities, PendingFacilities;
|
||||
public int OpenShifts, OpenJobs, Applies;
|
||||
public int PushSubs, QueueNew, QueueFlagged, OpenReports;
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
Users = await _db.Users.CountAsync();
|
||||
Employers = await _db.Users.CountAsync(u => u.Role == UserRole.FacilityAdmin);
|
||||
Staff = await _db.Users.CountAsync(u => u.Role == UserRole.Doctor);
|
||||
Banned = await _db.Users.CountAsync(u => u.IsBanned);
|
||||
Facilities = await _db.Facilities.CountAsync();
|
||||
VerifiedFacilities = await _db.Facilities.CountAsync(f => f.IsVerified);
|
||||
PendingFacilities = Facilities - VerifiedFacilities;
|
||||
OpenShifts = await _db.Shifts.CountAsync(s => s.Status == ShiftStatus.Open && s.Date >= today);
|
||||
OpenJobs = await _db.JobOpenings.CountAsync(j => j.Status == ShiftStatus.Open);
|
||||
Applies = await _db.InterestEvents.CountAsync(e => e.EventType == InterestEventType.Apply);
|
||||
PushSubs = await _db.WebPushSubscriptions.CountAsync();
|
||||
QueueNew = await _db.RawListings.CountAsync(r => r.Status == RawListingStatus.New);
|
||||
QueueFlagged = await _db.RawListings.CountAsync(r => r.Status == RawListingStatus.Flagged);
|
||||
OpenReports = await _db.Reports.CountAsync(r => r.Status == ReportStatus.Open);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Admin.ReportsModel
|
||||
@{
|
||||
ViewData["Title"] = "گزارشهای تخلف";
|
||||
string TypeLabel(ReportTargetType t) => t switch
|
||||
{
|
||||
ReportTargetType.Shift => "شیفت", ReportTargetType.Job => "استخدام",
|
||||
ReportTargetType.Facility => "مرکز", _ => "کاربر"
|
||||
};
|
||||
string StatusLabel(ReportStatus s) => s switch
|
||||
{
|
||||
ReportStatus.Open => "باز", ReportStatus.Resolved => "رسیدگیشده", _ => "ردشده"
|
||||
};
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>گزارشهای تخلف</h1>
|
||||
<p class="muted"><a asp-page="/Admin/Overview">← داشبورد</a> · <a asp-page="/Admin/Users">کاربران</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
@if (Model.Reports.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">گزارشی ثبت نشده است.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var r in Model.Reports)
|
||||
{
|
||||
<div class="card card-pad" style="margin-bottom:10px; @(r.Status == ReportStatus.Open ? "" : "opacity:.6;")">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center; gap:8px; flex-wrap:wrap;">
|
||||
<strong>@TypeLabel(r.TargetType): @(r.TargetLabel ?? ("#" + r.TargetId))</strong>
|
||||
<span class="badge @(r.Status == ReportStatus.Open ? "badge-day" : "badge-type")">@StatusLabel(r.Status)</span>
|
||||
</div>
|
||||
<p style="margin:8px 0;">«@r.Reason»</p>
|
||||
<div class="muted" style="font-size:12px;">@JalaliDate.ToLongDate(DateOnly.FromDateTime(r.CreatedAt)) · گزارشدهنده: @(r.ReporterUserId is not null ? "کاربر #" + r.ReporterUserId : "مهمان")</div>
|
||||
<div style="display:flex; gap:8px; margin-top:10px;">
|
||||
<a class="btn btn-outline" style="padding:6px 12px;" href="@JobsMedical.Web.Pages.Admin.ReportsModel.TargetUrl(r)" target="_blank">مشاهده مورد</a>
|
||||
@if (r.Status == ReportStatus.Open)
|
||||
{
|
||||
<form method="post"><button asp-page-handler="Resolve" asp-route-id="@r.Id" class="btn btn-outline" style="padding:6px 12px;">رسیدگی شد</button></form>
|
||||
<form method="post"><button asp-page-handler="Dismiss" asp-route-id="@r.Id" class="btn btn-outline" style="padding:6px 12px;">رد گزارش</button></form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Admin;
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class ReportsModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public ReportsModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<Report> Reports { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync() =>
|
||||
Reports = await _db.Reports
|
||||
.OrderBy(r => r.Status).ThenByDescending(r => r.CreatedAt)
|
||||
.Take(200).ToListAsync();
|
||||
|
||||
public async Task<IActionResult> OnPostResolveAsync(int id) => await SetStatus(id, ReportStatus.Resolved);
|
||||
public async Task<IActionResult> OnPostDismissAsync(int id) => await SetStatus(id, ReportStatus.Dismissed);
|
||||
|
||||
private async Task<IActionResult> SetStatus(int id, ReportStatus st)
|
||||
{
|
||||
var r = await _db.Reports.FindAsync(id);
|
||||
if (r is null) return NotFound();
|
||||
r.Status = st;
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public static string TargetUrl(Report r) => r.TargetType switch
|
||||
{
|
||||
ReportTargetType.Shift => $"/Shifts/Details/{r.TargetId}",
|
||||
ReportTargetType.Job => $"/Jobs/Details/{r.TargetId}",
|
||||
ReportTargetType.Facility => "/Admin/Facilities",
|
||||
_ => "/Admin/Users",
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,14 @@
|
||||
</div>
|
||||
|
||||
<div class="container section" style="max-width:680px;">
|
||||
@if (Model.SmsTest is not null) { <div class="alert alert-success">@Model.SmsTest</div> }
|
||||
<form method="post" class="card card-pad" style="margin-bottom:14px; display:flex; gap:8px; align-items:end; flex-wrap:wrap;">
|
||||
<div class="filter-group" style="margin:0; flex:1; min-width:160px;">
|
||||
<label>ارسال پیامک آزمایشی به</label>
|
||||
<input type="tel" name="TestPhone" dir="ltr" placeholder="۰۹۱۲ ..." />
|
||||
</div>
|
||||
<button type="submit" asp-page-handler="TestSms" class="btn btn-outline">ارسال آزمایشی</button>
|
||||
</form>
|
||||
@if (Model.Saved is not null)
|
||||
{
|
||||
<div class="alert alert-success">✓ @Model.Saved</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using JobsMedical.Web.Services.Scraping;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -10,7 +11,12 @@ namespace JobsMedical.Web.Pages.Admin;
|
||||
public class SettingsModel : PageModel
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
public SettingsModel(SettingsService settings) => _settings = settings;
|
||||
private readonly ISmsSender _sms;
|
||||
public SettingsModel(SettingsService settings, ISmsSender sms)
|
||||
{
|
||||
_settings = settings;
|
||||
_sms = sms;
|
||||
}
|
||||
|
||||
[BindProperty] public IngestionMode Mode { get; set; }
|
||||
[BindProperty] public int AutoPublishMinConfidence { get; set; }
|
||||
@@ -41,7 +47,9 @@ public class SettingsModel : PageModel
|
||||
[BindProperty] public string? VapidPublicKey { get; set; }
|
||||
[BindProperty] public string? VapidPrivateKey { get; set; }
|
||||
[BindProperty] public string? VapidSubject { get; set; }
|
||||
[BindProperty] public string? TestPhone { get; set; }
|
||||
[TempData] public string? Saved { get; set; }
|
||||
[TempData] public string? SmsTest { get; set; }
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
@@ -112,4 +120,19 @@ public class SettingsModel : PageModel
|
||||
Saved = "تنظیمات ذخیره شد.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostTestSmsAsync()
|
||||
{
|
||||
var s = await _settings.GetAsync();
|
||||
var phone = OtpService.Normalize(TestPhone ?? "");
|
||||
if (phone.Length < 10) { SmsTest = "شماره معتبر وارد کنید."; return RedirectToPage(); }
|
||||
if (!s.SmsEnabled) { SmsTest = "ابتدا SMS را فعال و ذخیره کنید."; return RedirectToPage(); }
|
||||
try
|
||||
{
|
||||
var ok = await _sms.SendOtpAsync(phone, Random.Shared.Next(10000, 100000).ToString(), s);
|
||||
SmsTest = ok ? $"پیامک آزمایشی به {phone} ارسال شد." : "ارسال ناموفق بود (پاسخ منفی از سرویس).";
|
||||
}
|
||||
catch (Exception ex) { SmsTest = "خطا در ارسال: " + ex.Message; }
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Admin.UsersModel
|
||||
@{
|
||||
ViewData["Title"] = "مدیریت کاربران";
|
||||
string RoleLabel(UserRole r) => r switch { UserRole.Admin => "مدیر", UserRole.FacilityAdmin => "کارفرما", _ => "کادر درمان" };
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>مدیریت کاربران</h1>
|
||||
<p class="muted">
|
||||
<a asp-page="/Admin/Index">صف آگهیها</a> ·
|
||||
<a asp-page="/Admin/Overview">داشبورد</a> ·
|
||||
<a asp-page="/Admin/Reports">گزارشها</a> ·
|
||||
<a asp-page="/Admin/Broadcast">ارسال اعلان</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
@if (TempData["err"] is string e) { <div class="alert" style="background:#fdeaea; color:var(--danger);">@e</div> }
|
||||
|
||||
<form method="get" class="card card-pad" style="display:flex; gap:8px; align-items:end; flex-wrap:wrap; margin-bottom:14px;">
|
||||
<div class="filter-group" style="margin:0; flex:1; min-width:160px;">
|
||||
<label>جستجو (شماره/نام)</label>
|
||||
<input type="text" name="Q" value="@Model.Q" dir="ltr" />
|
||||
</div>
|
||||
<div class="filter-group" style="margin:0;">
|
||||
<label>نقش</label>
|
||||
<select name="RoleFilter">
|
||||
<option value="">همه</option>
|
||||
<option value="0" selected="@(Model.RoleFilter == UserRole.Doctor)">کادر درمان</option>
|
||||
<option value="2" selected="@(Model.RoleFilter == UserRole.FacilityAdmin)">کارفرما</option>
|
||||
<option value="1" selected="@(Model.RoleFilter == UserRole.Admin)">مدیر</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline">فیلتر</button>
|
||||
</form>
|
||||
|
||||
@foreach (var row in Model.Users)
|
||||
{
|
||||
var u = row.User;
|
||||
<div class="card card-pad" style="margin-bottom:10px; display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap;">
|
||||
<div>
|
||||
<strong dir="ltr">@JalaliDate.ToPersianDigits(u.Phone)</strong>
|
||||
@if (!string.IsNullOrEmpty(u.FullName)) { <text> — @u.FullName</text> }
|
||||
<span class="badge badge-type">@RoleLabel(u.Role)</span>
|
||||
@if (row.Facilities > 0) { <span class="badge badge-job">@JalaliDate.ToPersianDigits(row.Facilities.ToString()) مرکز</span> }
|
||||
@if (u.IsBanned) { <span class="badge" style="background:#fdeaea;color:var(--danger);">مسدود</span> }
|
||||
<div class="muted" style="font-size:12px;">عضویت: @JalaliDate.ToLongDate(DateOnly.FromDateTime(u.CreatedAt))@(u.IsBanned && u.BanReason != null ? " — دلیل مسدودی: " + u.BanReason : "")</div>
|
||||
</div>
|
||||
@if (u.Role != UserRole.Admin)
|
||||
{
|
||||
@if (u.IsBanned)
|
||||
{
|
||||
<form method="post"><button asp-page-handler="Unban" asp-route-id="@u.Id" class="btn btn-outline">رفع مسدودی</button></form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" style="display:flex; gap:6px;" onsubmit="return confirm('این کاربر مسدود شود؟');">
|
||||
<input type="text" name="reason" placeholder="دلیل (اختیاری)" style="width:150px;" />
|
||||
<button asp-page-handler="Ban" asp-route-id="@u.Id" class="btn btn-outline" style="color:var(--danger); border-color:var(--danger);">مسدود کردن</button>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (Model.Users.Count == 0) { <div class="card empty-state">کاربری یافت نشد.</div> }
|
||||
</div>
|
||||
@@ -0,0 +1,59 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Admin;
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class UsersModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public UsersModel(AppDbContext db) => _db = db;
|
||||
|
||||
public record Row(User User, int Facilities);
|
||||
public List<Row> Users { get; private set; } = new();
|
||||
[BindProperty(SupportsGet = true)] public string? Q { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public UserRole? RoleFilter { get; set; }
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
IQueryable<User> q = _db.Users;
|
||||
if (!string.IsNullOrWhiteSpace(Q))
|
||||
{
|
||||
var s = Q.Trim();
|
||||
q = q.Where(u => u.Phone.Contains(s) || (u.FullName != null && u.FullName.Contains(s)));
|
||||
}
|
||||
if (RoleFilter is not null) q = q.Where(u => u.Role == RoleFilter);
|
||||
|
||||
var users = await q.OrderByDescending(u => u.CreatedAt).Take(300).ToListAsync();
|
||||
var ids = users.Select(u => u.Id).ToList();
|
||||
var facCounts = await _db.Facilities.Where(f => f.OwnerUserId != null && ids.Contains(f.OwnerUserId.Value))
|
||||
.GroupBy(f => f.OwnerUserId!.Value).Select(g => new { g.Key, C = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Key, x => x.C);
|
||||
|
||||
Users = users.Select(u => new Row(u, facCounts.GetValueOrDefault(u.Id))).ToList();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostBanAsync(int id, string? reason)
|
||||
{
|
||||
var u = await _db.Users.FindAsync(id);
|
||||
if (u is null) return NotFound();
|
||||
if (u.Role == UserRole.Admin) { TempData["err"] = "نمیتوان مدیر را مسدود کرد."; return RedirectToPage(new { Q, RoleFilter }); }
|
||||
u.IsBanned = true;
|
||||
u.BanReason = string.IsNullOrWhiteSpace(reason) ? "نقض قوانین" : reason.Trim();
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage(new { Q, RoleFilter });
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostUnbanAsync(int id)
|
||||
{
|
||||
var u = await _db.Users.FindAsync(id);
|
||||
if (u is null) return NotFound();
|
||||
u.IsBanned = false; u.BanReason = null;
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage(new { Q, RoleFilter });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user