using System.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using ROLAC.API.Data; using ROLAC.API.DTOs.Members; using ROLAC.API.DTOs.Shared; using ROLAC.API.Entities; namespace ROLAC.API.Services; public class MemberService : IMemberService { private readonly AppDbContext _db; private readonly IHttpContextAccessor _http; public MemberService(AppDbContext db, IHttpContextAccessor http) { _db = db; _http = http; } private string CurrentUserId => _http.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system"; // ── GetPaged ───────────────────────────────────────────────────────────── public async Task> GetPagedAsync( int page, int pageSize, string? search, string? status, bool? hasUser) { var query = _db.Members.AsQueryable(); if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); query = query.Where(m => m.FirstName_en.ToLower().Contains(s) || m.LastName_en.ToLower().Contains(s) || (m.NickName != null && m.NickName.ToLower().Contains(s)) || (m.FirstName_zh != null && m.FirstName_zh.Contains(search)) || (m.LastName_zh != null && m.LastName_zh.Contains(search)) || (m.Email != null && m.Email.ToLower().Contains(s))); } if (!string.IsNullOrWhiteSpace(status)) query = query.Where(m => m.Status == status); // Build a lookup of MemberId → UserId for hasUser filtering and projection. // Done as a separate query so InMemory provider works without cross-DbSet joins. var memberUserMap = await _db.Users .Where(u => u.MemberId != null) .Select(u => new { u.MemberId, u.Id }) .ToDictionaryAsync(u => u.MemberId!.Value, u => u.Id); if (hasUser.HasValue) { var linkedMemberIds = memberUserMap.Keys.ToHashSet(); query = hasUser.Value ? query.Where(m => linkedMemberIds.Contains(m.Id)) : query.Where(m => !linkedMemberIds.Contains(m.Id)); } var total = await query.CountAsync(); var members = await query .OrderBy(m => m.LastName_en).ThenBy(m => m.FirstName_en) .Skip((page - 1) * pageSize).Take(pageSize) .AsNoTracking() .ToListAsync(); var items = members.Select(m => new MemberListItemDto { Id = m.Id, FirstName_en = m.FirstName_en, LastName_en = m.LastName_en, NickName = m.NickName, FirstName_zh = m.FirstName_zh, LastName_zh = m.LastName_zh, Status = m.Status, Email = m.Email, PhoneCell = m.PhoneCell, JoinDate = m.JoinDate, LinkedUserId = memberUserMap.TryGetValue(m.Id, out var uid) ? uid : null, }).ToList(); return new PagedResult { Items = items, TotalCount = total, Page = page, PageSize = pageSize }; } // ── GetById ────────────────────────────────────────────────────────────── public async Task GetByIdAsync(int id) { var m = await _db.Members .AsNoTracking() .FirstOrDefaultAsync(x => x.Id == id); if (m is null) return null; // Separate lookup for linked user — avoids InMemory cross-DbSet join issues. var linkedUser = await _db.Users .Where(u => u.MemberId == id) .Select(u => u.Id) .FirstOrDefaultAsync(); return new MemberDto { Id = m.Id, FirstName_en = m.FirstName_en, LastName_en = m.LastName_en, NickName = m.NickName, FirstName_zh = m.FirstName_zh, LastName_zh = m.LastName_zh, Gender = m.Gender, DateOfBirth = m.DateOfBirth, BaptismDate = m.BaptismDate, BaptismChurch = m.BaptismChurch, Email = m.Email, PhoneCell = m.PhoneCell, PhoneHome = m.PhoneHome, Address = m.Address, City = m.City, State = m.State, ZipCode = m.ZipCode, Country = m.Country, PhotoBlobPath = m.PhotoBlobPath, Status = m.Status, LanguagePreference = m.LanguagePreference, JoinDate = m.JoinDate, Notes = m.Notes, FamilyUnitId = m.FamilyUnitId, LinkedUserId = linkedUser, CreatedAt = m.CreatedAt, UpdatedAt = m.UpdatedAt, }; } // ── Create ─────────────────────────────────────────────────────────────── public async Task CreateAsync(CreateMemberRequest r) { var member = MapFromRequest(r); _db.Members.Add(member); await _db.SaveChangesAsync(); return member.Id; } // ── Update ─────────────────────────────────────────────────────────────── public async Task UpdateAsync(int id, UpdateMemberRequest r) { var m = await _db.Members.FindAsync(id) ?? throw new KeyNotFoundException($"Member {id} not found."); ApplyRequest(m, r); await _db.SaveChangesAsync(); } // ── Delete (soft) ──────────────────────────────────────────────────────── public async Task DeleteAsync(int id) { var m = await _db.Members.FindAsync(id) ?? throw new KeyNotFoundException($"Member {id} not found."); m.IsDeleted = true; m.DeletedAt = DateTimeOffset.UtcNow; m.DeletedBy = CurrentUserId; await _db.SaveChangesAsync(); // Detach so that subsequent FindAsync calls bypass the identity cache and // hit the store — where the global query filter (IsDeleted = false) applies. _db.Entry(m).State = Microsoft.EntityFrameworkCore.EntityState.Detached; } // ── Helpers ────────────────────────────────────────────────────────────── private static Member MapFromRequest(CreateMemberRequest r) => new() { FirstName_en = r.FirstName_en, LastName_en = r.LastName_en, NickName = r.NickName, FirstName_zh = r.FirstName_zh, LastName_zh = r.LastName_zh, Gender = r.Gender, DateOfBirth = r.DateOfBirth, BaptismDate = r.BaptismDate, BaptismChurch = r.BaptismChurch, Email = r.Email, PhoneCell = r.PhoneCell, PhoneHome = r.PhoneHome, Address = r.Address, City = r.City, State = r.State, ZipCode = r.ZipCode, Country = r.Country, Status = r.Status, LanguagePreference = r.LanguagePreference, JoinDate = r.JoinDate, Notes = r.Notes, FamilyUnitId = r.FamilyUnitId, }; private static void ApplyRequest(Member m, CreateMemberRequest r) { m.FirstName_en = r.FirstName_en; m.LastName_en = r.LastName_en; m.NickName = r.NickName; m.FirstName_zh = r.FirstName_zh; m.LastName_zh = r.LastName_zh; m.Gender = r.Gender; m.DateOfBirth = r.DateOfBirth; m.BaptismDate = r.BaptismDate; m.BaptismChurch = r.BaptismChurch; m.Email = r.Email; m.PhoneCell = r.PhoneCell; m.PhoneHome = r.PhoneHome; m.Address = r.Address; m.City = r.City; m.State = r.State; m.ZipCode = r.ZipCode; m.Country = r.Country; m.Status = r.Status; m.LanguagePreference = r.LanguagePreference; m.JoinDate = r.JoinDate; m.Notes = r.Notes; m.FamilyUnitId = r.FamilyUnitId; } }