2026-06-04 11:56:07 +03:30
|
|
|
|
@using System.Security.Claims
|
2026-06-04 21:49:40 +03:30
|
|
|
|
@using Microsoft.EntityFrameworkCore
|
2026-06-04 11:56:07 +03:30
|
|
|
|
@inject JobsMedical.Web.Services.NotificationService Notifications
|
2026-06-04 21:49:40 +03:30
|
|
|
|
@inject JobsMedical.Web.Data.AppDbContext Db
|
2026-06-03 01:43:55 +03:30
|
|
|
|
@{
|
|
|
|
|
|
var title = ViewData["Title"] as string;
|
2026-06-04 11:56:07 +03:30
|
|
|
|
int unreadCount = 0;
|
2026-06-04 21:49:40 +03:30
|
|
|
|
int meId = 0;
|
2026-06-08 07:33:22 +03:30
|
|
|
|
string? meFullName = null;
|
|
|
|
|
|
string? mePhone = null;
|
2026-06-04 21:49:40 +03:30
|
|
|
|
bool meHasAvatar = false;
|
|
|
|
|
|
if (User.Identity?.IsAuthenticated == true && int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out meId))
|
2026-06-04 11:56:07 +03:30
|
|
|
|
{
|
2026-06-04 21:49:40 +03:30
|
|
|
|
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();
|
2026-06-08 07:33:22 +03:30
|
|
|
|
meFullName = string.IsNullOrWhiteSpace(info?.FullName) ? null : info!.FullName!.Trim();
|
|
|
|
|
|
mePhone = info?.Phone;
|
2026-06-04 21:49:40 +03:30
|
|
|
|
meHasAvatar = info?.HasAvatar ?? false;
|
2026-06-04 11:56:07 +03:30
|
|
|
|
}
|
2026-06-08 07:33:22 +03:30
|
|
|
|
// Avatar glyph/label: prefer a real name; never show a bare phone digit like "0".
|
|
|
|
|
|
var meInitial = meFullName is not null ? meFullName.Substring(0, 1) : "👤";
|
|
|
|
|
|
var meLabel = meFullName ?? "حساب من";
|
|
|
|
|
|
|
|
|
|
|
|
// Single, role-aware dashboard entry — the full menu lives in the panel sub-nav (_PanelNav).
|
|
|
|
|
|
var dashUrl = "/Me/Index"; var dashLabel = "داشبورد من"; var dashIcon = "🗂️";
|
|
|
|
|
|
if (User.IsInRole("Admin")) { dashUrl = "/Admin/Overview"; dashLabel = "پنل مدیریت"; dashIcon = "🛠️"; }
|
|
|
|
|
|
else if (User.IsInRole("FacilityAdmin")) { dashUrl = "/Employer/Index"; dashLabel = "پنل کارفرما"; dashIcon = "🏥"; }
|
2026-06-07 08:16:30 +03:30
|
|
|
|
|
|
|
|
|
|
// --- SEO context ---
|
|
|
|
|
|
var baseUrl = $"{Context.Request.Scheme}://{Context.Request.Host}";
|
|
|
|
|
|
var path = Context.Request.Path.Value ?? "/";
|
|
|
|
|
|
var canonical = baseUrl + (path == "/" ? "" : path); // canonical ignores query string
|
|
|
|
|
|
var pageDesc = ViewData["Description"] as string
|
|
|
|
|
|
?? "همکادر؛ سامانه یافتن شیفت و موقعیت استخدامی برای کادر درمان (پزشک، پرستار، ماما و تکنسین) در بیمارستانها و کلینیکهای تهران.";
|
|
|
|
|
|
var pageTitle = title is null ? "همکادر | شیفت و استخدام کادر درمان" : title + " | همکادر";
|
|
|
|
|
|
var ogImage = ViewData["OgImage"] as string ?? baseUrl + "/icons/icon-512.png";
|
|
|
|
|
|
// Private/applicant areas must never be indexed.
|
|
|
|
|
|
string[] noindexPrefixes = { "/Admin", "/Me", "/Employer", "/Account", "/Preferences" };
|
|
|
|
|
|
var noIndex = (ViewData["NoIndex"] as bool? ?? false)
|
|
|
|
|
|
|| noindexPrefixes.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
2026-06-08 07:33:22 +03:30
|
|
|
|
|
|
|
|
|
|
// Show the centralized dashboard sub-nav on any logged-in panel page.
|
|
|
|
|
|
string[] panelPrefixes = { "/Admin", "/Me", "/Employer", "/Preferences" };
|
|
|
|
|
|
var showPanelNav = User.Identity?.IsAuthenticated == true
|
|
|
|
|
|
&& panelPrefixes.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
2026-06-03 01:43:55 +03:30
|
|
|
|
}
|
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="fa" dir="rtl">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8" />
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
2026-06-07 08:16:30 +03:30
|
|
|
|
<title>@pageTitle</title>
|
|
|
|
|
|
<meta name="description" content="@pageDesc" />
|
|
|
|
|
|
@if (noIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
<meta name="robots" content="noindex, nofollow" />
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
<link rel="canonical" href="@canonical" />
|
|
|
|
|
|
}
|
|
|
|
|
|
@* Open Graph / Twitter — rich previews when shared in Telegram/Bale/etc. *@
|
|
|
|
|
|
<meta property="og:type" content="website" />
|
|
|
|
|
|
<meta property="og:site_name" content="همکادر" />
|
|
|
|
|
|
<meta property="og:title" content="@pageTitle" />
|
|
|
|
|
|
<meta property="og:description" content="@pageDesc" />
|
|
|
|
|
|
<meta property="og:url" content="@canonical" />
|
|
|
|
|
|
<meta property="og:image" content="@ogImage" />
|
|
|
|
|
|
<meta property="og:locale" content="fa_IR" />
|
|
|
|
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
|
|
|
|
<meta name="twitter:title" content="@pageTitle" />
|
|
|
|
|
|
<meta name="twitter:description" content="@pageDesc" />
|
2026-06-03 01:43:55 +03:30
|
|
|
|
@* Preload the body-weight font so the swap from Tahoma happens fast. Vazirmatn is
|
|
|
|
|
|
self-hosted under wwwroot/fonts (@@font-face in site.css) — no external CDN. *@
|
|
|
|
|
|
<link rel="preload" href="~/fonts/Vazirmatn-Regular.woff2" as="font" type="font/woff2" crossorigin />
|
|
|
|
|
|
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
2026-06-04 11:23:13 +03:30
|
|
|
|
|
|
|
|
|
|
@* PWA: installable app (Web/Windows/Android via this manifest; iOS via apple-* tags) *@
|
|
|
|
|
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
|
|
|
|
<meta name="theme-color" content="#0e8f8a" />
|
2026-06-08 06:28:01 +03:30
|
|
|
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
|
|
|
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png" />
|
|
|
|
|
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png" />
|
2026-06-04 11:23:13 +03:30
|
|
|
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
|
|
|
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
|
|
|
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
|
|
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
|
|
|
|
<meta name="apple-mobile-web-app-title" content="همکادر" />
|
2026-06-07 08:16:30 +03:30
|
|
|
|
@await RenderSectionAsync("Head", required: false)
|
2026-06-03 01:43:55 +03:30
|
|
|
|
</head>
|
2026-06-04 15:42:16 +03:30
|
|
|
|
<body data-unread="@unreadCount" data-authed="@(User.Identity?.IsAuthenticated == true ? "1" : "0")">
|
2026-06-03 01:43:55 +03:30
|
|
|
|
<header class="site-header">
|
|
|
|
|
|
<div class="container header-inner">
|
2026-06-04 17:39:03 +03:30
|
|
|
|
<a class="brand" asp-page="/Index" data-tour="home">
|
2026-06-03 01:43:55 +03:30
|
|
|
|
<span class="brand-mark">ه</span>
|
|
|
|
|
|
<span class="brand-text">همکادر</span>
|
|
|
|
|
|
</a>
|
2026-06-04 14:07:22 +03:30
|
|
|
|
|
|
|
|
|
|
@* Always-visible bell on mobile (next to the burger) so notifications stay one tap away *@
|
|
|
|
|
|
@if (User.Identity?.IsAuthenticated == true)
|
|
|
|
|
|
{
|
2026-06-04 15:42:16 +03:30
|
|
|
|
<a class="bell-mobile js-bell" asp-page="/Me/Notifications" title="اعلانها">🔔@if (unreadCount > 0) {<span class="bell-badge">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
|
2026-06-04 14:07:22 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
<input type="checkbox" id="nav-toggle" class="nav-toggle" hidden />
|
2026-06-04 17:39:03 +03:30
|
|
|
|
<label for="nav-toggle" class="nav-burger" aria-label="باز/بستن منو" data-tour="menu">
|
2026-06-04 14:07:22 +03:30
|
|
|
|
<span></span><span></span><span></span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="nav-collapse">
|
|
|
|
|
|
<nav class="main-nav">
|
2026-06-07 21:38:08 +03:30
|
|
|
|
<a asp-page="/Index" class="@(path == "/" ? "active" : null)">خانه</a>
|
|
|
|
|
|
<a asp-page="/Shifts/Index" data-tour="shifts" class="@(path.StartsWith("/Shifts") ? "active" : null)">شیفتها</a>
|
|
|
|
|
|
<a asp-page="/Jobs/Index" data-tour="jobs" class="@(path.StartsWith("/Jobs") ? "active" : null)">استخدام</a>
|
|
|
|
|
|
<a asp-page="/Facilities/Index" class="@(path.StartsWith("/Facilities") ? "active" : null)">مراکز درمانی</a>
|
|
|
|
|
|
<a asp-page="/Calendar/Index" class="@(path.StartsWith("/Calendar") ? "active" : null)">تقویم هفتگی</a>
|
2026-06-04 14:07:22 +03:30
|
|
|
|
</nav>
|
|
|
|
|
|
<div class="header-actions">
|
2026-06-07 21:38:08 +03:30
|
|
|
|
<a class="btn btn-accent btn-sm cta-post" asp-page="/Employer/Index" data-tour="post">+ ثبت آگهی</a>
|
2026-06-04 14:07:22 +03:30
|
|
|
|
@if (User.Identity?.IsAuthenticated == true)
|
2026-06-03 01:43:55 +03:30
|
|
|
|
{
|
2026-06-04 17:39:03 +03:30
|
|
|
|
<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>
|
2026-06-04 21:49:40 +03:30
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
}
|
2026-06-07 22:12:51 +03:30
|
|
|
|
<span class="avatar-name">@meLabel</span>
|
2026-06-04 21:49:40 +03:30
|
|
|
|
<span class="avatar-caret">▾</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<nav class="profile-dropdown">
|
2026-06-08 07:33:22 +03:30
|
|
|
|
<div class="pd-id">
|
|
|
|
|
|
@if (meHasAvatar)
|
|
|
|
|
|
{
|
|
|
|
|
|
<img class="avatar-img" src="/avatar/@meId" alt="" />
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="avatar-fallback">@meInitial</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
<div class="pd-id-text">
|
|
|
|
|
|
<strong>@(meFullName ?? "کاربر همکادر")</strong>
|
|
|
|
|
|
@if (mePhone is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="muted" dir="ltr">@mePhone</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pd-sep"></div>
|
|
|
|
|
|
<a href="@dashUrl" data-tour="panel">@dashIcon @dashLabel</a>
|
2026-06-04 21:49:40 +03:30
|
|
|
|
<a asp-page="/Me/Profile">👤 ویرایش پروفایل</a>
|
|
|
|
|
|
<div class="pd-sep"></div>
|
|
|
|
|
|
<form method="post" asp-page="/Account/Logout">
|
|
|
|
|
|
<button type="submit" class="pd-logout">🚪 خروج</button>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
</div>
|
2026-06-03 01:43:55 +03:30
|
|
|
|
}
|
2026-06-04 14:07:22 +03:30
|
|
|
|
else
|
2026-06-03 06:26:54 +03:30
|
|
|
|
{
|
2026-06-04 17:39:03 +03:30
|
|
|
|
<a class="btn btn-outline btn-sm" asp-page="/Account/Login" data-tour="login">ورود</a>
|
2026-06-03 06:26:54 +03:30
|
|
|
|
}
|
2026-06-04 14:07:22 +03:30
|
|
|
|
</div>
|
2026-06-03 01:43:55 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main role="main">
|
2026-06-08 07:33:22 +03:30
|
|
|
|
@if (showPanelNav)
|
|
|
|
|
|
{
|
|
|
|
|
|
<partial name="_PanelNav" />
|
|
|
|
|
|
}
|
2026-06-03 01:43:55 +03:30
|
|
|
|
@RenderBody()
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<footer class="site-footer">
|
|
|
|
|
|
<div class="container footer-inner">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="brand-mark sm">ه</span>
|
|
|
|
|
|
<strong>همکادر</strong>
|
|
|
|
|
|
<p class="muted">سامانه واسط میان کادر درمان و مراکز درمانی برای شیفت و استخدام</p>
|
|
|
|
|
|
</div>
|
2026-06-04 11:23:13 +03:30
|
|
|
|
<div class="muted">
|
2026-06-04 16:48:10 +03:30
|
|
|
|
<div class="footer-links">
|
|
|
|
|
|
<a asp-page="/Download" style="font-weight:700;">📲 دریافت اپلیکیشن</a>
|
2026-06-04 17:39:03 +03:30
|
|
|
|
<a asp-page="/Help">راهنما</a>
|
2026-06-04 16:48:10 +03:30
|
|
|
|
<a asp-page="/Privacy">حریم خصوصی</a>
|
|
|
|
|
|
<a asp-page="/Rules">قوانین و مقررات</a>
|
|
|
|
|
|
<a asp-page="/Terms">شرایط استفاده</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
© ۱۴۰۵ همکادر — همه حقوق محفوظ است
|
2026-06-04 11:23:13 +03:30
|
|
|
|
</div>
|
2026-06-03 01:43:55 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
|
2026-06-04 15:42:16 +03:30
|
|
|
|
<div id="toast-host" class="toast-host" aria-live="polite"></div>
|
|
|
|
|
|
|
2026-06-04 11:23:13 +03:30
|
|
|
|
@* Register the PWA service worker (offline + push notifications). *@
|
|
|
|
|
|
<script>
|
|
|
|
|
|
if ('serviceWorker' in navigator) {
|
|
|
|
|
|
window.addEventListener('load', function () { navigator.serviceWorker.register('/sw.js').catch(function () {}); });
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
2026-06-04 15:42:16 +03:30
|
|
|
|
|
2026-06-04 17:39:03 +03:30
|
|
|
|
@* 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>
|
|
|
|
|
|
|
2026-06-04 21:49:40 +03:30
|
|
|
|
@* 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>
|
|
|
|
|
|
|
2026-06-04 15:42:16 +03:30
|
|
|
|
@* 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)
|
|
|
|
|
|
{
|
|
|
|
|
|
<script>
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
var faDigits = ['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'];
|
|
|
|
|
|
function toFa(s){ return String(s).replace(/[0-9]/g, function(d){ return faDigits[+d]; }); }
|
|
|
|
|
|
var count = parseInt(document.body.getAttribute('data-unread') || '0', 10) || 0;
|
|
|
|
|
|
|
|
|
|
|
|
function paintBell() {
|
|
|
|
|
|
document.querySelectorAll('.js-bell').forEach(function (bell) {
|
|
|
|
|
|
var badge = bell.querySelector('.bell-badge');
|
|
|
|
|
|
if (count > 0) {
|
|
|
|
|
|
if (!badge) { badge = document.createElement('span'); badge.className = 'bell-badge'; bell.appendChild(badge); }
|
|
|
|
|
|
badge.textContent = toFa(count > 99 ? '99+' : count);
|
|
|
|
|
|
} else if (badge) { badge.remove(); }
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toast(n) {
|
|
|
|
|
|
var host = document.getElementById('toast-host');
|
|
|
|
|
|
if (!host) return;
|
|
|
|
|
|
var el = document.createElement('a');
|
|
|
|
|
|
el.className = 'toast';
|
|
|
|
|
|
el.href = n.url || '/';
|
|
|
|
|
|
el.innerHTML = '<span class="toast-ico">🔔</span><span class="toast-body"><strong></strong><span></span></span>';
|
|
|
|
|
|
el.querySelector('strong').textContent = n.title || 'همکادر';
|
|
|
|
|
|
el.querySelector('.toast-body span').textContent = n.body || '';
|
|
|
|
|
|
host.appendChild(el);
|
|
|
|
|
|
requestAnimationFrame(function(){ el.classList.add('show'); });
|
|
|
|
|
|
setTimeout(function(){ el.classList.remove('show'); setTimeout(function(){ el.remove(); }, 300); }, 6000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function osNotify(n) {
|
|
|
|
|
|
if (!('Notification' in window) || Notification.permission !== 'granted' || !navigator.serviceWorker) return;
|
|
|
|
|
|
navigator.serviceWorker.ready.then(function (reg) {
|
|
|
|
|
|
reg.showNotification(n.title || 'همکادر', {
|
|
|
|
|
|
body: n.body || '', icon: '/icons/icon-192.png', badge: '/icons/icon-192.png',
|
|
|
|
|
|
dir: 'rtl', lang: 'fa', tag: n.url || '/', data: { url: n.url || '/' }
|
|
|
|
|
|
});
|
|
|
|
|
|
}).catch(function(){});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!('EventSource' in window)) return;
|
|
|
|
|
|
var es;
|
|
|
|
|
|
function connect() {
|
|
|
|
|
|
es = new EventSource('/notifications/stream');
|
|
|
|
|
|
es.addEventListener('notice', function (ev) {
|
|
|
|
|
|
var n; try { n = JSON.parse(ev.data); } catch (_) { return; }
|
|
|
|
|
|
count++; paintBell(); toast(n); osNotify(n);
|
|
|
|
|
|
});
|
|
|
|
|
|
// EventSource auto-reconnects on transient errors; nothing else needed.
|
|
|
|
|
|
}
|
|
|
|
|
|
connect();
|
|
|
|
|
|
})();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
}
|
2026-06-03 01:43:55 +03:30
|
|
|
|
@await RenderSectionAsync("Scripts", required: false)
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|