[Applications] Applicant pipeline: employer accept/reject + status to applicant
CI/CD / CI · dotnet build (push) Successful in 43s
CI/CD / Deploy · hamkadr (push) Successful in 43s

InterestEvent gains a Status (ApplicationStatus: Interested→Accepted/Rejected; migration, default Interested). Employer/Listings shows each applicant's status with پذیرفتن/رد buttons (ownership-checked handlers update the status and notify the applicant via bell/SSE/push linking to the listing). The کارجو panel (/Me) now shows a status badge (در انتظار بررسی / پذیرفته شد / رد شد) on each applied shift/job. Reusable _ApplicantRow partial for the employer list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 21:27:53 +03:30
parent 60c1997642
commit 167d263560
10 changed files with 1373 additions and 7 deletions
@@ -51,7 +51,7 @@
<ul style="margin:6px 0 0; padding-inline-start:18px; font-size:13.5px;">
@foreach (var a in row.Applicants)
{
<li>@(a.Name ?? "کاربر") — <span dir="ltr">@JalaliDate.ToPersianDigits(a.Phone)</span></li>
<partial name="_ApplicantRow" model="a" />
}
@if (row.Guests > 0)
{
@@ -102,7 +102,7 @@
<ul style="margin:6px 0 0; padding-inline-start:18px; font-size:13.5px;">
@foreach (var a in row.Applicants)
{
<li>@(a.Name ?? "کاربر") — <span dir="ltr">@JalaliDate.ToPersianDigits(a.Phone)</span></li>
<partial name="_ApplicantRow" model="a" />
}
@if (row.Guests > 0)
{
@@ -1,6 +1,7 @@
using System.Security.Claims;
using JobsMedical.Web.Data;
using JobsMedical.Web.Models;
using JobsMedical.Web.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -12,9 +13,14 @@ namespace JobsMedical.Web.Pages.Employer;
public class ListingsModel : PageModel
{
private readonly AppDbContext _db;
public ListingsModel(AppDbContext db) => _db = db;
private readonly NotificationService _notify;
public ListingsModel(AppDbContext db, NotificationService notify)
{
_db = db;
_notify = notify;
}
public record Applicant(string? Name, string Phone, DateTime When);
public record Applicant(string? Name, string Phone, DateTime When, long EventId, int UserId, ApplicationStatus Status);
public record ShiftRow(Shift Shift, List<Applicant> Applicants, int Guests);
public record JobRow(JobOpening Job, List<Applicant> Applicants, int Guests);
@@ -57,6 +63,37 @@ public class ListingsModel : PageModel
return RedirectToPage(new { FacilityId = j.FacilityId });
}
// --- Applicant decisions ---
public Task<IActionResult> OnPostAcceptAsync(long eventId) => SetStatus(eventId, ApplicationStatus.Accepted, true);
public Task<IActionResult> OnPostRejectAsync(long eventId) => SetStatus(eventId, ApplicationStatus.Rejected, false);
private async Task<IActionResult> SetStatus(long eventId, ApplicationStatus status, bool accepted)
{
var ev = await _db.InterestEvents
.Include(e => e.Shift).ThenInclude(s => s!.Facility)
.Include(e => e.Shift).ThenInclude(s => s!.Role)
.Include(e => e.JobOpening).ThenInclude(j => j!.Facility)
.FirstOrDefaultAsync(e => e.Id == eventId && e.EventType == InterestEventType.Apply);
if (ev is null) return NotFound();
var facilityId = ev.Shift?.FacilityId ?? ev.JobOpening?.FacilityId ?? 0;
if (!await OwnsAsync(facilityId)) return Forbid();
ev.Status = status;
await _db.SaveChangesAsync();
// Notify the applicant (only when they're a registered user we can reach).
var applicantId = await _db.Visitors.Where(v => v.Id == ev.VisitorId).Select(v => v.UserId).FirstOrDefaultAsync();
if (applicantId is int uid)
{
var (title, url) = ev.JobOpening is not null
? (ev.JobOpening.Title, $"/Jobs/Details/{ev.JobOpeningId}")
: ($"شیفت {ev.Shift?.Role?.Name} — {ev.Shift?.Facility?.Name}", $"/Shifts/Details/{ev.ShiftId}");
await _notify.NotifyApplicantStatusAsync(uid, title, accepted, url);
}
return RedirectToPage(new { FacilityId = facilityId });
}
private async Task<bool> OwnsAsync(int facilityId)
{
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
@@ -99,7 +136,7 @@ public class ListingsModel : PageModel
var uid = visitorUser.GetValueOrDefault(e.VisitorId);
if (uid is int id && users.TryGetValue(id, out var u))
{
if (seen.Add(id)) applicants.Add(new Applicant(u.FullName, u.Phone, e.CreatedAt));
if (seen.Add(id)) applicants.Add(new Applicant(u.FullName, u.Phone, e.CreatedAt, e.Id, id, e.Status));
}
else guests++;
}
@@ -0,0 +1,26 @@
@model JobsMedical.Web.Pages.Employer.ListingsModel.Applicant
@{
var s = Model.Status;
}
<li style="margin-bottom:8px;">
<span>@(Model.Name ?? "کاربر") — <span dir="ltr">@JalaliDate.ToPersianDigits(Model.Phone)</span></span>
@if (s == JobsMedical.Web.Models.ApplicationStatus.Accepted)
{
<span class="badge badge-verified">✓ پذیرفته شد</span>
}
else if (s == JobsMedical.Web.Models.ApplicationStatus.Rejected)
{
<span class="badge badge-gender">رد شد</span>
}
else
{
<span style="display:inline-flex; gap:6px; margin-inline-start:8px; vertical-align:middle;">
<form method="post" asp-page-handler="Accept" asp-route-eventId="@Model.EventId" style="display:inline;">
<button type="submit" class="btn btn-accent" style="padding:3px 12px; font-size:12px;">پذیرفتن</button>
</form>
<form method="post" asp-page-handler="Reject" asp-route-eventId="@Model.EventId" style="display:inline;">
<button type="submit" class="btn btn-outline" style="padding:3px 12px; font-size:12px; color:var(--danger); border-color:var(--danger);">رد</button>
</form>
</span>
}
</li>