[Profile] Show applicant avatar + resume to employers; profile-completeness nudge
Employer Listings: each applicant row now shows their avatar (or initials) and a «مشاهده رزومه» link when they uploaded one (served via /resume/{id}, already access-controlled to the receiving employer). Applicant projection avoids loading avatar/resume blobs. Me panel: a nudge banner prompts users to complete their profile (name/photo/resume) when any is missing, linking to /Me/Profile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,8 @@ public class ListingsModel : PageModel
|
||||
_notify = notify;
|
||||
}
|
||||
|
||||
public record Applicant(string? Name, string Phone, DateTime When, long EventId, int UserId, ApplicationStatus Status);
|
||||
public record Applicant(string? Name, string Phone, DateTime When, long EventId, int UserId,
|
||||
ApplicationStatus Status, bool HasAvatar, bool HasResume);
|
||||
public record ShiftRow(Shift Shift, List<Applicant> Applicants, int Guests);
|
||||
public record JobRow(JobOpening Job, List<Applicant> Applicants, int Guests);
|
||||
|
||||
@@ -124,7 +125,9 @@ public class ListingsModel : PageModel
|
||||
var visitorUser = await _db.Visitors.Where(v => visitorIds.Contains(v.Id))
|
||||
.ToDictionaryAsync(v => v.Id, v => v.UserId);
|
||||
var userIds = visitorUser.Values.Where(u => u != null).Select(u => u!.Value).Distinct().ToList();
|
||||
var users = await _db.Users.Where(u => userIds.Contains(u.Id)).ToDictionaryAsync(u => u.Id);
|
||||
var users = await _db.Users.Where(u => userIds.Contains(u.Id))
|
||||
.Select(u => new { u.Id, u.FullName, u.Phone, HasAvatar = u.Avatar != null, HasResume = u.Resume != null })
|
||||
.ToDictionaryAsync(u => u.Id);
|
||||
|
||||
(List<Applicant> applicants, int guests) Resolve(IEnumerable<InterestEvent> evs)
|
||||
{
|
||||
@@ -136,7 +139,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, e.Id, id, e.Status));
|
||||
if (seen.Add(id)) applicants.Add(new Applicant(u.FullName, u.Phone, e.CreatedAt, e.Id, id, e.Status, u.HasAvatar, u.HasResume));
|
||||
}
|
||||
else guests++;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
@model JobsMedical.Web.Pages.Employer.ListingsModel.Applicant
|
||||
@{
|
||||
var s = Model.Status;
|
||||
var nm = (Model.Name ?? Model.Phone).Trim();
|
||||
var initial = nm.Length > 0 ? nm.Substring(0, 1) : "؟";
|
||||
}
|
||||
<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;">
|
||||
<li class="applicant-row">
|
||||
<span class="avatar-sm">
|
||||
@if (Model.HasAvatar)
|
||||
{
|
||||
<img src="/avatar/@Model.UserId" alt="" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="avatar-fallback">@initial</span>
|
||||
}
|
||||
</span>
|
||||
<span class="applicant-info">
|
||||
<span>@(Model.Name ?? "کاربر") — <span dir="ltr">@JalaliDate.ToPersianDigits(Model.Phone)</span></span>
|
||||
@if (Model.HasResume)
|
||||
{
|
||||
<a href="/resume/@Model.UserId" target="_blank" class="resume-link">📎 مشاهده رزومه</a>
|
||||
}
|
||||
</span>
|
||||
<span class="applicant-actions">
|
||||
@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
|
||||
{
|
||||
<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>
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
@if (Model.ProfileIncomplete)
|
||||
{
|
||||
<div class="profile-nudge">
|
||||
<span class="pn-text">✨ پروفایلت را کامل کن (نام، تصویر، رزومه) تا شانس پذیرشات بیشتر شود.</span>
|
||||
<a class="btn btn-accent btn-sm" asp-page="/Me/Profile">تکمیل پروفایل</a>
|
||||
</div>
|
||||
}
|
||||
<div class="rec-banner">
|
||||
<div>
|
||||
<h2 style="margin:0 0 4px;">✨ پیشنهادهای ویژه شما</h2>
|
||||
|
||||
@@ -31,11 +31,14 @@ public class IndexModel : PageModel
|
||||
public List<JobOpening> AppliedJobs { get; private set; } = new();
|
||||
public Dictionary<int, ApplicationStatus> ShiftAppStatus { get; private set; } = new();
|
||||
public Dictionary<int, ApplicationStatus> JobAppStatus { get; private set; } = new();
|
||||
public bool ProfileIncomplete { get; private set; }
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
CurrentUser = await _db.Users.FindAsync(userId);
|
||||
ProfileIncomplete = CurrentUser is not null &&
|
||||
(string.IsNullOrWhiteSpace(CurrentUser.FullName) || CurrentUser.Avatar is null || CurrentUser.Resume is null);
|
||||
Prefs = await _interest.GetPreferencesAsync();
|
||||
Recommendations = await _recs.GetForVisitorAsync(6);
|
||||
|
||||
|
||||
@@ -111,6 +111,20 @@ a { color: inherit; text-decoration: none; }
|
||||
.pd-sep { height: 1px; background: var(--line); margin: 4px 0; }
|
||||
.pd-logout { color: var(--danger); }
|
||||
|
||||
/* Applicant rows (employer listings) */
|
||||
.applicant-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; list-style: none; }
|
||||
.avatar-sm { flex: 0 0 auto; }
|
||||
.avatar-sm img, .avatar-sm .avatar-fallback { width: 30px; height: 30px; border-radius: 50%; object-fit: cover; display: grid; place-items: center; }
|
||||
.avatar-sm .avatar-fallback { background: var(--primary); color: #fff; font-weight: 800; font-size: 13px; }
|
||||
.applicant-info { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
|
||||
.resume-link { font-size: 12px; color: var(--primary); font-weight: 600; }
|
||||
.applicant-actions { display: inline-flex; gap: 6px; align-items: center; flex: 0 0 auto; }
|
||||
|
||||
/* Profile-completeness nudge */
|
||||
.profile-nudge { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
|
||||
background: #fff7ed; border: 1px solid var(--accent); border-radius: 12px; padding: 12px 16px; margin-bottom: 16px; }
|
||||
.profile-nudge .pn-text { font-weight: 600; font-size: 14px; }
|
||||
|
||||
/* Large avatar on the profile editor page */
|
||||
.avatar-lg { width: 84px; height: 84px; border-radius: 50%; overflow: hidden; flex: 0 0 auto;
|
||||
background: var(--primary-soft); display: grid; place-items: center; }
|
||||
|
||||
Reference in New Issue
Block a user