diff --git a/src/JobsMedical.Web/Program.cs b/src/JobsMedical.Web/Program.cs index 2c183e2..b6e4544 100644 --- a/src/JobsMedical.Web/Program.cs +++ b/src/JobsMedical.Web/Program.cs @@ -369,21 +369,46 @@ app.MapGet("/search/suggest", async (string? q, AppDbContext db) => var jobCut = JobsMedical.Web.Services.Scraping.ListingPolicy.JobCutoffUtc; var talentCut = JobsMedical.Web.Services.Scraping.ListingPolicy.TalentCutoffUtc; - var shifts = await db.Shifts + // Plain (un-marked) snippet around the first occurrence of the term — the client highlights it. + static string? Snip(string? text, string term, string? fallback) + { + if (!string.IsNullOrWhiteSpace(text)) + { + var flat = System.Text.RegularExpressions.Regex.Replace(text, @"\s+", " ").Trim(); + var i = flat.IndexOf(term, StringComparison.OrdinalIgnoreCase); + if (i >= 0) + { + var start = Math.Max(0, i - 40); + var end = Math.Min(flat.Length, i + term.Length + 40); + return (start > 0 ? "…" : "") + flat.Substring(start, end - start) + (end < flat.Length ? "…" : ""); + } + } + 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.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) - .Select(s => new SuggestItem("شیفت", s.Role.Name + " — " + s.Facility.Name, "/Shifts/Details/" + s.Id, s.Facility.City.Name + " · " + s.SpecialtyRequired)).ToListAsync(); - var jobs = await db.JobOpenings + .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.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) - .Select(j => new SuggestItem("استخدام", j.Title, "/Jobs/Details/" + j.Id, j.Facility.Name + " · " + j.Facility.City.Name)).ToListAsync(); - var talent = await db.TalentListings + .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.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) - .Select(t => new SuggestItem("آماده‌به‌کار", (t.PersonName ?? t.Role.Name) + " — " + t.City.Name, "/Talent/Details/" + t.Id, t.Tags)).ToListAsync(); + .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(); // round-robin merge so all three types appear, capped at 5 var merged = new List(); diff --git a/src/JobsMedical.Web/wwwroot/css/site.css b/src/JobsMedical.Web/wwwroot/css/site.css index 9678b3d..e735569 100644 --- a/src/JobsMedical.Web/wwwroot/css/site.css +++ b/src/JobsMedical.Web/wwwroot/css/site.css @@ -294,9 +294,10 @@ mark { background: #fff3bf; color: inherit; padding: 0 2px; border-radius: 3px; .nav-search-results a:hover { background: var(--primary-soft); } .nav-search-results .ns-type { flex: 0 0 auto; font-size: 11px; font-weight: 700; color: var(--primary-dark); background: var(--primary-soft); border-radius: 6px; padding: 2px 7px; } -.nav-search-results .ns-text { flex: 1; display: flex; flex-direction: column; min-width: 0; } -.nav-search-results .ns-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.nav-search-results .ns-sub { font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.nav-search-results .ns-text { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.nav-search-results .ns-label { font-weight: 800; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.nav-search-results .ns-sub { font-size: 11.5px; color: var(--muted); line-height: 1.5; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } /* ES-style matched snippet shown under a search-result card */ .search-snippet { font-size: 12.5px; color: var(--muted); line-height: 1.6; margin: 4px 0 2px; background: var(--bg); border-inline-start: 3px solid var(--primary-soft); padding: 5px 9px; border-radius: 6px; }