Add medjobs.ir scraper + employer/employee choice at signup
CI/CD / CI · dotnet build (push) Successful in 1m22s
CI/CD / Deploy · hamkadr (push) Successful in 1m37s

- MedjobsListingSource: crawls medjobs.ir sitemaps (ad_listing-sitemapN) → fetches ad pages → title+description → engine (dedupe/parse/validate/publish as SEO job pages). Configured in /Admin/Settings (enable + max ads/run).
- Login/register now asks 'کادر درمان' vs 'کارفرما/مرکز': new accounts get Doctor vs FacilityAdmin role; post-login routes to /Me, /Employer, or /Admin accordingly.
- Verified live: medjobs run fetched real ads into the review queue; employer signup → /Employer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 06:12:10 +03:30
parent d828ea9f35
commit e2e26150cb
12 changed files with 1106 additions and 3 deletions
@@ -17,11 +17,25 @@
@if (!Model.CodeSent)
{
<form method="post">
<div class="filter-group">
<label>وارد می‌شوی به‌عنوان…</label>
<div class="acct-toggle">
<label class="acct-opt">
<input type="radio" name="AccountType" value="employee" checked="@(Model.AccountType != "employer")" />
<span>کادر درمان<br /><small>دنبال شیفت/استخدام</small></span>
</label>
<label class="acct-opt">
<input type="radio" name="AccountType" value="employer" checked="@(Model.AccountType == "employer")" />
<span>کارفرما / مرکز درمانی<br /><small>انتشار شیفت/استخدام</small></span>
</label>
</div>
</div>
<div class="filter-group">
<label>شماره موبایل</label>
<input type="tel" name="Phone" value="@Model.Phone" placeholder="۰۹۱۲ ..." dir="ltr" />
</div>
<button type="submit" asp-page-handler="RequestCode" class="btn btn-accent btn-block btn-lg">دریافت کد تأیید</button>
<p class="muted" style="font-size:12px; margin-bottom:0;">انتخاب نوع حساب فقط هنگام ثبت‌نام اولیه اعمال می‌شود.</p>
</form>
}
else
@@ -35,6 +49,7 @@
}
<form method="post">
<input type="hidden" name="Phone" value="@Model.Phone" />
<input type="hidden" name="AccountType" value="@Model.AccountType" />
<div class="filter-group">
<label>کد تأیید پنج‌رقمی</label>
<input type="text" name="Code" placeholder="- - - - -" dir="ltr" inputmode="numeric" />
@@ -27,6 +27,8 @@ public class LoginModel : PageModel
[BindProperty] public string Phone { get; set; } = "";
[BindProperty] public string? Code { get; set; }
/// <summary>"employee" (کادر درمان) or "employer" (کارفرما/مرکز) — only used when creating a new account.</summary>
[BindProperty] public string AccountType { get; set; } = "employee";
public bool CodeSent { get; private set; }
public string? DevCode { get; private set; } // shown only in dev (no SMS gateway yet)
@@ -61,10 +63,18 @@ public class LoginModel : PageModel
// Find or create the user. The configured admin phone is granted the Admin role.
var user = await _db.Users.FirstOrDefaultAsync(u => u.Phone == phone);
var isAdmin = phone == OtpService.Normalize(_config["Auth:AdminPhone"] ?? "");
var isEmployer = string.Equals(AccountType, "employer", StringComparison.OrdinalIgnoreCase);
if (user is null)
{
user = new User { Phone = phone, IsPhoneVerified = true,
Role = isAdmin ? UserRole.Admin : UserRole.Doctor };
// New account: the chosen type decides the role (employer → facility panel).
user = new User
{
Phone = phone,
IsPhoneVerified = true,
Role = isAdmin ? UserRole.Admin
: isEmployer ? UserRole.FacilityAdmin
: UserRole.Doctor,
};
_db.Users.Add(user);
}
else
@@ -87,6 +97,13 @@ public class LoginModel : PageModel
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
AuthHelper.BuildPrincipal(user));
return LocalRedirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl);
if (!string.IsNullOrEmpty(returnUrl)) return LocalRedirect(returnUrl);
// Route to the right panel for the account type.
return user.Role switch
{
UserRole.Admin => RedirectToPage("/Admin/Index"),
UserRole.FacilityAdmin => RedirectToPage("/Employer/Index"),
_ => RedirectToPage("/Me/Index"),
};
}
}
@@ -105,6 +105,16 @@
</div>
</div>
<div class="filter-group">
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
<input type="checkbox" name="MedjobsEnabled" value="true" style="width:auto;" checked="@Model.MedjobsEnabled" />
مدجابز (medjobs.ir) — خواندن کامل آگهی‌ها از سایت‌مپ
</label>
<label style="margin-top:6px;">حداکثر آگهی در هر اجرا</label>
<input type="number" name="MedjobsMaxAds" min="1" max="500" value="@Model.MedjobsMaxAds" dir="ltr" />
<p class="muted" style="font-size:12px; margin:4px 0 0;">آگهی‌های تکراری به‌صورت خودکار رد می‌شوند؛ هر اجرا فقط آگهی‌های جدید را می‌آورد.</p>
</div>
<button type="submit" class="btn btn-accent btn-block btn-lg">ذخیره تنظیمات</button>
</form>
</div>
@@ -30,6 +30,8 @@ public class SettingsModel : PageModel
[BindProperty] public bool DivarEnabled { get; set; }
[BindProperty] public string? DivarCity { get; set; }
[BindProperty] public string? DivarQueries { get; set; }
[BindProperty] public bool MedjobsEnabled { get; set; }
[BindProperty] public int MedjobsMaxAds { get; set; } = 40;
[TempData] public string? Saved { get; set; }
public async Task OnGetAsync()
@@ -52,6 +54,8 @@ public class SettingsModel : PageModel
DivarEnabled = s.DivarEnabled;
DivarCity = s.DivarCity;
DivarQueries = s.DivarQueries;
MedjobsEnabled = s.MedjobsEnabled;
MedjobsMaxAds = s.MedjobsMaxAds;
}
public async Task<IActionResult> OnPostAsync()
@@ -75,6 +79,8 @@ public class SettingsModel : PageModel
DivarEnabled = DivarEnabled,
DivarCity = DivarCity,
DivarQueries = DivarQueries,
MedjobsEnabled = MedjobsEnabled,
MedjobsMaxAds = MedjobsMaxAds,
});
Saved = "تنظیمات ذخیره شد.";
return RedirectToPage();