diff --git a/APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.html b/APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.html
new file mode 100644
index 0000000..e13083d
--- /dev/null
+++ b/APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.html
@@ -0,0 +1,68 @@
+
+
+ 儲存常用的固定費用(房租、網路、餐費…)為範本,下次可快速套用。費用日期不會儲存。
+ 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 }}"?
+
+
+
+
+
+
diff --git a/APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.scss b/APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.scss
new file mode 100644
index 0000000..12beaad
--- /dev/null
+++ b/APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.scss
@@ -0,0 +1,3 @@
+.page {
+ padding: 0.5rem 0;
+}
diff --git a/APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.ts b/APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.ts
new file mode 100644
index 0000000..16bd681
--- /dev/null
+++ b/APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.ts
@@ -0,0 +1,84 @@
+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(); });
+ }
+}