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:
@@ -84,16 +84,8 @@ public class LoginModel : PageModel
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
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);
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(identity));
|
||||
AuthHelper.BuildPrincipal(user));
|
||||
|
||||
return LocalRedirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,16 @@
|
||||
<a class="btn btn-outline" asp-page="/Preferences/Index">تنظیم علاقهمندیها</a>
|
||||
</div>
|
||||
|
||||
@if (User.IsInRole("FacilityAdmin") || User.IsInRole("Admin"))
|
||||
{
|
||||
<p><a asp-page="/Employer/Index">→ ورود به پنل کارفرما</a></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted">مرکز درمانی هستی و میخواهی شیفت یا استخدام منتشر کنی؟
|
||||
<a asp-page="/Employer/RegisterFacility">مرکز خود را ثبت کن</a></p>
|
||||
}
|
||||
|
||||
<h2 style="font-size:20px;">شیفتهای ذخیرهشده</h2>
|
||||
@if (Model.SavedShifts.Count == 0)
|
||||
{
|
||||
|
||||
@@ -81,9 +81,9 @@
|
||||
<div style="flex:1;"><label>شروع</label><input type="time" name="StartTime" value="@Model.StartTime.ToString("HH:mm")" dir="ltr" /></div>
|
||||
<div style="flex:1;"><label>پایان</label><input type="time" name="EndTime" value="@Model.EndTime.ToString("HH:mm")" dir="ltr" /></div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>حقوق هر شیفت (تومان)</label>
|
||||
<input type="number" name="PayAmount" value="@Model.PayAmount" dir="ltr" />
|
||||
<div class="filter-group" style="display:flex; gap:8px;">
|
||||
<div style="flex:1;"><label>مبلغ مقطوع (تومان)</label><input type="number" name="PayAmount" value="@Model.PayAmount" dir="ltr" /></div>
|
||||
<div style="flex:1;"><label>یا سهم درآمد (٪)</label><input type="number" name="SharePercent" value="@Model.SharePercent" min="1" max="100" dir="ltr" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ public class ReviewModel : PageModel
|
||||
[BindProperty] public TimeOnly StartTime { get; set; }
|
||||
[BindProperty] public TimeOnly EndTime { get; set; }
|
||||
[BindProperty] public long? PayAmount { get; set; }
|
||||
[BindProperty] public int? SharePercent { get; set; }
|
||||
[BindProperty] public bool Negotiable { get; set; }
|
||||
// Job fields
|
||||
[BindProperty] public string? Title { get; set; }
|
||||
@@ -60,6 +61,7 @@ public class ReviewModel : PageModel
|
||||
(StartTime, EndTime) = DefaultTimes(ShiftType);
|
||||
ShiftDate = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(1);
|
||||
Negotiable = Parsed.PayNegotiable;
|
||||
SharePercent = Parsed.SharePercent;
|
||||
if (Parsed.PayAmount is not null) { PayAmount = Parsed.PayAmount; SalaryMin = Parsed.PayAmount; }
|
||||
Description = Raw.RawText;
|
||||
Title = Parsed.RoleName is not null ? $"استخدام {Parsed.RoleName}" : "موقعیت استخدامی";
|
||||
@@ -84,8 +86,10 @@ public class ReviewModel : PageModel
|
||||
ShiftType = ShiftType,
|
||||
SpecialtyRequired = role?.Name ?? "",
|
||||
Description = Description,
|
||||
PayType = Negotiable ? PayType.Negotiable : PayType.PerShift,
|
||||
PayType = Negotiable ? PayType.Negotiable
|
||||
: (PayAmount is null && SharePercent is not null ? PayType.Percentage : PayType.PerShift),
|
||||
PayAmount = Negotiable ? null : PayAmount,
|
||||
SharePercent = Negotiable ? null : SharePercent,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Aggregated,
|
||||
SourceUrl = Raw.SourceUrl,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Employer.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "پنل کارفرما";
|
||||
string TypeLabel(FacilityType t) => t switch
|
||||
{
|
||||
FacilityType.Hospital => "بیمارستان",
|
||||
FacilityType.Clinic => "کلینیک",
|
||||
_ => "درمانگاه",
|
||||
};
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>پنل مرکز درمانی</h1>
|
||||
<p class="muted">شیفتها و موقعیتهای استخدامی مرکز خود را مدیریت کن.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
@if (Model.Facilities.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">
|
||||
<p>هنوز مرکزی ثبت نکردهای.</p>
|
||||
<a class="btn btn-accent btn-lg" asp-page="/Employer/RegisterFacility">ثبت مرکز درمانی</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="rec-banner">
|
||||
<div>
|
||||
<h2 style="margin:0 0 4px;">انتشار فرصت جدید</h2>
|
||||
<span style="opacity:.9; font-size:14px;">شیفت یا موقعیت استخدامی منتشر کن</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<a class="btn btn-outline" asp-page="/Employer/PostShift">+ شیفت</a>
|
||||
<a class="btn btn-outline" asp-page="/Employer/PostJob">+ استخدام</a>
|
||||
<a class="btn btn-outline" asp-page="/Employer/RegisterFacility">+ مرکز</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3">
|
||||
@foreach (var c in Model.Facilities)
|
||||
{
|
||||
<div class="card card-pad">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span class="facility" style="font-weight:800; font-size:16px;">@c.Facility.Name</span>
|
||||
@if (c.Facility.IsVerified)
|
||||
{
|
||||
<span class="badge badge-verified">✓ تأیید شده</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge badge-type">در انتظار تأیید</span>
|
||||
}
|
||||
</div>
|
||||
<p class="muted" style="margin:8px 0;">
|
||||
@TypeLabel(c.Facility.Type) — 📍 @c.Facility.City?.Name@(c.Facility.District is not null ? "، " + c.Facility.District.Name : "")
|
||||
</p>
|
||||
<div class="info-row"><span class="k">شیفتهای باز</span><span class="v">@JalaliDate.ToPersianDigits(c.OpenShifts.ToString())</span></div>
|
||||
<div class="info-row"><span class="k">موقعیتهای استخدامی</span><span class="v">@JalaliDate.ToPersianDigits(c.OpenJobs.ToString())</span></div>
|
||||
<div class="info-row"><span class="k">اعلام تمایلها</span><span class="v" style="color:var(--accent)">@JalaliDate.ToPersianDigits(c.Applicants.ToString())</span></div>
|
||||
<a class="btn btn-outline btn-block" style="margin-top:12px;" asp-page="/Employer/Listings" asp-route-facilityId="@c.Facility.Id">مدیریت آگهیها و متقاضیان</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Employer;
|
||||
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public IndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
public record FacilityCard(Facility Facility, int OpenShifts, int OpenJobs, int Applicants);
|
||||
public List<FacilityCard> Facilities { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
|
||||
var facilities = await _db.Facilities
|
||||
.Include(f => f.City).Include(f => f.District)
|
||||
.Where(f => f.OwnerUserId == userId)
|
||||
.OrderBy(f => f.Name).ToListAsync();
|
||||
|
||||
foreach (var f in facilities)
|
||||
{
|
||||
var openShifts = await _db.Shifts.CountAsync(s =>
|
||||
s.FacilityId == f.Id && s.Status == ShiftStatus.Open && s.Date >= today);
|
||||
var openJobs = await _db.JobOpenings.CountAsync(j =>
|
||||
j.FacilityId == f.Id && j.Status == ShiftStatus.Open);
|
||||
|
||||
var shiftIds = await _db.Shifts.Where(s => s.FacilityId == f.Id).Select(s => s.Id).ToListAsync();
|
||||
var jobIds = await _db.JobOpenings.Where(j => j.FacilityId == f.Id).Select(j => j.Id).ToListAsync();
|
||||
var applicants = await _db.InterestEvents.CountAsync(e =>
|
||||
e.EventType == InterestEventType.Apply &&
|
||||
((e.ShiftId != null && shiftIds.Contains(e.ShiftId.Value)) ||
|
||||
(e.JobOpeningId != null && jobIds.Contains(e.JobOpeningId.Value))));
|
||||
|
||||
Facilities.Add(new FacilityCard(f, openShifts, openJobs, applicants));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Employer.ListingsModel
|
||||
@{
|
||||
ViewData["Title"] = "مدیریت آگهیها";
|
||||
string StatusLabel(ShiftStatus s) => s switch
|
||||
{
|
||||
ShiftStatus.Open => "باز",
|
||||
ShiftStatus.Filled => "پر شده",
|
||||
ShiftStatus.Expired => "منقضی",
|
||||
_ => "لغو شده",
|
||||
};
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>مدیریت آگهیها — @Model.Facility?.Name</h1>
|
||||
<p class="muted">
|
||||
<a asp-page="/Employer/Index">← بازگشت به پنل</a>
|
||||
· <a asp-page="/Employer/PostShift">+ شیفت</a>
|
||||
· <a asp-page="/Employer/PostJob">+ استخدام</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<h2 style="font-size:20px;">شیفتها</h2>
|
||||
@if (Model.Shifts.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">هنوز شیفتی منتشر نکردهای.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var row in Model.Shifts)
|
||||
{
|
||||
var s = row.Shift;
|
||||
<div class="card card-pad" style="margin-bottom:12px;">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<strong>@s.Role?.Name — @JalaliDate.ToLongDate(s.Date) — @JalaliDate.Time(s.StartTime)</strong>
|
||||
<span class="badge @(s.Status == ShiftStatus.Open ? "badge-verified" : "badge-type")">@StatusLabel(s.Status)</span>
|
||||
</div>
|
||||
<p class="muted" style="margin:6px 0;">@JalaliDate.Toman(s.PayAmount)</p>
|
||||
|
||||
<div style="border-top:1px solid var(--line); padding-top:10px; margin-top:6px;">
|
||||
<strong style="font-size:14px;">متقاضیان (@JalaliDate.ToPersianDigits((row.Applicants.Count + row.Guests).ToString()))</strong>
|
||||
@if (row.Applicants.Count == 0 && row.Guests == 0)
|
||||
{
|
||||
<p class="muted" style="font-size:13px; margin:6px 0 0;">هنوز کسی اعلام تمایل نکرده.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul style="margin:6px 0 0; padding-inline-start:18px; font-size:13.5px;">
|
||||
@foreach (var a in row.Applicants)
|
||||
{
|
||||
<li>@(a.Name ?? "کاربر") — <span dir="ltr">@JalaliDate.ToPersianDigits(a.Phone)</span></li>
|
||||
}
|
||||
@if (row.Guests > 0)
|
||||
{
|
||||
<li class="muted">@JalaliDate.ToPersianDigits(row.Guests.ToString()) بازدیدکنندهی مهمان (بدون ورود)</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:8px; margin-top:12px;">
|
||||
@if (s.Status == ShiftStatus.Open)
|
||||
{
|
||||
<form method="post"><button asp-page-handler="CloseShift" asp-route-id="@s.Id" class="btn btn-outline" style="padding:6px 12px;">بستن (پر شد)</button></form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post"><button asp-page-handler="ReopenShift" asp-route-id="@s.Id" class="btn btn-outline" style="padding:6px 12px;">بازگشایی</button></form>
|
||||
}
|
||||
<form method="post" onsubmit="return confirm('این شیفت حذف شود؟');"><button asp-page-handler="DeleteShift" asp-route-id="@s.Id" class="btn btn-outline" style="padding:6px 12px; color:var(--danger); border-color:var(--danger);">حذف</button></form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<h2 style="font-size:20px; margin-top:30px;">موقعیتهای استخدامی</h2>
|
||||
@if (Model.Jobs.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">هنوز موقعیتی منتشر نکردهای.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var row in Model.Jobs)
|
||||
{
|
||||
var j = row.Job;
|
||||
<div class="card card-pad" style="margin-bottom:12px;">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<strong>@j.Title — @j.Role?.Name</strong>
|
||||
<span class="badge @(j.Status == ShiftStatus.Open ? "badge-verified" : "badge-type")">@StatusLabel(j.Status)</span>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--line); padding-top:10px; margin-top:8px;">
|
||||
<strong style="font-size:14px;">متقاضیان (@JalaliDate.ToPersianDigits((row.Applicants.Count + row.Guests).ToString()))</strong>
|
||||
@if (row.Applicants.Count == 0 && row.Guests == 0)
|
||||
{
|
||||
<p class="muted" style="font-size:13px; margin:6px 0 0;">هنوز کسی اعلام تمایل نکرده.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul style="margin:6px 0 0; padding-inline-start:18px; font-size:13.5px;">
|
||||
@foreach (var a in row.Applicants)
|
||||
{
|
||||
<li>@(a.Name ?? "کاربر") — <span dir="ltr">@JalaliDate.ToPersianDigits(a.Phone)</span></li>
|
||||
}
|
||||
@if (row.Guests > 0)
|
||||
{
|
||||
<li class="muted">@JalaliDate.ToPersianDigits(row.Guests.ToString()) بازدیدکنندهی مهمان</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:12px;">
|
||||
@if (j.Status == ShiftStatus.Open)
|
||||
{
|
||||
<form method="post"><button asp-page-handler="CloseJob" asp-route-id="@j.Id" class="btn btn-outline" style="padding:6px 12px;">بستن</button></form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post"><button asp-page-handler="ReopenJob" asp-route-id="@j.Id" class="btn btn-outline" style="padding:6px 12px;">بازگشایی</button></form>
|
||||
}
|
||||
<form method="post" onsubmit="return confirm('این موقعیت حذف شود؟');"><button asp-page-handler="DeleteJob" asp-route-id="@j.Id" class="btn btn-outline" style="padding:6px 12px; color:var(--danger); border-color:var(--danger);">حذف</button></form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Employer;
|
||||
|
||||
[Authorize]
|
||||
public class ListingsModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public ListingsModel(AppDbContext db) => _db = db;
|
||||
|
||||
public record Applicant(string? Name, string Phone, DateTime When);
|
||||
public record ShiftRow(Shift Shift, List<Applicant> Applicants, int Guests);
|
||||
public record JobRow(JobOpening Job, List<Applicant> Applicants, int Guests);
|
||||
|
||||
public Facility? Facility { get; private set; }
|
||||
public List<ShiftRow> Shifts { get; private set; } = new();
|
||||
public List<JobRow> Jobs { get; private set; } = new();
|
||||
|
||||
[BindProperty(SupportsGet = true)] public int FacilityId { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
if (!await OwnsAsync(FacilityId)) return Forbid();
|
||||
await LoadAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
// --- Lifecycle actions (all ownership-checked) ---
|
||||
public Task<IActionResult> OnPostCloseShiftAsync(int id) => MutateShift(id, s => s.Status = ShiftStatus.Filled);
|
||||
public Task<IActionResult> OnPostReopenShiftAsync(int id) => MutateShift(id, s => s.Status = ShiftStatus.Open);
|
||||
public Task<IActionResult> OnPostDeleteShiftAsync(int id) => MutateShift(id, s => _db.Shifts.Remove(s));
|
||||
public Task<IActionResult> OnPostCloseJobAsync(int id) => MutateJob(id, j => j.Status = ShiftStatus.Filled);
|
||||
public Task<IActionResult> OnPostReopenJobAsync(int id) => MutateJob(id, j => j.Status = ShiftStatus.Open);
|
||||
public Task<IActionResult> OnPostDeleteJobAsync(int id) => MutateJob(id, j => _db.JobOpenings.Remove(j));
|
||||
|
||||
private async Task<IActionResult> MutateShift(int id, Action<Shift> apply)
|
||||
{
|
||||
var s = await _db.Shifts.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (s is null || !await OwnsAsync(s.FacilityId)) return Forbid();
|
||||
apply(s);
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage(new { FacilityId = s.FacilityId });
|
||||
}
|
||||
|
||||
private async Task<IActionResult> MutateJob(int id, Action<JobOpening> apply)
|
||||
{
|
||||
var j = await _db.JobOpenings.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (j is null || !await OwnsAsync(j.FacilityId)) return Forbid();
|
||||
apply(j);
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage(new { FacilityId = j.FacilityId });
|
||||
}
|
||||
|
||||
private async Task<bool> OwnsAsync(int facilityId)
|
||||
{
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
return await _db.Facilities.AnyAsync(f => f.Id == facilityId && f.OwnerUserId == userId);
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
Facility = await _db.Facilities.Include(f => f.City).FirstOrDefaultAsync(f => f.Id == FacilityId);
|
||||
|
||||
var shifts = await _db.Shifts.Include(s => s.Role)
|
||||
.Where(s => s.FacilityId == FacilityId)
|
||||
.OrderByDescending(s => s.Date).ToListAsync();
|
||||
var jobs = await _db.JobOpenings.Include(j => j.Role)
|
||||
.Where(j => j.FacilityId == FacilityId)
|
||||
.OrderByDescending(j => j.CreatedAt).ToListAsync();
|
||||
|
||||
// Pull all "Apply" events for these listings, then resolve applicant identities.
|
||||
var shiftIds = shifts.Select(s => s.Id).ToList();
|
||||
var jobIds = jobs.Select(j => j.Id).ToList();
|
||||
var events = await _db.InterestEvents
|
||||
.Where(e => e.EventType == InterestEventType.Apply &&
|
||||
((e.ShiftId != null && shiftIds.Contains(e.ShiftId.Value)) ||
|
||||
(e.JobOpeningId != null && jobIds.Contains(e.JobOpeningId.Value))))
|
||||
.ToListAsync();
|
||||
|
||||
var visitorIds = events.Select(e => e.VisitorId).Distinct().ToList();
|
||||
var visitorUser = await _db.Visitors.Where(v => visitorIds.Contains(v.Id))
|
||||
.ToDictionaryAsync(v => v.Id, v => v.UserId);
|
||||
var userIds = visitorUser.Values.Where(u => u != null).Select(u => u!.Value).Distinct().ToList();
|
||||
var users = await _db.Users.Where(u => userIds.Contains(u.Id)).ToDictionaryAsync(u => u.Id);
|
||||
|
||||
(List<Applicant> applicants, int guests) Resolve(IEnumerable<InterestEvent> evs)
|
||||
{
|
||||
var applicants = new List<Applicant>();
|
||||
var guests = 0;
|
||||
var seen = new HashSet<int>();
|
||||
foreach (var e in evs.OrderByDescending(e => e.CreatedAt))
|
||||
{
|
||||
var uid = visitorUser.GetValueOrDefault(e.VisitorId);
|
||||
if (uid is int id && users.TryGetValue(id, out var u))
|
||||
{
|
||||
if (seen.Add(id)) applicants.Add(new Applicant(u.FullName, u.Phone, e.CreatedAt));
|
||||
}
|
||||
else guests++;
|
||||
}
|
||||
return (applicants, guests);
|
||||
}
|
||||
|
||||
Shifts = shifts.Select(s =>
|
||||
{
|
||||
var (a, g) = Resolve(events.Where(e => e.ShiftId == s.Id));
|
||||
return new ShiftRow(s, a, g);
|
||||
}).ToList();
|
||||
Jobs = jobs.Select(j =>
|
||||
{
|
||||
var (a, g) = Resolve(events.Where(e => e.JobOpeningId == j.Id));
|
||||
return new JobRow(j, a, g);
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Employer.PostJobModel
|
||||
@{
|
||||
ViewData["Title"] = "انتشار موقعیت استخدامی";
|
||||
}
|
||||
|
||||
<div class="page-head"><div class="container"><h1>انتشار موقعیت استخدامی</h1></div></div>
|
||||
|
||||
<div class="container section" style="max-width:560px;">
|
||||
@if (Model.Error is not null)
|
||||
{
|
||||
<div class="alert" style="background:#fdeaea; color:var(--danger);">@Model.Error</div>
|
||||
}
|
||||
@if (Model.MyFacilities.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">
|
||||
ابتدا یک مرکز ثبت کن.
|
||||
<a class="btn btn-accent" asp-page="/Employer/RegisterFacility">ثبت مرکز</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" class="card card-pad">
|
||||
<div class="filter-group">
|
||||
<label>مرکز درمانی</label>
|
||||
<select name="FacilityId">
|
||||
@foreach (var f in Model.MyFacilities)
|
||||
{
|
||||
<option value="@f.Id">@f.Name — @f.City?.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>عنوان موقعیت</label>
|
||||
<input type="text" name="Title" value="@Model.Title" placeholder="مثلاً استخدام پرستار بخش اورژانس" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نقش</label>
|
||||
<select name="RoleId">
|
||||
@foreach (var r in Model.Roles)
|
||||
{
|
||||
<option value="@r.Id">@r.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع همکاری</label>
|
||||
<select name="EmploymentType">
|
||||
<option value="0">تماموقت</option>
|
||||
<option value="1">پارهوقت</option>
|
||||
<option value="2">قراردادی</option>
|
||||
<option value="3">طرح</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group" style="display:flex; gap:8px;">
|
||||
<div style="flex:1;"><label>حقوق ماهانه از</label><input type="number" name="SalaryMin" value="@Model.SalaryMin" dir="ltr" /></div>
|
||||
<div style="flex:1;"><label>تا</label><input type="number" name="SalaryMax" value="@Model.SalaryMax" dir="ltr" /></div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-weight:600;">
|
||||
<input type="checkbox" name="Negotiable" value="true" style="width:auto;" checked="@Model.Negotiable" /> توافقی
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>شرح موقعیت</label>
|
||||
<textarea name="Description" rows="3">@Model.Description</textarea>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>شرایط احراز</label>
|
||||
<textarea name="Requirements" rows="2">@Model.Requirements</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-accent btn-block btn-lg">انتشار موقعیت</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Employer;
|
||||
|
||||
[Authorize]
|
||||
public class PostJobModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public PostJobModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<Facility> MyFacilities { get; private set; } = new();
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
public string? Error { get; private set; }
|
||||
|
||||
[BindProperty] public int FacilityId { get; set; }
|
||||
[BindProperty] public int RoleId { get; set; }
|
||||
[BindProperty] public string Title { get; set; } = "";
|
||||
[BindProperty] public EmploymentType EmploymentType { get; set; }
|
||||
[BindProperty] public long? SalaryMin { get; set; }
|
||||
[BindProperty] public long? SalaryMax { get; set; }
|
||||
[BindProperty] public bool Negotiable { get; set; }
|
||||
[BindProperty] public string? Description { get; set; }
|
||||
[BindProperty] public string? Requirements { get; set; }
|
||||
|
||||
public async Task OnGetAsync() => await LoadListsAsync();
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await LoadListsAsync();
|
||||
if (!MyFacilities.Any(f => f.Id == FacilityId))
|
||||
{
|
||||
Error = "این مرکز متعلق به شما نیست.";
|
||||
return Page();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Title)) { Error = "عنوان موقعیت الزامی است."; return Page(); }
|
||||
|
||||
_db.JobOpenings.Add(new JobOpening
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
RoleId = RoleId,
|
||||
Title = Title.Trim(),
|
||||
EmploymentType = EmploymentType,
|
||||
SalaryMin = Negotiable ? null : SalaryMin,
|
||||
SalaryMax = Negotiable ? null : SalaryMax,
|
||||
Description = Description,
|
||||
Requirements = Requirements,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Direct,
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage("/Employer/Index");
|
||||
}
|
||||
|
||||
private async Task LoadListsAsync()
|
||||
{
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
MyFacilities = await _db.Facilities.Include(f => f.City)
|
||||
.Where(f => f.OwnerUserId == userId).OrderBy(f => f.Name).ToListAsync();
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Employer.PostShiftModel
|
||||
@{
|
||||
ViewData["Title"] = "انتشار شیفت";
|
||||
}
|
||||
|
||||
<div class="page-head"><div class="container"><h1>انتشار شیفت جدید</h1></div></div>
|
||||
|
||||
<div class="container section" style="max-width:560px;">
|
||||
@if (Model.Error is not null)
|
||||
{
|
||||
<div class="alert" style="background:#fdeaea; color:var(--danger);">@Model.Error</div>
|
||||
}
|
||||
@if (Model.MyFacilities.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">
|
||||
ابتدا یک مرکز ثبت کن.
|
||||
<a class="btn btn-accent" asp-page="/Employer/RegisterFacility">ثبت مرکز</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" class="card card-pad">
|
||||
<div class="filter-group">
|
||||
<label>مرکز درمانی</label>
|
||||
<select name="FacilityId">
|
||||
@foreach (var f in Model.MyFacilities)
|
||||
{
|
||||
<option value="@f.Id">@f.Name — @f.City?.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نقش مورد نیاز</label>
|
||||
<select name="RoleId">
|
||||
@foreach (var r in Model.Roles)
|
||||
{
|
||||
<option value="@r.Id">@r.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>تاریخ (میلادی)</label>
|
||||
<input type="date" name="Date" value="@Model.Date.ToString("yyyy-MM-dd")" dir="ltr" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع شیفت</label>
|
||||
<select name="ShiftType">
|
||||
<option value="0">صبح</option>
|
||||
<option value="1">عصر</option>
|
||||
<option value="2">شب</option>
|
||||
<option value="3">آنکال</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group" style="display:flex; gap:8px;">
|
||||
<div style="flex:1;"><label>شروع</label><input type="time" name="StartTime" value="@Model.StartTime.ToString("HH:mm")" dir="ltr" /></div>
|
||||
<div style="flex:1;"><label>پایان</label><input type="time" name="EndTime" value="@Model.EndTime.ToString("HH:mm")" dir="ltr" /></div>
|
||||
</div>
|
||||
<div class="filter-group" style="display:flex; gap:8px;">
|
||||
<div style="flex:1;">
|
||||
<label>مبلغ مقطوع هر شیفت (تومان)</label>
|
||||
<input type="number" name="PayAmount" value="@Model.PayAmount" dir="ltr" />
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label>یا سهم درآمد (٪)</label>
|
||||
<input type="number" name="SharePercent" value="@Model.SharePercent" min="1" max="100" dir="ltr" placeholder="مثلاً ۵۰" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted" style="font-size:12px;">میتوانی فقط مبلغ، فقط درصد، یا هر دو را وارد کنی؛ اگر هر دو پر شود به کاربر «به انتخاب شما» نمایش داده میشود.</p>
|
||||
<div class="filter-group">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-weight:600;">
|
||||
<input type="checkbox" name="Negotiable" value="true" style="width:auto;" checked="@Model.Negotiable" /> توافقی (بدون مبلغ مشخص)
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>توضیحات</label>
|
||||
<textarea name="Description" rows="3">@Model.Description</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-accent btn-block btn-lg">انتشار شیفت</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Employer;
|
||||
|
||||
[Authorize]
|
||||
public class PostShiftModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public PostShiftModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<Facility> MyFacilities { get; private set; } = new();
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
public string? Error { get; private set; }
|
||||
|
||||
[BindProperty] public int FacilityId { get; set; }
|
||||
[BindProperty] public int RoleId { get; set; }
|
||||
[BindProperty] public DateOnly Date { get; set; }
|
||||
[BindProperty] public ShiftType ShiftType { get; set; }
|
||||
[BindProperty] public TimeOnly StartTime { get; set; }
|
||||
[BindProperty] public TimeOnly EndTime { get; set; }
|
||||
[BindProperty] public long? PayAmount { get; set; }
|
||||
[BindProperty] public int? SharePercent { get; set; } // سهم درآمد (٪)
|
||||
[BindProperty] public bool Negotiable { get; set; }
|
||||
[BindProperty] public string? Description { get; set; }
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
await LoadListsAsync();
|
||||
Date = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(1);
|
||||
StartTime = new TimeOnly(8, 0);
|
||||
EndTime = new TimeOnly(14, 0);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await LoadListsAsync();
|
||||
if (!MyFacilities.Any(f => f.Id == FacilityId))
|
||||
{
|
||||
Error = "این مرکز متعلق به شما نیست.";
|
||||
return Page();
|
||||
}
|
||||
var role = await _db.Roles.FindAsync(RoleId);
|
||||
_db.Shifts.Add(new Shift
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
RoleId = RoleId,
|
||||
Date = Date,
|
||||
StartTime = StartTime,
|
||||
EndTime = EndTime,
|
||||
ShiftType = ShiftType,
|
||||
SpecialtyRequired = role?.Name ?? "",
|
||||
Description = Description,
|
||||
PayType = Negotiable ? PayType.Negotiable
|
||||
: (PayAmount is null && SharePercent is not null ? PayType.Percentage : PayType.PerShift),
|
||||
PayAmount = Negotiable ? null : PayAmount,
|
||||
SharePercent = Negotiable ? null : SharePercent,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Direct, // posted directly by the facility
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage("/Employer/Index");
|
||||
}
|
||||
|
||||
private async Task LoadListsAsync()
|
||||
{
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
MyFacilities = await _db.Facilities.Include(f => f.City)
|
||||
.Where(f => f.OwnerUserId == userId).OrderBy(f => f.Name).ToListAsync();
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Employer.RegisterFacilityModel
|
||||
@{
|
||||
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;">
|
||||
@if (Model.Error is not null)
|
||||
{
|
||||
<div class="alert" style="background:#fdeaea; color:var(--danger);">@Model.Error</div>
|
||||
}
|
||||
<form method="post" class="card card-pad">
|
||||
<div class="filter-group">
|
||||
<label>نام مرکز *</label>
|
||||
<input type="text" name="Name" value="@Model.Name" placeholder="مثلاً بیمارستان مهر" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع مرکز</label>
|
||||
<select name="Type">
|
||||
<option value="0">بیمارستان</option>
|
||||
<option value="1">کلینیک</option>
|
||||
<option value="2">درمانگاه</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>شهر *</label>
|
||||
<select name="CityId">
|
||||
<option value="0">انتخاب کنید…</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="DistrictId">
|
||||
<option value="">—</option>
|
||||
@foreach (var d in Model.Districts)
|
||||
{
|
||||
<option value="@d.Id" selected="@(Model.DistrictId == d.Id)">@d.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>آدرس</label>
|
||||
<input type="text" name="Address" value="@Model.Address" />
|
||||
</div>
|
||||
<div class="filter-group" style="display:flex; gap:8px;">
|
||||
<div style="flex:1;"><label>تلفن</label><input type="tel" name="Phone" value="@Model.Phone" dir="ltr" /></div>
|
||||
<div style="flex:1;"><label>شناسه بله</label><input type="text" name="BaleId" value="@Model.BaleId" dir="ltr" /></div>
|
||||
</div>
|
||||
<div class="filter-group" style="display:flex; gap:8px;">
|
||||
<div style="flex:1;"><label>عرض جغرافیایی (اختیاری)</label><input type="number" step="any" name="Lat" value="@Model.Lat" dir="ltr" /></div>
|
||||
<div style="flex:1;"><label>طول جغرافیایی (اختیاری)</label><input type="number" step="any" name="Lng" value="@Model.Lng" dir="ltr" /></div>
|
||||
</div>
|
||||
<p class="muted" style="font-size:12px;">مختصات برای نمایش در فیلتر «نزدیک من» استفاده میشود.</p>
|
||||
<button type="submit" class="btn btn-accent btn-block btn-lg">ثبت مرکز و ورود به پنل</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Employer;
|
||||
|
||||
[Authorize]
|
||||
public class RegisterFacilityModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public RegisterFacilityModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<City> Cities { get; private set; } = new();
|
||||
public List<District> Districts { get; private set; } = new();
|
||||
|
||||
[BindProperty] public string Name { get; set; } = "";
|
||||
[BindProperty] public FacilityType Type { get; set; }
|
||||
[BindProperty] public int CityId { get; set; }
|
||||
[BindProperty] public int? DistrictId { get; set; }
|
||||
[BindProperty] public string? Address { get; set; }
|
||||
[BindProperty] public string? Phone { get; set; }
|
||||
[BindProperty] public string? BaleId { get; set; }
|
||||
[BindProperty] public double? Lat { get; set; }
|
||||
[BindProperty] public double? Lng { get; set; }
|
||||
public string? Error { get; private set; }
|
||||
|
||||
public async Task OnGetAsync() => await LoadListsAsync();
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await LoadListsAsync();
|
||||
if (string.IsNullOrWhiteSpace(Name) || CityId == 0)
|
||||
{
|
||||
Error = "نام مرکز و شهر الزامی است.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
var facility = new Facility
|
||||
{
|
||||
Name = Name.Trim(),
|
||||
Type = Type,
|
||||
CityId = CityId,
|
||||
DistrictId = DistrictId,
|
||||
Address = Address?.Trim(),
|
||||
Phone = Phone?.Trim(),
|
||||
BaleId = BaleId?.Trim(),
|
||||
Lat = Lat,
|
||||
Lng = Lng,
|
||||
OwnerUserId = userId,
|
||||
IsVerified = false, // platform verifies later
|
||||
};
|
||||
_db.Facilities.Add(facility);
|
||||
|
||||
// Promote the user to FacilityAdmin (keep Admin if already admin) and refresh the cookie.
|
||||
var user = await _db.Users.FindAsync(userId);
|
||||
if (user is not null && user.Role == UserRole.Doctor)
|
||||
{
|
||||
user.Role = UserRole.FacilityAdmin;
|
||||
await _db.SaveChangesAsync();
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
AuthHelper.BuildPrincipal(user));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return RedirectToPage("/Employer/Index");
|
||||
}
|
||||
|
||||
private async Task LoadListsAsync()
|
||||
{
|
||||
Cities = await _db.Cities.OrderByDescending(c => c.IsActive).ThenBy(c => c.Name).ToListAsync();
|
||||
Districts = await _db.Districts.Where(d => d.IsActive).OrderBy(d => d.Name).ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,10 @@
|
||||
{
|
||||
<a asp-page="/Admin/Index" style="margin-inline-end:14px; font-weight:600;">پنل مدیریت</a>
|
||||
}
|
||||
@if (User.IsInRole("FacilityAdmin"))
|
||||
{
|
||||
<a asp-page="/Employer/Index" style="margin-inline-end:14px; font-weight:600;">پنل کارفرما</a>
|
||||
}
|
||||
<a asp-page="/Account/Profile" style="margin-inline-end:10px; font-weight:600;">پروفایل</a>
|
||||
<form method="post" asp-page="/Account/Logout" style="display:inline;">
|
||||
<button type="submit" class="btn btn-outline" style="padding:7px 14px;">خروج</button>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
<div class="foot">
|
||||
<span class="pay">@JalaliDate.Toman(s.PayAmount)</span>
|
||||
<span class="pay">@JalaliDate.PayLabel(s.PayType, s.PayAmount, s.SharePercent)</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">جزئیات</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<div class="row">📅 @JalaliDate.WeekDayName(Model.Date)، @JalaliDate.ToLongDate(Model.Date)</div>
|
||||
<div class="row">🕐 @JalaliDate.Time(Model.StartTime) تا @JalaliDate.Time(Model.EndTime)</div>
|
||||
<div class="foot">
|
||||
<span class="pay">@JalaliDate.Toman(Model.PayAmount)</span>
|
||||
<span class="pay">@JalaliDate.PayLabel(Model.PayType, Model.PayAmount, Model.SharePercent)</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">جزئیات</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<div class="info-row"><span class="k">ساعت</span><span class="v">@JalaliDate.Time(s.StartTime) تا @JalaliDate.Time(s.EndTime)</span></div>
|
||||
<div class="info-row"><span class="k">مدت</span><span class="v">@JalaliDate.ToPersianDigits(s.DurationHours.ToString("0.#")) ساعت</span></div>
|
||||
<div class="info-row"><span class="k">نقش مورد نیاز</span><span class="v">@(s.Role?.Name ?? s.SpecialtyRequired)</span></div>
|
||||
<div class="info-row"><span class="k">حقوق</span><span class="v" style="color:var(--primary-dark)">@JalaliDate.Toman(s.PayAmount)</span></div>
|
||||
<div class="info-row"><span class="k">پرداخت</span><span class="v" style="color:var(--primary-dark)">@JalaliDate.PayLabel(s.PayType, s.PayAmount, s.SharePercent)</span></div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(s.Description))
|
||||
@@ -74,10 +74,13 @@
|
||||
|
||||
<aside>
|
||||
<div class="card card-pad">
|
||||
<div class="pay" style="font-size:20px; margin-bottom:6px; color:var(--primary-dark); font-weight:800;">
|
||||
@JalaliDate.Toman(s.PayAmount)
|
||||
<div class="pay" style="font-size:19px; margin-bottom:6px; color:var(--primary-dark); font-weight:800;">
|
||||
@JalaliDate.PayLabel(s.PayType, s.PayAmount, s.SharePercent)
|
||||
</div>
|
||||
<p class="muted" style="font-size:13px; margin-top:0;">@(s.PayType == PayType.Negotiable ? "توافقی با مرکز درمانی" : "برای هر شیفت")</p>
|
||||
@if (s.PayAmount is not null && s.SharePercent is not null)
|
||||
{
|
||||
<p class="muted" style="font-size:13px; margin-top:0;">میتوانی هنگام هماهنگی، یکی از دو حالت را با مرکز انتخاب کنی.</p>
|
||||
}
|
||||
@if (Model.Saved)
|
||||
{
|
||||
<div class="alert alert-success" style="margin-bottom:12px;">✓ این فرصت ذخیره شد و در پیشنهادهای شما لحاظ میشود.</div>
|
||||
|
||||
@@ -95,6 +95,13 @@
|
||||
فقط شیفتهای با حقوق مشخص
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-weight:600;">
|
||||
<input type="checkbox" name="ShareOnly" value="true" style="width:auto;"
|
||||
onchange="this.form.submit()" checked="@Model.ShareOnly" />
|
||||
فقط شیفتهای سهم درآمد (درصدی)
|
||||
</label>
|
||||
</div>
|
||||
<a asp-page="/Shifts/Index" class="btn btn-outline btn-block">حذف فیلترها</a>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user