Move recommendations to a dedicated page + consolidate preferences there
The personalized «پیشنهادهای ویژه شما» feed lived on the homepage and its settings on a separate /Preferences page. New /Recommendations page combines both — the recommendation cards plus the preference controls (role/city/shift-type/pay/gender) that drive them, so the settings sit next to their result. Saving prefs reloads the feed in place. - Homepage: recommendation section replaced with a CTA card linking to /Recommendations; the model no longer loads recommendations. - Nav: «✨ پیشنهادها» entry added. - /Preferences now redirects to /Recommendations (old links/bookmarks keep working). - Page is NoIndex (personalized to the visitor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,39 +41,17 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (Model.Recommendations.Count > 0)
|
||||
{
|
||||
<section class="section" style="padding-bottom:0;">
|
||||
<div class="container">
|
||||
@if (Model.HasPersonalization)
|
||||
{
|
||||
<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>
|
||||
}
|
||||
else
|
||||
{
|
||||
<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>
|
||||
}
|
||||
<div class="grid grid-3">
|
||||
@foreach (var rec in Model.Recommendations)
|
||||
{
|
||||
<partial name="_RecommendationCard" model="rec" />
|
||||
}
|
||||
<section class="section" style="padding-bottom:0;">
|
||||
<div class="container">
|
||||
<a asp-page="/Recommendations/Index" class="rec-banner" style="text-decoration:none; color:inherit;">
|
||||
<div>
|
||||
<h2 style="margin:0 0 4px;">✨ پیشنهادهای ویژه شما</h2>
|
||||
<span style="opacity:.9; font-size:14px;">فرصتهای متناسب با نقش، شهر و فعالیت شما — همه یکجا</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
<span class="btn btn-outline">مشاهده پیشنهادها ←</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
|
||||
@@ -9,18 +9,12 @@ namespace JobsMedical.Web.Pages;
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly RecommendationService _recs;
|
||||
private readonly InterestService _interest;
|
||||
|
||||
public IndexModel(AppDbContext db, RecommendationService recs, InterestService interest)
|
||||
public IndexModel(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
_recs = recs;
|
||||
_interest = interest;
|
||||
}
|
||||
|
||||
public List<Recommendation> Recommendations { get; private set; } = new();
|
||||
public bool HasPersonalization { get; private set; }
|
||||
public List<Shift> LatestShifts { get; private set; } = new();
|
||||
public List<JobOpening> LatestJobs { get; private set; } = new();
|
||||
public List<TalentListing> LatestTalent { get; private set; } = new();
|
||||
@@ -35,11 +29,6 @@ public class IndexModel : PageModel
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
|
||||
Recommendations = await _recs.GetForVisitorAsync(6);
|
||||
// "Personalized" = we actually used a signal (prefs or behavior), not just cold-start freshness.
|
||||
HasPersonalization = (await _interest.GetPreferencesAsync())?.HasAny == true
|
||||
|| (await _interest.RecentEventsAsync(1)).Count > 0;
|
||||
|
||||
LatestShifts = await _db.Shifts
|
||||
.Include(s => s.Facility).ThenInclude(f => f.City)
|
||||
.Include(s => s.Role)
|
||||
|
||||
@@ -29,31 +29,13 @@ public class IndexModel : PageModel
|
||||
|
||||
public bool Saved { get; private set; }
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
await LoadListsAsync();
|
||||
var prefs = await _interest.GetPreferencesAsync();
|
||||
if (prefs is not null)
|
||||
{
|
||||
RoleId = prefs.RoleId;
|
||||
CityId = prefs.CityId;
|
||||
PreferredShiftType = prefs.PreferredShiftType;
|
||||
MinPay = prefs.MinPay;
|
||||
Gender = prefs.Gender;
|
||||
}
|
||||
}
|
||||
// Preferences have moved onto the «پیشنهادهای ویژه شما» page (settings next to their result).
|
||||
// Keep this route working by redirecting any old link/bookmark there.
|
||||
public IActionResult OnGet() => RedirectToPage("/Recommendations/Index");
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await _interest.SavePreferencesAsync(RoleId, CityId, PreferredShiftType, MinPay, Gender);
|
||||
// Back to home so the personalized feed is the immediate payoff.
|
||||
TempData["prefsSaved"] = true;
|
||||
return RedirectToPage("/Index");
|
||||
}
|
||||
|
||||
private async Task LoadListsAsync()
|
||||
{
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
Cities = await _db.Cities.Where(c => c.IsActive).OrderBy(c => c.Name).ToListAsync();
|
||||
return RedirectToPage("/Recommendations/Index");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
@page "/Recommendations"
|
||||
@model JobsMedical.Web.Pages.Recommendations.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "پیشنهادهای ویژه شما";
|
||||
ViewData["Description"] = "پیشنهادهای شخصیسازیشدهٔ شیفت و استخدام برای شما در همکادر — بر اساس نقش، شهر و فعالیت شما.";
|
||||
ViewData["NoIndex"] = true; // personalized to the visitor — not an indexable page
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>✨ پیشنهادهای ویژه شما</h1>
|
||||
<p class="muted">
|
||||
@(Model.HasPersonalization
|
||||
? "بر اساس علاقهمندیها و فعالیت شما انتخاب شدهاند. علاقهمندیها را پایینتر تنظیم کن."
|
||||
: "نقش، شهر و نوع شیفت دلخواهت را تنظیم کن تا بهترین فرصتها را برایت پیدا کنیم.")
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
@if (Model.Saved)
|
||||
{
|
||||
<div class="alert alert-success">✓ علاقهمندیها ذخیره شد — پیشنهادها بهروزرسانی شدند.</div>
|
||||
}
|
||||
|
||||
@* Preferences — the settings that drive the feed, collapsed by default once personalized. *@
|
||||
<details class="card card-pad" style="margin-bottom:18px;" @(Model.HasPersonalization ? "" : "open")>
|
||||
<summary style="font-weight:800; cursor:pointer; font-size:16px;">⚙️ تنظیم علاقهمندیها</summary>
|
||||
<form method="post" style="margin-top:14px;">
|
||||
<div class="grid grid-3">
|
||||
<div class="filter-group">
|
||||
<label>نقش / رشته</label>
|
||||
<select name="RoleId">
|
||||
<option value="">مهم نیست</option>
|
||||
@foreach (var r in Model.Roles)
|
||||
{
|
||||
<option value="@r.Id" selected="@(Model.RoleId == r.Id)">@r.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>شهر</label>
|
||||
<select name="CityId">
|
||||
<option value="">مهم نیست</option>
|
||||
@foreach (var c in Model.Cities)
|
||||
{
|
||||
<option value="@c.Id" selected="@(Model.CityId == c.Id)">@c.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع شیفت ترجیحی</label>
|
||||
<select name="PreferredShiftType">
|
||||
<option value="">مهم نیست</option>
|
||||
<option value="0" selected="@(Model.PreferredShiftType == ShiftType.Day)">صبح</option>
|
||||
<option value="1" selected="@(Model.PreferredShiftType == ShiftType.Evening)">عصر</option>
|
||||
<option value="2" selected="@(Model.PreferredShiftType == ShiftType.Night)">شب</option>
|
||||
<option value="3" selected="@(Model.PreferredShiftType == ShiftType.OnCall)">آنکال</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>جنسیت شما</label>
|
||||
<select name="Gender">
|
||||
<option value="0" selected="@(Model.Gender == JobsMedical.Web.Models.Gender.Any)">نمیخواهم بگویم</option>
|
||||
<option value="1" selected="@(Model.Gender == JobsMedical.Web.Models.Gender.Male)">آقا</option>
|
||||
<option value="2" selected="@(Model.Gender == JobsMedical.Web.Models.Gender.Female)">خانم</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>حداقل حقوق مورد انتظار (تومان)</label>
|
||||
<input type="number" name="MinPay" value="@Model.MinPay" placeholder="مثلاً ۲۰۰۰۰۰۰۰" dir="ltr" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-lg" style="margin-top:6px;">ذخیره و دیدن پیشنهادها</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
@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="/Jobs/Index">استخدام</a> و <a asp-page="/Shifts/Index">شیفتها</a> ببین تا پیشنهادها شخصی شوند.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,66 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Recommendations;
|
||||
|
||||
/// <summary>
|
||||
/// Dedicated «پیشنهادهای ویژه شما» page: the personalized recommendation feed plus the preference
|
||||
/// controls that drive it (role/city/shift-type/pay/gender), in one place — moved off the homepage
|
||||
/// and consolidating the old /Preferences screen so the settings live next to their result.
|
||||
/// </summary>
|
||||
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 List<Recommendation> Recommendations { get; private set; } = new();
|
||||
public bool HasPersonalization { get; private set; }
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
public List<City> Cities { get; private set; } = new();
|
||||
|
||||
[BindProperty] public int? RoleId { get; set; }
|
||||
[BindProperty] public int? CityId { get; set; }
|
||||
[BindProperty] public ShiftType? PreferredShiftType { get; set; }
|
||||
[BindProperty] public long? MinPay { get; set; }
|
||||
[BindProperty] public Gender Gender { get; set; }
|
||||
[TempData] public bool Saved { get; set; }
|
||||
|
||||
public async Task OnGetAsync() => await LoadAsync();
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await _interest.SavePreferencesAsync(RoleId, CityId, PreferredShiftType, MinPay, Gender);
|
||||
Saved = true;
|
||||
return RedirectToPage(); // reload so the feed reflects the new preferences immediately
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
Cities = await _db.Cities.Where(c => c.IsActive).OrderBy(c => c.Name).ToListAsync();
|
||||
Recommendations = await _recs.GetForVisitorAsync(12);
|
||||
|
||||
var prefs = await _interest.GetPreferencesAsync();
|
||||
HasPersonalization = prefs?.HasAny == true || (await _interest.RecentEventsAsync(1)).Count > 0;
|
||||
if (prefs is not null)
|
||||
{
|
||||
RoleId = prefs.RoleId;
|
||||
CityId = prefs.CityId;
|
||||
PreferredShiftType = prefs.PreferredShiftType;
|
||||
MinPay = prefs.MinPay;
|
||||
Gender = prefs.Gender;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,7 @@
|
||||
<div class="nav-collapse">
|
||||
<nav class="main-nav">
|
||||
<a asp-page="/Index" class="@(path == "/" ? "active" : null)">خانه</a>
|
||||
<a asp-page="/Recommendations/Index" class="@(path.StartsWith("/Recommendations") ? "active" : null)">✨ پیشنهادها</a>
|
||||
<a href="/Shifts" data-tour="shifts" class="@(path.StartsWith("/Shifts") ? "active" : null)">شیفتها</a>
|
||||
<a href="/Jobs" data-tour="jobs" class="@(path.StartsWith("/Jobs") ? "active" : null)">استخدام</a>
|
||||
<a asp-page="/Talent/Index" class="@(path.StartsWith("/Talent") ? "active" : null)">آماده به کار</a>
|
||||
|
||||
Reference in New Issue
Block a user