From 60c1997642f7eae03bcfd42f22605f6c1ad64596 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 4 Jun 2026 19:43:52 +0330 Subject: [PATCH] [Notify] Notify the employer when someone applies to their listing Applications were recorded but the facility owner was never pinged. NotificationService gains NotifyShiftApplicationAsync/NotifyJobApplicationAsync (look up the facility owner, notify via in-app bell + live SSE + push, linking to /Employer/Listings). InterestService fires them when a NEW Apply event is saved (after the duplicate guard, so no repeat pings; View/Save/Dismiss don't notify). No-op for admin-managed facilities with no owner. Co-Authored-By: Claude Opus 4.8 --- .../Services/InterestService.cs | 6 +++++- .../Services/NotificationService.cs | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/JobsMedical.Web/Services/InterestService.cs b/src/JobsMedical.Web/Services/InterestService.cs index 896abe4..2977c95 100644 --- a/src/JobsMedical.Web/Services/InterestService.cs +++ b/src/JobsMedical.Web/Services/InterestService.cs @@ -12,11 +12,13 @@ public class InterestService { private readonly AppDbContext _db; private readonly VisitorContext _visitor; + private readonly NotificationService _notify; - public InterestService(AppDbContext db, VisitorContext visitor) + public InterestService(AppDbContext db, VisitorContext visitor, NotificationService notify) { _db = db; _visitor = visitor; + _notify = notify; } public string VisitorId => _visitor.VisitorId; @@ -53,6 +55,7 @@ public class InterestService EventType = type, }); await _db.SaveChangesAsync(); + if (type == InterestEventType.Apply) await _notify.NotifyShiftApplicationAsync(shiftId); // ping the employer } public async Task LogJobAsync(InterestEventType type, int jobOpeningId) @@ -69,6 +72,7 @@ public class InterestService EventType = type, }); await _db.SaveChangesAsync(); + if (type == InterestEventType.Apply) await _notify.NotifyJobApplicationAsync(jobOpeningId); // ping the employer } public Task GetPreferencesAsync() diff --git a/src/JobsMedical.Web/Services/NotificationService.cs b/src/JobsMedical.Web/Services/NotificationService.cs index 93203ec..754a519 100644 --- a/src/JobsMedical.Web/Services/NotificationService.cs +++ b/src/JobsMedical.Web/Services/NotificationService.cs @@ -65,6 +65,27 @@ public class NotificationService await AddAsync(users, $"استخدام جدید: {j.Title}", j.Facility.Name, $"/Jobs/Details/{j.Id}"); } + /// Notify the facility owner that someone declared interest in their shift. + public async Task NotifyShiftApplicationAsync(int shiftId) + { + var s = await _db.Shifts.Include(x => x.Facility).Include(x => x.Role) + .FirstOrDefaultAsync(x => x.Id == shiftId); + if (s?.Facility?.OwnerUserId is not int owner) return; // admin-managed facility → no owner to notify + var body = $"{s.Facility.Name} — {JalaliDate.WeekDayName(s.Date)} {JalaliDate.Time(s.StartTime)}"; + await AddAsync(new List { owner }, $"متقاضی جدید برای شیفت {s.Role?.Name}", body, + $"/Employer/Listings?facilityId={s.FacilityId}"); + } + + /// Notify the facility owner that someone declared interest in their hiring opening. + public async Task NotifyJobApplicationAsync(int jobId) + { + var j = await _db.JobOpenings.Include(x => x.Facility) + .FirstOrDefaultAsync(x => x.Id == jobId); + if (j?.Facility?.OwnerUserId is not int owner) return; + await AddAsync(new List { owner }, $"متقاضی جدید: {j.Title}", j.Facility.Name, + $"/Employer/Listings?facilityId={j.FacilityId}"); + } + /// Users with a non-empty preference that matches the listing (via their visitor link). private async Task> MatchingUserIdsAsync(int roleId, int cityId, ShiftType? shiftType) {