docs: implementation plan for bilingual dropdown options
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,673 @@
|
||||
# Bilingual Dropdown Options (English/中文) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make business dropdown options display English and Chinese together as `English/中文` (e.g. `Worship/敬拜`, `Cash/現金`) across the Angular app, without changing stored values or the database.
|
||||
|
||||
**Architecture:** Two seams. (1) DB-backed lookups (giving categories, ministries, expense category groups/subs) already carry `name_en` + `name_zh`; their API services compute a display-only `label = bilingual(name_en, name_zh)` at load time and templates bind `textField="label"`. (2) Hard-coded enum dropdowns (payment method, expense status, member status, gender, language, roles) move to a central `shared/i18n/option-lists.ts` of `{value,label}` arrays. A single `bilingual()` helper produces the `英文/中文` string. Values bound into forms/DB are unchanged.
|
||||
|
||||
**Tech Stack:** Angular 20 (standalone components), Kendo UI for Angular v20 (DropDownList / MultiSelect), Karma + Jasmine (`ng test`), Tailwind v4 for layout. Spec: `docs/superpowers/specs/2026-05-29-bilingual-dropdown-options-design.md`.
|
||||
|
||||
**Conventions to honor:**
|
||||
- Kendo: whenever `textField`/`valueField` are set against an object array, also set `[valuePrimitive]="true"` so the form binds the scalar value, not the whole object.
|
||||
- Format: `英文/中文`, no spaces, English first; if no Chinese, show English only.
|
||||
- Work directly on `main` (repo convention — recent feature work is committed straight to `main`). The working tree already has unrelated modified/deleted files; **stage only the files each task names** — never `git add -A`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
**New files**
|
||||
- `APP/src/app/shared/i18n/bilingual.ts` — the `bilingual(en, zh)` helper. One responsibility: format a bilingual display string.
|
||||
- `APP/src/app/shared/i18n/bilingual.spec.ts` — unit tests for the helper.
|
||||
- `APP/src/app/shared/i18n/option-lists.ts` — central `{value,label}` option arrays for all hard-coded enums.
|
||||
- `APP/src/app/shared/i18n/option-lists.spec.ts` — guards values/shape of the option arrays.
|
||||
|
||||
**Modified (DTO / service — compute `label`)**
|
||||
- `APP/src/app/features/giving/models/giving.model.ts`
|
||||
- `APP/src/app/features/expense/models/expense.model.ts`
|
||||
- `APP/src/app/features/giving/services/giving-category-api.service.ts`
|
||||
- `APP/src/app/features/expense/services/ministry-api.service.ts`
|
||||
- `APP/src/app/features/expense/services/expense-category-api.service.ts`
|
||||
|
||||
**Modified (templates / components — bind labels)**
|
||||
- `APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.{html,ts}`
|
||||
- `APP/src/app/features/giving/pages/givings-page/givings-page.component.{html,ts}`
|
||||
- `APP/src/app/features/expense/pages/expenses-page/expenses-page.component.{html,ts}`
|
||||
- `APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.{html,ts}`
|
||||
- `APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.{html,ts}`
|
||||
- `APP/src/app/features/members/pages/members-page/members-page.component.{html,ts}`
|
||||
- `APP/src/app/features/users/components/create-user-dialog/create-user-dialog.component.{html,ts}`
|
||||
- `APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.{html,ts}`
|
||||
- `APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.{html,ts}`
|
||||
|
||||
**Docs**
|
||||
- `docs/PLANNING.md` — add the dropdown-bilingual convention section.
|
||||
|
||||
**Testing note:** The helper and option-lists get real Jasmine unit tests (`npm run test:ci`). The template/wiring tasks are display-only changes verified by a clean `ng build` (type safety) plus the final browser preview pass (Task 10) — the repo has no Karma DOM tests for these Kendo pages, and adding brittle ones is out of scope.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `bilingual()` helper
|
||||
|
||||
**Files:**
|
||||
- Create: `APP/src/app/shared/i18n/bilingual.ts`
|
||||
- Test: `APP/src/app/shared/i18n/bilingual.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `APP/src/app/shared/i18n/bilingual.spec.ts`:
|
||||
```ts
|
||||
import { bilingual } from './bilingual';
|
||||
|
||||
describe('bilingual', () => {
|
||||
it('joins English and Chinese with a slash, no spaces', () => {
|
||||
expect(bilingual('Worship', '敬拜')).toBe('Worship/敬拜');
|
||||
});
|
||||
|
||||
it('returns English only when Chinese is null', () => {
|
||||
expect(bilingual('Zelle', null)).toBe('Zelle');
|
||||
});
|
||||
|
||||
it('returns English only when Chinese is undefined', () => {
|
||||
expect(bilingual('PayPal')).toBe('PayPal');
|
||||
});
|
||||
|
||||
it('returns English only when Chinese is an empty string', () => {
|
||||
expect(bilingual('Other', '')).toBe('Other');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd APP && npm run test:ci`
|
||||
Expected: FAIL — `Cannot find module './bilingual'` (compilation error).
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
Create `APP/src/app/shared/i18n/bilingual.ts`:
|
||||
```ts
|
||||
/**
|
||||
* Display-only bilingual label: English first, Chinese second, joined by '/'
|
||||
* with no spaces. Falls back to English alone when no Chinese is provided.
|
||||
* Does NOT alter stored values — for display binding only.
|
||||
*/
|
||||
export const bilingual = (en: string, zh?: string | null): string =>
|
||||
zh ? `${en}/${zh}` : en;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd APP && npm run test:ci`
|
||||
Expected: PASS (4 specs in the `bilingual` suite).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add APP/src/app/shared/i18n/bilingual.ts APP/src/app/shared/i18n/bilingual.spec.ts
|
||||
git commit -m "feat(i18n): add bilingual() display helper"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Central option lists for hard-coded enums
|
||||
|
||||
**Files:**
|
||||
- Create: `APP/src/app/shared/i18n/option-lists.ts`
|
||||
- Test: `APP/src/app/shared/i18n/option-lists.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `APP/src/app/shared/i18n/option-lists.spec.ts`:
|
||||
```ts
|
||||
import {
|
||||
PAYMENT_METHOD_OPTIONS, EXPENSE_STATUS_OPTIONS, MEMBER_STATUS_OPTIONS,
|
||||
GENDER_OPTIONS, LANGUAGE_OPTIONS, ROLE_OPTIONS,
|
||||
} from './option-lists';
|
||||
|
||||
describe('option-lists', () => {
|
||||
it('payment methods preserve raw values and show bilingual labels', () => {
|
||||
expect(PAYMENT_METHOD_OPTIONS.map(o => o.value))
|
||||
.toEqual(['Cash', 'Check', 'Zelle', 'PayPal', 'Other']);
|
||||
expect(PAYMENT_METHOD_OPTIONS.find(o => o.value === 'Cash')!.label).toBe('Cash/現金');
|
||||
expect(PAYMENT_METHOD_OPTIONS.find(o => o.value === 'Zelle')!.label).toBe('Zelle');
|
||||
});
|
||||
|
||||
it('expense statuses preserve raw enum values', () => {
|
||||
expect(EXPENSE_STATUS_OPTIONS.map(o => o.value))
|
||||
.toEqual(['Draft', 'PendingApproval', 'Approved', 'Paid', 'Rejected']);
|
||||
});
|
||||
|
||||
it('member statuses preserve raw values', () => {
|
||||
expect(MEMBER_STATUS_OPTIONS.map(o => o.value))
|
||||
.toEqual(['Member', 'Visitor', 'Inactive', 'Former']);
|
||||
});
|
||||
|
||||
it('gender values stay M/F/Other', () => {
|
||||
expect(GENDER_OPTIONS.map(o => o.value)).toEqual(['M', 'F', 'Other']);
|
||||
});
|
||||
|
||||
it('language values stay en/zh-TW', () => {
|
||||
expect(LANGUAGE_OPTIONS.map(o => o.value)).toEqual(['en', 'zh-TW']);
|
||||
});
|
||||
|
||||
it('role options cover all 13 role codes', () => {
|
||||
expect(ROLE_OPTIONS.length).toBe(13);
|
||||
expect(ROLE_OPTIONS.map(o => o.value)).toContain('super_admin');
|
||||
expect(ROLE_OPTIONS.map(o => o.value)).toContain('visitor');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd APP && npm run test:ci`
|
||||
Expected: FAIL — `Cannot find module './option-lists'`.
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
Create `APP/src/app/shared/i18n/option-lists.ts`:
|
||||
```ts
|
||||
/**
|
||||
* Central bilingual option lists for hard-coded enum dropdowns.
|
||||
* `value` is the raw value stored/sent to the API (unchanged from before);
|
||||
* `label` is the display string `英文/中文`. Bind in templates with
|
||||
* textField="label" valueField="value" [valuePrimitive]="true".
|
||||
*/
|
||||
export interface BilingualOption { readonly value: string; readonly label: string; }
|
||||
|
||||
export const PAYMENT_METHOD_OPTIONS: readonly BilingualOption[] = [
|
||||
{ value: 'Cash', label: 'Cash/現金' },
|
||||
{ value: 'Check', label: 'Check/支票' },
|
||||
{ value: 'Zelle', label: 'Zelle' },
|
||||
{ value: 'PayPal', label: 'PayPal' },
|
||||
{ value: 'Other', label: 'Other/其他' },
|
||||
];
|
||||
|
||||
export const EXPENSE_STATUS_OPTIONS: readonly BilingualOption[] = [
|
||||
{ value: 'Draft', label: 'Draft/草稿' },
|
||||
{ value: 'PendingApproval', label: 'PendingApproval/待審核' },
|
||||
{ value: 'Approved', label: 'Approved/已核准' },
|
||||
{ value: 'Paid', label: 'Paid/已付款' },
|
||||
{ value: 'Rejected', label: 'Rejected/已拒絕' },
|
||||
];
|
||||
|
||||
export const MEMBER_STATUS_OPTIONS: readonly BilingualOption[] = [
|
||||
{ value: 'Member', label: 'Member/會友' },
|
||||
{ value: 'Visitor', label: 'Visitor/訪客' },
|
||||
{ value: 'Inactive', label: 'Inactive/未活躍' },
|
||||
{ value: 'Former', label: 'Former/已離開' },
|
||||
];
|
||||
|
||||
export const GENDER_OPTIONS: readonly BilingualOption[] = [
|
||||
{ value: 'M', label: 'Male/男' },
|
||||
{ value: 'F', label: 'Female/女' },
|
||||
{ value: 'Other', label: 'Other/其他' },
|
||||
];
|
||||
|
||||
export const LANGUAGE_OPTIONS: readonly BilingualOption[] = [
|
||||
{ value: 'en', label: 'English/英文' },
|
||||
{ value: 'zh-TW', label: '中文/Chinese' },
|
||||
];
|
||||
|
||||
export const ROLE_OPTIONS: readonly BilingualOption[] = [
|
||||
{ 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/訪客' },
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd APP && npm run test:ci`
|
||||
Expected: PASS (all `option-lists` specs).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add APP/src/app/shared/i18n/option-lists.ts APP/src/app/shared/i18n/option-lists.spec.ts
|
||||
git commit -m "feat(i18n): add central bilingual option lists for enum dropdowns"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: DB-backed DTOs gain `label`; services compute it
|
||||
|
||||
**Files:**
|
||||
- Modify: `APP/src/app/features/giving/models/giving.model.ts` (add `label?` to `GivingCategoryDto`)
|
||||
- Modify: `APP/src/app/features/expense/models/expense.model.ts` (add `label?` to `MinistryDto`, `ExpenseCategoryGroupDto`, `ExpenseSubCategoryDto`)
|
||||
- Modify: `APP/src/app/features/giving/services/giving-category-api.service.ts`
|
||||
- Modify: `APP/src/app/features/expense/services/ministry-api.service.ts`
|
||||
- Modify: `APP/src/app/features/expense/services/expense-category-api.service.ts`
|
||||
|
||||
> No unit test here — this is a typed data-shaping change verified by `ng build`. Behavior is exercised end-to-end in Task 10.
|
||||
|
||||
- [ ] **Step 1: Add `label?` to the giving DTO**
|
||||
|
||||
In `giving.model.ts`, add `label?: string;` to `GivingCategoryDto` (keep the existing `isActive`/`sortOrder` fields — only the `label?` line is new):
|
||||
```ts
|
||||
export interface GivingCategoryDto {
|
||||
id: number;
|
||||
name_en: string;
|
||||
name_zh: string | null;
|
||||
description_en: string | null;
|
||||
description_zh: string | null;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
/** Display-only bilingual label, computed in the API service. */
|
||||
label?: string;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `label?` to the expense DTOs**
|
||||
|
||||
In `expense.model.ts`, add `label?: string;` to `MinistryDto`, `ExpenseCategoryGroupDto`, and `ExpenseSubCategoryDto`. Example for `MinistryDto`:
|
||||
```ts
|
||||
export interface MinistryDto {
|
||||
id: number;
|
||||
name_en: string;
|
||||
name_zh: string | null;
|
||||
sortOrder: number;
|
||||
isActive: boolean;
|
||||
/** Display-only bilingual label, computed in the API service. */
|
||||
label?: string;
|
||||
}
|
||||
```
|
||||
Do the same (`label?: string;`) for `ExpenseCategoryGroupDto` and `ExpenseSubCategoryDto`.
|
||||
|
||||
- [ ] **Step 3: Compute `label` in `GivingCategoryApiService.getAll`**
|
||||
|
||||
In `giving-category-api.service.ts`, the current import is `import { Observable } from 'rxjs';` — change it to `import { Observable, map } from 'rxjs';` and add `import { bilingual } from '../../../shared/i18n/bilingual';`. Then pipe the existing `getAll` (keep the `HttpParams` exactly as-is, just append `.pipe(...)`):
|
||||
```ts
|
||||
getAll(includeInactive = false): Observable<GivingCategoryDto[]> {
|
||||
const params = new HttpParams().set('includeInactive', includeInactive);
|
||||
return this.http.get<GivingCategoryDto[]>(this.endpoint, { params }).pipe(
|
||||
map(list => list.map(c => ({ ...c, label: bilingual(c.name_en, c.name_zh) }))),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Compute `label` in the ministry service**
|
||||
|
||||
Open `ministry-api.service.ts`. Find the method that returns `Observable<MinistryDto[]>` (e.g. `getAll`). Add the same imports (`map` from `rxjs`, `bilingual`) and pipe each item:
|
||||
```ts
|
||||
.pipe(map(list => list.map(m => ({ ...m, label: bilingual(m.name_en, m.name_zh) }))))
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Compute `label` for expense category groups AND their sub-categories**
|
||||
|
||||
Open `expense-category-api.service.ts`. Find the method returning `Observable<ExpenseCategoryGroupDto[]>` (e.g. `getAll`). Decorate both the group and its nested `subCategories`:
|
||||
```ts
|
||||
import { map } from 'rxjs';
|
||||
import { bilingual } from '../../../shared/i18n/bilingual';
|
||||
// ...
|
||||
.pipe(map(groups => groups.map(g => ({
|
||||
...g,
|
||||
label: bilingual(g.name_en, g.name_zh),
|
||||
subCategories: g.subCategories.map(s => ({ ...s, label: bilingual(s.name_en, s.name_zh) })),
|
||||
}))))
|
||||
```
|
||||
If sub-categories are fetched by a separate method/endpoint instead of nested, apply the same `.pipe(map(list => list.map(s => ({ ...s, label: bilingual(s.name_en, s.name_zh) }))))` to that method too.
|
||||
|
||||
- [ ] **Step 6: Build to verify types**
|
||||
|
||||
Run: `cd APP && npm run build`
|
||||
Expected: build succeeds with no TypeScript errors.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add APP/src/app/features/giving/models/giving.model.ts APP/src/app/features/expense/models/expense.model.ts APP/src/app/features/giving/services/giving-category-api.service.ts APP/src/app/features/expense/services/ministry-api.service.ts APP/src/app/features/expense/services/expense-category-api.service.ts
|
||||
git commit -m "feat(i18n): compute bilingual label on giving/ministry/expense-category lookups"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Offering-session page — Type dropdown + line echo
|
||||
|
||||
**Files:**
|
||||
- Modify: `APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.html`
|
||||
- Modify: `APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.ts`
|
||||
|
||||
- [ ] **Step 1: Bind the Type dropdown to `label`**
|
||||
|
||||
In the HTML, the giving Type dropdown currently reads:
|
||||
```html
|
||||
<kendo-dropdownlist [data]="categories" textField="name_en" valueField="id" [valuePrimitive]="true"
|
||||
[(ngModel)]="entry.givingCategoryId"></kendo-dropdownlist>
|
||||
```
|
||||
Change `textField="name_en"` to `textField="label"`. Leave `valueField`, `valuePrimitive`, and `ngModel` unchanged.
|
||||
|
||||
- [ ] **Step 2: Convert the Method (payment) dropdown to bilingual options**
|
||||
|
||||
The Method dropdown currently binds a bare string array:
|
||||
```html
|
||||
<kendo-dropdownlist [data]="paymentMethods" [(ngModel)]="entry.paymentMethod"></kendo-dropdownlist>
|
||||
```
|
||||
Change it to use the central options with primitive binding:
|
||||
```html
|
||||
<kendo-dropdownlist [data]="paymentMethods" textField="label" valueField="value" [valuePrimitive]="true"
|
||||
[(ngModel)]="entry.paymentMethod"></kendo-dropdownlist>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update the component to use central payment options and bilingual line echo**
|
||||
|
||||
In `offering-session-page.component.ts`:
|
||||
- Add import: `import { PAYMENT_METHOD_OPTIONS } from '../../../../shared/i18n/option-lists';`
|
||||
- Replace the field `readonly paymentMethods: PaymentMethod[] = ['Cash', 'Check', 'Zelle', 'PayPal', 'Other'];` with:
|
||||
```ts
|
||||
readonly paymentMethods = PAYMENT_METHOD_OPTIONS;
|
||||
```
|
||||
- In `addLine()`, change the line-echo from English-only to the bilingual label:
|
||||
```ts
|
||||
categoryName: cat?.label ?? '',
|
||||
```
|
||||
(was `categoryName: cat?.name_en ?? ''`).
|
||||
|
||||
- [ ] **Step 4: Build to verify**
|
||||
|
||||
Run: `cd APP && npm run build`
|
||||
Expected: build succeeds. (`entry.paymentMethod` is still a `PaymentMethod` string; `[valuePrimitive]` binds `value` which is that string.)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.html APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.ts
|
||||
git commit -m "feat(i18n): bilingual Type/Method dropdowns + line echo on offering session"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Givings page — Type filter, Type dialog, Method
|
||||
|
||||
**Files:**
|
||||
- Modify: `APP/src/app/features/giving/pages/givings-page/givings-page.component.html`
|
||||
- Modify: `APP/src/app/features/giving/pages/givings-page/givings-page.component.ts`
|
||||
|
||||
- [ ] **Step 1: Bind both category dropdowns to `label`**
|
||||
|
||||
In the HTML there are two category dropdowns (a filter near line 9 and a dialog near line 48), both `textField="name_en"`. Change `textField="name_en"` → `textField="label"` on both.
|
||||
|
||||
- [ ] **Step 2: Fix the filter `defaultItem` to carry a bilingual label**
|
||||
|
||||
The filter dropdown has a `[defaultItem]="{ id: null, name_en: 'All types' }"`. Because `textField` is now `label`, change it to:
|
||||
```html
|
||||
[defaultItem]="{ id: null, label: 'All types/全部類型' }"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Convert the Method dropdown to bilingual options**
|
||||
|
||||
Change the payment dropdown from the bare-string form:
|
||||
```html
|
||||
<kendo-dropdownlist [data]="paymentMethods" [(ngModel)]="form.paymentMethod"></kendo-dropdownlist>
|
||||
```
|
||||
to:
|
||||
```html
|
||||
<kendo-dropdownlist [data]="paymentMethods" textField="label" valueField="value" [valuePrimitive]="true"
|
||||
[(ngModel)]="form.paymentMethod"></kendo-dropdownlist>
|
||||
```
|
||||
(Match the existing `[(ngModel)]` target — it may be `form.paymentMethod` or similar; keep whatever the file already binds.)
|
||||
|
||||
- [ ] **Step 4: Update the component to use central payment options**
|
||||
|
||||
In `givings-page.component.ts`:
|
||||
- Add import: `import { PAYMENT_METHOD_OPTIONS } from '../../../../shared/i18n/option-lists';`
|
||||
- Replace the `readonly paymentMethods: PaymentMethod[] = [...]` field with `readonly paymentMethods = PAYMENT_METHOD_OPTIONS;`
|
||||
|
||||
- [ ] **Step 5: Build to verify**
|
||||
|
||||
Run: `cd APP && npm run build`
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add APP/src/app/features/giving/pages/givings-page/givings-page.component.html APP/src/app/features/giving/pages/givings-page/givings-page.component.ts
|
||||
git commit -m "feat(i18n): bilingual category + method dropdowns on givings page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Expense — list filters + form dialog
|
||||
|
||||
**Files:**
|
||||
- Modify: `APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html`
|
||||
- Modify: `APP/src/app/features/expense/pages/expenses-page/expenses-page.component.ts`
|
||||
- Modify: `APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.html`
|
||||
|
||||
- [ ] **Step 1: Ministry filter → `label`, fix its `defaultItem`**
|
||||
|
||||
In `expenses-page.component.html`, the Ministry filter is `textField="name_en"` with `[defaultItem]="{ id: null, name_en: 'All Ministries' }"`. Change `textField="name_en"` → `textField="label"` and the defaultItem to:
|
||||
```html
|
||||
[defaultItem]="{ id: null, label: 'All Ministries/全部事工' }"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Convert the Status filter to bilingual options**
|
||||
|
||||
The Status filter binds a bare string array: `<kendo-dropdownlist [data]="statuses" [(ngModel)]="filter.status" [defaultItem]="null">` (keep the exact `ngModel`/`defaultItem` the file uses). Change it to:
|
||||
```html
|
||||
<kendo-dropdownlist [data]="statuses" textField="label" valueField="value" [valuePrimitive]="true"
|
||||
[(ngModel)]="filter.status" [defaultItem]="{ value: null, label: 'All Status/全部狀態' }"></kendo-dropdownlist>
|
||||
```
|
||||
(If the existing code used `[defaultItem]="null"`, replace with the object above so the "all" row still renders a label; the bound value stays `null`.)
|
||||
|
||||
- [ ] **Step 3: Update the expenses-page component to central status options**
|
||||
|
||||
In `expenses-page.component.ts`:
|
||||
- Add import: `import { EXPENSE_STATUS_OPTIONS } from '../../../../shared/i18n/option-lists';`
|
||||
- Replace `readonly statuses: ExpenseStatus[] = ['Draft', 'PendingApproval', 'Approved', 'Paid', 'Rejected'];` with `readonly statuses = EXPENSE_STATUS_OPTIONS;`
|
||||
|
||||
- [ ] **Step 4: Form dialog — Ministry / Group / Sub dropdowns → `label`**
|
||||
|
||||
In `expense-form-dialog.component.html`, three dropdowns use `textField="name_en"` (Ministry ~line 22, Group ~line 34, Sub ~line 47). Change all three to `textField="label"`. For any `defaultItem` placeholders on these (e.g. `{ id: null, name_en: '-- Select ministry --' }`), change the key to `label` with bilingual text:
|
||||
```html
|
||||
[defaultItem]="{ id: null, label: '-- Select ministry --/請選擇事工' }"
|
||||
```
|
||||
Apply the equivalent bilingual placeholder for the group and sub dropdowns (e.g. `-- Select category --/請選擇大類`, `-- Select sub-category --/請選擇子項`).
|
||||
|
||||
- [ ] **Step 5: Build to verify**
|
||||
|
||||
Run: `cd APP && npm run build`
|
||||
Expected: build succeeds. (`filter.status` remains an `ExpenseStatus | null`; primitive binding supplies the `value`.)
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html APP/src/app/features/expense/pages/expenses-page/expenses-page.component.ts APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.html
|
||||
git commit -m "feat(i18n): bilingual ministry/category/status dropdowns on expense pages"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Member form + members page — Gender & Status
|
||||
|
||||
**Files:**
|
||||
- Modify: `APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.html`
|
||||
- Modify: `APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.ts`
|
||||
- Modify: `APP/src/app/features/members/pages/members-page/members-page.component.html`
|
||||
- Modify: `APP/src/app/features/members/pages/members-page/members-page.component.ts`
|
||||
|
||||
- [ ] **Step 1: member-form-dialog — switch Gender + Status to central options**
|
||||
|
||||
In `member-form-dialog.component.ts`:
|
||||
- Add import: `import { GENDER_OPTIONS, MEMBER_STATUS_OPTIONS } from '../../../../shared/i18n/option-lists';`
|
||||
- Replace the local `genderOptions = [{ text:'Male', value:'M' }, ...]` field with `readonly genderOptions = GENDER_OPTIONS;`
|
||||
- Replace `readonly statusOptions = ['Member', 'Visitor', 'Inactive', 'Former'];` with `readonly statusOptions = MEMBER_STATUS_OPTIONS;`
|
||||
|
||||
- [ ] **Step 2: member-form-dialog — update HTML bindings**
|
||||
|
||||
In `member-form-dialog.component.html`:
|
||||
- Gender dropdown currently `textField="text" valueField="value"`. Change `textField="text"` → `textField="label"`. Ensure `[valuePrimitive]="true"` is present (add it if missing). If there is a Gender `[defaultItem]="{ text: '-- Select --', value: null }"`, change `text` → `label` and make it bilingual: `[defaultItem]="{ label: '-- Select --/請選擇', value: null }"`.
|
||||
- Status dropdown currently `<kendo-dropdownlist formControlName="status" [data]="statusOptions">` (bare strings). Change to:
|
||||
```html
|
||||
<kendo-dropdownlist formControlName="status" [data]="statusOptions"
|
||||
textField="label" valueField="value" [valuePrimitive]="true"></kendo-dropdownlist>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: members-page — Status filter to central options + an "all" row**
|
||||
|
||||
In `members-page.component.ts`:
|
||||
- Add import: `import { MEMBER_STATUS_OPTIONS } from '../../../../shared/i18n/option-lists';`
|
||||
- The current field is `readonly statusOptions = ['', 'Member', 'Visitor', 'Inactive', 'Former'];` where `''` means "all". Replace with:
|
||||
```ts
|
||||
readonly statusOptions = [
|
||||
{ value: '', label: 'All Status/全部狀態' },
|
||||
...MEMBER_STATUS_OPTIONS,
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 4: members-page — update HTML binding**
|
||||
|
||||
In `members-page.component.html`, the Status filter is `<kendo-dropdownlist [data]="statusOptions" ...>` binding bare strings. Add the object bindings (keep the existing `[(ngModel)]`/filter target):
|
||||
```html
|
||||
<kendo-dropdownlist [data]="statusOptions" textField="label" valueField="value" [valuePrimitive]="true"
|
||||
...keep existing ngModel/handlers...></kendo-dropdownlist>
|
||||
```
|
||||
The empty-string value still flows through unchanged as the "all" selection.
|
||||
|
||||
- [ ] **Step 5: Build to verify**
|
||||
|
||||
Run: `cd APP && npm run build`
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.html APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.ts APP/src/app/features/members/pages/members-page/members-page.component.html APP/src/app/features/members/pages/members-page/members-page.component.ts
|
||||
git commit -m "feat(i18n): bilingual gender/status dropdowns on member form + members page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: User dialogs — Language & Roles
|
||||
|
||||
**Files:**
|
||||
- Modify: `APP/src/app/features/users/components/create-user-dialog/create-user-dialog.component.{html,ts}`
|
||||
- Modify: `APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.{html,ts}`
|
||||
- Modify: `APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.{html,ts}`
|
||||
|
||||
> Three dialogs share the same shape (a `langOptions` array and a `roleOptions = [...ALL_ROLES]`). Apply the identical change to each — they are listed explicitly so you don't skip one.
|
||||
|
||||
- [ ] **Step 1: users/create-user-dialog — component**
|
||||
|
||||
In `users/components/create-user-dialog/create-user-dialog.component.ts`:
|
||||
- Add import: `import { LANGUAGE_OPTIONS, ROLE_OPTIONS } from '../../../../shared/i18n/option-lists';`
|
||||
- Replace the local `langOptions = [{ text:'English', value:'en' }, { text:'中文', value:'zh-TW' }]` with `readonly langOptions = LANGUAGE_OPTIONS;`
|
||||
- Replace `roleOptions: string[] = [...ALL_ROLES];` with `readonly roleOptions = ROLE_OPTIONS;` (remove the now-unused `ALL_ROLES` import if nothing else uses it).
|
||||
|
||||
- [ ] **Step 2: users/create-user-dialog — HTML**
|
||||
|
||||
In the matching `.html`:
|
||||
- Language dropdown is `[data]="langOptions" textField="text" valueField="value"`. Change `textField="text"` → `textField="label"`. Ensure `[valuePrimitive]="true"` is present.
|
||||
- Roles multiselect is `<kendo-multiselect formControlName="roles" [data]="roleOptions" ...>` (bare strings). Add object bindings:
|
||||
```html
|
||||
<kendo-multiselect formControlName="roles" [data]="roleOptions"
|
||||
textField="label" valueField="value" [valuePrimitive]="true" ...keep existing attrs...></kendo-multiselect>
|
||||
```
|
||||
`[valuePrimitive]="true"` ensures the form control still holds an array of role-code strings.
|
||||
|
||||
- [ ] **Step 3: users/edit-user-dialog — apply the same component + HTML changes**
|
||||
|
||||
Repeat Step 1 and Step 2 in `users/components/edit-user-dialog/create…`→ the `edit-user-dialog.component.ts` and `.html`. The `edit` dialog also pre-selects existing roles; because values are still role-code strings and `[valuePrimitive]="true"`, the existing `formControlName="roles"` value binds correctly with no further change.
|
||||
|
||||
- [ ] **Step 4: members/create-user-dialog — apply the same changes**
|
||||
|
||||
Repeat Step 1 and Step 2 in `members/components/create-user-dialog/create-user-dialog.component.ts` and `.html`. (Import path depth is the same four levels: `../../../../shared/i18n/option-lists`.)
|
||||
|
||||
- [ ] **Step 5: Build to verify**
|
||||
|
||||
Run: `cd APP && npm run build`
|
||||
Expected: build succeeds. If the build flags an unused `ALL_ROLES` import, remove it in the offending file.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add APP/src/app/features/users/components/create-user-dialog/create-user-dialog.component.html APP/src/app/features/users/components/create-user-dialog/create-user-dialog.component.ts APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.html APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.ts APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.html APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.ts
|
||||
git commit -m "feat(i18n): bilingual language + roles selectors in user dialogs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Document the convention
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/PLANNING.md`
|
||||
|
||||
- [ ] **Step 1: Append the convention section**
|
||||
|
||||
Add a new section to `docs/PLANNING.md` (place it under the relevant frontend/UI area; if unsure, append at the end before any trailing footer):
|
||||
```markdown
|
||||
## 下拉雙語顯示約定 (Dropdown Bilingual Display)
|
||||
|
||||
所有業務下拉選單的選項同時顯示英文與中文,格式 `英文/中文`(無空格、英文在前;無中文則只顯示英文)。
|
||||
|
||||
- **DB 查表下拉**(giving categories / ministries / expense category groups & subs):
|
||||
在對應的 API service 載入時計算 `label = bilingual(name_en, name_zh)`,模板綁 `textField="label"`。
|
||||
不改 DB、不改 schema、不改儲存值。
|
||||
- **寫死 enum 下拉**(payment method / expense status / member status / gender / language / roles):
|
||||
選項一律定義在 `APP/src/app/shared/i18n/option-lists.ts`,型別 `{ value, label }`,
|
||||
模板綁 `textField="label" valueField="value" [valuePrimitive]="true"`。送出/儲存的 `value` 不變。
|
||||
- 共用工具:`APP/src/app/shared/i18n/bilingual.ts` 的 `bilingual(en, zh)`。
|
||||
- 新增下拉時沿用以上兩種模式,不要在 component 內各自寫死中文字串。
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/PLANNING.md
|
||||
git commit -m "docs: record dropdown bilingual display convention"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Full verification (build + browser preview)
|
||||
|
||||
**Files:** none (verification only).
|
||||
|
||||
- [ ] **Step 1: Clean build**
|
||||
|
||||
Run: `cd APP && npm run build`
|
||||
Expected: succeeds with no errors.
|
||||
|
||||
- [ ] **Step 2: Run the unit suites**
|
||||
|
||||
Run: `cd APP && npm run test:ci`
|
||||
Expected: `bilingual` and `option-lists` suites PASS (and no previously-passing suite regresses).
|
||||
|
||||
- [ ] **Step 3: Start the app and verify in the browser preview**
|
||||
|
||||
Start the dev server (`preview_start` / `npm start`) and, using the preview tools, confirm each of the following renders `英文/中文`:
|
||||
- Offering-session page: **Type** shows e.g. `Worship/敬拜`; **Method** shows `Cash/現金`. Add one line → the **Type** column in the lines grid below also shows the bilingual label.
|
||||
- Expense form dialog: **Ministry**, **大類 (group)**, **子項 (sub)** all bilingual.
|
||||
- Expenses page: **Ministry** and **Status** filters bilingual.
|
||||
- Member form dialog: **Gender**, **Status** bilingual. Members page: **Status** filter bilingual.
|
||||
- User dialogs (create/edit): **Language** bilingual; **Roles** multiselect options bilingual.
|
||||
|
||||
Capture `preview_screenshot` evidence for the offering-session and expense dialog as proof.
|
||||
|
||||
- [ ] **Step 4: Confirm stored values are unchanged**
|
||||
|
||||
In the preview, select bilingual options and submit one offering line and (if feasible) one expense; via `preview_network` confirm the request payload still sends raw values (`paymentMethod: "Cash"`, `givingCategoryId: <int>`, status codes, role codes) — NOT the `英文/中文` label.
|
||||
|
||||
- [ ] **Step 5: Final confirmation**
|
||||
|
||||
No commit needed (verification only). Report results; if any dropdown still shows English-only or a payload carries a label string, return to the relevant task and fix.
|
||||
|
||||
---
|
||||
|
||||
## Notes for the implementer
|
||||
|
||||
- **Import-path depth:** `shared/i18n/...` is referenced from `features/<area>/pages/<page>/` (4 levels up → `../../../../shared/i18n/...`) and from `features/<area>/components/<comp>/` (also 4 levels up). Services under `features/<area>/services/` are 3 levels up (`../../../shared/i18n/...`). Verify the relative depth per file; the build will catch a wrong path.
|
||||
- **Don't touch stored values.** Every change is display-only. `[valuePrimitive]="true"` + `valueField="value"` is what keeps form/DB values identical to today.
|
||||
- **Out of scope (per spec §5):** server-sourced grid name columns (e.g. expense list ministry/category names), Angular i18n framework, DB/seed/migration changes, dashboard template demo dropdowns.
|
||||
Reference in New Issue
Block a user