From c46e628f6a0ad7d2b15502c19ccaa90503f70f48 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sun, 7 Jun 2026 07:14:03 +0330 Subject: [PATCH] [Profile] Show applicant avatar + resume to employers; profile-completeness nudge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Pages/Employer/Listings.cshtml.cs | 9 ++-- .../Pages/Employer/_ApplicantRow.cshtml | 48 +++++++++++++------ src/JobsMedical.Web/Pages/Me/Index.cshtml | 7 +++ src/JobsMedical.Web/Pages/Me/Index.cshtml.cs | 3 ++ src/JobsMedical.Web/wwwroot/css/site.css | 14 ++++++ 5 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/JobsMedical.Web/Pages/Employer/Listings.cshtml.cs b/src/JobsMedical.Web/Pages/Employer/Listings.cshtml.cs index b25e21e..10601af 100644 --- a/src/JobsMedical.Web/Pages/Employer/Listings.cshtml.cs +++ b/src/JobsMedical.Web/Pages/Employer/Listings.cshtml.cs @@ -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 Applicants, int Guests); public record JobRow(JobOpening Job, List 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 applicants, int guests) Resolve(IEnumerable 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++; } diff --git a/src/JobsMedical.Web/Pages/Employer/_ApplicantRow.cshtml b/src/JobsMedical.Web/Pages/Employer/_ApplicantRow.cshtml index 58909c1..2a43239 100644 --- a/src/JobsMedical.Web/Pages/Employer/_ApplicantRow.cshtml +++ b/src/JobsMedical.Web/Pages/Employer/_ApplicantRow.cshtml @@ -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) : "؟"; } -
  • - @(Model.Name ?? "کاربر") — @JalaliDate.ToPersianDigits(Model.Phone) - @if (s == JobsMedical.Web.Models.ApplicationStatus.Accepted) - { - ✓ پذیرفته شد - } - else if (s == JobsMedical.Web.Models.ApplicationStatus.Rejected) - { - رد شد - } - else - { - +
  • + + @if (Model.HasAvatar) + { + + } + else + { + @initial + } + + + @(Model.Name ?? "کاربر") — @JalaliDate.ToPersianDigits(Model.Phone) + @if (Model.HasResume) + { + 📎 مشاهده رزومه + } + + + @if (s == JobsMedical.Web.Models.ApplicationStatus.Accepted) + { + ✓ پذیرفته شد + } + else if (s == JobsMedical.Web.Models.ApplicationStatus.Rejected) + { + رد شد + } + else + {
    -
    - } + } +
  • diff --git a/src/JobsMedical.Web/Pages/Me/Index.cshtml b/src/JobsMedical.Web/Pages/Me/Index.cshtml index 2d57501..dbc0a70 100644 --- a/src/JobsMedical.Web/Pages/Me/Index.cshtml +++ b/src/JobsMedical.Web/Pages/Me/Index.cshtml @@ -27,6 +27,13 @@
    + @if (Model.ProfileIncomplete) + { +
    + ✨ پروفایلت را کامل کن (نام، تصویر، رزومه) تا شانس پذیرش‌ات بیشتر شود. + تکمیل پروفایل +
    + }

    ✨ پیشنهادهای ویژه شما

    diff --git a/src/JobsMedical.Web/Pages/Me/Index.cshtml.cs b/src/JobsMedical.Web/Pages/Me/Index.cshtml.cs index f7a4c22..c110bab 100644 --- a/src/JobsMedical.Web/Pages/Me/Index.cshtml.cs +++ b/src/JobsMedical.Web/Pages/Me/Index.cshtml.cs @@ -31,11 +31,14 @@ public class IndexModel : PageModel public List AppliedJobs { get; private set; } = new(); public Dictionary ShiftAppStatus { get; private set; } = new(); public Dictionary 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); diff --git a/src/JobsMedical.Web/wwwroot/css/site.css b/src/JobsMedical.Web/wwwroot/css/site.css index 5002a2a..1b9ae6e 100644 --- a/src/JobsMedical.Web/wwwroot/css/site.css +++ b/src/JobsMedical.Web/wwwroot/css/site.css @@ -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; }