# 下拉選項雙語顯示（English/中文）— 設計文件

**日期:** 2026-05-29
**狀態:** 已核准設計，待寫實作計畫
**範圍:** 前端（Angular）。不動資料庫、不動 schema、不動既有後端 API 回傳的列表名稱。

---

## 1. 目標

針對大多數下拉選單，選項**同時顯示英文與中文**，格式為 `英文/中文`（例：`Worship/敬拜`、`Cash/現金`）。

**設計決定（已與使用者確認）：**
- **顯示時拼接、資料分開存** — 中英文在資料層維持分開（DB 兩欄 / 前端 value 不變），只在「顯示」時組成 `英文/中文`。送出/儲存的值完全不變。
- **全站套用 + 未來約定** — 現有所有業務下拉都改；並定成往後新增下拉的統一慣例。
- **回顯一致性** — 下拉改雙語後，**前端自己組得出來的**已選值回顯也一起改（目前僅奉獻錄入頁下方明細的 Type 欄）。後端帶 `name_en` 過來的列表欄位（如支出列表）本次不動。

**格式規則：**
- 英文在前、中文在後，中間以 `/` 分隔、**無空格**。
- 沒有中文時只顯示英文（如品牌字 `Zelle`、`PayPal`）。

---

## 2. 現況盤點

下拉選項來源分兩類：

### 第 1 類 — DB 查表（已有 `name_en` + `name_zh`，中文已種子，但前端只顯示英文）

後端實體與前端 DTO 皆已具備 `name_en`（非空）+ `name_zh`（可空），種子資料已填中文（如 Worship/敬拜、Tithe/什一奉獻）。前端目前一律綁 `textField="name_en"`，因此中文存在卻未顯示。**故此類為純前端顯示改動，不需動 DB 或 schema。**

涉及下拉：
| 下拉 | 模板位置 | 資料來源 |
|------|----------|----------|
| 奉獻錄入「Type」 | `app/features/giving/pages/offering-session-page/offering-session-page.component.html:39` | `categories` ← `GivingCategoryApiService.getAll` |
| Givings 類型篩選 + 對話框「Type」 | `app/features/giving/pages/givings-page/givings-page.component.html:9, 48` | `categories` |
| 支出「Ministry」篩選 | `app/features/expense/pages/expenses-page/expenses-page.component.html:18` | `ministries` ← `MinistryApiService.getAll` |
| 支出對話框 Ministry / 大類 / 子項（3 個） | `app/features/expense/components/expense-form-dialog/expense-form-dialog.component.html:19, 32, 45` | `ministries` / `groups` / `subs` |

### 第 2 類 — 前端寫死的 enum 陣列（後端僅存 `string`，無 C# enum）

散落在各 `*.component.ts`，標籤目前為純英文（部分為純字串陣列、部分已是 `{text,value}` 物件陣列）。

| Enum | 型別/陣列位置 | 綁定形式 |
|------|----------------|----------|
| PaymentMethod | `giving.model.ts:1`；陣列 `offering-session-page.component.ts:36`、`givings-page.component.ts:44` | 純字串陣列（直接綁字串） |
| ExpenseStatus | `expense.model.ts:2`；陣列 `expenses-page.component.ts:34` | 純字串陣列 |
| Member Status | `member-form-dialog.component.ts:30`、`members-page.component.ts:39` | 純字串陣列 |
| Gender | `member-form-dialog.component.ts:31-35` | `{text,value}` 物件陣列 |
| Language | `member-form-dialog.ts`、`users/create-user-dialog.ts`、`users/edit-user-dialog.ts`、`members/create-user-dialog.ts` | `{text,value}` 物件陣列（已含「中文」） |
| Roles（multiselect） | `roleOptions: string[] = [...ALL_ROLES]`；`ALL_ROLES` 於 `users/models/user.model.ts:48` | 純字串陣列（13 個 snake_case 代碼） |

> Dashboard 範本殘留的示範下拉（`dashboard.html`）與未使用的通用元件不在範圍內。

---

## 3. 做法：方案 A（共用工具 + 兩處收斂）

評估過三種做法：
- **方案 A（採用）** — 共用 `bilingual()` 工具；DB 查表類在 service 層算出 `label`，enum 類集中到 `option-lists.ts`。模板改動最小、與既有 `textField` 寫法一致、可定為全站慣例。
- 方案 B（不採用）— 每個下拉用 Kendo item/value `<ng-template>` + pipe。不動 DTO，但每個下拉需兩段樣板，8 個下拉冗長易漏，未來新下拉也須每次手寫。
- 方案 C（不採用）— 後端 C# 直接回傳合併字串。違反「前端顯示時拼接、資料分開存」，且幫不到第 2 類前端 enum。

---

## 4. 詳細設計

### 4.1 共用工具（新檔）

`APP/src/app/shared/i18n/bilingual.ts`
```ts
/** 顯示用雙語標籤：英文在前、中文在後，無中文則只回英文。 */
export const bilingual = (en: string, zh?: string | null): string =>
  zh ? `${en}/${zh}` : en;
```

`APP/src/app/shared/i18n/option-lists.ts` — 集中所有寫死 enum 的雙語選項，型別為 `ReadonlyArray<{ value: string; label: string }>`。供全站及未來新下拉引用。

### 4.2 第 1 類：DB 查表下拉

1. **DTO 加唯讀 `label`**（可空，顯示用）：`GivingCategoryDto`、`MinistryDto`、`ExpenseCategoryGroupDto`、`ExpenseSubCategoryDto` 各加 `label?: string;`。
2. **Service 載入時計算 `label`**：在 `GivingCategoryApiService`、`MinistryApiService`、`ExpenseCategoryApiService`（含 groups 與 subs）的回傳 `map` 中，對每筆資料補 `label: bilingual(x.name_en, x.name_zh)`。
   - 集中在 service 層的理由：所有現有消費端自動雙語；未來新下拉只要綁同一 service 即免費雙語 → 即「未來約定」。
3. **模板**：對應下拉的 `textField="name_en"` 全部改為 `textField="label"`。
4. **defaultItem 提示字改雙語**：例如
   - `{ id: null, name_en: 'All types' }` → `{ id: null, label: 'All types/全部類型' }`
   - `{ id: null, name_en: 'All Ministries' }` → `{ id: null, label: 'All Ministries/全部事工' }`
   - 「-- Select ministry --」等 → 改為對應雙語提示。
   （defaultItem 物件需改為帶 `label` 欄以對應新的 `textField`。）
5. **奉獻明細回顯**：`offering-session-page.component.ts` 的 `addLine()` 中 `categoryName: cat?.name_en ?? ''` 改為 `categoryName: cat?.label ?? ''`，使下方明細 Type 欄與下拉一致。

### 4.3 第 2 類：寫死 enum 下拉（集中到 `option-lists.ts`）

預設譯名（**使用者已核可，可日後再微調**）：

```ts
export const PAYMENT_METHOD_OPTIONS = [
  { value: 'Cash',   label: 'Cash/現金' },
  { value: 'Check',  label: 'Check/支票' },
  { value: 'Zelle',  label: 'Zelle' },
  { value: 'PayPal', label: 'PayPal' },
  { value: 'Other',  label: 'Other/其他' },
] as const;

export const EXPENSE_STATUS_OPTIONS = [
  { value: 'Draft',           label: 'Draft/草稿' },
  { value: 'PendingApproval', label: 'PendingApproval/待審核' },
  { value: 'Approved',        label: 'Approved/已核准' },
  { value: 'Paid',            label: 'Paid/已付款' },
  { value: 'Rejected',        label: 'Rejected/已拒絕' },
] as const;

export const MEMBER_STATUS_OPTIONS = [
  { value: 'Member',   label: 'Member/會友' },
  { value: 'Visitor',  label: 'Visitor/訪客' },
  { value: 'Inactive', label: 'Inactive/未活躍' },
  { value: 'Former',   label: 'Former/已離開' },
] as const;

export const GENDER_OPTIONS = [
  { value: 'M',     label: 'Male/男' },
  { value: 'F',     label: 'Female/女' },
  { value: 'Other', label: 'Other/其他' },
] as const;

export const LANGUAGE_OPTIONS = [
  { value: 'en',    label: 'English/英文' },
  { value: 'zh-TW', label: '中文/Chinese' },
] as const;

export const ROLE_OPTIONS = [
  { value: 'super_admin',     label: 'Super Admin/系統管理員' },
  { value: 'pastor',          label: 'Pastor/牧師' },
  { value: 'board_member',    label: 'Board Member/理事' },
  { value: 'coworker_chair',  label: 'Coworker Chair/同工會主席' },
  { value: 'ministry_leader', label: 'Ministry Leader/事工領袖' },
  { value: 'district_leader', label: 'District Leader/區長' },
  { value: 'cell_leader',     label: 'Cell Leader/小組長' },
  { value: 'coworker',        label: 'Coworker/同工' },
  { value: 'finance',         label: 'Finance/財務同工' },
  { value: 'secretary',       label: 'Secretary/行政秘書' },
  { value: 'worship_leader',  label: 'Worship Leader/敬拜領袖' },
  { value: 'member',          label: 'Member/一般教友' },
  { value: 'visitor',         label: 'Visitor/訪客' },
] as const;
```

**綁定調整：**
- 原本綁純字串陣列者（PaymentMethod、ExpenseStatus、Member Status）改用上述物件陣列，模板補 `textField="label" valueField="value" [valuePrimitive]="true"`。**儲存/送出的值維持原本字串，不變。**
- Gender、Language 已是物件陣列，改引用集中常數；為全站一致，欄名統一用 `label`（取代原本的 `text`），模板對應改 `textField="label"`。
- Roles multiselect 改綁 `ROLE_OPTIONS`，加 `textField="label" valueField="value" [valuePrimitive]="true"`，確保 `formControlName="roles"` 仍取得代碼陣列。
- 含篩選用空值的（如 members-page 的 `''` = 全部）保留一個 `{ value: '', label: 'All Status/全部狀態' }` 或沿用 `defaultItem`，行為不變。

### 4.4 約定文件

`docs/PLANNING.md` 新增一節「下拉雙語顯示約定」：
- DB 查表下拉 → 用 service 計算的 `label`（`bilingual(name_en, name_zh)`），模板綁 `textField="label"`。
- 寫死 enum 下拉 → 一律定義在 `shared/i18n/option-lists.ts`，`{value,label}`，模板綁 `textField="label" valueField="value" [valuePrimitive]="true"`。
- 格式 `英文/中文`、無空格、無中文則只英文。

---

## 5. 不在範圍（YAGNI / 本次不做）

- 不導入 Angular i18n / 翻譯框架（使用者要的是「同時顯示兩種語言」，非切換語系）。
- 不改 DB、不改 EF 實體、不改 migration、不改種子資料。
- 不改後端 API 回傳的列表名稱欄位（如支出列表的 ministry/category 名稱），故該等表格欄位本次維持英文。
- Dashboard 範本示範下拉與未使用的通用元件不處理。

---

## 6. 驗證

1. 前端 `build` 通過（型別正確）。
2. 起 dev server，以 preview 截圖確認：
   - 奉獻錄入頁：Type 顯示「Worship/敬拜」等、Method 顯示「Cash/現金」等；加入一筆後，下方明細 Type 欄同步雙語。
   - 支出對話框：Ministry / 大類 / 子項雙語。
   - 會友表單：Gender、Status、Language 雙語。
   - 使用者對話框：Roles multiselect 雙語。
3. 確認**送出後實際儲存的值不變**（仍為 `Cash`、`Member`、角色代碼、類別 id 等）。

---

## 7. 影響檔案清單（預估）

**新增**
- `APP/src/app/shared/i18n/bilingual.ts`
- `APP/src/app/shared/i18n/option-lists.ts`

**修改（DTO / Service）**
- `app/features/giving/models/giving.model.ts`（`GivingCategoryDto` 加 `label`）
- `app/features/expense/models/expense.model.ts`（`MinistryDto`、`ExpenseCategoryGroupDto`、`ExpenseSubCategoryDto` 加 `label`）
- `GivingCategoryApiService`、`MinistryApiService`、`ExpenseCategoryApiService`（map 計算 `label`）

**修改（模板 / 元件）**
- `offering-session-page.component.{html,ts}`
- `givings-page.component.{html,ts}`
- `expenses-page.component.{html,ts}`
- `expense-form-dialog.component.{html,ts}`
- `member-form-dialog.component.{html,ts}`
- `members-page.component.{html,ts}`
- `users/create-user-dialog.component.{html,ts}`、`users/edit-user-dialog.component.{html,ts}`
- `members/create-user-dialog.component.{html,ts}`

**文件**
- `docs/PLANNING.md`（新增約定一節）
