Admin: bulk-delete published ingested posts; talent: point to source when no phone
CI/CD / CI · dotnet build (push) Successful in 1m52s
CI/CD / Deploy · hamkadr (push) Successful in 2m41s

- /Admin/Ingested: "حذف گروهی همه‌ی منتشرشده‌ها" button removes, in one
  transaction, every aggregated Shift/Job/Talent published from ingestion
  plus the approved (Normalized) raw items that produced them. Confirms
  first and reports counts. Raw rows deleted before the posts (they hold
  the FKs); DB cascade clears applications/interest events.
- Talent details: when the contact number couldn't be extracted (e.g.
  Divar's login-gated reveal), show a prominent "مشاهده شماره در دیوار/مدجابز ↗"
  link to the original ad instead of the call button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-08 08:36:12 +03:30
parent a5d6e212e2
commit b092a5cfe5
3 changed files with 53 additions and 4 deletions
@@ -16,6 +16,24 @@
</div>
<div class="container section">
@if (Model.Message is not null)
{
<div class="alert alert-success">✓ @Model.Message</div>
}
@{ int publishedCount = Model.Counts.GetValueOrDefault(JobsMedical.Web.Models.RawListingStatus.Normalized); }
@if (publishedCount > 0)
{
<form method="post" asp-page-handler="DeletePublished"
onsubmit="return confirm('همه آگهی‌های منتشرشده از جمع‌آوری (شیفت/استخدام/آماده‌به‌کار) و آیتم‌های تأییدشده‌ی متناظر حذف می‌شوند. این کار بازگشت‌ناپذیر است. ادامه می‌دهی؟');"
style="margin-bottom:14px;">
<button type="submit" class="btn btn-outline" style="color:var(--danger); border-color:var(--danger);">
🗑 حذف گروهی همه‌ی منتشرشده‌ها (@JalaliDate.ToPersianDigits(publishedCount.ToString()))
</button>
<span class="muted" style="font-size:12px; margin-inline-start:8px;">آگهی‌های منتشرشده روی سایت را که از جمع‌آوری ساخته شده‌اند یکجا حذف می‌کند.</span>
</form>
}
<div class="ing-filters">
@Html.Raw(Pill("all", "همه", Model.Counts.Values.Sum()))
@Html.Raw(Pill("new", "در صف", C(JobsMedical.Web.Models.RawListingStatus.New)))
@@ -1,5 +1,6 @@
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;
@@ -18,6 +19,7 @@ public class IngestedModel : PageModel
public List<RawListing> Items { get; private set; } = new();
public int Total { get; private set; }
public Dictionary<RawListingStatus, int> Counts { get; private set; } = new();
[TempData] public string? Message { get; set; }
[BindProperty(SupportsGet = true)] public string? Status { get; set; } // new|flagged|published|discarded|all
[BindProperty(SupportsGet = true)] public string? Source { get; set; }
@@ -43,4 +45,23 @@ public class IngestedModel : PageModel
Total = await q.CountAsync();
Items = await q.OrderByDescending(r => r.FetchedAt).Take(200).ToListAsync();
}
/// <summary>
/// Bulk-delete everything that was published from ingestion: the aggregated Shift/Job/Talent
/// posts on the site AND the approved (Normalized) raw items that produced them. Done in a
/// transaction; the linked raw rows are removed first since they hold FKs to the posts.
/// </summary>
public async Task<IActionResult> OnPostDeletePublishedAsync()
{
await using var tx = await _db.Database.BeginTransactionAsync();
var raws = await _db.RawListings.Where(r => r.Status == RawListingStatus.Normalized).ExecuteDeleteAsync();
var shifts = await _db.Shifts.Where(s => s.Source == ShiftSource.Aggregated).ExecuteDeleteAsync();
var jobs = await _db.JobOpenings.Where(j => j.Source == ShiftSource.Aggregated).ExecuteDeleteAsync();
var talent = await _db.TalentListings.Where(t => t.Source == ShiftSource.Aggregated).ExecuteDeleteAsync();
await tx.CommitAsync();
string P(int n) => JalaliDate.ToPersianDigits(n.ToString());
Message = $"حذف شد: {P(shifts)} شیفت، {P(jobs)} استخدام، {P(talent)} آماده‌به‌کار و {P(raws)} آیتم جمع‌آوری.";
return RedirectToPage(new { Status });
}
}
@@ -19,6 +19,13 @@
var digits = new string(t.Phone.Where(char.IsDigit).ToArray());
if (digits.Length >= 7) telHref = "tel:" + digits;
}
// Friendly source name (used to point users to the original ad when no number was extracted).
string? sourceName = null;
if (!string.IsNullOrWhiteSpace(t.SourceUrl))
{
var host = System.Uri.TryCreate(t.SourceUrl, UriKind.Absolute, out var su) ? su.Host : t.SourceUrl!;
sourceName = host.Contains("divar") ? "دیوار" : host.Contains("medjobs") ? "مدجابز" : host;
}
}
<div class="page-head">
@@ -68,14 +75,17 @@
<a href="@telHref" class="btn btn-accent btn-block btn-lg" dir="ltr">📞 @t.Phone</a>
<p class="muted" style="font-size:12px; margin:10px 0 0;">با این فرد مستقیم تماس بگیرید.</p>
}
else if (!string.IsNullOrWhiteSpace(t.SourceUrl))
{
@* Number wasn't extractable (e.g. behind a login-gated reveal) — point to the source. *@
<p class="muted" style="margin-top:0;">شماره مستقیم استخراج نشد.</p>
<a href="@t.SourceUrl" target="_blank" rel="nofollow noopener" class="btn btn-accent btn-block btn-lg">مشاهده شماره در @sourceName ↗</a>
<p class="muted" style="font-size:12px; margin:10px 0 0;">این آگهی از @sourceName جمع‌آوری شده؛ برای دریافت شماره به آگهی اصلی مراجعه کن.</p>
}
else
{
<p class="muted">شماره تماس ثبت نشده است.</p>
}
@if (!string.IsNullOrWhiteSpace(t.SourceUrl))
{
<a href="@t.SourceUrl" target="_blank" rel="nofollow noopener" class="btn btn-outline btn-block" style="margin-top:8px;">منبع آگهی ↗</a>
}
</div>
</aside>
</div>