From 704b68be169f7259d4d165fca02dcecd84d296d7 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sat, 20 Jun 2026 14:30:08 +0330 Subject: [PATCH] Search typeahead: show total found count in the dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /search/suggest endpoint now returns { items, total } — each filtered query is reused for both the Take(5) preview and a CountAsync — and the dropdown's footer link reads «مشاهده همه N نتیجه برای «q»» (Persian digits) instead of a bare «همه نتایج». The /Search page already showed counts. Co-Authored-By: Claude Opus 4.8 --- .../Pages/Shared/_Layout.cshtml | 9 +++-- src/JobsMedical.Web/Program.cs | 34 ++++++++++--------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/JobsMedical.Web/Pages/Shared/_Layout.cshtml b/src/JobsMedical.Web/Pages/Shared/_Layout.cshtml index 9a312a2..73ee605 100644 --- a/src/JobsMedical.Web/Pages/Shared/_Layout.cshtml +++ b/src/JobsMedical.Web/Pages/Shared/_Layout.cshtml @@ -254,15 +254,18 @@ timer = setTimeout(function () { fetch('/search/suggest?q=' + encodeURIComponent(q)) .then(function (r) { return r.json(); }) - .then(function (items) { - if (!items || !items.length) { hide(); return; } + .then(function (data) { + var items = (data && data.items) || []; + var total = (data && data.total) || items.length; + if (!items.length) { hide(); return; } + function fa(n) { return String(n).replace(/[0-9]/g, function (d) { return '۰۱۲۳۴۵۶۷۸۹'[+d]; }); } var html = items.map(function (it) { var sub = it.sub ? '' + hi(it.sub, q) + '' : ''; return '' + esc(it.type) + '' + hi(it.label, q) + '' + sub + ''; }).join(''); - html += 'همه نتایج برای «' + esc(q) + '» ←'; + html += 'مشاهده همه ' + fa(total) + ' نتیجه برای «' + esc(q) + '» ←'; box.innerHTML = html; box.style.display = 'block'; }).catch(function () { hide(); }); diff --git a/src/JobsMedical.Web/Program.cs b/src/JobsMedical.Web/Program.cs index 1eb5c0d..0222a72 100644 --- a/src/JobsMedical.Web/Program.cs +++ b/src/JobsMedical.Web/Program.cs @@ -473,30 +473,32 @@ app.MapGet("/search/suggest", async (string? q, AppDbContext db) => return fallback; } - var shiftRows = await db.Shifts - .Where(s => s.Status == ShiftStatus.Open && s.Date >= today && - (EF.Functions.ILike(s.Facility.Name, like) || EF.Functions.ILike(s.Role.Name, like) - || EF.Functions.ILike(s.SpecialtyRequired, like) || EF.Functions.ILike(s.Description ?? "", like))) - .OrderByDescending(s => s.CreatedAt).Take(5) + // Define each filtered query once, then reuse it for BOTH the Take(5) preview and the total count. + var shiftQ = db.Shifts.Where(s => s.Status == ShiftStatus.Open && s.Date >= today && + (EF.Functions.ILike(s.Facility.Name, like) || EF.Functions.ILike(s.Role.Name, like) + || EF.Functions.ILike(s.SpecialtyRequired, like) || EF.Functions.ILike(s.Description ?? "", like))); + var jobQ = db.JobOpenings.Where(j => j.Status == ShiftStatus.Open && j.CreatedAt >= jobCut && + (EF.Functions.ILike(j.Title, like) || EF.Functions.ILike(j.Facility.Name, like) + || EF.Functions.ILike(j.Role.Name, like) || EF.Functions.ILike(j.Description ?? "", like))); + var talentQ = db.TalentListings.Where(t => t.Status == ShiftStatus.Open && t.CreatedAt >= talentCut && + (EF.Functions.ILike(t.Tags ?? "", like) || EF.Functions.ILike(t.Role.Name, like) || EF.Functions.ILike(t.City.Name, like) + || EF.Functions.ILike(t.PersonName ?? "", like) || EF.Functions.ILike(t.Description ?? "", like))); + + var shiftRows = await shiftQ.OrderByDescending(s => s.CreatedAt).Take(5) .Select(s => new { s.Id, Role = s.Role.Name, Fac = s.Facility.Name, City = s.Facility.City.Name, s.Description }).ToListAsync(); var shifts = shiftRows.Select(s => new SuggestItem("شیفت", s.Role + " — " + s.Fac, "/Shifts/Details/" + s.Id, Snip(s.Description, term, s.City))).ToList(); - var jobRows = await db.JobOpenings - .Where(j => j.Status == ShiftStatus.Open && j.CreatedAt >= jobCut && - (EF.Functions.ILike(j.Title, like) || EF.Functions.ILike(j.Facility.Name, like) - || EF.Functions.ILike(j.Role.Name, like) || EF.Functions.ILike(j.Description ?? "", like))) - .OrderByDescending(j => j.CreatedAt).Take(5) + var jobRows = await jobQ.OrderByDescending(j => j.CreatedAt).Take(5) .Select(j => new { j.Id, j.Title, Fac = j.Facility.Name, City = j.Facility.City.Name, j.Description }).ToListAsync(); var jobs = jobRows.Select(j => new SuggestItem("استخدام", j.Title, "/Jobs/Details/" + j.Id, Snip(j.Description, term, j.Fac + " · " + j.City))).ToList(); - var talentRows = await db.TalentListings - .Where(t => t.Status == ShiftStatus.Open && t.CreatedAt >= talentCut && - (EF.Functions.ILike(t.Tags ?? "", like) || EF.Functions.ILike(t.Role.Name, like) || EF.Functions.ILike(t.City.Name, like) - || EF.Functions.ILike(t.PersonName ?? "", like) || EF.Functions.ILike(t.Description ?? "", like))) - .OrderByDescending(t => t.CreatedAt).Take(5) + var talentRows = await talentQ.OrderByDescending(t => t.CreatedAt).Take(5) .Select(t => new { t.Id, t.PersonName, Role = t.Role.Name, City = t.City.Name, t.Tags, t.Description }).ToListAsync(); var talent = talentRows.Select(t => new SuggestItem("آماده‌به‌کار", (t.PersonName ?? t.Role) + " — " + t.City, "/Talent/Details/" + t.Id, Snip(t.Description ?? t.Tags, term, t.Tags))).ToList(); + // Total matches across all three types (drives the result count shown in the dropdown). + var total = await shiftQ.CountAsync() + await jobQ.CountAsync() + await talentQ.CountAsync(); + // round-robin merge so all three types appear, capped at 5 var merged = new List(); for (var i = 0; i < 5 && merged.Count < 5; i++) @@ -505,7 +507,7 @@ app.MapGet("/search/suggest", async (string? q, AppDbContext db) => if (merged.Count < 5 && i < jobs.Count) merged.Add(jobs[i]); if (merged.Count < 5 && i < talent.Count) merged.Add(talent[i]); } - return Results.Json(merged.Take(5)); + return Results.Json(new { items = merged.Take(5), total }); }); app.Run();