Initial commit — Hamkadr (همکادر) healthcare-staffing marketplace

ASP.NET Core 10 Razor Pages + PostgreSQL/EF Core. RTL Persian, Jalali dates, self-hosted Vazirmatn, teal/coral brand.

Features:
- Shift listings: browse/filter (city, district, role, type, pay), weekly Jalali calendar, detail + interest handoff, near-me distance sort
- Hiring (استخدام) listings with employment type + salary range
- Pattern-engine recommendations + anonymous interest tracking (visitor cookie)
- Heuristic Persian listing-parser + admin queue (raw channel post → shift/job)
- Phone-OTP cookie auth + visitor-history linking + profile

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 01:43:55 +03:30
commit 2fb86a435e
150 changed files with 90993 additions and 0 deletions
@@ -0,0 +1,56 @@
@page
@model JobsMedical.Web.Pages.Preferences.IndexModel
@{
ViewData["Title"] = "علاقه‌مندی‌های شما";
}
<div class="page-head">
<div class="container">
<h1>علاقه‌مندی‌های شما</h1>
<p class="muted">بگو دنبال چه فرصتی هستی تا «همکادر» بهترین شیفت‌ها و موقعیت‌ها را برایت پیشنهاد دهد.</p>
</div>
</div>
<div class="container section" style="max-width:560px;">
<form method="post" class="card card-pad">
<div class="filter-group">
<label>نقش / رشته</label>
<select name="RoleId">
<option value="">مهم نیست</option>
@foreach (var r in Model.Roles)
{
<option value="@r.Id" selected="@(Model.RoleId == r.Id)">@r.Name</option>
}
</select>
</div>
<div class="filter-group">
<label>شهر</label>
<select name="CityId">
<option value="">مهم نیست</option>
@foreach (var c in Model.Cities)
{
<option value="@c.Id" selected="@(Model.CityId == c.Id)">@c.Name</option>
}
</select>
</div>
<div class="filter-group">
<label>نوع شیفت ترجیحی</label>
<select name="PreferredShiftType">
<option value="">مهم نیست</option>
<option value="0" selected="@(Model.PreferredShiftType == ShiftType.Day)">صبح</option>
<option value="1" selected="@(Model.PreferredShiftType == ShiftType.Evening)">عصر</option>
<option value="2" selected="@(Model.PreferredShiftType == ShiftType.Night)">شب</option>
<option value="3" selected="@(Model.PreferredShiftType == ShiftType.OnCall)">آنکال</option>
</select>
</div>
<div class="filter-group">
<label>حداقل حقوق مورد انتظار (تومان)</label>
<input type="number" name="MinPay" value="@Model.MinPay" placeholder="مثلاً ۲۰۰۰۰۰۰" dir="ltr" />
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg">ذخیره و دیدن پیشنهادها</button>
</form>
</div>
@@ -0,0 +1,57 @@
using JobsMedical.Web.Data;
using JobsMedical.Web.Models;
using JobsMedical.Web.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace JobsMedical.Web.Pages.Preferences;
public class IndexModel : PageModel
{
private readonly AppDbContext _db;
private readonly InterestService _interest;
public IndexModel(AppDbContext db, InterestService interest)
{
_db = db;
_interest = interest;
}
public List<Role> Roles { get; private set; } = new();
public List<City> Cities { get; private set; } = new();
[BindProperty] public int? RoleId { get; set; }
[BindProperty] public int? CityId { get; set; }
[BindProperty] public ShiftType? PreferredShiftType { get; set; }
[BindProperty] public long? MinPay { get; set; }
public bool Saved { get; private set; }
public async Task OnGetAsync()
{
await LoadListsAsync();
var prefs = await _interest.GetPreferencesAsync();
if (prefs is not null)
{
RoleId = prefs.RoleId;
CityId = prefs.CityId;
PreferredShiftType = prefs.PreferredShiftType;
MinPay = prefs.MinPay;
}
}
public async Task<IActionResult> OnPostAsync()
{
await _interest.SavePreferencesAsync(RoleId, CityId, PreferredShiftType, MinPay);
// Back to home so the personalized feed is the immediate payoff.
TempData["prefsSaved"] = true;
return RedirectToPage("/Index");
}
private async Task LoadListsAsync()
{
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
Cities = await _db.Cities.Where(c => c.IsActive).OrderBy(c => c.Name).ToListAsync();
}
}