Fix FK violation when publishing a crawled listing without a facility
OnPostPublishAsync inserted a Shift/Job with FacilityId=0 when no facility was selected (e.g. the dropdown is empty because no facilities exist yet), throwing FK_Shifts_Facilities_FacilityId and surfacing the production error page. - Resolve-or-create the facility before insert: use the picked one, else create an unverified Facility from a typed name (reusing same-named). - Guard the role too; on missing facility/role redirect back with a Persian error message instead of 500. - Review form: add "new facility name" input + "— none —" option + error alert; add .alert-error style. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,10 @@
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
@if (Model.Error is not null)
|
||||
{
|
||||
<div class="alert alert-error" style="margin-bottom:16px;">⚠ @Model.Error</div>
|
||||
}
|
||||
<div class="detail-grid">
|
||||
<div>
|
||||
<div class="card card-pad">
|
||||
@@ -47,11 +51,14 @@
|
||||
<div class="filter-group">
|
||||
<label>مرکز درمانی</label>
|
||||
<select name="FacilityId">
|
||||
<option value="0">— انتخاب نشده —</option>
|
||||
@foreach (var f in Model.Facilities)
|
||||
{
|
||||
<option value="@f.Id">@f.Name — @f.City?.Name</option>
|
||||
}
|
||||
</select>
|
||||
<input type="text" name="NewFacilityName" placeholder="یا نام مرکز جدید را وارد کن…" style="margin-top:6px;" />
|
||||
<p class="muted" style="font-size:11px; margin:4px 0 0;">اگر مرکز در فهرست نیست، نامش را اینجا بنویس تا بهصورت «تأییدنشده» ساخته شود.</p>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نقش</label>
|
||||
|
||||
@@ -27,9 +27,12 @@ public class ReviewModel : PageModel
|
||||
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
|
||||
@@ -77,6 +80,21 @@ public class ReviewModel : PageModel
|
||||
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)
|
||||
@@ -84,7 +102,7 @@ public class ReviewModel : PageModel
|
||||
var role = await _db.Roles.FindAsync(RoleId);
|
||||
var shift = new Shift
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
FacilityId = facilityId.Value,
|
||||
RoleId = RoleId,
|
||||
Date = ShiftDate,
|
||||
StartTime = StartTime,
|
||||
@@ -111,7 +129,7 @@ public class ReviewModel : PageModel
|
||||
{
|
||||
var job = new JobOpening
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
FacilityId = facilityId.Value,
|
||||
RoleId = RoleId,
|
||||
Title = string.IsNullOrWhiteSpace(Title) ? "موقعیت استخدامی" : Title.Trim(),
|
||||
EmploymentType = EmploymentType,
|
||||
@@ -150,6 +168,41 @@ public class ReviewModel : PageModel
|
||||
_ => (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;
|
||||
|
||||
// Reuse a same-named facility if one already exists, else create it.
|
||||
var name = NewFacilityName.Trim();
|
||||
var existing = await _db.Facilities.FirstOrDefaultAsync(f => f.Name == name);
|
||||
if (existing is not null) return existing.Id;
|
||||
|
||||
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
|
||||
|
||||
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();
|
||||
|
||||
@@ -399,6 +399,7 @@ label { font-size: 13px; }
|
||||
.legal li { margin-bottom: 6px; }
|
||||
.alert { padding: 12px 16px; border-radius: 10px; margin-bottom: 16px; font-weight: 600; }
|
||||
.alert-success { background: var(--primary-soft); color: var(--primary-dark); }
|
||||
.alert-error { background: #fdecea; color: #b3261e; border: 1px solid #f5c2c0; }
|
||||
|
||||
/* notification bell badge */
|
||||
.bell-badge { position:absolute; top:-6px; inset-inline-start:-8px; background:var(--accent); color:#fff;
|
||||
|
||||
Reference in New Issue
Block a user