Notify matching users when a new shift/job is posted (in-app notifications)
- Notification entity + NotificationService: on publish, notify users whose saved prefs match the listing (role/city/+shift type); users with no preference aren't spammed - Wired into PostShift, PostJob, and Admin Review publish - 🔔 bell with unread count in the header (@inject) + /Me/Notifications page (mark-all-read on open) - Reliable in-app delivery (works in Iran without FCM); Web Push can ride the same records later - Verified: employee pref → employer posts matching shift → employee bell=۱ + 'شیفت جدید: پزشک عمومی' Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,13 @@ public class ReviewModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IListingParser _parser;
|
||||
private readonly NotificationService _notify;
|
||||
|
||||
public ReviewModel(AppDbContext db, IListingParser parser)
|
||||
public ReviewModel(AppDbContext db, IListingParser parser, NotificationService notify)
|
||||
{
|
||||
_db = db;
|
||||
_parser = parser;
|
||||
_notify = notify;
|
||||
}
|
||||
|
||||
public RawListing? Raw { get; private set; }
|
||||
@@ -75,6 +77,8 @@ public class ReviewModel : PageModel
|
||||
Raw = await _db.RawListings.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (Raw is null) return NotFound();
|
||||
|
||||
Shift? createdShift = null;
|
||||
JobOpening? createdJob = null;
|
||||
if (Kind == ListingKind.Shift)
|
||||
{
|
||||
var role = await _db.Roles.FindAsync(RoleId);
|
||||
@@ -101,6 +105,7 @@ public class ReviewModel : PageModel
|
||||
await _db.SaveChangesAsync();
|
||||
Raw.Status = RawListingStatus.Normalized;
|
||||
Raw.LinkedShiftId = shift.Id;
|
||||
createdShift = shift;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -120,8 +125,11 @@ public class ReviewModel : PageModel
|
||||
};
|
||||
_db.JobOpenings.Add(job);
|
||||
Raw.Status = RawListingStatus.Normalized;
|
||||
createdJob = job;
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
if (createdShift is not null) await _notify.NotifyNewShiftAsync(createdShift.Id);
|
||||
if (createdJob is not null) await _notify.NotifyNewJobAsync(createdJob.Id);
|
||||
return RedirectToPage("/Admin/Index");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ public class PostJobModel : PageModel
|
||||
private readonly AppDbContext _db;
|
||||
private readonly CaptchaService _captcha;
|
||||
private readonly SubmissionGuard _guard;
|
||||
private readonly NotificationService _notify;
|
||||
|
||||
public PostJobModel(AppDbContext db, CaptchaService captcha, SubmissionGuard guard)
|
||||
public PostJobModel(AppDbContext db, CaptchaService captcha, SubmissionGuard guard, NotificationService notify)
|
||||
{
|
||||
_db = db;
|
||||
_captcha = captcha;
|
||||
_guard = guard;
|
||||
_notify = notify;
|
||||
}
|
||||
|
||||
public List<Facility> MyFacilities { get; private set; } = new();
|
||||
@@ -68,7 +70,7 @@ public class PostJobModel : PageModel
|
||||
if (await _guard.PostingRateExceededAsync(uid))
|
||||
{ Error = $"در یک ساعت اخیر بیش از حد مجاز ({SubmissionGuard.MaxListingsPerHour}) آگهی ثبت کردهاید. بعداً تلاش کنید."; NewCaptcha(); return Page(); }
|
||||
|
||||
_db.JobOpenings.Add(new JobOpening
|
||||
var job = new JobOpening
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
RoleId = RoleId,
|
||||
@@ -81,8 +83,10 @@ public class PostJobModel : PageModel
|
||||
Requirements = Requirements,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Direct,
|
||||
});
|
||||
};
|
||||
_db.JobOpenings.Add(job);
|
||||
await _db.SaveChangesAsync();
|
||||
await _notify.NotifyNewJobAsync(job.Id); // notify matching staff
|
||||
return RedirectToPage("/Employer/Index");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ public class PostShiftModel : PageModel
|
||||
private readonly AppDbContext _db;
|
||||
private readonly CaptchaService _captcha;
|
||||
private readonly SubmissionGuard _guard;
|
||||
private readonly NotificationService _notify;
|
||||
|
||||
public PostShiftModel(AppDbContext db, CaptchaService captcha, SubmissionGuard guard)
|
||||
public PostShiftModel(AppDbContext db, CaptchaService captcha, SubmissionGuard guard, NotificationService notify)
|
||||
{
|
||||
_db = db;
|
||||
_captcha = captcha;
|
||||
_guard = guard;
|
||||
_notify = notify;
|
||||
}
|
||||
|
||||
public List<Facility> MyFacilities { get; private set; } = new();
|
||||
@@ -72,7 +74,7 @@ public class PostShiftModel : PageModel
|
||||
{ Error = $"در یک ساعت اخیر بیش از حد مجاز ({SubmissionGuard.MaxListingsPerHour}) آگهی ثبت کردهاید. بعداً تلاش کنید."; NewCaptcha(); return Page(); }
|
||||
|
||||
var role = await _db.Roles.FindAsync(RoleId);
|
||||
_db.Shifts.Add(new Shift
|
||||
var shift = new Shift
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
RoleId = RoleId,
|
||||
@@ -89,8 +91,10 @@ public class PostShiftModel : PageModel
|
||||
GenderRequirement = GenderRequirement,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Direct, // posted directly by the facility
|
||||
});
|
||||
};
|
||||
_db.Shifts.Add(shift);
|
||||
await _db.SaveChangesAsync();
|
||||
await _notify.NotifyNewShiftAsync(shift.Id); // notify matching staff
|
||||
return RedirectToPage("/Employer/Index");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Me.NotificationsModel
|
||||
@{
|
||||
ViewData["Title"] = "اعلانها";
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>🔔 اعلانها</h1>
|
||||
<p class="muted">فرصتهای جدید متناسب با علاقهمندیهای تو.
|
||||
<a asp-page="/Preferences/Index">تنظیم علاقهمندیها</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section" style="max-width:680px;">
|
||||
@if (Model.Items.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">
|
||||
هنوز اعلانی نداری. وقتی شیفت یا استخدام متناسب با علاقهمندیهایت منتشر شود، اینجا میبینی.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var n in Model.Items)
|
||||
{
|
||||
<a class="card card-pad" href="@(n.Url ?? "#")"
|
||||
style="display:block; margin-bottom:10px; @(n.IsRead ? "" : "border-inline-start:4px solid var(--accent);")">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
|
||||
<strong>@(n.IsRead ? "" : "🟠 ")@n.Title</strong>
|
||||
<span class="muted" style="font-size:12px;">@JalaliDate.ToLongDate(DateOnly.FromDateTime(n.CreatedAt))</span>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(n.Body))
|
||||
{
|
||||
<p class="muted" style="margin:6px 0 0; font-size:13.5px;">@n.Body</p>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Me;
|
||||
|
||||
[Authorize]
|
||||
public class NotificationsModel : PageModel
|
||||
{
|
||||
private readonly NotificationService _svc;
|
||||
public NotificationsModel(NotificationService svc) => _svc = svc;
|
||||
|
||||
public List<Notification> Items { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var uid = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
Items = await _svc.ListAsync(uid); // capture read-state for display
|
||||
await _svc.MarkAllReadAsync(uid); // opening the page clears the bell
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
@using System.Security.Claims
|
||||
@inject JobsMedical.Web.Services.NotificationService Notifications
|
||||
@{
|
||||
var title = ViewData["Title"] as string;
|
||||
int unreadCount = 0;
|
||||
if (User.Identity?.IsAuthenticated == true && int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var _uid))
|
||||
{
|
||||
unreadCount = await Notifications.UnreadCountAsync(_uid);
|
||||
}
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
@@ -49,6 +56,7 @@
|
||||
{
|
||||
<a asp-page="/Employer/Index" style="margin-inline-end:14px; font-weight:600;">پنل کارفرما</a>
|
||||
}
|
||||
<a asp-page="/Me/Notifications" title="اعلانها" style="margin-inline-end:12px; position:relative; font-size:18px;">🔔@if (unreadCount > 0) {<span class="bell-badge">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
|
||||
<a asp-page="/Me/Index" style="margin-inline-end:10px; font-weight:600;">پنل کارجو</a>
|
||||
<form method="post" asp-page="/Account/Logout" style="display:inline;">
|
||||
<button type="submit" class="btn btn-outline" style="padding:7px 14px;">خروج</button>
|
||||
|
||||
Reference in New Issue
Block a user