Add gender requirement (آقا/خانم/فرقی نمیکند) + employee (کارجو) panel
- 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:
@@ -0,0 +1,77 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Me.IndexModel
|
||||
@{
|
||||
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;">
|
||||
@if (Model.Prefs?.HasAny == true)
|
||||
{
|
||||
<text>بر اساس نقش، شهر، نوع شیفت و جنسیت شما</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>علاقهمندیهایت را تنظیم کن تا دقیقتر شوند</text>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<a class="btn btn-outline" asp-page="/Preferences/Index">تنظیم علاقهمندیها</a>
|
||||
</div>
|
||||
|
||||
@if (Model.Recommendations.Count > 0)
|
||||
{
|
||||
<div class="grid grid-3">
|
||||
@foreach (var rec in Model.Recommendations)
|
||||
{
|
||||
<partial name="_RecommendationCard" model="rec" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card empty-state">فعلاً پیشنهادی نیست. <a asp-page="/Shifts/Index">جستجوی شیفتها</a></div>
|
||||
}
|
||||
|
||||
<h2 style="font-size:20px; margin-top:32px;">شیفتهای ذخیرهشده</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 && Model.AppliedJobs.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" /> }
|
||||
@foreach (var j in Model.AppliedJobs) { <partial name="_JobCard" model="j" /> }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Me;
|
||||
|
||||
/// <summary>The employee / job-seeker (کارجو) panel — the staff-side counterpart to /Employer.</summary>
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly RecommendationService _recs;
|
||||
private readonly InterestService _interest;
|
||||
|
||||
public IndexModel(AppDbContext db, RecommendationService recs, InterestService interest)
|
||||
{
|
||||
_db = db;
|
||||
_recs = recs;
|
||||
_interest = interest;
|
||||
}
|
||||
|
||||
public User? CurrentUser { get; private set; }
|
||||
public UserPreferences? Prefs { get; private set; }
|
||||
public List<Recommendation> Recommendations { get; private set; } = new();
|
||||
public List<Shift> SavedShifts { get; private set; } = new();
|
||||
public List<Shift> AppliedShifts { get; private set; } = new();
|
||||
public List<JobOpening> AppliedJobs { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
CurrentUser = await _db.Users.FindAsync(userId);
|
||||
Prefs = await _interest.GetPreferencesAsync();
|
||||
Recommendations = await _recs.GetForVisitorAsync(6);
|
||||
|
||||
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)).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();
|
||||
}
|
||||
Reference in New Issue
Block a user