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
@@ -430,6 +430,43 @@ namespace JobsMedical.Web.Migrations
b.ToTable("JobOpenings");
});
modelBuilder.Entity("JobsMedical.Web.Models.Notification", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Body")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsRead")
.HasColumnType("boolean");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Url")
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("UserId", "IsRead", "CreatedAt");
b.ToTable("Notifications");
});
modelBuilder.Entity("JobsMedical.Web.Models.RawListing", b =>
{
b.Property<int>("Id")
@@ -843,6 +880,17 @@ namespace JobsMedical.Web.Migrations
b.Navigation("Role");
});
modelBuilder.Entity("JobsMedical.Web.Models.Notification", b =>
{
b.HasOne("JobsMedical.Web.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("JobsMedical.Web.Models.RawListing", b =>
{
b.HasOne("JobsMedical.Web.Models.Shift", "LinkedShift")