diff --git a/API/ROLAC.API/DTOs/Expense/ExpenseDtos.cs b/API/ROLAC.API/DTOs/Expense/ExpenseDtos.cs index e0278ab..9527125 100644 --- a/API/ROLAC.API/DTOs/Expense/ExpenseDtos.cs +++ b/API/ROLAC.API/DTOs/Expense/ExpenseDtos.cs @@ -26,7 +26,8 @@ public class ExpenseListItemDto public string PrimaryCategoryName { get; set; } = ""; // first line's category (list hint; full breakdown via detail) public string? VendorName { get; set; } public int? MemberId { get; set; } - public string? MemberName { get; set; } + public string? MemberName { get; set; } // legal name "FirstName_en LastName_en" (used on the printed check) + public string? MemberNickName { get; set; } // "NickName LastName_en"; null when the member has no distinct nickname public string ExpenseDate { get; set; } = ""; // yyyy-MM-dd public bool HasReceipt { get; set; } public string? CheckNumber { get; set; } diff --git a/API/ROLAC.API/Services/ExpenseService.cs b/API/ROLAC.API/Services/ExpenseService.cs index 1a407d9..5c2bfe3 100644 --- a/API/ROLAC.API/Services/ExpenseService.cs +++ b/API/ROLAC.API/Services/ExpenseService.cs @@ -83,8 +83,12 @@ public class ExpenseService : IExpenseService var minNames = await _db.Ministries.AsNoTracking().ToDictionaryAsync(m => m.Id, m => $"{m.Name_en} / {m.Name_zh}"); var grpNames = await _db.ExpenseCategoryGroups.AsNoTracking().ToDictionaryAsync(g => g.Id, g => $"{g.Name_en} / {g.Name_zh}"); var memberIds = rows.Where(r => r.MemberId != null).Select(r => r.MemberId!.Value).ToHashSet(); - var memNames = await _db.Members.AsNoTracking().Where(m => memberIds.Contains(m.Id)) - .ToDictionaryAsync(m => m.Id, m => $"{m.FirstName_en} {m.LastName_en}"); + var memberNames = await _db.Members.AsNoTracking().Where(m => memberIds.Contains(m.Id)) + .Select(m => new { m.Id, m.FirstName_en, m.LastName_en, m.NickName }) + .ToDictionaryAsync( + m => m.Id, + m => new MemberPayeeName($"{m.FirstName_en} {m.LastName_en}", + BuildNickPayeeName(m.NickName, m.FirstName_en, m.LastName_en))); var reviewerNames = await ResolveUserNamesAsync(rows.Select(r => r.ReviewedBy)); // Line count + first line's category, per expense on this page. @@ -108,7 +112,8 @@ public class ExpenseService : IExpenseService LineCount = ls?.Count ?? 0, PrimaryCategoryName = grpNames.GetValueOrDefault(firstGroupId, ""), VendorName = e.VendorName, MemberId = e.MemberId, - MemberName = e.MemberId != null ? memNames.GetValueOrDefault(e.MemberId.Value) : null, + MemberName = e.MemberId != null ? memberNames.GetValueOrDefault(e.MemberId.Value)?.Legal : null, + MemberNickName = e.MemberId != null ? memberNames.GetValueOrDefault(e.MemberId.Value)?.Nick : null, ExpenseDate = e.ExpenseDate.ToString("yyyy-MM-dd"), HasReceipt = e.ReceiptBlobPath != null, CheckNumber = e.CheckNumber, @@ -145,14 +150,41 @@ public class ExpenseService : IExpenseService : (u.Email ?? u.Id)); } + // Member payee names carried to the frontend: the legal name (printed on the check) and an + // optional friendly "NickName LastName" line shown above it. + private sealed record MemberPayeeName(string Legal, string? Nick); + + // Build the friendly "NickName LastName" payee line, or null when the member has no distinct + // nickname (mirrors the frontend memberDisplayName rule: a nickname equal to the first name is not shown). + private static string? BuildNickPayeeName(string? nickName, string firstNameEn, string lastNameEn) + { + bool hasDistinctNickName = !string.IsNullOrWhiteSpace(nickName) && nickName != firstNameEn; + if (!hasDistinctNickName) + { + return null; + } + return $"{nickName} {lastNameEn}"; + } + public async Task GetByIdAsync(int id) { var e = await _db.Expenses.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); if (e is null) return null; var minName = await _db.Ministries.Where(m => m.Id == e.MinistryId).Select(m => m.Name_en).FirstOrDefaultAsync() ?? ""; - string? memName = e.MemberId != null - ? await _db.Members.Where(m => m.Id == e.MemberId).Select(m => m.FirstName_en + " " + m.LastName_en).FirstOrDefaultAsync() - : null; + string? memberName = null; + string? memberNickName = null; + if (e.MemberId != null) + { + var member = await _db.Members.AsNoTracking() + .Where(m => m.Id == e.MemberId) + .Select(m => new { m.FirstName_en, m.LastName_en, m.NickName }) + .FirstOrDefaultAsync(); + if (member != null) + { + memberName = $"{member.FirstName_en} {member.LastName_en}"; + memberNickName = BuildNickPayeeName(member.NickName, member.FirstName_en, member.LastName_en); + } + } var reviewerName = e.ReviewedBy != null ? (await ResolveUserNamesAsync(new[] { e.ReviewedBy })).GetValueOrDefault(e.ReviewedBy) @@ -174,7 +206,7 @@ public class ExpenseService : IExpenseService MinistryId = e.MinistryId, MinistryName = minName, LineCount = lineDtos.Count, PrimaryCategoryName = lineDtos.Count > 0 ? lineDtos[0].CategoryGroupName : "", - VendorName = e.VendorName, MemberId = e.MemberId, MemberName = memName, + VendorName = e.VendorName, MemberId = e.MemberId, MemberName = memberName, MemberNickName = memberNickName, ExpenseDate = e.ExpenseDate.ToString("yyyy-MM-dd"), HasReceipt = e.ReceiptBlobPath != null, CheckNumber = e.CheckNumber, Notes = e.Notes, ReviewNotes = e.ReviewNotes, ReviewedByName = reviewerName, ReviewedAt = e.ReviewedAt, diff --git a/APP/src/app/features/expense/components/expense-review-dialog/expense-review-dialog.component.html b/APP/src/app/features/expense/components/expense-review-dialog/expense-review-dialog.component.html index 5f93b33..f8540b3 100644 --- a/APP/src/app/features/expense/components/expense-review-dialog/expense-review-dialog.component.html +++ b/APP/src/app/features/expense/components/expense-review-dialog/expense-review-dialog.component.html @@ -10,7 +10,17 @@
Date / 日期
{{ expense.expenseDate }}
Ministry / 事工
{{ expense.ministryName }}
-
Payee / 收款人
{{ expense.vendorName || expense.memberName || '—' }}
+
Payee / 收款人
+
+ {{ expense.vendorName }} + + +
{{ expense.memberNickName }}
+
{{ expense.memberName }}
+
+ +
+
Description / 說明
{{ expense.description }}
Status / 狀態
{{ expense.status }}
diff --git a/APP/src/app/features/expense/models/expense.model.ts b/APP/src/app/features/expense/models/expense.model.ts index 07193c5..48cc39d 100644 --- a/APP/src/app/features/expense/models/expense.model.ts +++ b/APP/src/app/features/expense/models/expense.model.ts @@ -24,7 +24,8 @@ export interface ExpenseListItemDto { id: number; type: ExpenseType; status: ExpenseStatus; amount: number; description: string; ministryId: number; ministryName: string; lineCount: number; primaryCategoryName: string; vendorName: string | null; - memberId: number | null; memberName: string | null; expenseDate: string; hasReceipt: boolean; + memberId: number | null; memberName: string | null; memberNickName: string | null; + expenseDate: string; hasReceipt: boolean; checkNumber: string | null; reviewedByName: string | null; reviewedAt: string | null; reviewNotes: string | null; } diff --git a/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html b/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html index 30590ab..bd2b652 100644 --- a/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html +++ b/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html @@ -48,9 +48,16 @@ - + - {{ dataItem.vendorName || dataItem.memberName || '—' }} + {{ dataItem.vendorName }} + + +
{{ dataItem.memberNickName }}
+
{{ dataItem.memberName }}
+
+ +