using JobsMedical.Web.Data; using JobsMedical.Web.Models; using Microsoft.EntityFrameworkCore; namespace JobsMedical.Web.Services; /// /// Persists visitor preferences and behavioral events. Creates the row /// lazily on first write, so anonymous browsing doesn't hit the DB on every request. /// 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 GetPreferencesAsync() { var id = VisitorId; if (string.IsNullOrEmpty(id)) return Task.FromResult(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(); } /// Recent events for this visitor (newest first) — the behavioral signal. public Task> RecentEventsAsync(int take = 100) { var id = VisitorId; if (string.IsNullOrEmpty(id)) return Task.FromResult(new List()); return _db.InterestEvents.AsNoTracking() .Where(e => e.VisitorId == id) .OrderByDescending(e => e.CreatedAt) .Take(take) .ToListAsync(); } }