[Profile] Editable profile (avatar + resume) + role-based profile dropdown menu
CI/CD / CI · dotnet build (push) Successful in 44s
CI/CD / Deploy · hamkadr (push) Successful in 57s

Every user gets a full editable profile at /Me/Profile: name, role, city, specialty/title, license, years, bio + avatar image upload + resume upload (PDF/image). Avatar/resume stored in-DB on User (migration, 5 nullable columns). Endpoints: /avatar/{id} (public) and /resume/{id} (owner, admin, or an employer who received that user's application). Nav: replaced the scattered action links with an avatar button + dropdown listing all of the user's pages by role (profile, کارجو panel, alerts, preferences, notifications; employer panel; admin panel + settings; logout) — shows the avatar image or initials; collapses into the burger menu on mobile; closes on outside-click.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 21:49:40 +03:30
parent 167d263560
commit e633463906
9 changed files with 1689 additions and 15 deletions
+57 -15
View File
@@ -1,12 +1,22 @@
@using System.Security.Claims
@using Microsoft.EntityFrameworkCore
@inject JobsMedical.Web.Services.NotificationService Notifications
@inject JobsMedical.Web.Data.AppDbContext Db
@{
var title = ViewData["Title"] as string;
int unreadCount = 0;
if (User.Identity?.IsAuthenticated == true && int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var _uid))
int meId = 0;
string? meName = null;
bool meHasAvatar = false;
if (User.Identity?.IsAuthenticated == true && int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out meId))
{
unreadCount = await Notifications.UnreadCountAsync(_uid);
unreadCount = await Notifications.UnreadCountAsync(meId);
var info = await Db.Users.Where(u => u.Id == meId)
.Select(u => new { u.FullName, u.Phone, HasAvatar = u.Avatar != null }).FirstOrDefaultAsync();
meName = string.IsNullOrWhiteSpace(info?.FullName) ? info?.Phone : info!.FullName;
meHasAvatar = info?.HasAvatar ?? false;
}
var meInitial = string.IsNullOrWhiteSpace(meName) ? "؟" : meName!.Trim().Substring(0, 1);
}
<!DOCTYPE html>
<html lang="fa" dir="rtl">
@@ -62,20 +72,44 @@
<div class="header-actions">
@if (User.Identity?.IsAuthenticated == true)
{
@if (User.IsInRole("Admin"))
{
<a class="nav-action" asp-page="/Admin/Overview">پنل مدیریت</a>
<a class="nav-action" asp-page="/Admin/Settings">تنظیمات</a>
}
@if (User.IsInRole("FacilityAdmin"))
{
<a class="nav-action" asp-page="/Employer/Index">پنل کارفرما</a>
}
<a class="nav-action bell-inline js-bell" asp-page="/Me/Notifications" title="اعلان‌ها" data-tour="bell"><span class="bell-ico">🔔</span><span class="bell-label">اعلان‌ها</span>@if (unreadCount > 0) {<span class="bell-badge">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
<a class="nav-action" asp-page="/Me/Index" data-tour="panel">پنل کارجو</a>
<form method="post" asp-page="/Account/Logout" style="display:contents;">
<button type="submit" class="btn btn-outline btn-sm">خروج</button>
</form>
<div class="profile-menu">
<input type="checkbox" id="profile-toggle" class="profile-toggle" hidden />
<label for="profile-toggle" class="avatar-btn" data-tour="profile" aria-label="منوی کاربر">
@if (meHasAvatar)
{
<img class="avatar-img" src="/avatar/@meId" alt="پروفایل" />
}
else
{
<span class="avatar-fallback">@meInitial</span>
}
<span class="avatar-caret">▾</span>
</label>
<nav class="profile-dropdown">
<div class="pd-head">@meName</div>
<a asp-page="/Me/Profile">👤 ویرایش پروفایل</a>
<a asp-page="/Me/Index" data-tour="panel">🗂️ پنل کارجو</a>
<a asp-page="/Me/Alerts">🔎 هشدارهای شغلی</a>
<a asp-page="/Preferences/Index">⭐ علاقه‌مندی‌ها</a>
<a asp-page="/Me/Notifications">🔔 اعلان‌ها@if (unreadCount > 0) {<span class="bell-badge" style="position:static; margin-inline-start:6px;">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
@if (User.IsInRole("FacilityAdmin"))
{
<a asp-page="/Employer/Index">🏥 پنل کارفرما</a>
}
@if (User.IsInRole("Admin"))
{
<div class="pd-sep"></div>
<a asp-page="/Admin/Overview">🛠️ پنل مدیریت</a>
<a asp-page="/Admin/Settings">⚙️ تنظیمات</a>
}
<div class="pd-sep"></div>
<form method="post" asp-page="/Account/Logout">
<button type="submit" class="pd-logout">🚪 خروج</button>
</form>
</nav>
</div>
}
else
{
@@ -122,6 +156,14 @@
@* Self-hosted guided app tour (no CDN). Auto-runs once for new visitors; re-runnable from /Help. *@
<script src="~/js/tour.js" asp-append-version="true" defer></script>
@* Close the profile dropdown when clicking outside it. *@
<script>
document.addEventListener('click', function (e) {
var t = document.getElementById('profile-toggle');
if (t && t.checked && !e.target.closest('.profile-menu')) t.checked = false;
});
</script>
@* Live in-app notifications over SSE (our own origin — works in Iran, no Google push).
Updates the bell badge, shows a toast, and fires a local OS notification when allowed. *@
@if (User.Identity?.IsAuthenticated == true)