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) در ۷ روز
+
+
+
+
+
اعلام تمایل — ۱۴ روز اخیر
+
+ @foreach (var b in Model.ApplyByDay)
+ {
+ var h = (int)(b.Count / (double)Model.MaxBar * 120) + 2;
+
+ }
+
+
+
+
+
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 @@
}
@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();