[Applications] Applicant pipeline: employer accept/reject + status to applicant
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:
@@ -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>
|
||||
Reference in New Issue
Block a user