From 41dce076d6fe0aebdd238e783ab53a07fd2462f0 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Thu, 25 Jun 2026 15:14:16 -0700 Subject: [PATCH] feat(expense-snapshot): snapshot management page (rename/delete) Co-Authored-By: Claude Opus 4.8 --- .../expense-snapshots-page.component.html | 68 +++++++++++++++ .../expense-snapshots-page.component.scss | 3 + .../expense-snapshots-page.component.ts | 84 +++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.html create mode 100644 APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.scss create mode 100644 APP/src/app/features/expense/pages/expense-snapshots-page/expense-snapshots-page.component.ts 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. +

+ + + + + +
+
+
+ {{ 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(); }); + } +}