Add scrape/ingestion engine + validation, and 24h shift hour-range visualization
Scrape engine (Services/Scraping/): pluggable IListingSource (working sample + Telegram/Divar credential-ready stubs) → IngestionService (content-hash dedupe → parse → validate → review queue) → ListingValidator (completeness score + spam screen) → IngestionWorker (config-gated hosted service). RawListing gains ContentHash/Confidence/ValidationNotes; RawListingStatus.Flagged. Admin /Admin gets run-now, source list, confidence + flagged queue. Hour-range viz: _HourBar 24h timeline bar (colored by type, overnight wrap) on shift cards, recommendation cards, and detail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
@model JobsMedical.Web.Models.Shift
|
||||
@using System.Globalization
|
||||
@{
|
||||
var s = Model;
|
||||
var ci = CultureInfo.InvariantCulture;
|
||||
int sm = s.StartTime.Hour * 60 + s.StartTime.Minute;
|
||||
int em = s.EndTime.Hour * 60 + s.EndTime.Minute;
|
||||
var typeClass = s.ShiftType switch
|
||||
{
|
||||
ShiftType.Day => "day",
|
||||
ShiftType.Evening => "evening",
|
||||
ShiftType.Night => "night",
|
||||
_ => "oncall",
|
||||
};
|
||||
|
||||
// Build one or two segments (overnight shifts wrap past midnight). On-call = whole day.
|
||||
var segs = new List<(double left, double width)>();
|
||||
if (s.ShiftType == ShiftType.OnCall || em == sm)
|
||||
segs.Add((0, 100));
|
||||
else if (em > sm)
|
||||
segs.Add((sm / 1440.0 * 100, (em - sm) / 1440.0 * 100));
|
||||
else
|
||||
{
|
||||
segs.Add((sm / 1440.0 * 100, (1440 - sm) / 1440.0 * 100));
|
||||
segs.Add((0, em / 1440.0 * 100));
|
||||
}
|
||||
string Pct(double v) => v.ToString("0.##", ci);
|
||||
}
|
||||
<div class="hourbar-wrap" title="@JalaliDate.Time(s.StartTime) تا @JalaliDate.Time(s.EndTime)">
|
||||
<div class="hourbar">
|
||||
<span class="hourbar-grid" style="left:25%"></span>
|
||||
<span class="hourbar-grid" style="left:50%"></span>
|
||||
<span class="hourbar-grid" style="left:75%"></span>
|
||||
@foreach (var seg in segs)
|
||||
{
|
||||
<span class="hourbar-fill @typeClass" style="left:@Pct(seg.left)%; width:@Pct(seg.width)%"></span>
|
||||
}
|
||||
</div>
|
||||
<div class="hourbar-axis">
|
||||
<span>۰</span><span>۶</span><span>۱۲</span><span>۱۸</span><span>۲۴</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
@model JobsMedical.Web.Models.RawListing
|
||||
@{
|
||||
var c = Model.Confidence;
|
||||
var confClass = c >= 70 ? "badge-verified" : c >= 50 ? "badge-day" : "badge-type";
|
||||
}
|
||||
<div class="card card-pad" style="margin-bottom:12px;">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center; gap:8px; flex-wrap:wrap;">
|
||||
<strong>@Model.SourceChannel</strong>
|
||||
<span style="display:flex; gap:8px; align-items:center;">
|
||||
<span class="badge @confClass">اطمینان @JalaliDate.ToPersianDigits(c.ToString())٪</span>
|
||||
<span class="muted" style="font-size:12px;">@JalaliDate.ToLongDate(DateOnly.FromDateTime(Model.FetchedAt))</span>
|
||||
</span>
|
||||
</div>
|
||||
<p style="margin:10px 0; white-space:pre-wrap;">@Model.RawText</p>
|
||||
@if (!string.IsNullOrEmpty(Model.ValidationNotes))
|
||||
{
|
||||
<p class="muted" style="font-size:12.5px; margin:0 0 10px;">⚠ @Model.ValidationNotes</p>
|
||||
}
|
||||
<a class="btn btn-accent" asp-page="/Admin/Review" asp-route-id="@Model.Id">بررسی و انتشار ←</a>
|
||||
</div>
|
||||
@@ -22,6 +22,7 @@
|
||||
<span>📍 @s.Facility?.City?.Name</span>
|
||||
</div>
|
||||
<div class="row">📅 @JalaliDate.WeekDayName(s.Date)، @JalaliDate.ToLongDate(s.Date) — 🕐 @JalaliDate.Time(s.StartTime)</div>
|
||||
<partial name="_HourBar" model="s" />
|
||||
|
||||
@* The "why" — what makes a pattern engine trustworthy: every pick is explained. *@
|
||||
<div class="rec-reasons">
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
}
|
||||
<div class="row">📅 @JalaliDate.WeekDayName(Model.Date)، @JalaliDate.ToLongDate(Model.Date)</div>
|
||||
<div class="row">🕐 @JalaliDate.Time(Model.StartTime) تا @JalaliDate.Time(Model.EndTime)</div>
|
||||
<partial name="_HourBar" model="Model" />
|
||||
<div class="foot">
|
||||
<span class="pay">@JalaliDate.PayLabel(Model.PayType, Model.PayAmount, Model.SharePercent)</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">جزئیات</span>
|
||||
|
||||
Reference in New Issue
Block a user