diff --git a/src/JobsMedical.Web/Pages/Admin/Analytics.cshtml b/src/JobsMedical.Web/Pages/Admin/Analytics.cshtml new file mode 100644 index 0000000..ec3f7a4 --- /dev/null +++ b/src/JobsMedical.Web/Pages/Admin/Analytics.cshtml @@ -0,0 +1,45 @@ +@page +@model JobsMedical.Web.Pages.Admin.AnalyticsModel +@{ + ViewData["Title"] = "آمار و تحلیل"; + string Fa(int n) => JalaliDate.ToPersianDigits(n.ToString()); +} +
+
+

📊 آمار و تحلیل

+

← پنل مدیریت

+
+
+ +
+
+
کاربران
@Fa(Model.Users)
+@Fa(Model.NewUsers7) در ۷ روز
+
مراکز
@Fa(Model.Facilities)
@Fa(Model.VerifiedFacilities) تأییدشده
+
شیفت‌های باز
@Fa(Model.OpenShifts)
+
استخدام‌های باز
@Fa(Model.OpenJobs)
+
اعلام تمایل‌ها
@Fa(Model.Applications)
+@Fa(Model.NewApps7) در ۷ روز
+
نظرات
@Fa(Model.Reviews)
+
+ +
+

اعلام تمایل — ۱۴ روز اخیر

+
+ @foreach (var b in Model.ApplyByDay) + { + var h = (int)(b.Count / (double)Model.MaxBar * 120) + 2; +
+
+ @Fa(b.Day.Day) +
+ } +
+
+ +
+ صف آگهی‌ها + مراکز + نظرات + گزارش‌ها + کاربران +
+
diff --git a/src/JobsMedical.Web/Pages/Admin/Analytics.cshtml.cs b/src/JobsMedical.Web/Pages/Admin/Analytics.cshtml.cs new file mode 100644 index 0000000..f23cdde --- /dev/null +++ b/src/JobsMedical.Web/Pages/Admin/Analytics.cshtml.cs @@ -0,0 +1,56 @@ +using JobsMedical.Web.Data; +using JobsMedical.Web.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace JobsMedical.Web.Pages.Admin; + +[Authorize(Roles = "Admin")] +public class AnalyticsModel : PageModel +{ + private readonly AppDbContext _db; + public AnalyticsModel(AppDbContext db) => _db = db; + + public int Users { get; private set; } + public int Facilities { get; private set; } + public int VerifiedFacilities { get; private set; } + public int OpenShifts { get; private set; } + public int OpenJobs { get; private set; } + public int Applications { get; private set; } + public int Reviews { get; private set; } + public int NewUsers7 { get; private set; } + public int NewApps7 { get; private set; } + + public record DayBar(DateOnly Day, int Count); + public List ApplyByDay { get; private set; } = new(); + public int MaxBar { get; private set; } = 1; + + public async Task OnGetAsync() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + Users = await _db.Users.CountAsync(); + Facilities = await _db.Facilities.CountAsync(); + VerifiedFacilities = await _db.Facilities.CountAsync(f => f.IsVerified); + OpenShifts = await _db.Shifts.CountAsync(s => s.Status == ShiftStatus.Open && s.Date >= today); + OpenJobs = await _db.JobOpenings.CountAsync(j => j.Status == ShiftStatus.Open); + Applications = await _db.InterestEvents.CountAsync(e => e.EventType == InterestEventType.Apply); + Reviews = await _db.Reviews.CountAsync(); + + var since7 = DateTime.UtcNow.AddDays(-7); + NewUsers7 = await _db.Users.CountAsync(u => u.CreatedAt >= since7); + NewApps7 = await _db.InterestEvents.CountAsync(e => e.EventType == InterestEventType.Apply && e.CreatedAt >= since7); + + var since14 = DateTime.UtcNow.Date.AddDays(-13); + var stamps = await _db.InterestEvents + .Where(e => e.EventType == InterestEventType.Apply && e.CreatedAt >= since14) + .Select(e => e.CreatedAt).ToListAsync(); + var byDay = stamps.GroupBy(d => DateOnly.FromDateTime(d.Date)).ToDictionary(g => g.Key, g => g.Count()); + for (var i = 0; i < 14; i++) + { + var day = DateOnly.FromDateTime(since14).AddDays(i); + ApplyByDay.Add(new DayBar(day, byDay.GetValueOrDefault(day))); + } + MaxBar = Math.Max(1, ApplyByDay.Count > 0 ? ApplyByDay.Max(b => b.Count) : 1); + } +} diff --git a/src/JobsMedical.Web/Pages/Admin/Overview.cshtml b/src/JobsMedical.Web/Pages/Admin/Overview.cshtml index 6280d55..e9be931 100644 --- a/src/JobsMedical.Web/Pages/Admin/Overview.cshtml +++ b/src/JobsMedical.Web/Pages/Admin/Overview.cshtml @@ -14,6 +14,8 @@ مراکز · گزارش‌ها · ارسال اعلان · + نظرات · + آمار · تنظیمات

diff --git a/src/JobsMedical.Web/Pages/Me/Index.cshtml b/src/JobsMedical.Web/Pages/Me/Index.cshtml index dbc0a70..3a1e07d 100644 --- a/src/JobsMedical.Web/Pages/Me/Index.cshtml +++ b/src/JobsMedical.Web/Pages/Me/Index.cshtml @@ -92,6 +92,9 @@
@b.txt +
+ +
} @foreach (var j in Model.AppliedJobs) diff --git a/src/JobsMedical.Web/Pages/Me/Index.cshtml.cs b/src/JobsMedical.Web/Pages/Me/Index.cshtml.cs index c110bab..6a0a4dd 100644 --- a/src/JobsMedical.Web/Pages/Me/Index.cshtml.cs +++ b/src/JobsMedical.Web/Pages/Me/Index.cshtml.cs @@ -3,6 +3,7 @@ using JobsMedical.Web.Data; using JobsMedical.Web.Models; using JobsMedical.Web.Services; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; @@ -67,6 +68,21 @@ public class IndexModel : PageModel .ToDictionary(g => g.Key, g => g.OrderByDescending(e => e.CreatedAt).First().Status); } + public Task OnPostWithdrawShiftAsync(int id) => WithdrawAsync(id, isJob: false); + public Task OnPostWithdrawJobAsync(int id) => WithdrawAsync(id, isJob: true); + + private async Task WithdrawAsync(int id, bool isJob) + { + var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + var visitorIds = await _db.Visitors.Where(v => v.UserId == userId).Select(v => v.Id).ToListAsync(); + var evs = _db.InterestEvents.Where(e => visitorIds.Contains(e.VisitorId) + && e.EventType == InterestEventType.Apply + && (isJob ? e.JobOpeningId == id : e.ShiftId == id)); + _db.InterestEvents.RemoveRange(evs); + await _db.SaveChangesAsync(); + return RedirectToPage(); + } + private Task> ShiftsByIds(List ids) => _db.Shifts .Include(s => s.Facility).ThenInclude(f => f.City) .Include(s => s.Facility).ThenInclude(f => f.District) diff --git a/src/JobsMedical.Web/Pages/Me/Profile.cshtml b/src/JobsMedical.Web/Pages/Me/Profile.cshtml index 2637d65..83d95d7 100644 --- a/src/JobsMedical.Web/Pages/Me/Profile.cshtml +++ b/src/JobsMedical.Web/Pages/Me/Profile.cshtml @@ -95,4 +95,12 @@ + +
+

حذف حساب کاربری

+

با حذف حساب، اطلاعات پروفایل، رزومه، هشدارها و درخواست‌های شما حذف می‌شود. این کار بازگشت‌ناپذیر است.

+
+ +
+
diff --git a/src/JobsMedical.Web/Pages/Me/Profile.cshtml.cs b/src/JobsMedical.Web/Pages/Me/Profile.cshtml.cs index 6c189d0..121e7f9 100644 --- a/src/JobsMedical.Web/Pages/Me/Profile.cshtml.cs +++ b/src/JobsMedical.Web/Pages/Me/Profile.cshtml.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using JobsMedical.Web.Data; using JobsMedical.Web.Models; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -115,6 +116,18 @@ public class ProfileModel : PageModel return RedirectToPage(); } + /// Permanently delete the account + its data (per the privacy policy). + public async Task OnPostDeleteAccountAsync() + { + var uid = Uid; + // Detach anonymous browsing history (keep events, drop the user link), then remove the user. + await _db.Visitors.Where(v => v.UserId == uid) + .ExecuteUpdateAsync(s => s.SetProperty(v => v.UserId, (int?)null)); + await _db.Users.Where(u => u.Id == uid).ExecuteDeleteAsync(); // cascades profile/alerts/reviews/applications + await HttpContext.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); + return RedirectToPage("/Index"); + } + private async Task LoadListsAsync() { Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();