-
- @JalaliDate.Toman(s.PayAmount)
+
+ @JalaliDate.PayLabel(s.PayType, s.PayAmount, s.SharePercent)
-
@(s.PayType == PayType.Negotiable ? "توافقی با مرکز درمانی" : "برای هر شیفت")
+ @if (s.PayAmount is not null && s.SharePercent is not null)
+ {
+
میتوانی هنگام هماهنگی، یکی از دو حالت را با مرکز انتخاب کنی.
+ }
@if (Model.Saved)
{
✓ این فرصت ذخیره شد و در پیشنهادهای شما لحاظ میشود.
diff --git a/src/JobsMedical.Web/Pages/Shifts/Index.cshtml b/src/JobsMedical.Web/Pages/Shifts/Index.cshtml
index c8cfbce..5c2a2be 100644
--- a/src/JobsMedical.Web/Pages/Shifts/Index.cshtml
+++ b/src/JobsMedical.Web/Pages/Shifts/Index.cshtml
@@ -95,6 +95,13 @@
فقط شیفتهای با حقوق مشخص
+
+
+
حذف فیلترها
diff --git a/src/JobsMedical.Web/Pages/Shifts/Index.cshtml.cs b/src/JobsMedical.Web/Pages/Shifts/Index.cshtml.cs
index 7dcca3a..4f29f02 100644
--- a/src/JobsMedical.Web/Pages/Shifts/Index.cshtml.cs
+++ b/src/JobsMedical.Web/Pages/Shifts/Index.cshtml.cs
@@ -18,6 +18,7 @@ public class IndexModel : PageModel
[BindProperty(SupportsGet = true)] public int? FacilityId { get; set; }
[BindProperty(SupportsGet = true)] public ShiftType? ShiftType { get; set; }
[BindProperty(SupportsGet = true)] public bool PaidOnly { get; set; }
+ [BindProperty(SupportsGet = true)] public bool ShareOnly { get; set; } // فقط شیفتهای سهم درآمد
// "Near me": the browser sends the visitor's coordinates and we sort by distance.
[BindProperty(SupportsGet = true)] public double? Lat { get; set; }
@@ -56,6 +57,7 @@ public class IndexModel : PageModel
if (FacilityId is not null) q = q.Where(s => s.FacilityId == FacilityId);
if (ShiftType is not null) q = q.Where(s => s.ShiftType == ShiftType);
if (PaidOnly) q = q.Where(s => s.PayAmount != null);
+ if (ShareOnly) q = q.Where(s => s.SharePercent != null);
var results = await q.ToListAsync();
diff --git a/src/JobsMedical.Web/Services/AuthHelper.cs b/src/JobsMedical.Web/Services/AuthHelper.cs
new file mode 100644
index 0000000..2ad2de9
--- /dev/null
+++ b/src/JobsMedical.Web/Services/AuthHelper.cs
@@ -0,0 +1,23 @@
+using System.Security.Claims;
+using JobsMedical.Web.Models;
+using Microsoft.AspNetCore.Authentication.Cookies;
+
+namespace JobsMedical.Web.Services;
+
+///
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).
+public static class AuthHelper
+{
+ public static ClaimsPrincipal BuildPrincipal(User user)
+ {
+ var claims = new List
+ {
+ 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);
+ }
+}
diff --git a/src/JobsMedical.Web/Services/JalaliDate.cs b/src/JobsMedical.Web/Services/JalaliDate.cs
index f9d60e4..de11256 100644
--- a/src/JobsMedical.Web/Services/JalaliDate.cs
+++ b/src/JobsMedical.Web/Services/JalaliDate.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using JobsMedical.Web.Models;
namespace JobsMedical.Web.Services;
@@ -88,4 +89,22 @@ public static class JalaliDate
/// Format a Toman amount, e.g. "۱٬۵۰۰٬۰۰۰ تومان" or "توافقی" if null.
public static string Toman(long? amount)
=> amount is null ? "توافقی" : ToPersianDigits(amount.Value.ToString("#,0")) + " تومان";
+
+ ///
+ /// 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.
+ ///
+ public static string PayLabel(PayType payType, long? amount, int? sharePercent)
+ {
+ var parts = new List();
+ 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];
+ }
}
diff --git a/src/JobsMedical.Web/Services/ListingParser.cs b/src/JobsMedical.Web/Services/ListingParser.cs
index 97dd8d5..49ad0b5 100644
--- a/src/JobsMedical.Web/Services/ListingParser.cs
+++ b/src/JobsMedical.Web/Services/ListingParser.cs
@@ -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 ---