[Applicant+Admin] Withdraw application, delete account, admin analytics dashboard
CI/CD / CI · dotnet build (push) Successful in 47s
CI/CD / Deploy · hamkadr (push) Successful in 1m28s

Applicant: 'انصراف از درخواست' on /Me removes the Apply event for that shift/job. Account: 'حذف حساب من' on /Me/Profile permanently deletes the user + cascades (profile, alerts, reviews, applications), detaches anonymous visitor history, and signs out (per privacy policy). Admin: /Admin/Analytics dashboard — totals (users, facilities/verified, open shifts/jobs, applications, reviews), 7-day deltas, and a 14-day applications bar chart; linked from Overview alongside the new نظرات moderation page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 07:52:49 +03:30
parent d87afb577c
commit aa61efd46f
7 changed files with 143 additions and 0 deletions
@@ -92,6 +92,9 @@
<div>
<span class="badge @b.cls" style="margin-bottom:6px; display:inline-block;">@b.txt</span>
<partial name="_ShiftCard" model="s" />
<form method="post" asp-page-handler="WithdrawShift" asp-route-id="@s.Id" onsubmit="return confirm('از این فرصت انصراف می‌دهی؟');">
<button class="btn btn-outline" style="width:100%; padding:5px; font-size:12px; margin-top:6px; color:var(--danger); border-color:var(--danger);">انصراف از درخواست</button>
</form>
</div>
}
@foreach (var j in Model.AppliedJobs)
@@ -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<IActionResult> OnPostWithdrawShiftAsync(int id) => WithdrawAsync(id, isJob: false);
public Task<IActionResult> OnPostWithdrawJobAsync(int id) => WithdrawAsync(id, isJob: true);
private async Task<IActionResult> 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<List<Shift>> ShiftsByIds(List<int> ids) => _db.Shifts
.Include(s => s.Facility).ThenInclude(f => f.City)
.Include(s => s.Facility).ThenInclude(f => f.District)
@@ -95,4 +95,12 @@
<button type="submit" class="btn btn-accent btn-block btn-lg">ذخیره پروفایل</button>
</form>
<div class="card card-pad" style="margin-top:14px; border-color:var(--danger);">
<h3 style="margin-top:0; color:var(--danger);">حذف حساب کاربری</h3>
<p class="muted" style="font-size:13px;">با حذف حساب، اطلاعات پروفایل، رزومه، هشدارها و درخواست‌های شما حذف می‌شود. این کار بازگشت‌ناپذیر است.</p>
<form method="post" asp-page-handler="DeleteAccount" onsubmit="return confirm('آیا از حذف کامل حساب خود مطمئنی؟ این کار بازگشت‌ناپذیر است.');">
<button type="submit" class="btn btn-outline" style="color:var(--danger); border-color:var(--danger);">حذف حساب من</button>
</form>
</div>
</div>
@@ -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();
}
/// <summary>Permanently delete the account + its data (per the privacy policy).</summary>
public async Task<IActionResult> 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();