[Alerts] Customizable job alerts + Help capabilities showcase
CI/CD / CI · dotnet build (push) Successful in 1m8s
CI/CD / Deploy · hamkadr (push) Successful in 1m7s

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:
soroush.asadi
2026-06-04 18:17:56 +03:30
parent 42deac1261
commit 213faadf55
11 changed files with 1727 additions and 3 deletions
@@ -45,7 +45,9 @@ public class NotificationService
.FirstOrDefaultAsync(x => x.Id == shiftId);
if (s is null) return;
var users = await MatchingUserIdsAsync(s.RoleId, s.Facility.CityId, s.ShiftType);
var prefUsers = await MatchingUserIdsAsync(s.RoleId, s.Facility.CityId, s.ShiftType);
var alertUsers = await ShiftAlertUserIdsAsync(s);
var users = prefUsers.Union(alertUsers).Distinct().ToList();
var title = $"شیفت جدید: {s.Role.Name}";
var body = $"{s.Facility.Name} — {JalaliDate.WeekDayName(s.Date)} {JalaliDate.Time(s.StartTime)}";
await AddAsync(users, title, body, $"/Shifts/Details/{s.Id}");
@@ -57,7 +59,9 @@ public class NotificationService
.FirstOrDefaultAsync(x => x.Id == jobId);
if (j is null) return;
var users = await MatchingUserIdsAsync(j.RoleId, j.Facility.CityId, null);
var prefUsers = await MatchingUserIdsAsync(j.RoleId, j.Facility.CityId, null);
var alertUsers = await JobAlertUserIdsAsync(j);
var users = prefUsers.Union(alertUsers).Distinct().ToList();
await AddAsync(users, $"استخدام جدید: {j.Title}", j.Facility.Name, $"/Jobs/Details/{j.Id}");
}
@@ -75,6 +79,36 @@ public class NotificationService
return await q.Distinct().ToListAsync();
}
/// <summary>Owners of active job alerts that match this shift.</summary>
private async Task<List<int>> ShiftAlertUserIdsAsync(Shift s)
{
var cityId = s.Facility.CityId;
var districtId = s.Facility.DistrictId;
return await _db.JobAlerts.Where(a => a.IsActive
&& a.Scope != AlertScope.Jobs
&& (a.RoleId == null || a.RoleId == s.RoleId)
&& (a.CityId == null || a.CityId == cityId)
&& (a.DistrictId == null || a.DistrictId == districtId)
&& (a.ShiftType == null || a.ShiftType == s.ShiftType)
&& (a.MinPay == null || (s.PayAmount != null && s.PayAmount >= a.MinPay)))
.Select(a => a.UserId).Distinct().ToListAsync();
}
/// <summary>Owners of active job alerts that match this hiring opening.</summary>
private async Task<List<int>> JobAlertUserIdsAsync(JobOpening j)
{
var cityId = j.Facility.CityId;
var districtId = j.Facility.DistrictId;
return await _db.JobAlerts.Where(a => a.IsActive
&& a.Scope != AlertScope.Shifts
&& (a.RoleId == null || a.RoleId == j.RoleId)
&& (a.CityId == null || a.CityId == cityId)
&& (a.DistrictId == null || a.DistrictId == districtId)
&& (a.EmploymentType == null || a.EmploymentType == j.EmploymentType)
&& (a.MinPay == null || ((j.SalaryMax ?? j.SalaryMin) != null && (j.SalaryMax ?? j.SalaryMin) >= a.MinPay)))
.Select(a => a.UserId).Distinct().ToListAsync();
}
private async Task AddAsync(List<int> userIds, string title, string? body, string url)
{
if (userIds.Count == 0) return;