` (after line 6), as the first child of the left form column:
+
+```html
+
+
+
+
+
+```
+
+Then add the name-prompt dialog as a sibling of the main `
`, immediately after its closing `` (after line 213):
+
+```html
+
+
+
+
+
費用日期不會存入範本 / The Expense Date is not saved in a snapshot.
+
+
+
+
+
+
+```
+
+- [ ] **Step 6: Verify the build compiles**
+
+Run from `APP/`: `npx ng build --configuration development`
+Expected: build completes with no errors referencing `expense-form-dialog`.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add APP/src/app/features/expense/components/expense-form-dialog/
+git commit -m "feat(expense-snapshot): load/save snapshot in vendor payment form"
+```
+
+---
+
+## Task 8: Management page
+
+**Files:**
+- Create: `APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.ts`
+- Create: `APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.html`
+- Create: `APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.scss`
+
+- [ ] **Step 1: Create the component class**
+
+Rename re-uses the `update` endpoint: the page fetches the full snapshot, swaps `name`, and PUTs it back (the line set is preserved). Create `expense-snapshots-page.component.ts`:
+
+```typescript
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { GridModule } from '@progress/kendo-angular-grid';
+import { ButtonsModule } from '@progress/kendo-angular-buttons';
+import { InputsModule } from '@progress/kendo-angular-inputs';
+import { DialogsModule } from '@progress/kendo-angular-dialog';
+import { ExpenseSnapshotApiService } from '../../services/expense-snapshot-api.service';
+import { ExpenseSnapshotDto } from '../../models/expense-snapshot.model';
+import { switchMap } from 'rxjs';
+
+@Component({
+ selector: 'app-expense-snapshots-page',
+ standalone: true,
+ imports: [CommonModule, FormsModule, GridModule, ButtonsModule, InputsModule, DialogsModule],
+ templateUrl: './expense-snapshots-page.component.html',
+ styleUrls: ['./expense-snapshots-page.component.scss'],
+})
+export class ExpenseSnapshotsPageComponent implements OnInit {
+ rows: ExpenseSnapshotDto[] = [];
+ loading = false;
+
+ /** Row being renamed (drives the rename dialog); null when closed. */
+ renameRow: ExpenseSnapshotDto | null = null;
+ renameValue = '';
+ renameSaving = false;
+
+ /** Row pending delete confirmation. */
+ deleteRow: ExpenseSnapshotDto | null = null;
+
+ constructor(private api: ExpenseSnapshotApiService) {}
+
+ ngOnInit(): void { this.load(); }
+
+ load(): void {
+ this.loading = true;
+ this.api.getAll().subscribe({
+ next: list => { this.rows = list; this.loading = false; },
+ error: () => { this.loading = false; },
+ });
+ }
+
+ openRename(row: ExpenseSnapshotDto): void {
+ this.renameRow = row;
+ this.renameValue = row.name;
+ }
+ cancelRename(): void { this.renameRow = null; }
+
+ confirmRename(): void {
+ const row = this.renameRow;
+ const name = this.renameValue.trim();
+ if (!row || !name || this.renameSaving) return;
+ this.renameSaving = true;
+ // Fetch the full snapshot, swap the name, PUT it back (lines/fields preserved).
+ this.api.getById(row.id).pipe(
+ switchMap(full => this.api.update(row.id, {
+ name,
+ ministryId: full.ministryId,
+ description: full.description,
+ vendorName: full.vendorName,
+ checkNumber: full.checkNumber,
+ notes: full.notes,
+ lines: full.lines.map(l => ({
+ categoryGroupId: l.categoryGroupId,
+ subCategoryId: l.subCategoryId,
+ amount: l.amount,
+ functionalClass: l.functionalClass,
+ description: l.description,
+ })),
+ })),
+ ).subscribe({
+ next: () => { this.renameSaving = false; this.renameRow = null; this.load(); },
+ error: () => { this.renameSaving = false; },
+ });
+ }
+
+ openDelete(row: ExpenseSnapshotDto): void { this.deleteRow = row; }
+ cancelDelete(): void { this.deleteRow = null; }
+
+ confirmDelete(): void {
+ if (!this.deleteRow) return;
+ this.api.delete(this.deleteRow.id).subscribe(() => { this.deleteRow = null; this.load(); });
+ }
+}
+```
+
+- [ ] **Step 2: Create the template (mobile-friendly: desktop grid + card list)**
+
+Per house rule, desktop uses `hidden md:block`, mobile uses a `md:hidden` card list, and the card list's flex layout lives in Tailwind utilities (never component SCSS). Create `expense-snapshots-page.component.html`:
+
+```html
+
+
+ 儲存常用的固定費用(房租、網路、餐費…)為範本,下次可快速套用。費用日期不會儲存。
+ Save recurring fixed expenses as snapshots to quickly re-use them. The Expense Date is never saved.
+
+
+
+
+
+
+
+ {{ dataItem.vendorName || '—' }}
+
+
+
+
+
+ {{ dataItem.createdByName || '—' }}
+ {{ dataItem.createdAt | date:'yyyy-MM-dd' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.name }}
+ {{ row.totalAmount | currency }}
+
+
{{ row.vendorName || '—' }} · {{ row.ministryName }}
+
{{ row.createdByName || '—' }} · {{ row.createdAt | date:'yyyy-MM-dd' }}
+
+
+
+
+
+
尚無範本 / No snapshots yet.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 確定刪除「{{ deleteRow.name }}」? / Delete "{{ deleteRow.name }}"?
+
+
+
+
+
+
+```
+
+- [ ] **Step 3: Create a minimal SCSS file**
+
+Per house rule, do NOT put `display` rules here. Create `expense-snapshots-page.component.scss`:
+
+```scss
+.page {
+ padding: 0.5rem 0;
+}
+```
+
+- [ ] **Step 4: Verify the build compiles**
+
+Run from `APP/`: `npx ng build --configuration development`
+Expected: build completes with no errors referencing `expense-snapshots-page`.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add APP/src/app/features/expense/pages/expense-snapshots-page/
+git commit -m "feat(expense-snapshot): snapshot management page (rename/delete)"
+```
+
+---
+
+## Task 9: Route + sidebar nav
+
+**Files:**
+- Modify: `APP/src/app/app.routes.ts` (finance routes block, near line 192)
+- Modify: `APP/src/app/portals/user-portal/user-portal.component.ts:139` (Expenses nav group)
+
+- [ ] **Step 1: Import the page component in the routes file**
+
+In `APP/src/app/app.routes.ts`, add an import alongside the other expense page imports (e.g. near the `ExpensesPageComponent` import):
+
+```typescript
+import { ExpenseSnapshotsPageComponent } from './features/expense/pages/expense-snapshots-page/expense-snapshots-page.component';
+```
+
+(Confirm the exact relative path matches the other expense page imports in that file; they live under `./features/expense/pages/...`.)
+
+- [ ] **Step 2: Add the route**
+
+In `app.routes.ts`, immediately after the `finance/expenses` route object (closes at line 164), add:
+
+```typescript
+ {
+ path: 'finance/expense-snapshots',
+ component: ExpenseSnapshotsPageComponent,
+ canActivate: [PermissionGuard],
+ data: {
+ permission: { module: PermissionModules.Expenses, action: 'read' },
+ title: 'Expense Snapshots', titleZh: '費用範本', section: 'Finance',
+ },
+ },
+```
+
+- [ ] **Step 3: Add the sidebar nav item**
+
+In `APP/src/app/portals/user-portal/user-portal.component.ts`, inside the `Expenses` finance group's `items` array, after the `Expense Categories` item (line 133-134), add:
+
+```typescript
+ { text: 'Expense Snapshots', icon: categorizeIcon, path: '/user-portal/finance/expense-snapshots',
+ permission: { module: PermissionModules.Expenses, action: 'read' } },
+```
+
+(`categorizeIcon` is already imported and used in this file.)
+
+- [ ] **Step 4: Verify the build compiles**
+
+Run from `APP/`: `npx ng build --configuration development`
+Expected: build completes, 0 errors.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add APP/src/app/app.routes.ts APP/src/app/portals/user-portal/user-portal.component.ts
+git commit -m "feat(expense-snapshot): route + sidebar nav for snapshot management"
+```
+
+---
+
+## Task 10: End-to-end verification
+
+**Files:** none (manual + automated verification)
+
+- [ ] **Step 1: Backend — full build + targeted tests green**
+
+Run: `dotnet build API/ROLAC.API/ROLAC.API.csproj -c Release` → Build succeeded.
+Run: `dotnet test API/ROLAC.API.Tests/ROLAC.API.Tests.csproj -c Release --filter FullyQualifiedName~ExpenseSnapshotServiceTests` → 7 passed.
+
+- [ ] **Step 2: Frontend — service tests + build green**
+
+Run from `APP/`:
+```powershell
+$env:CHROME_BIN = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
+npx ng test --include="**/expense-snapshot-api.service.spec.ts" --watch=false --browsers=ChromeHeadless
+npx ng build --configuration development
+```
+Expected: 3 specs pass; build succeeds.
+
+- [ ] **Step 3: Manual smoke test**
+
+Start the API and the frontend (see the Build/Run env memory). As a finance user:
+1. Finance → Expenses → **+ Vendor Payment**. Fill ministry, description, vendor, one line with an amount. Click **存為範本 / Save as snapshot**, name it "Smoke Rent", Save. Confirm no error.
+2. Close the dialog, reopen **+ Vendor Payment**. In **Load from snapshot**, pick "Smoke Rent". Confirm ministry/description/vendor/line all prefill and **Expense Date stays today** (not blank/altered).
+3. Finance → **Expense Snapshots**. Confirm "Smoke Rent" appears with vendor, amount, and your name as creator. **Rename** it → confirm the new name shows. **Delete** it → confirm it disappears.
+4. Resize the browser to mobile width on the Expense Snapshots page → confirm the card list shows (not the grid) and actions work.
+
+- [ ] **Step 4: Final commit (if any manual-fix tweaks were needed)**
+
+```bash
+git add -A
+git commit -m "test(expense-snapshot): verification fixes"
+```
+
+(Skip if nothing changed.)
+
+---
+
+## Self-Review Notes
+
+- **Spec coverage:** capture fields (Task 1/3), shared + creator tag resolved at read (Task 4 `ResolveUserNamesAsync` + `GetByIdName` test), exclude ExpenseDate/receipt/MemberId (entities omit them; Task 7 `applySnapshot` keeps today's date), save-from-form (Task 7), quick picker + management page both present (Tasks 7 & 8 — spec Q4=C), rename + delete (Task 8), `Expenses:Write` gating (Task 5), mobile-friendly management page (Task 8). All covered.
+- **CheckNumber:** captured and editable per spec; no special handling needed.
+- **Type consistency:** `CreateExpenseSnapshotRequest`/`UpdateExpenseSnapshotRequest`, `ExpenseSnapshotDto`, `ExpenseSnapshotLineDto`, `ExpenseLineInput` names match across backend DTOs (Task 3), service (Task 4), frontend model (Task 6), and consumers (Tasks 7-8). Service method names (`getAll/getById/create/update/delete`) are identical across interface, service, and Angular service.
diff --git a/docs/superpowers/specs/2026-06-25-vendor-payment-snapshot-design.md b/docs/superpowers/specs/2026-06-25-vendor-payment-snapshot-design.md
index f437a61..868d34e 100644
--- a/docs/superpowers/specs/2026-06-25-vendor-payment-snapshot-design.md
+++ b/docs/superpowers/specs/2026-06-25-vendor-payment-snapshot-design.md
@@ -29,7 +29,7 @@ Everything needed to refill the vendor-payment form **except `ExpenseDate`**.
| Label | `Name` (required, user-supplied, e.g. "Monthly Rent — Landlord X") |
| Header | `MinistryId`, `Description`, `VendorName`, `CheckNumber`, `Notes` |
| Lines (1..n) | `CategoryGroupId`, `SubCategoryId`, `Amount`, `FunctionalClass`, `Description` |
-| Audit | `CreatedBy`, `CreatedByName`, `CreatedAt` (+ soft-delete / auditable fields) |
+| Audit | `CreatedBy` + `CreatedAt` (auto-stamped by `AuditSaveChangesInterceptor`); the creator display name is resolved at read time, not stored |
**Excluded:** `ExpenseDate` (always starts fresh / today), the receipt file, and `MemberId`
(not used in vendor mode).
@@ -50,9 +50,10 @@ overwrites it. Captured value is shown editable in the form.
### Entities (`API/ROLAC.API/Entities/`)
-- **`ExpenseSnapshot`** — header. Fields: `Id`, `Name`, `MinistryId`, `Description`,
- `VendorName`, `CheckNumber`, `Notes`, `CreatedBy`, `CreatedByName`, plus auditable
- (`CreatedAt`, `UpdatedAt`, `UpdatedBy`) and soft-delete (`IsDeleted`). Owns `Lines`.
+- **`ExpenseSnapshot`** — header, extends `SoftDeleteEntity` (so it gets `CreatedBy`,
+ `CreatedAt`, `UpdatedBy`, `UpdatedAt`, `IsDeleted` auto-stamped). Fields: `Id`, `Name`,
+ `MinistryId`, `Description`, `VendorName`, `CheckNumber`, `Notes`. Owns `Lines`. The
+ creator's display name is resolved at read time (mirroring `ReviewedByName`), not stored.
- **`ExpenseSnapshotLine`** — mirrors `ExpenseLine`: `Id`, `SnapshotId` (cascade),
`CategoryGroupId`, `SubCategoryId`, `Amount`, `FunctionalClass`, `Description`.
Category FKs use `Restrict` delete (same as `ExpenseLine`).
@@ -76,7 +77,7 @@ permission check used by `ExpensesController`.
|---|---|---|
| GET | `/` | List all snapshots (shared), newest first, with `createdByName` + totals |
| GET | `/{id}` | Full snapshot incl. lines (for apply / management edit) |
-| POST | `/` | Create from form payload; stamps `CreatedBy`/`CreatedByName` from current user |
+| POST | `/` | Create from form payload (`CreatedBy`/`CreatedAt` auto-stamped by the interceptor) |
| PUT | `/{id}` | Update (rename and/or re-save fields from the management page) |
| DELETE | `/{id}` | Soft-delete |