[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:
@@ -45,13 +45,20 @@
|
||||
<div class="card card-pad">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span class="facility" style="font-weight:800; font-size:16px;">@c.Facility.Name</span>
|
||||
@if (c.Facility.IsVerified)
|
||||
@switch (c.Facility.Verification)
|
||||
{
|
||||
<span class="badge badge-verified">✓ تأیید شده</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge badge-type">در انتظار تأیید</span>
|
||||
case JobsMedical.Web.Models.VerificationStatus.Verified:
|
||||
<span class="badge badge-verified">✓ تأیید شده</span>
|
||||
break;
|
||||
case JobsMedical.Web.Models.VerificationStatus.Pending:
|
||||
<span class="badge badge-type">در حال بررسی</span>
|
||||
break;
|
||||
case JobsMedical.Web.Models.VerificationStatus.Rejected:
|
||||
<span class="badge badge-gender">رد شده</span>
|
||||
break;
|
||||
default:
|
||||
<span class="badge badge-type">تأیید نشده</span>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
<p class="muted" style="margin:8px 0;">
|
||||
@@ -61,6 +68,12 @@
|
||||
<div class="info-row"><span class="k">موقعیتهای استخدامی</span><span class="v">@JalaliDate.ToPersianDigits(c.OpenJobs.ToString())</span></div>
|
||||
<div class="info-row"><span class="k">اعلام تمایلها</span><span class="v" style="color:var(--accent)">@JalaliDate.ToPersianDigits(c.Applicants.ToString())</span></div>
|
||||
<a class="btn btn-outline btn-block" style="margin-top:12px;" asp-page="/Employer/Listings" asp-route-facilityId="@c.Facility.Id">مدیریت آگهیها و متقاضیان</a>
|
||||
@if (c.Facility.Verification != JobsMedical.Web.Models.VerificationStatus.Verified)
|
||||
{
|
||||
<a class="btn btn-outline btn-block" style="margin-top:8px;" asp-page="/Employer/Verify" asp-route-id="@c.Facility.Id">
|
||||
@(c.Facility.Verification == JobsMedical.Web.Models.VerificationStatus.Pending ? "مشاهده/افزودن مدارک تأیید" : "درخواست تأیید و بارگذاری مدارک")
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
@page "{id:int}"
|
||||
@model JobsMedical.Web.Pages.Employer.VerifyModel
|
||||
@{
|
||||
ViewData["Title"] = "تأیید مرکز درمانی";
|
||||
var v = Model.Facility.Verification;
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>تأیید مرکز: @Model.Facility.Name</h1>
|
||||
<p class="muted"><a asp-page="/Employer/Index">← بازگشت به پنل</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section" style="max-width:680px;">
|
||||
@if (Model.Msg is not null) { <div class="alert alert-success">@Model.Msg</div> }
|
||||
|
||||
<div class="card card-pad" style="margin-bottom:14px;">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<strong>وضعیت تأیید</strong>
|
||||
@switch (v)
|
||||
{
|
||||
case JobsMedical.Web.Models.VerificationStatus.Verified:
|
||||
<span class="badge badge-verified">✓ تأیید شده</span>
|
||||
break;
|
||||
case JobsMedical.Web.Models.VerificationStatus.Pending:
|
||||
<span class="badge badge-type">در حال بررسی</span>
|
||||
break;
|
||||
case JobsMedical.Web.Models.VerificationStatus.Rejected:
|
||||
<span class="badge badge-gender">رد شده</span>
|
||||
break;
|
||||
default:
|
||||
<span class="badge badge-type">تأیید نشده</span>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
@if (v == JobsMedical.Web.Models.VerificationStatus.Rejected && !string.IsNullOrWhiteSpace(Model.Facility.VerificationNote))
|
||||
{
|
||||
<p class="muted" style="margin:8px 0 0; color:var(--danger);">دلیل رد: @Model.Facility.VerificationNote — میتوانید مدارک اصلاحشده را دوباره بارگذاری کنید.</p>
|
||||
}
|
||||
else if (v == JobsMedical.Web.Models.VerificationStatus.Pending)
|
||||
{
|
||||
<p class="muted" style="margin:8px 0 0;">مدارک شما ارسال شد و توسط تیم پشتیبانی بررسی میشود.</p>
|
||||
}
|
||||
else if (v == JobsMedical.Web.Models.VerificationStatus.Verified)
|
||||
{
|
||||
<p class="muted" style="margin:8px 0 0;">این مرکز تأیید شده و نشان «✓ تأیید شده» را در آگهیها نمایش میدهد.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" style="margin-bottom:14px;">
|
||||
<h3 style="margin-top:0;">بارگذاری مدارک</h3>
|
||||
<p class="muted" style="font-size:13px; margin-top:0;">مجوز فعالیت، پروانه مطب/مرکز، یا هر سندی که واقعیبودن مرکز را نشان دهد. تصویر (JPG/PNG/WebP) یا PDF، حداکثر ۵ مگابایت برای هر فایل.</p>
|
||||
<form method="post" asp-page-handler="Upload" asp-route-id="@Model.Facility.Id" enctype="multipart/form-data">
|
||||
<div class="filter-group">
|
||||
<input type="file" name="files" multiple accept="image/jpeg,image/png,image/webp,application/pdf" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-accent btn-block btn-lg">بارگذاری و ارسال برای بررسی</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h3 style="margin-top:0;">مدارک بارگذاریشده (@JalaliDate.ToPersianDigits(Model.Docs.Count.ToString()))</h3>
|
||||
@if (Model.Docs.Count == 0)
|
||||
{
|
||||
<p class="muted">هنوز مدرکی بارگذاری نشده است.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:flex; flex-direction:column; gap:8px;">
|
||||
@foreach (var d in Model.Docs)
|
||||
{
|
||||
<div class="info-row" style="align-items:center;">
|
||||
<span class="k"><a href="/facility-doc/@d.Id" target="_blank">@d.FileName</a> <span class="muted" style="font-size:12px;">(@JalaliDate.ToPersianDigits((d.Size / 1024).ToString()) کیلوبایت)</span></span>
|
||||
<span class="v">
|
||||
<form method="post" asp-page-handler="DeleteDoc" asp-route-id="@Model.Facility.Id" asp-route-docId="@d.Id" style="display:inline;" onsubmit="return confirm('حذف این مدرک؟');">
|
||||
<button type="submit" class="btn btn-outline" style="padding:4px 12px; color:var(--danger); border-color:var(--danger);">حذف</button>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Security.Claims;
|
||||
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.Employer;
|
||||
|
||||
[Authorize]
|
||||
public class VerifyModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public VerifyModel(AppDbContext db) => _db = db;
|
||||
|
||||
public Facility Facility { get; private set; } = null!;
|
||||
public List<FacilityDocument> Docs { get; private set; } = new();
|
||||
[TempData] public string? Msg { get; set; }
|
||||
|
||||
private static readonly string[] Allowed = { "image/jpeg", "image/png", "image/webp", "application/pdf" };
|
||||
private const long MaxBytes = 5 * 1024 * 1024; // 5 MB per file
|
||||
|
||||
private int Uid => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(int id)
|
||||
{
|
||||
var f = await _db.Facilities.Include(x => x.City).Include(x => x.District)
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == Uid);
|
||||
if (f is null) return NotFound();
|
||||
Facility = f;
|
||||
Docs = await _db.FacilityDocuments.Where(d => d.FacilityId == id)
|
||||
.OrderByDescending(d => d.UploadedAt).ToListAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostUploadAsync(int id, List<IFormFile> files)
|
||||
{
|
||||
var f = await _db.Facilities.FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == Uid);
|
||||
if (f is null) return NotFound();
|
||||
|
||||
int added = 0;
|
||||
foreach (var file in files ?? new())
|
||||
{
|
||||
if (file.Length == 0) continue;
|
||||
if (file.Length > MaxBytes) { Msg = "هر فایل باید کمتر از ۵ مگابایت باشد."; continue; }
|
||||
var ct = (file.ContentType ?? "").ToLowerInvariant();
|
||||
if (!Allowed.Contains(ct)) { Msg = "فقط تصویر (JPG/PNG/WebP) یا PDF مجاز است."; continue; }
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await file.CopyToAsync(ms);
|
||||
_db.FacilityDocuments.Add(new FacilityDocument
|
||||
{
|
||||
FacilityId = id,
|
||||
FileName = Path.GetFileName(file.FileName),
|
||||
ContentType = ct,
|
||||
Size = file.Length,
|
||||
Data = ms.ToArray(),
|
||||
});
|
||||
added++;
|
||||
}
|
||||
|
||||
if (added > 0)
|
||||
{
|
||||
if (f.Verification != VerificationStatus.Verified)
|
||||
{
|
||||
f.Verification = VerificationStatus.Pending; // submitting docs = request review
|
||||
f.VerificationRequestedAt = DateTime.UtcNow;
|
||||
f.VerificationNote = null;
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
Msg = $"{added} سند بارگذاری و برای بررسی ارسال شد.";
|
||||
}
|
||||
return RedirectToPage(new { id });
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDeleteDocAsync(int id, int docId)
|
||||
{
|
||||
var f = await _db.Facilities.FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == Uid);
|
||||
if (f is null) return NotFound();
|
||||
var d = await _db.FacilityDocuments.FirstOrDefaultAsync(x => x.Id == docId && x.FacilityId == id);
|
||||
if (d is not null) { _db.FacilityDocuments.Remove(d); await _db.SaveChangesAsync(); }
|
||||
return RedirectToPage(new { id });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user