feat(expense-snapshot): snapshot management page (rename/delete)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+68
@@ -0,0 +1,68 @@
|
|||||||
|
<div class="page">
|
||||||
|
<p class="mb-4 text-sm text-gray-600">
|
||||||
|
儲存常用的固定費用(房租、網路、餐費…)為範本,下次可快速套用。費用日期不會儲存。<br>
|
||||||
|
Save recurring fixed expenses as snapshots to quickly re-use them. The Expense Date is never saved.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Desktop: grid -->
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<kendo-grid [data]="rows" [loading]="loading">
|
||||||
|
<kendo-grid-column field="name" title="Snapshot / 範本" [width]="240"></kendo-grid-column>
|
||||||
|
<kendo-grid-column field="vendorName" title="Vendor / 廠商" [width]="180">
|
||||||
|
<ng-template kendoGridCellTemplate let-dataItem>{{ dataItem.vendorName || '—' }}</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
<kendo-grid-column field="ministryName" title="Ministry / 事工"></kendo-grid-column>
|
||||||
|
<kendo-grid-column field="totalAmount" title="Amount / 金額" [width]="120" format="c2"></kendo-grid-column>
|
||||||
|
<kendo-grid-column title="Created by / 建立者" [width]="200">
|
||||||
|
<ng-template kendoGridCellTemplate let-dataItem>
|
||||||
|
{{ dataItem.createdByName || '—' }}<br>
|
||||||
|
<span class="text-xs text-gray-500">{{ dataItem.createdAt | date:'yyyy-MM-dd' }}</span>
|
||||||
|
</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
<kendo-grid-column title="Actions" [width]="160">
|
||||||
|
<ng-template kendoGridCellTemplate let-dataItem>
|
||||||
|
<button kendoButton fillMode="flat" (click)="openRename(dataItem)">Rename</button>
|
||||||
|
<button kendoButton fillMode="flat" themeColor="error" (click)="openDelete(dataItem)">Delete</button>
|
||||||
|
</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
</kendo-grid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile: card list -->
|
||||||
|
<div class="md:hidden flex flex-col gap-3">
|
||||||
|
<div *ngFor="let row of rows" class="rounded border border-gray-200 p-3 flex flex-col gap-1">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="font-semibold">{{ row.name }}</span>
|
||||||
|
<span class="tabular-nums">{{ row.totalAmount | currency }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">{{ row.vendorName || '—' }} · {{ row.ministryName }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ row.createdByName || '—' }} · {{ row.createdAt | date:'yyyy-MM-dd' }}</div>
|
||||||
|
<div class="flex gap-2 pt-1">
|
||||||
|
<button kendoButton size="small" fillMode="outline" (click)="openRename(row)">Rename</button>
|
||||||
|
<button kendoButton size="small" fillMode="outline" themeColor="error" (click)="openDelete(row)">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p *ngIf="!loading && rows.length === 0" class="text-sm text-gray-500">尚無範本 / No snapshots yet.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rename dialog -->
|
||||||
|
<kendo-dialog *ngIf="renameRow" title="重新命名 / Rename Snapshot" [width]="420" [maxWidth]="'95vw'" (close)="cancelRename()">
|
||||||
|
<label class="flex flex-col gap-1 p-2">名稱 / Name
|
||||||
|
<kendo-textbox [(ngModel)]="renameValue"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
<kendo-dialog-actions>
|
||||||
|
<button kendoButton (click)="cancelRename()">Cancel</button>
|
||||||
|
<button kendoButton themeColor="primary" [disabled]="!renameValue.trim() || renameSaving"
|
||||||
|
(click)="confirmRename()">{{ renameSaving ? 'Saving…' : 'Save' }}</button>
|
||||||
|
</kendo-dialog-actions>
|
||||||
|
</kendo-dialog>
|
||||||
|
|
||||||
|
<!-- Delete confirm dialog -->
|
||||||
|
<kendo-dialog *ngIf="deleteRow" title="刪除 / Delete Snapshot" [width]="420" [maxWidth]="'95vw'" (close)="cancelDelete()">
|
||||||
|
<p class="p-2">確定刪除「{{ deleteRow.name }}」? / Delete "{{ deleteRow.name }}"?</p>
|
||||||
|
<kendo-dialog-actions>
|
||||||
|
<button kendoButton (click)="cancelDelete()">Cancel</button>
|
||||||
|
<button kendoButton themeColor="error" (click)="confirmDelete()">Delete</button>
|
||||||
|
</kendo-dialog-actions>
|
||||||
|
</kendo-dialog>
|
||||||
|
</div>
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
.page {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
+84
@@ -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(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user