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:
@@ -0,0 +1,145 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Admin;
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class ReviewModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IListingParser _parser;
|
||||
|
||||
public ReviewModel(AppDbContext db, IListingParser parser)
|
||||
{
|
||||
_db = db;
|
||||
_parser = parser;
|
||||
}
|
||||
|
||||
public RawListing? Raw { get; private set; }
|
||||
public ParsedListing? Parsed { get; private set; }
|
||||
public List<Facility> Facilities { get; private set; } = new();
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
|
||||
// The editable form (prefilled from the parser, admin can override everything).
|
||||
[BindProperty] public ListingKind Kind { get; set; }
|
||||
[BindProperty] public int FacilityId { get; set; }
|
||||
[BindProperty] public int RoleId { get; set; }
|
||||
[BindProperty] public string? Description { get; set; }
|
||||
// Shift fields
|
||||
[BindProperty] public DateOnly ShiftDate { 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 bool Negotiable { get; set; }
|
||||
// Job fields
|
||||
[BindProperty] public string? Title { get; set; }
|
||||
[BindProperty] public EmploymentType EmploymentType { get; set; }
|
||||
[BindProperty] public long? SalaryMin { get; set; }
|
||||
[BindProperty] public long? SalaryMax { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(int id)
|
||||
{
|
||||
await LoadListsAsync();
|
||||
Raw = await _db.RawListings.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (Raw is null) return NotFound();
|
||||
|
||||
Parsed = _parser.Parse(Raw.RawText,
|
||||
Roles.Select(r => r.Name), await CityNamesAsync(), await DistrictNamesAsync());
|
||||
|
||||
// Prefill the form from the parser's best guess.
|
||||
Kind = Parsed.Kind;
|
||||
RoleId = Roles.FirstOrDefault(r => r.Name == Parsed.RoleName)?.Id ?? Roles.FirstOrDefault()?.Id ?? 0;
|
||||
ShiftType = Parsed.ShiftType ?? ShiftType.Day;
|
||||
EmploymentType = Parsed.EmploymentType ?? EmploymentType.FullTime;
|
||||
(StartTime, EndTime) = DefaultTimes(ShiftType);
|
||||
ShiftDate = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(1);
|
||||
Negotiable = Parsed.PayNegotiable;
|
||||
if (Parsed.PayAmount is not null) { PayAmount = Parsed.PayAmount; SalaryMin = Parsed.PayAmount; }
|
||||
Description = Raw.RawText;
|
||||
Title = Parsed.RoleName is not null ? $"استخدام {Parsed.RoleName}" : "موقعیت استخدامی";
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostPublishAsync(int id)
|
||||
{
|
||||
Raw = await _db.RawListings.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (Raw is null) return NotFound();
|
||||
|
||||
if (Kind == ListingKind.Shift)
|
||||
{
|
||||
var role = await _db.Roles.FindAsync(RoleId);
|
||||
var shift = new Shift
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
RoleId = RoleId,
|
||||
Date = ShiftDate,
|
||||
StartTime = StartTime,
|
||||
EndTime = EndTime,
|
||||
ShiftType = ShiftType,
|
||||
SpecialtyRequired = role?.Name ?? "",
|
||||
Description = Description,
|
||||
PayType = Negotiable ? PayType.Negotiable : PayType.PerShift,
|
||||
PayAmount = Negotiable ? null : PayAmount,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Aggregated,
|
||||
SourceUrl = Raw.SourceUrl,
|
||||
};
|
||||
_db.Shifts.Add(shift);
|
||||
await _db.SaveChangesAsync();
|
||||
Raw.Status = RawListingStatus.Normalized;
|
||||
Raw.LinkedShiftId = shift.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
var job = new JobOpening
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
RoleId = RoleId,
|
||||
Title = string.IsNullOrWhiteSpace(Title) ? "موقعیت استخدامی" : Title.Trim(),
|
||||
EmploymentType = EmploymentType,
|
||||
SalaryMin = Negotiable ? null : SalaryMin,
|
||||
SalaryMax = Negotiable ? null : SalaryMax,
|
||||
Description = Description,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Aggregated,
|
||||
SourceUrl = Raw.SourceUrl,
|
||||
};
|
||||
_db.JobOpenings.Add(job);
|
||||
Raw.Status = RawListingStatus.Normalized;
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage("/Admin/Index");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDiscardAsync(int id)
|
||||
{
|
||||
var raw = await _db.RawListings.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (raw is null) return NotFound();
|
||||
raw.Status = RawListingStatus.Discarded;
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage("/Admin/Index");
|
||||
}
|
||||
|
||||
private static (TimeOnly, TimeOnly) DefaultTimes(ShiftType t) => t switch
|
||||
{
|
||||
ShiftType.Day => (new TimeOnly(8, 0), new TimeOnly(14, 0)),
|
||||
ShiftType.Evening => (new TimeOnly(14, 0), new TimeOnly(20, 0)),
|
||||
ShiftType.Night => (new TimeOnly(20, 0), new TimeOnly(8, 0)),
|
||||
_ => (new TimeOnly(8, 0), new TimeOnly(8, 0)),
|
||||
};
|
||||
|
||||
private async Task LoadListsAsync()
|
||||
{
|
||||
Facilities = await _db.Facilities.Include(f => f.City).OrderBy(f => f.Name).ToListAsync();
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
}
|
||||
|
||||
private Task<List<string>> CityNamesAsync() => _db.Cities.Select(c => c.Name).ToListAsync();
|
||||
private Task<List<string>> DistrictNamesAsync() => _db.Districts.Select(d => d.Name).ToListAsync();
|
||||
}
|
||||
Reference in New Issue
Block a user