This commit is contained in:
Chris Chen
2026-06-20 15:13:23 -07:00
parent b6c50a38aa
commit f55807fa7d
32 changed files with 866 additions and 18 deletions
@@ -1,7 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Observable } from 'rxjs';
import { Observable, from, of, map, switchMap } from 'rxjs';
import { buildProofPdf } from '../../services/proof-pdf.builder';
import { GridModule } from '@progress/kendo-angular-grid';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
@@ -56,6 +57,10 @@ export class OfferingSessionPageComponent implements OnInit {
showQuickAdd = false;
submitting = false;
// Paper-proof attachments staged client-side; merged into one PDF on submit.
pendingProofFiles: File[] = [];
proofBusy = false;
sessions: OfferingSessionListItemDto[] = [];
editingSessionId: number | null = null;
@@ -99,6 +104,7 @@ export class OfferingSessionPageComponent implements OnInit {
this.viewSession = null;
this.buffer = [];
this.cashTotal = 0; this.checkTotal = 0; this.notes = null;
this.pendingProofFiles = [];
this.resetEntry();
this.mode = 'workspace';
}
@@ -167,6 +173,7 @@ export class OfferingSessionPageComponent implements OnInit {
this.cashTotal = dto.cashTotal;
this.checkTotal = dto.checkTotal;
this.notes = dto.notes;
this.pendingProofFiles = [];
this.buffer = dto.givings.map(g => ({
memberId: g.memberId, givingCategoryId: g.givingCategoryId, amount: g.amount,
paymentMethod: g.paymentMethod, checkNumber: g.checkNumber,
@@ -254,13 +261,21 @@ export class OfferingSessionPageComponent implements OnInit {
notes: l.notes,
})),
};
const obs: Observable<unknown> = this.editingSessionId != null
? this.api.replace(this.editingSessionId, req)
: this.api.create(req);
obs.subscribe({
const isEdit = this.editingSessionId != null;
// Save the session first, then resolve to its id so the proof PDF can be attached.
const savedId$: Observable<number> = isEdit
? this.api.replace(this.editingSessionId!, req).pipe(map(() => this.editingSessionId!))
: this.api.create(req).pipe(map(r => r.id));
savedId$.pipe(
switchMap(id => this.pendingProofFiles.length === 0
? of(void 0)
: from(buildProofPdf(this.pendingProofFiles)).pipe(
switchMap(({ blob }) => this.api.uploadProof(id, blob)))),
).subscribe({
next: () => {
this.submitting = false;
alert(this.editingSessionId != null ? 'Offering session updated.' : 'Offering session submitted.');
alert(isEdit ? 'Offering session updated.' : 'Offering session submitted.');
this.resetSession();
this.mode = 'landing';
this.loadSessions();
@@ -272,6 +287,65 @@ export class OfferingSessionPageComponent implements OnInit {
});
}
// ── Paper proof ───────────────────────────────────────────────────────────
/** Stage selected attachment files (appends, so multiple picks accumulate). */
onProofFilesSelected(event: Event): void {
const input = event.target as HTMLInputElement;
const files = Array.from(input.files ?? []);
if (files.length) this.pendingProofFiles = [...this.pendingProofFiles, ...files];
input.value = ''; // allow re-selecting the same file
}
removeProofFile(i: number): void {
this.pendingProofFiles = this.pendingProofFiles.filter((_, idx) => idx !== i);
}
/** Open the stored proof PDF in a new tab (authenticated blob fetch). */
openProof(id: number): void {
this.api.downloadProof(id).subscribe(blob => {
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
setTimeout(() => URL.revokeObjectURL(url), 60_000);
});
}
/** Replace the stored proof from the read-only view (rebuild + upload + refresh). */
replaceProof(event: Event): void {
const input = event.target as HTMLInputElement;
const files = Array.from(input.files ?? []);
input.value = '';
if (!files.length || !this.viewSession) return;
const id = this.viewSession.id;
this.proofBusy = true;
from(buildProofPdf(files)).pipe(
switchMap(({ blob }) => this.api.uploadProof(id, blob)),
switchMap(() => this.api.getById(id)),
).subscribe({
next: dto => { this.viewSession = dto; this.proofBusy = false; },
error: (err: { error?: { message?: string } }) => {
this.proofBusy = false;
alert(err?.error?.message ?? 'Proof upload failed.');
},
});
}
removeProof(): void {
if (!this.viewSession) return;
if (!confirm('Remove the paper proof for this session? / 移除此 session 的紙本證明?')) return;
const id = this.viewSession.id;
this.proofBusy = true;
this.api.deleteProof(id).pipe(
switchMap(() => this.api.getById(id)),
).subscribe({
next: dto => { this.viewSession = dto; this.proofBusy = false; },
error: (err: { error?: { message?: string } }) => {
this.proofBusy = false;
alert(err?.error?.message ?? 'Remove failed.');
},
});
}
/** Clear the whole working session back to a fresh state (today, empty buffer). */
private resetSession(): void {
this.editingSessionId = null;
@@ -279,6 +353,7 @@ export class OfferingSessionPageComponent implements OnInit {
this.viewSession = null;
this.buffer = [];
this.cashTotal = 0; this.checkTotal = 0; this.notes = null;
this.pendingProofFiles = [];
this.sessionDate = new Date();
this.resetEntry();
this.checkDate();