[Verify+Complaints] Facility document review + facility complaints; card location line
Card: move location to its own line above the date in the shift card (job card already did). Verification workflow: employers upload documents (license/permit) on a new Employer/Verify page; uploading marks the facility Pending. Admins see pending facilities with their documents on Admin/Facilities, can download each doc, and approve (تأیید شد) or reject with a reason. Documents stored as bytea in the DB (survives deploys via the existing volume); served only to the owner or an admin via /facility-doc/{id}. Facility model gains Verification status enum + note + requested-at; IsVerified kept in sync. Complaints: registered users/visitors can file a شکایت about a facility from shift/job detail pages (targets ReportTargetType.Facility, surfaces in Admin/Reports as مرکز). Migration backfills existing verified facilities to Verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,25 +15,27 @@
|
||||
<h1>تأیید مراکز درمانی</h1>
|
||||
<p class="muted">
|
||||
<a asp-page="/Admin/Index">← صف آگهیها</a>
|
||||
· @JalaliDate.ToPersianDigits(Model.Pending.Count.ToString()) مرکز در انتظار تأیید
|
||||
· @JalaliDate.ToPersianDigits(Model.Awaiting.Count.ToString()) مرکز منتظر بررسی
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<h2 style="font-size:20px;">در انتظار تأیید</h2>
|
||||
@if (Model.Pending.Count == 0)
|
||||
@if (Model.Msg is not null) { <div class="alert alert-success">@Model.Msg</div> }
|
||||
|
||||
<h2 style="font-size:20px;">منتظر بررسی (مدارک ارسالشده)</h2>
|
||||
@if (Model.Awaiting.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">مرکزی در انتظار تأیید نیست.</div>
|
||||
<div class="card empty-state">مرکزی منتظر بررسی نیست.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var f in Model.Pending)
|
||||
foreach (var f in Model.Awaiting)
|
||||
{
|
||||
<div class="card card-pad" style="margin-bottom:10px;">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
|
||||
<div>
|
||||
<strong>@f.Name</strong> — @TypeLabel(f.Type)
|
||||
<strong>@f.Name</strong> — @TypeLabel(f.Type) <span class="badge badge-type">در حال بررسی</span>
|
||||
<div class="muted" style="font-size:13px;">
|
||||
📍 @f.City?.Name@(f.District is not null ? "، " + f.District.Name : "")
|
||||
@if (f.OwnerUser is not null) { <text> · مالک: <span dir="ltr">@JalaliDate.ToPersianDigits(f.OwnerUser.Phone)</span></text> }
|
||||
@@ -41,8 +43,32 @@
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(f.Address)) { <div class="muted" style="font-size:13px;">@f.Address</div> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin:10px 0;">
|
||||
<strong style="font-size:13px;">مدارک (@JalaliDate.ToPersianDigits(f.Documents.Count.ToString())):</strong>
|
||||
@if (f.Documents.Count == 0)
|
||||
{
|
||||
<span class="muted" style="font-size:13px;"> — مدرکی بارگذاری نشده.</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:6px;">
|
||||
@foreach (var d in f.Documents)
|
||||
{
|
||||
<a class="btn btn-outline" style="padding:6px 12px; font-size:13px;" href="/facility-doc/@d.Id" target="_blank">📎 @d.FileName</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end;">
|
||||
<form method="post">
|
||||
<button asp-page-handler="Verify" asp-route-id="@f.Id" class="btn btn-accent" style="white-space:nowrap;">✓ تأیید</button>
|
||||
<button asp-page-handler="Verify" asp-route-id="@f.Id" class="btn btn-accent" style="white-space:nowrap;">✓ تأیید شد</button>
|
||||
</form>
|
||||
<form method="post" style="display:flex; gap:6px; flex:1; min-width:220px;">
|
||||
<input type="text" name="note" placeholder="دلیل رد (اختیاری)" style="flex:1;" />
|
||||
<button asp-page-handler="Reject" asp-route-id="@f.Id" class="btn btn-outline" style="white-space:nowrap; color:var(--danger); border-color:var(--danger);">رد</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,4 +97,24 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.Others.Count > 0)
|
||||
{
|
||||
<h2 style="font-size:20px; margin-top:30px;">سایر مراکز (بدون درخواست تأیید)</h2>
|
||||
foreach (var f in Model.Others)
|
||||
{
|
||||
<div class="card card-pad" style="margin-bottom:10px;">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
|
||||
<div>
|
||||
<strong>@f.Name</strong> — @TypeLabel(f.Type)
|
||||
@if (f.Verification == JobsMedical.Web.Models.VerificationStatus.Rejected) { <span class="badge badge-gender">رد شده</span> }
|
||||
<div class="muted" style="font-size:13px;">📍 @f.City?.Name@(f.District is not null ? "، " + f.District.Name : "")</div>
|
||||
</div>
|
||||
<form method="post">
|
||||
<button asp-page-handler="Verify" asp-route-id="@f.Id" class="btn btn-accent" style="white-space:nowrap;">✓ تأیید مستقیم</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -13,20 +13,45 @@ public class FacilitiesModel : PageModel
|
||||
private readonly AppDbContext _db;
|
||||
public FacilitiesModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<Facility> Pending { get; private set; } = new();
|
||||
public List<Facility> Awaiting { get; private set; } = new(); // requested review (Pending)
|
||||
public List<Facility> Others { get; private set; } = new(); // unverified / rejected, no pending request
|
||||
public List<Facility> Verified { get; private set; } = new();
|
||||
[TempData] public string? Msg { get; set; }
|
||||
|
||||
public async Task OnGetAsync() => await LoadAsync();
|
||||
|
||||
public async Task<IActionResult> OnPostVerifyAsync(int id) => await SetVerified(id, true);
|
||||
public async Task<IActionResult> OnPostUnverifyAsync(int id) => await SetVerified(id, false);
|
||||
|
||||
private async Task<IActionResult> SetVerified(int id, bool value)
|
||||
public async Task<IActionResult> OnPostVerifyAsync(int id)
|
||||
{
|
||||
var f = await _db.Facilities.FindAsync(id);
|
||||
if (f is null) return NotFound();
|
||||
f.IsVerified = value;
|
||||
f.IsVerified = true;
|
||||
f.Verification = VerificationStatus.Verified;
|
||||
f.VerificationNote = null;
|
||||
await _db.SaveChangesAsync();
|
||||
Msg = $"«{f.Name}» تأیید شد.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostRejectAsync(int id, string? note)
|
||||
{
|
||||
var f = await _db.Facilities.FindAsync(id);
|
||||
if (f is null) return NotFound();
|
||||
f.IsVerified = false;
|
||||
f.Verification = VerificationStatus.Rejected;
|
||||
f.VerificationNote = string.IsNullOrWhiteSpace(note) ? "مدارک کافی نبود." : note.Trim();
|
||||
await _db.SaveChangesAsync();
|
||||
Msg = $"«{f.Name}» رد شد.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostUnverifyAsync(int id)
|
||||
{
|
||||
var f = await _db.Facilities.FindAsync(id);
|
||||
if (f is null) return NotFound();
|
||||
f.IsVerified = false;
|
||||
f.Verification = VerificationStatus.Unverified;
|
||||
await _db.SaveChangesAsync();
|
||||
Msg = $"تأیید «{f.Name}» لغو شد.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
@@ -34,8 +59,10 @@ public class FacilitiesModel : PageModel
|
||||
{
|
||||
var all = await _db.Facilities
|
||||
.Include(f => f.City).Include(f => f.District).Include(f => f.OwnerUser)
|
||||
.OrderBy(f => f.Name).ToListAsync();
|
||||
Pending = all.Where(f => !f.IsVerified).ToList();
|
||||
.Include(f => f.Documents)
|
||||
.OrderByDescending(f => f.VerificationRequestedAt).ThenBy(f => f.Name).ToListAsync();
|
||||
Awaiting = all.Where(f => f.Verification == VerificationStatus.Pending).ToList();
|
||||
Verified = all.Where(f => f.IsVerified).ToList();
|
||||
Others = all.Where(f => !f.IsVerified && f.Verification != VerificationStatus.Pending).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user