Files
hamkadr/src/JobsMedical.Web/Pages/Admin/Review.cshtml.cs
T

237 lines
10 KiB
C#
Raw Normal View History

using JobsMedical.Web.Data;
using JobsMedical.Web.Models;
using JobsMedical.Web.Services;
using JobsMedical.Web.Services.Scraping;
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;
private readonly NotificationService _notify;
public ReviewModel(AppDbContext db, IListingParser parser, NotificationService notify)
{
_db = db;
_parser = parser;
_notify = notify;
}
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();
[TempData] public string? Error { get; set; }
// 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 string? NewFacilityName { get; set; } // create a facility on the fly if none picked
[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 int? SharePercent { get; set; }
[BindProperty] public bool Negotiable { get; set; }
[BindProperty] public Gender GenderRequirement { 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;
SharePercent = Parsed.SharePercent;
GenderRequirement = Parsed.Gender;
if (Parsed.PayAmount is not null) { PayAmount = Parsed.PayAmount; SalaryMin = Parsed.PayAmount; }
Description = Raw.RawText;
Title = Parsed.RoleName is not null ? $"استخدام {Parsed.RoleName}" : "موقعیت استخدامی";
// Facility: try to match the listing's facility to one we already have; otherwise
// prefill the "new facility" box so publishing creates it.
if (!string.IsNullOrWhiteSpace(Parsed.FacilityName))
{
var cityId = await _db.Cities.Where(c => c.Name == Parsed.CityName)
.Select(c => (int?)c.Id).FirstOrDefaultAsync();
var match = FacilityMatcher.FindBest(Facilities, Parsed.FacilityName, cityId);
if (match is not null)
{
FacilityId = match.Id;
Parsed.Notes.Add($"مرکز منطبق در سیستم: «{match.Name}» — همین انتخاب شد.");
}
else
{
NewFacilityName = Parsed.FacilityName;
Parsed.Notes.Add($"مرکز جدید پیشنهادی: «{Parsed.FacilityName}» — هنگام انتشار ساخته می‌شود.");
}
}
return Page();
}
public async Task<IActionResult> OnPostPublishAsync(int id)
{
Raw = await _db.RawListings.FirstOrDefaultAsync(r => r.Id == id);
if (Raw is null) return NotFound();
// Resolve the facility: prefer the picked one; otherwise create from the typed name.
// This prevents FK_Shifts_Facilities_FacilityId violations when no facility is selected
// (e.g. the dropdown is empty because no facilities exist yet).
var facilityId = await ResolveFacilityIdAsync();
if (facilityId is null)
{
Error = "یک مرکز درمانی معتبر انتخاب کن، یا در کادر «نام مرکز جدید» نام مرکز را وارد کن تا ساخته شود.";
return RedirectToPage(new { id });
}
if (!await _db.Roles.AnyAsync(r => r.Id == RoleId))
{
Error = "یک نقش معتبر انتخاب کن.";
return RedirectToPage(new { id });
}
Shift? createdShift = null;
JobOpening? createdJob = null;
if (Kind == ListingKind.Shift)
{
var role = await _db.Roles.FindAsync(RoleId);
var shift = new Shift
{
FacilityId = facilityId.Value,
RoleId = RoleId,
Date = ShiftDate,
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,
GenderRequirement = GenderRequirement,
Status = ShiftStatus.Open,
Source = ShiftSource.Aggregated,
SourceUrl = Raw.SourceUrl,
};
_db.Shifts.Add(shift);
await _db.SaveChangesAsync();
Raw.Status = RawListingStatus.Normalized;
Raw.LinkedShiftId = shift.Id;
createdShift = shift;
}
else
{
var job = new JobOpening
{
FacilityId = facilityId.Value,
RoleId = RoleId,
Title = string.IsNullOrWhiteSpace(Title) ? "موقعیت استخدامی" : Title.Trim(),
EmploymentType = EmploymentType,
SalaryMin = Negotiable ? null : SalaryMin,
SalaryMax = Negotiable ? null : SalaryMax,
GenderRequirement = GenderRequirement,
Description = Description,
Status = ShiftStatus.Open,
Source = ShiftSource.Aggregated,
SourceUrl = Raw.SourceUrl,
};
_db.JobOpenings.Add(job);
Raw.Status = RawListingStatus.Normalized;
createdJob = job;
}
await _db.SaveChangesAsync();
if (createdShift is not null) await _notify.NotifyNewShiftAsync(createdShift.Id);
if (createdJob is not null) await _notify.NotifyNewJobAsync(createdJob.Id);
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)),
};
/// <summary>
/// Returns a valid existing FacilityId, creating a new unverified facility from
/// <see cref="NewFacilityName"/> when nothing valid is selected. Returns null when
/// neither a valid facility is picked nor a name is provided.
/// </summary>
private async Task<int?> ResolveFacilityIdAsync()
{
if (FacilityId > 0 && await _db.Facilities.AnyAsync(f => f.Id == FacilityId))
return FacilityId;
if (string.IsNullOrWhiteSpace(NewFacilityName))
return null;
var name = NewFacilityName.Trim();
var cityId = await _db.Cities.OrderByDescending(c => c.IsActive)
.Select(c => (int?)c.Id).FirstOrDefaultAsync();
if (cityId is null) return null; // no cities seeded — cannot create a facility
// Reuse an existing facility that's exactly or closely the same (Persian-aware fuzzy
// match), so we don't create duplicates like «بیمارستان میلاد» vs «میلاد».
var all = await _db.Facilities.ToListAsync();
var match = FacilityMatcher.FindBest(all, name, cityId);
if (match is not null) return match.Id;
var facility = new Facility
{
Name = name,
CityId = cityId.Value,
Type = FacilityType.Hospital,
Verification = VerificationStatus.Unverified,
IsVerified = false,
};
_db.Facilities.Add(facility);
await _db.SaveChangesAsync();
return facility.Id;
}
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();
}