Fix FK violation when publishing a crawled listing without a facility
CI/CD / CI · dotnet build (push) Successful in 1m31s
CI/CD / Deploy · hamkadr (push) Successful in 1m44s

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:
soroush.asadi
2026-06-08 07:09:18 +03:30
parent 5f769b0293
commit a2fc70ae57
3 changed files with 63 additions and 2 deletions
@@ -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();
+1
View File
@@ -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;