Add gender requirement (آقا/خانم/فرقی نمی‌کند) + employee (کارجو) panel
CI/CD / CI · dotnet build (push) Successful in 6m23s
CI/CD / Deploy · hamkadr (push) Failing after 6m30s

- Gender enum + GenderRequirement on Shift/JobOpening + Gender on UserPreferences (migration)
- Employer PostShift/PostJob + admin Review have a gender select; parser detects آقا/خانم/مرد/زن
- Gender badge on cards + detail; gender filter on Shifts/Jobs; gender in preferences
- Recommendations exclude listings whose gender requirement conflicts with the person's gender
- Two panels: new /Me employee (کارجو) panel (recommendations + saved + applied + prefs) alongside /Employer; nav routes by role; /Account/Profile → /Me

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 00:19:32 +03:30
parent 8f5d926d42
commit 6cfdd16c42
35 changed files with 1177 additions and 123 deletions
@@ -1,77 +1,3 @@
@page
@model JobsMedical.Web.Pages.Account.ProfileModel
@{
ViewData["Title"] = "پروفایل من";
string RoleLabel(UserRole r) => r switch
{
UserRole.Admin => "مدیر",
UserRole.FacilityAdmin => "مدیر مرکز درمانی",
_ => "کادر درمان",
};
}
<div class="page-head">
<div class="container">
<h1>پروفایل من</h1>
<p class="muted">
📱 <span dir="ltr">@JalaliDate.ToPersianDigits(Model.CurrentUser?.Phone ?? "")</span>
— @RoleLabel(Model.CurrentUser?.Role ?? UserRole.Doctor)
</p>
</div>
</div>
<div class="container section">
<div class="rec-banner">
<div>
<h2 style="margin:0 0 4px;">علاقه‌مندی‌هایت را کامل کن</h2>
<span style="opacity:.9; font-size:14px;">تا پیشنهادهای دقیق‌تری بگیری</span>
</div>
<a class="btn btn-outline" asp-page="/Preferences/Index">تنظیم علاقه‌مندی‌ها</a>
</div>
@if (User.IsInRole("FacilityAdmin") || User.IsInRole("Admin"))
{
<p><a asp-page="/Employer/Index">→ ورود به پنل کارفرما</a></p>
}
else
{
<p class="muted">مرکز درمانی هستی و می‌خواهی شیفت یا استخدام منتشر کنی؟
<a asp-page="/Employer/RegisterFacility">مرکز خود را ثبت کن</a></p>
}
<h2 style="font-size:20px;">شیفت‌های ذخیره‌شده</h2>
@if (Model.SavedShifts.Count == 0)
{
<div class="card empty-state">هنوز شیفتی ذخیره نکرده‌ای.</div>
}
else
{
<div class="grid grid-3">
@foreach (var s in Model.SavedShifts) { <partial name="_ShiftCard" model="s" /> }
</div>
}
<h2 style="font-size:20px; margin-top:32px;">شیفت‌هایی که اعلام تمایل کردی</h2>
@if (Model.AppliedShifts.Count == 0)
{
<div class="card empty-state">هنوز برای شیفتی اعلام تمایل نکرده‌ای.</div>
}
else
{
<div class="grid grid-3">
@foreach (var s in Model.AppliedShifts) { <partial name="_ShiftCard" model="s" /> }
</div>
}
<h2 style="font-size:20px; margin-top:32px;">موقعیت‌های استخدامی که اعلام تمایل کردی</h2>
@if (Model.AppliedJobs.Count == 0)
{
<div class="card empty-state">هنوز برای موقعیتی اعلام تمایل نکرده‌ای.</div>
}
else
{
<div class="grid grid-3">
@foreach (var j in Model.AppliedJobs) { <partial name="_JobCard" model="j" /> }
</div>
}
</div>
@* Redirects to /Me (employee panel). *@
@@ -1,53 +1,12 @@
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.Account;
// Profile was merged into the employee panel (/Me). Keep the old URL working.
[Authorize]
public class ProfileModel : PageModel
{
private readonly AppDbContext _db;
public ProfileModel(AppDbContext db) => _db = db;
public User? CurrentUser { get; private set; }
public List<Shift> SavedShifts { get; private set; } = new();
public List<JobOpening> AppliedJobs { get; private set; } = new();
public List<Shift> AppliedShifts { get; private set; } = new();
public async Task OnGetAsync()
{
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
CurrentUser = await _db.Users.FindAsync(userId);
// All visitor ids this account has been linked to (across devices).
var visitorIds = await _db.Visitors.Where(v => v.UserId == userId).Select(v => v.Id).ToListAsync();
var events = await _db.InterestEvents
.Where(e => visitorIds.Contains(e.VisitorId))
.OrderByDescending(e => e.CreatedAt)
.ToListAsync();
var savedShiftIds = events.Where(e => e.EventType == InterestEventType.Save && e.ShiftId != null)
.Select(e => e.ShiftId!.Value).Distinct().ToList();
var appliedShiftIds = events.Where(e => e.EventType == InterestEventType.Apply && e.ShiftId != null)
.Select(e => e.ShiftId!.Value).Distinct().ToList();
var appliedJobIds = events.Where(e => e.EventType == InterestEventType.Apply && e.JobOpeningId != null)
.Select(e => e.JobOpeningId!.Value).Distinct().ToList();
SavedShifts = await ShiftsByIds(savedShiftIds);
AppliedShifts = await ShiftsByIds(appliedShiftIds);
AppliedJobs = await _db.JobOpenings
.Include(j => j.Facility).ThenInclude(f => f.City).Include(j => j.Role)
.Where(j => appliedJobIds.Contains(j.Id)).ToListAsync();
}
private Task<List<Shift>> ShiftsByIds(List<int> ids) => _db.Shifts
.Include(s => s.Facility).ThenInclude(f => f.City)
.Include(s => s.Facility).ThenInclude(f => f.District)
.Include(s => s.Role)
.Where(s => ids.Contains(s.Id)).ToListAsync();
public IActionResult OnGet() => RedirectToPage("/Me/Index");
}