@@ -26,7 +26,8 @@ public class ExpenseListItemDto
|
|||||||
public string PrimaryCategoryName { get; set; } = ""; // first line's category (list hint; full breakdown via detail)
|
public string PrimaryCategoryName { get; set; } = ""; // first line's category (list hint; full breakdown via detail)
|
||||||
public string? VendorName { get; set; }
|
public string? VendorName { get; set; }
|
||||||
public int? MemberId { 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 string ExpenseDate { get; set; } = ""; // yyyy-MM-dd
|
||||||
public bool HasReceipt { get; set; }
|
public bool HasReceipt { get; set; }
|
||||||
public string? CheckNumber { get; set; }
|
public string? CheckNumber { get; set; }
|
||||||
|
|||||||
@@ -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 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 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 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))
|
var memberNames = await _db.Members.AsNoTracking().Where(m => memberIds.Contains(m.Id))
|
||||||
.ToDictionaryAsync(m => m.Id, m => $"{m.FirstName_en} {m.LastName_en}");
|
.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));
|
var reviewerNames = await ResolveUserNamesAsync(rows.Select(r => r.ReviewedBy));
|
||||||
|
|
||||||
// Line count + first line's category, per expense on this page.
|
// Line count + first line's category, per expense on this page.
|
||||||
@@ -108,7 +112,8 @@ public class ExpenseService : IExpenseService
|
|||||||
LineCount = ls?.Count ?? 0,
|
LineCount = ls?.Count ?? 0,
|
||||||
PrimaryCategoryName = grpNames.GetValueOrDefault(firstGroupId, ""),
|
PrimaryCategoryName = grpNames.GetValueOrDefault(firstGroupId, ""),
|
||||||
VendorName = e.VendorName, MemberId = e.MemberId,
|
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"),
|
ExpenseDate = e.ExpenseDate.ToString("yyyy-MM-dd"),
|
||||||
HasReceipt = e.ReceiptBlobPath != null,
|
HasReceipt = e.ReceiptBlobPath != null,
|
||||||
CheckNumber = e.CheckNumber,
|
CheckNumber = e.CheckNumber,
|
||||||
@@ -145,14 +150,41 @@ public class ExpenseService : IExpenseService
|
|||||||
: (u.Email ?? u.Id));
|
: (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<ExpenseDto?> GetByIdAsync(int id)
|
public async Task<ExpenseDto?> GetByIdAsync(int id)
|
||||||
{
|
{
|
||||||
var e = await _db.Expenses.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
|
var e = await _db.Expenses.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
|
||||||
if (e is null) return null;
|
if (e is null) return null;
|
||||||
var minName = await _db.Ministries.Where(m => m.Id == e.MinistryId).Select(m => m.Name_en).FirstOrDefaultAsync() ?? "";
|
var minName = await _db.Ministries.Where(m => m.Id == e.MinistryId).Select(m => m.Name_en).FirstOrDefaultAsync() ?? "";
|
||||||
string? memName = e.MemberId != null
|
string? memberName = null;
|
||||||
? await _db.Members.Where(m => m.Id == e.MemberId).Select(m => m.FirstName_en + " " + m.LastName_en).FirstOrDefaultAsync()
|
string? memberNickName = null;
|
||||||
: 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
|
var reviewerName = e.ReviewedBy != null
|
||||||
? (await ResolveUserNamesAsync(new[] { e.ReviewedBy })).GetValueOrDefault(e.ReviewedBy)
|
? (await ResolveUserNamesAsync(new[] { e.ReviewedBy })).GetValueOrDefault(e.ReviewedBy)
|
||||||
@@ -174,7 +206,7 @@ public class ExpenseService : IExpenseService
|
|||||||
MinistryId = e.MinistryId, MinistryName = minName,
|
MinistryId = e.MinistryId, MinistryName = minName,
|
||||||
LineCount = lineDtos.Count,
|
LineCount = lineDtos.Count,
|
||||||
PrimaryCategoryName = lineDtos.Count > 0 ? lineDtos[0].CategoryGroupName : "",
|
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,
|
ExpenseDate = e.ExpenseDate.ToString("yyyy-MM-dd"), HasReceipt = e.ReceiptBlobPath != null,
|
||||||
CheckNumber = e.CheckNumber, Notes = e.Notes, ReviewNotes = e.ReviewNotes,
|
CheckNumber = e.CheckNumber, Notes = e.Notes, ReviewNotes = e.ReviewNotes,
|
||||||
ReviewedByName = reviewerName, ReviewedAt = e.ReviewedAt,
|
ReviewedByName = reviewerName, ReviewedAt = e.ReviewedAt,
|
||||||
|
|||||||
+11
-1
@@ -10,7 +10,17 @@
|
|||||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
|
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
|
||||||
<div class="text-gray-500">Date / 日期</div><div>{{ expense.expenseDate }}</div>
|
<div class="text-gray-500">Date / 日期</div><div>{{ expense.expenseDate }}</div>
|
||||||
<div class="text-gray-500">Ministry / 事工</div><div>{{ expense.ministryName }}</div>
|
<div class="text-gray-500">Ministry / 事工</div><div>{{ expense.ministryName }}</div>
|
||||||
<div class="text-gray-500">Payee / 收款人</div><div>{{ expense.vendorName || expense.memberName || '—' }}</div>
|
<div class="text-gray-500">Payee / 收款人</div>
|
||||||
|
<div>
|
||||||
|
<ng-container *ngIf="expense.vendorName; else memberPayee">{{ expense.vendorName }}</ng-container>
|
||||||
|
<ng-template #memberPayee>
|
||||||
|
<ng-container *ngIf="expense.memberName; else dash">
|
||||||
|
<div *ngIf="expense.memberNickName">{{ expense.memberNickName }}</div>
|
||||||
|
<div [class.text-gray-500]="expense.memberNickName" [class.text-xs]="expense.memberNickName">{{ expense.memberName }}</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #dash>—</ng-template>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
<div class="text-gray-500">Description / 說明</div><div>{{ expense.description }}</div>
|
<div class="text-gray-500">Description / 說明</div><div>{{ expense.description }}</div>
|
||||||
<div class="text-gray-500">Status / 狀態</div><div>{{ expense.status }}</div>
|
<div class="text-gray-500">Status / 狀態</div><div>{{ expense.status }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export interface ExpenseListItemDto {
|
|||||||
id: number; type: ExpenseType; status: ExpenseStatus; amount: number; description: string;
|
id: number; type: ExpenseType; status: ExpenseStatus; amount: number; description: string;
|
||||||
ministryId: number; ministryName: string; lineCount: number; primaryCategoryName: string;
|
ministryId: number; ministryName: string; lineCount: number; primaryCategoryName: string;
|
||||||
vendorName: string | null;
|
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;
|
checkNumber: string | null;
|
||||||
reviewedByName: string | null; reviewedAt: string | null; reviewNotes: string | null;
|
reviewedByName: string | null; reviewedAt: string | null; reviewNotes: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,16 @@
|
|||||||
</kendo-grid-column>
|
</kendo-grid-column>
|
||||||
|
|
||||||
<kendo-grid-column field="description" title="Description"></kendo-grid-column>
|
<kendo-grid-column field="description" title="Description"></kendo-grid-column>
|
||||||
<kendo-grid-column title="Payee" [width]="150">
|
<kendo-grid-column title="Payee" [width]="180">
|
||||||
<ng-template kendoGridCellTemplate let-dataItem>
|
<ng-template kendoGridCellTemplate let-dataItem>
|
||||||
{{ dataItem.vendorName || dataItem.memberName || '—' }}
|
<ng-container *ngIf="dataItem.vendorName; else memberPayee">{{ dataItem.vendorName }}</ng-container>
|
||||||
|
<ng-template #memberPayee>
|
||||||
|
<ng-container *ngIf="dataItem.memberName; else dash">
|
||||||
|
<div *ngIf="dataItem.memberNickName">{{ dataItem.memberNickName }}</div>
|
||||||
|
<div [class.text-gray-500]="dataItem.memberNickName" [class.text-xs]="dataItem.memberNickName">{{ dataItem.memberName }}</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #dash>—</ng-template>
|
||||||
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</kendo-grid-column>
|
</kendo-grid-column>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user