diff --git a/src/JobsMedical.Web/Pages/Admin/Index.cshtml b/src/JobsMedical.Web/Pages/Admin/Index.cshtml index 60beb26..32ed939 100644 --- a/src/JobsMedical.Web/Pages/Admin/Index.cshtml +++ b/src/JobsMedical.Web/Pages/Admin/Index.cshtml @@ -64,7 +64,10 @@
@if (Model.Runs.Count > 0) { -

تاریخچه جمع‌آوری

+

+ تاریخچه جمع‌آوری + همه نتایج جمع‌آوری ← +

diff --git a/src/JobsMedical.Web/Pages/Admin/Index.cshtml.cs b/src/JobsMedical.Web/Pages/Admin/Index.cshtml.cs index 21f3036..a34beed 100644 --- a/src/JobsMedical.Web/Pages/Admin/Index.cshtml.cs +++ b/src/JobsMedical.Web/Pages/Admin/Index.cshtml.cs @@ -49,6 +49,14 @@ public class IndexModel : PageModel return RedirectToPage(); } + /// Fast triage — reject (discard) a queued/flagged item without opening the review page. + public async Task OnPostQuickDiscardAsync(int id) + { + var raw = await _db.RawListings.FirstOrDefaultAsync(r => r.Id == id); + if (raw is not null) { raw.Status = RawListingStatus.Discarded; await _db.SaveChangesAsync(); } + return RedirectToPage(); + } + public async Task OnPostRunIngestionAsync() { var s = await _ingest.RunAsync(); diff --git a/src/JobsMedical.Web/Pages/Admin/Ingested.cshtml b/src/JobsMedical.Web/Pages/Admin/Ingested.cshtml new file mode 100644 index 0000000..c9e8c86 --- /dev/null +++ b/src/JobsMedical.Web/Pages/Admin/Ingested.cshtml @@ -0,0 +1,68 @@ +@page +@model JobsMedical.Web.Pages.Admin.IngestedModel +@{ + ViewData["Title"] = "نتایج جمع‌آوری"; + string P(int n) => JalaliDate.ToPersianDigits(n.ToString()); + int C(JobsMedical.Web.Models.RawListingStatus s) => Model.Counts.GetValueOrDefault(s); + string Pill(string key, string label, int count) => + $"{label} ({P(count)})"; +} + + + +
+
+

نتایج جمع‌آوری

+

← صف بررسی — همه‌ی آگهی‌های جمع‌آوری‌شده و وضعیت هرکدام.

+
+
+ +
+
+ @Html.Raw(Pill("all", "همه", Model.Counts.Values.Sum())) + @Html.Raw(Pill("new", "در صف", C(JobsMedical.Web.Models.RawListingStatus.New))) + @Html.Raw(Pill("flagged", "پرچم‌خورده", C(JobsMedical.Web.Models.RawListingStatus.Flagged))) + @Html.Raw(Pill("published", "منتشرشده", C(JobsMedical.Web.Models.RawListingStatus.Normalized))) + @Html.Raw(Pill("discarded", "ردشده/اسپم", C(JobsMedical.Web.Models.RawListingStatus.Discarded))) +
+ +

@P(Model.Total) نتیجه (نمایش حداکثر ۲۰۰ مورد اخیر).

+ + @if (Model.Items.Count == 0) + { +
موردی با این فیلتر نیست.
+ } + else + { + foreach (var r in Model.Items) + { + var (cls, label) = r.Status switch + { + JobsMedical.Web.Models.RawListingStatus.New => ("badge-day", "در صف"), + JobsMedical.Web.Models.RawListingStatus.Flagged => ("badge-type", "پرچم‌خورده"), + JobsMedical.Web.Models.RawListingStatus.Normalized => ("badge-verified", "منتشر شد"), + _ => ("badge-gender", "رد شد"), + }; +
+
+ @r.SourceChannel + + @label + اطمینان @P(r.Confidence)٪ + @JalaliDate.ToLongDate(DateOnly.FromDateTime(r.FetchedAt)) + +
+

@(r.RawText.Length > 320 ? r.RawText.Substring(0,320) + "…" : r.RawText)

+ @if (!string.IsNullOrEmpty(r.ValidationNotes)) {

⚠ @r.ValidationNotes

} + @if (r.Status == JobsMedical.Web.Models.RawListingStatus.New || r.Status == JobsMedical.Web.Models.RawListingStatus.Flagged) + { + بررسی و انتشار ← + } + else if (r.LinkedShiftId is int sid) + { + مشاهده آگهی منتشرشده + } +
+ } + } +
diff --git a/src/JobsMedical.Web/Pages/Admin/Ingested.cshtml.cs b/src/JobsMedical.Web/Pages/Admin/Ingested.cshtml.cs new file mode 100644 index 0000000..1cb0c05 --- /dev/null +++ b/src/JobsMedical.Web/Pages/Admin/Ingested.cshtml.cs @@ -0,0 +1,46 @@ +using JobsMedical.Web.Data; +using JobsMedical.Web.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace JobsMedical.Web.Pages.Admin; + +/// Every crawled item with its outcome (queued / published / flagged / discarded), +/// filterable by status and source — the full audit trail of ingestion. +[Authorize(Roles = "Admin")] +public class IngestedModel : PageModel +{ + private readonly AppDbContext _db; + public IngestedModel(AppDbContext db) => _db = db; + + public List Items { get; private set; } = new(); + public int Total { get; private set; } + public Dictionary Counts { get; private set; } = new(); + + [BindProperty(SupportsGet = true)] public string? Status { get; set; } // new|flagged|published|discarded|all + [BindProperty(SupportsGet = true)] public string? Source { get; set; } + + public async Task OnGetAsync() + { + Counts = await _db.RawListings.GroupBy(r => r.Status) + .Select(g => new { g.Key, C = g.Count() }).ToDictionaryAsync(x => x.Key, x => x.C); + + var q = _db.RawListings.AsNoTracking().AsQueryable(); + + var st = Status?.ToLowerInvariant() switch + { + "new" => (RawListingStatus?)RawListingStatus.New, + "flagged" => RawListingStatus.Flagged, + "published" => RawListingStatus.Normalized, + "discarded" => RawListingStatus.Discarded, + _ => null, + }; + if (st is not null) q = q.Where(r => r.Status == st); + if (!string.IsNullOrWhiteSpace(Source)) q = q.Where(r => r.SourceChannel.Contains(Source)); + + Total = await q.CountAsync(); + Items = await q.OrderByDescending(r => r.FetchedAt).Take(200).ToListAsync(); + } +} diff --git a/src/JobsMedical.Web/Pages/Shared/_PanelNav.cshtml b/src/JobsMedical.Web/Pages/Shared/_PanelNav.cshtml index 1a87e65..1ac54c1 100644 --- a/src/JobsMedical.Web/Pages/Shared/_PanelNav.cshtml +++ b/src/JobsMedical.Web/Pages/Shared/_PanelNav.cshtml @@ -14,6 +14,7 @@ { 📊 داشبورد 📥 صف آگهی‌ها + 📜 نتایج جمع‌آوری 🏥 مراکز 👥 کاربران 🛡️ گزارش‌ها diff --git a/src/JobsMedical.Web/Pages/Shared/_RawListingRow.cshtml b/src/JobsMedical.Web/Pages/Shared/_RawListingRow.cshtml index 307cf19..00276c5 100644 --- a/src/JobsMedical.Web/Pages/Shared/_RawListingRow.cshtml +++ b/src/JobsMedical.Web/Pages/Shared/_RawListingRow.cshtml @@ -16,5 +16,11 @@ {

⚠ @Model.ValidationNotes

} - بررسی و انتشار ← +
+ بررسی و انتشار ← +
+ + +
diff --git a/src/JobsMedical.Web/wwwroot/css/site.css b/src/JobsMedical.Web/wwwroot/css/site.css index fc46565..d6f0f64 100644 --- a/src/JobsMedical.Web/wwwroot/css/site.css +++ b/src/JobsMedical.Web/wwwroot/css/site.css @@ -111,6 +111,13 @@ a { color: inherit; text-decoration: none; } } .panel-nav a:hover { background: var(--primary-soft); color: var(--primary-dark); } .panel-nav a.active { background: var(--primary); color: #fff; } + +/* Ingestion-results filter pills */ +.ing-filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; } +.ing-pill { padding: 7px 14px; border-radius: 999px; border: 1px solid var(--line); + font-weight: 600; font-size: 13px; color: var(--muted); white-space: nowrap; } +.ing-pill:hover { border-color: var(--primary); color: var(--primary); } +.ing-pill.active { background: var(--primary); color: #fff; border-color: var(--primary); } .profile-dropdown { position: absolute; top: calc(100% + 8px); inset-inline-end: 0; min-width: 230px; z-index: 60; background: var(--surface); border: 1px solid var(--line); border-radius: 14px;