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>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user