Add hiring, AI parser+admin, OTP auth, employer dashboard, profit-share pay

- Hiring (استخدام) listings: JobOpening + /Jobs browse/detail + home section
- Heuristic Persian listing-parser + admin queue (/Admin) → publish shift/job
- Phone-OTP cookie auth + visitor-history linking + profile; Admin role gate
- Employer side: self-serve facility registration, dashboard, post/manage shifts & jobs, applicants list with contact
- Compensation models: fixed / hourly / profit-share (درصدی) / negotiable / choice (به انتخاب شما); SharePercent + JalaliDate.PayLabel; parser + filter

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 06:26:54 +03:30
parent 2fb86a435e
commit 563a40d1f4
30 changed files with 1761 additions and 27 deletions
@@ -0,0 +1,23 @@
using System.Security.Claims;
using JobsMedical.Web.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace JobsMedical.Web.Services;
/// <summary>Builds the cookie principal for a user. Shared by login and by role changes
/// (e.g. when a user registers a facility and becomes a FacilityAdmin mid-session).</summary>
public static class AuthHelper
{
public static ClaimsPrincipal BuildPrincipal(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.MobilePhone, user.Phone),
new(ClaimTypes.Name, user.FullName ?? user.Phone),
new(ClaimTypes.Role, user.Role.ToString()),
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
return new ClaimsPrincipal(identity);
}
}
@@ -1,4 +1,5 @@
using System.Globalization;
using JobsMedical.Web.Models;
namespace JobsMedical.Web.Services;
@@ -88,4 +89,22 @@ public static class JalaliDate
/// <summary>Format a Toman amount, e.g. "۱٬۵۰۰٬۰۰۰ تومان" or "توافقی" if null.</summary>
public static string Toman(long? amount)
=> amount is null ? "توافقی" : ToPersianDigits(amount.Value.ToString("#,0")) + " تومان";
/// <summary>
/// Human compensation label covering all models: fixed/hourly amount, profit-share %, or
/// BOTH (shown as "… یا … (به انتخاب شما)"), falling back to "توافقی". This is how Iranian
/// shifts are actually advertised — a fixed كارانه, a درصد سهم درآمد, or a choice between them.
/// </summary>
public static string PayLabel(PayType payType, long? amount, int? sharePercent)
{
var parts = new List<string>();
if (amount is not null)
parts.Add(ToPersianDigits(amount.Value.ToString("#,0")) + " تومان" + (payType == PayType.PerHour ? " (ساعتی)" : ""));
if (sharePercent is not null)
parts.Add(ToPersianDigits(sharePercent.Value.ToString()) + "٪ سهم درآمد");
if (parts.Count == 0) return "توافقی";
if (parts.Count > 1) return string.Join(" یا ", parts) + " (به انتخاب شما)";
return parts[0];
}
}
+12 -2
View File
@@ -11,6 +11,7 @@ public class ParsedListing
public ShiftType? ShiftType { get; set; }
public EmploymentType? EmploymentType { get; set; }
public long? PayAmount { get; set; } // shift pay or single salary figure
public int? SharePercent { get; set; } // profit-share % (درصدی / سهم درآمد)
public bool PayNegotiable { get; set; }
public string? CityName { get; set; }
public string? DistrictName { get; set; }
@@ -69,13 +70,22 @@ public class HeuristicListingParser : IListingParser
p.DistrictName = knownDistricts.OrderByDescending(d => d.Length)
.FirstOrDefault(d => text.Contains(Normalize(d)));
// --- Pay ---
// --- Profit share (درصدی / سهم) ---
var latinForShare = ToLatinDigits(text);
var share = Regex.Match(latinForShare, @"(\d{1,3})\s*(?:٪|%|درصد)");
if (!share.Success) share = Regex.Match(latinForShare, @"(?:٪|%)\s*(\d{1,3})");
if (share.Success && int.TryParse(share.Groups[1].Value, out var pct) && pct is > 0 and <= 100)
{ p.SharePercent = pct; p.Notes.Add($"سهم درآمد: {pct}٪"); }
else if (ContainsAny(text, "درصدی", "سهم درآمد", "شراکت", "پورسانت"))
{ p.Notes.Add("پرداخت درصدی/سهمی (درصد نامشخص)"); }
// --- Fixed pay ---
if (ContainsAny(text, "توافقی", "توافق")) { p.PayNegotiable = true; p.Notes.Add("حقوق: توافقی"); }
else
{
var amount = ExtractAmount(text);
if (amount is not null) { p.PayAmount = amount; p.Notes.Add($"حقوق تخمینی: {amount:#,0} تومان"); }
else p.Notes.Add("حقوق: تشخیص داده نشد");
else if (p.SharePercent is null) p.Notes.Add("حقوق: تشخیص داده نشد");
}
// --- Phone ---