[Alerts] Customizable job alerts + Help capabilities showcase
Job alerts (هشدار شغلی): users save what they want — scope (shift/job/both), role, city, shift type, employment type, minimum pay — and get notified when an employer posts a match. New JobAlert model + AlertScope enum + DbContext (user-cascade, role set-null, IsActive index) + migration. /Me/Alerts page to create/pause/delete alerts; entry point added to the کارجو panel. NotificationService.NotifyNewShift/Job now unions preference matches with active-alert matches (deduped) so alert owners are notified on publish. Help page gains an 'امکانات همکادر' capability showcase grid (with a 'ساخت هشدار شغلی' CTA) so the page demonstrates what the app does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Me.AlertsModel
|
||||
@{
|
||||
ViewData["Title"] = "هشدارهای شغلی";
|
||||
string ScopeLabel(JobsMedical.Web.Models.AlertScope s) => s switch
|
||||
{
|
||||
JobsMedical.Web.Models.AlertScope.Shifts => "شیفت",
|
||||
JobsMedical.Web.Models.AlertScope.Jobs => "استخدام",
|
||||
_ => "شیفت و استخدام",
|
||||
};
|
||||
string ShiftLabel(JobsMedical.Web.Models.ShiftType t) => t switch
|
||||
{
|
||||
JobsMedical.Web.Models.ShiftType.Day => "صبح",
|
||||
JobsMedical.Web.Models.ShiftType.Evening => "عصر",
|
||||
JobsMedical.Web.Models.ShiftType.Night => "شب",
|
||||
_ => "آنکال",
|
||||
};
|
||||
string EmpLabel(JobsMedical.Web.Models.EmploymentType t) => t switch
|
||||
{
|
||||
JobsMedical.Web.Models.EmploymentType.FullTime => "تماموقت",
|
||||
JobsMedical.Web.Models.EmploymentType.PartTime => "پارهوقت",
|
||||
JobsMedical.Web.Models.EmploymentType.Contract => "قراردادی",
|
||||
_ => "طرح",
|
||||
};
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>🔎 هشدارهای شغلی</h1>
|
||||
<p class="muted">بگو دنبال چه فرصتی هستی؛ هر وقت کارفرمایی آگهی متناسب ثبت کرد، فوری باخبرت میکنیم. <a asp-page="/Me/Index">← پنل کارجو</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section" style="max-width:760px;">
|
||||
@if (Model.Msg is not null) { <div class="alert alert-success">@Model.Msg</div> }
|
||||
|
||||
<div class="card card-pad" style="margin-bottom:16px;">
|
||||
<h3 style="margin-top:0;">ساخت هشدار جدید</h3>
|
||||
<form method="post" asp-page-handler="Create">
|
||||
<div class="filter-group">
|
||||
<label>نوع فرصت</label>
|
||||
<select name="Scope">
|
||||
<option value="0">شیفت و استخدام (هر دو)</option>
|
||||
<option value="1">فقط شیفت</option>
|
||||
<option value="2">فقط استخدام</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<div class="filter-group" style="flex:1; min-width:160px;">
|
||||
<label>نقش</label>
|
||||
<select name="RoleId">
|
||||
<option value="">هر نقشی</option>
|
||||
@foreach (var r in Model.Roles) { <option value="@r.Id">@r.Name</option> }
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group" style="flex:1; min-width:160px;">
|
||||
<label>شهر</label>
|
||||
<select name="CityId">
|
||||
<option value="">هر شهری</option>
|
||||
@foreach (var c in Model.Cities) { <option value="@c.Id">@c.Name</option> }
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<div class="filter-group" style="flex:1; min-width:160px;">
|
||||
<label>نوع شیفت (برای شیفت)</label>
|
||||
<select name="ShiftType">
|
||||
<option value="">فرقی نمیکند</option>
|
||||
<option value="0">صبح</option>
|
||||
<option value="1">عصر</option>
|
||||
<option value="2">شب</option>
|
||||
<option value="3">آنکال</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group" style="flex:1; min-width:160px;">
|
||||
<label>نوع همکاری (برای استخدام)</label>
|
||||
<select name="EmploymentType">
|
||||
<option value="">فرقی نمیکند</option>
|
||||
<option value="0">تماموقت</option>
|
||||
<option value="1">پارهوقت</option>
|
||||
<option value="2">قراردادی</option>
|
||||
<option value="3">طرح</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>حداقل حقوق/دستمزد مورد انتظار (تومان)</label>
|
||||
<input type="number" name="MinPay" min="0" step="100000" dir="ltr" placeholder="مثلاً ۲۰۰۰۰۰۰" />
|
||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">برای شیفت: مبلغ هر شیفت؛ برای استخدام: حقوق ماهانه. خالی = بدون محدودیت.</p>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>برچسب (اختیاری)</label>
|
||||
<input type="text" name="Label" placeholder="مثلاً پرستار شب تهران" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-accent btn-block btn-lg">ساخت هشدار</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h3>هشدارهای من (@JalaliDate.ToPersianDigits(Model.Alerts.Count.ToString()))</h3>
|
||||
@if (Model.Alerts.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">هنوز هشداری نساختهای. اولین هشدار را بالا بساز تا فرصتها از دستت نروند.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var a in Model.Alerts)
|
||||
{
|
||||
<div class="card card-pad" style="margin-bottom:10px; @(a.IsActive ? "" : "opacity:.6;")">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
|
||||
<div>
|
||||
<strong>@(a.Label ?? "هشدار شغلی")</strong>
|
||||
@if (!a.IsActive) { <span class="badge badge-type">غیرفعال</span> }
|
||||
<div class="muted" style="font-size:13px; margin-top:4px;">
|
||||
<span class="badge badge-type">@ScopeLabel(a.Scope)</span>
|
||||
@if (a.Role is not null) { <span class="badge badge-type">@a.Role.Name</span> }
|
||||
@if (a.City is not null) { <span>📍 @a.City.Name</span> }
|
||||
@if (a.ShiftType is not null) { <span class="badge badge-day">@ShiftLabel(a.ShiftType.Value)</span> }
|
||||
@if (a.EmploymentType is not null) { <span class="badge badge-job">@EmpLabel(a.EmploymentType.Value)</span> }
|
||||
@if (a.MinPay is not null) { <span>حداقل @JalaliDate.ToPersianDigits(a.MinPay.Value.ToString("#,0")) تومان</span> }
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<form method="post" asp-page-handler="Toggle" asp-route-id="@a.Id" style="display:inline;">
|
||||
<button type="submit" class="btn btn-outline" style="padding:5px 12px;">@(a.IsActive ? "توقف" : "فعالسازی")</button>
|
||||
</form>
|
||||
<form method="post" asp-page-handler="Delete" asp-route-id="@a.Id" style="display:inline;" onsubmit="return confirm('این هشدار حذف شود؟');">
|
||||
<button type="submit" class="btn btn-outline" style="padding:5px 12px; color:var(--danger); border-color:var(--danger);">حذف</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
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.Me;
|
||||
|
||||
/// <summary>Job alerts (هشدار شغلی) — saved searches that notify the user on a new matching listing.</summary>
|
||||
[Authorize]
|
||||
public class AlertsModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public AlertsModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
public List<City> Cities { get; private set; } = new();
|
||||
public List<JobAlert> Alerts { get; private set; } = new();
|
||||
[TempData] public string? Msg { get; set; }
|
||||
|
||||
[BindProperty] public string? Label { get; set; }
|
||||
[BindProperty] public AlertScope Scope { get; set; } = AlertScope.Any;
|
||||
[BindProperty] public int? RoleId { get; set; }
|
||||
[BindProperty] public int? CityId { get; set; }
|
||||
[BindProperty] public ShiftType? ShiftType { get; set; }
|
||||
[BindProperty] public EmploymentType? EmploymentType { get; set; }
|
||||
[BindProperty] public long? MinPay { get; set; }
|
||||
|
||||
private int Uid => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
|
||||
public async Task OnGetAsync() => await LoadAsync();
|
||||
|
||||
public async Task<IActionResult> OnPostCreateAsync()
|
||||
{
|
||||
if (await _db.JobAlerts.CountAsync(a => a.UserId == Uid) >= 20)
|
||||
{
|
||||
Msg = "حداکثر تعداد هشدار شغلی ساخته شده است.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
_db.JobAlerts.Add(new JobAlert
|
||||
{
|
||||
UserId = Uid,
|
||||
Label = string.IsNullOrWhiteSpace(Label) ? null : Label.Trim(),
|
||||
Scope = Scope,
|
||||
RoleId = RoleId,
|
||||
CityId = CityId,
|
||||
ShiftType = Scope == AlertScope.Jobs ? null : ShiftType,
|
||||
EmploymentType = Scope == AlertScope.Shifts ? null : EmploymentType,
|
||||
MinPay = MinPay is > 0 ? MinPay : null,
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
Msg = "هشدار شغلی ساخته شد. بهمحض ثبت آگهی متناسب، باخبر میشوی.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostToggleAsync(int id)
|
||||
{
|
||||
var a = await _db.JobAlerts.FirstOrDefaultAsync(x => x.Id == id && x.UserId == Uid);
|
||||
if (a is not null) { a.IsActive = !a.IsActive; await _db.SaveChangesAsync(); }
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDeleteAsync(int id)
|
||||
{
|
||||
var a = await _db.JobAlerts.FirstOrDefaultAsync(x => x.Id == id && x.UserId == Uid);
|
||||
if (a is not null) { _db.JobAlerts.Remove(a); await _db.SaveChangesAsync(); }
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
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();
|
||||
Alerts = await _db.JobAlerts.Include(a => a.Role).Include(a => a.City)
|
||||
.Where(a => a.UserId == Uid).OrderByDescending(a => a.CreatedAt).ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,10 @@
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<a class="btn btn-outline" asp-page="/Preferences/Index">تنظیم علاقهمندیها</a>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<a class="btn btn-outline" asp-page="/Me/Alerts">🔎 هشدارهای شغلی</a>
|
||||
<a class="btn btn-outline" asp-page="/Preferences/Index">تنظیم علاقهمندیها</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Recommendations.Count > 0)
|
||||
|
||||
Reference in New Issue
Block a user