[Facilities] Public facility pages + ratings & reviews

New /Facilities/Details public page: verified badge, info, Neshan map + directions, the facility's open shifts & jobs, and a complaint form; facility cards on /Facilities link to it. Ratings & reviews: Review model (1-5 stars + comment, one per user/facility, unique index, migration); logged-in users rate/review on the facility page; average + count shown in the header and the review list; admins moderate (hide/delete) at /Admin/Reviews.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 07:44:25 +03:30
parent 437258294b
commit d87afb577c
11 changed files with 1802 additions and 4 deletions
@@ -0,0 +1,40 @@
@page
@model JobsMedical.Web.Pages.Admin.ReviewsModel
@{
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:820px;">
@if (Model.Msg is not null) { <div class="alert alert-success">@Model.Msg</div> }
@if (Model.Items.Count == 0)
{
<div class="card empty-state">نظری ثبت نشده است.</div>
}
else
{
foreach (var r in Model.Items)
{
<div class="card card-pad" style="margin-bottom:8px; @(r.IsApproved ? "" : "opacity:.6;")">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
<div>
<strong>@r.Facility.Name</strong>
<span style="color:#f59e0b;">@(new string('★', r.Stars))</span>
@if (!r.IsApproved) { <span class="badge badge-type">پنهان</span> }
<div class="muted" style="font-size:13px;">@(r.User.FullName ?? "کاربر") · <span dir="ltr">@JalaliDate.ToPersianDigits(r.User.Phone)</span></div>
@if (!string.IsNullOrWhiteSpace(r.Comment)) { <p style="margin:6px 0 0;">@r.Comment</p> }
</div>
<div style="display:flex; gap:6px; flex-wrap:wrap;">
<form method="post" asp-page-handler="Toggle" asp-route-id="@r.Id"><button class="btn btn-outline" style="padding:4px 12px;">@(r.IsApproved ? "پنهان‌کردن" : "نمایش")</button></form>
<form method="post" asp-page-handler="Delete" asp-route-id="@r.Id" onsubmit="return confirm('حذف شود؟');"><button class="btn btn-outline" style="padding:4px 12px; color:var(--danger); border-color:var(--danger);">حذف</button></form>
</div>
</div>
</div>
}
}
</div>
@@ -0,0 +1,38 @@
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 ReviewsModel : PageModel
{
private readonly AppDbContext _db;
public ReviewsModel(AppDbContext db) => _db = db;
public List<Review> Items { get; private set; } = new();
[TempData] public string? Msg { get; set; }
public async Task OnGetAsync()
{
Items = await _db.Reviews.Include(r => r.Facility).Include(r => r.User)
.OrderByDescending(r => r.CreatedAt).Take(200).ToListAsync();
}
public async Task<IActionResult> OnPostToggleAsync(int id)
{
var r = await _db.Reviews.FindAsync(id);
if (r is not null) { r.IsApproved = !r.IsApproved; await _db.SaveChangesAsync(); Msg = r.IsApproved ? "نمایش داده شد." : "پنهان شد."; }
return RedirectToPage();
}
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var r = await _db.Reviews.FindAsync(id);
if (r is not null) { _db.Reviews.Remove(r); await _db.SaveChangesAsync(); Msg = "حذف شد."; }
return RedirectToPage();
}
}