Notify matching users when a new shift/job is posted (in-app notifications)
CI/CD / CI · dotnet build (push) Failing after 1m40s
CI/CD / Deploy · hamkadr (push) Has been skipped

- 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:
soroush.asadi
2026-06-04 11:56:07 +03:30
parent a02eb6a985
commit 10d4727bd5
14 changed files with 1302 additions and 7 deletions
@@ -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
}
}