Files
hamkadr/src/JobsMedical.Web/Services/InterestService.cs
T
soroush.asadi 2fb86a435e Initial commit — Hamkadr (همکادر) healthcare-staffing marketplace
ASP.NET Core 10 Razor Pages + PostgreSQL/EF Core. RTL Persian, Jalali dates, self-hosted Vazirmatn, teal/coral brand.

Features:
- Shift listings: browse/filter (city, district, role, type, pay), weekly Jalali calendar, detail + interest handoff, near-me distance sort
- Hiring (استخدام) listings with employment type + salary range
- Pattern-engine recommendations + anonymous interest tracking (visitor cookie)
- Heuristic Persian listing-parser + admin queue (raw channel post → shift/job)
- Phone-OTP cookie auth + visitor-history linking + profile

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 01:44:24 +03:30

103 lines
3.2 KiB
C#

using JobsMedical.Web.Data;
using JobsMedical.Web.Models;
using Microsoft.EntityFrameworkCore;
namespace JobsMedical.Web.Services;
/// <summary>
/// Persists visitor preferences and behavioral events. Creates the <see cref="Visitor"/> row
/// lazily on first write, so anonymous browsing doesn't hit the DB on every request.
/// </summary>
public class InterestService
{
private readonly AppDbContext _db;
private readonly VisitorContext _visitor;
public InterestService(AppDbContext db, VisitorContext visitor)
{
_db = db;
_visitor = visitor;
}
public string VisitorId => _visitor.VisitorId;
private async Task EnsureVisitorAsync()
{
var id = VisitorId;
if (string.IsNullOrEmpty(id)) return;
var exists = await _db.Visitors.AnyAsync(v => v.Id == id);
if (!exists)
{
_db.Visitors.Add(new Visitor { Id = id });
await _db.SaveChangesAsync();
}
else
{
await _db.Visitors.Where(v => v.Id == id)
.ExecuteUpdateAsync(s => s.SetProperty(v => v.LastSeenAt, DateTime.UtcNow));
}
}
public async Task LogAsync(InterestEventType type, int shiftId)
{
if (string.IsNullOrEmpty(VisitorId)) return;
await EnsureVisitorAsync();
_db.InterestEvents.Add(new InterestEvent
{
VisitorId = VisitorId,
ShiftId = shiftId,
EventType = type,
});
await _db.SaveChangesAsync();
}
public async Task LogJobAsync(InterestEventType type, int jobOpeningId)
{
if (string.IsNullOrEmpty(VisitorId)) return;
await EnsureVisitorAsync();
_db.InterestEvents.Add(new InterestEvent
{
VisitorId = VisitorId,
JobOpeningId = jobOpeningId,
EventType = type,
});
await _db.SaveChangesAsync();
}
public Task<UserPreferences?> GetPreferencesAsync()
{
var id = VisitorId;
if (string.IsNullOrEmpty(id)) return Task.FromResult<UserPreferences?>(null);
return _db.UserPreferences.AsNoTracking().FirstOrDefaultAsync(p => p.VisitorId == id);
}
public async Task SavePreferencesAsync(int? roleId, int? cityId, ShiftType? shiftType, long? minPay)
{
await EnsureVisitorAsync();
var prefs = await _db.UserPreferences.FirstOrDefaultAsync(p => p.VisitorId == VisitorId);
if (prefs is null)
{
prefs = new UserPreferences { VisitorId = VisitorId };
_db.UserPreferences.Add(prefs);
}
prefs.RoleId = roleId;
prefs.CityId = cityId;
prefs.PreferredShiftType = shiftType;
prefs.MinPay = minPay;
prefs.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
}
/// <summary>Recent events for this visitor (newest first) — the behavioral signal.</summary>
public Task<List<InterestEvent>> RecentEventsAsync(int take = 100)
{
var id = VisitorId;
if (string.IsNullOrEmpty(id)) return Task.FromResult(new List<InterestEvent>());
return _db.InterestEvents.AsNoTracking()
.Where(e => e.VisitorId == id)
.OrderByDescending(e => e.CreatedAt)
.Take(take)
.ToListAsync();
}
}