wip
This commit is contained in:
+59
-1
@@ -50,6 +50,11 @@
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
<kendo-grid-column field="lineCount" title="Lines" [width]="80"></kendo-grid-column>
|
||||
<kendo-grid-column title="Proof" [width]="70">
|
||||
<ng-template kendoGridCellTemplate let-s>
|
||||
<span *ngIf="s.hasProof" title="Paper proof attached · 已附證明">📎</span>
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
<kendo-grid-column field="systemTotal" title="System" [width]="120" format="c2"></kendo-grid-column>
|
||||
<kendo-grid-column field="difference" title="Diff" [width]="110" format="c2"></kendo-grid-column>
|
||||
<kendo-grid-column title="" [width]="110">
|
||||
@@ -152,6 +157,35 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Paper proof -->
|
||||
<section class="card rise" style="--d: 200ms">
|
||||
<div class="card__head">
|
||||
<h2 class="card__title">Paper Proof</h2>
|
||||
<span class="card__zh">紙本證明</span>
|
||||
</div>
|
||||
|
||||
<p class="proof-hint">
|
||||
Attach photos or PDFs of the count sheets / envelopes. Images are compressed and all files
|
||||
are merged into a single PDF when you submit.
|
||||
<br><span>附上點算單/信封的照片或 PDF。圖片會自動壓縮,送出時合併為單一 PDF。</span>
|
||||
</p>
|
||||
|
||||
<input type="file" multiple accept="image/*,application/pdf"
|
||||
class="proof-input" (change)="onProofFilesSelected($event)" />
|
||||
|
||||
<ul *ngIf="pendingProofFiles.length" class="proof-list">
|
||||
<li *ngFor="let f of pendingProofFiles; let i = index">
|
||||
<span class="proof-name">{{ f.name }}</span>
|
||||
<button kendoButton fillMode="flat" size="small" (click)="removeProofFile(i)">×</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p *ngIf="editingSessionId != null && pendingProofFiles.length" class="proof-warn">
|
||||
Selecting files here replaces any existing proof for this session.
|
||||
<br><span>於此選擇檔案會取代此 session 既有的證明。</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Reconcile & submit -->
|
||||
<section class="card rise" style="--d: 240ms">
|
||||
<div class="card__head">
|
||||
@@ -230,8 +264,32 @@
|
||||
</kendo-grid>
|
||||
</section>
|
||||
|
||||
<!-- Paper proof (view) -->
|
||||
<section class="card rise" style="--d: 200ms">
|
||||
<div class="card__head">
|
||||
<h2 class="card__title">Paper Proof</h2>
|
||||
<span class="card__zh">紙本證明</span>
|
||||
</div>
|
||||
|
||||
<input #proofInput type="file" multiple accept="image/*,application/pdf" hidden
|
||||
(change)="replaceProof($event)" />
|
||||
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<ng-container *ngIf="v.hasProof; else noProof">
|
||||
<button kendoButton themeColor="primary" (click)="openProof(v.id)" [disabled]="proofBusy">View proof PDF / 檢視證明</button>
|
||||
<button kendoButton (click)="proofInput.click()" [disabled]="proofBusy">Replace / 更換</button>
|
||||
<button kendoButton fillMode="flat" (click)="removeProof()" [disabled]="proofBusy">Remove / 移除</button>
|
||||
</ng-container>
|
||||
<ng-template #noProof>
|
||||
<span class="proof-none">No proof attached · 尚未附上證明</span>
|
||||
<button kendoButton themeColor="primary" (click)="proofInput.click()" [disabled]="proofBusy">Add proof / 新增證明</button>
|
||||
</ng-template>
|
||||
<span *ngIf="proofBusy" class="proof-busy">Working… · 處理中…</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Session notes -->
|
||||
<section *ngIf="v.notes" class="card rise" style="--d: 240ms">
|
||||
<section *ngIf="v.notes" class="card rise" style="--d: 280ms">
|
||||
<div class="card__head">
|
||||
<h2 class="card__title">Notes</h2>
|
||||
<span class="card__zh">備註</span>
|
||||
|
||||
+16
@@ -182,6 +182,22 @@
|
||||
.lines-footer { margin-top: 10px; color: var(--ink-soft); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.anon-chip { padding: 0.25rem 0.6rem; background: var(--kendo-color-base-subtle, #e6eaef); border-radius: 999px; font-size: 12px; font-weight: 600; }
|
||||
.notes-text { margin: 0; color: var(--ink); line-height: 1.5; white-space: pre-wrap; }
|
||||
|
||||
/* ---- Paper proof ---- */
|
||||
.proof-hint { margin: 0 0 12px; font-size: 13px; line-height: 1.5; color: var(--ink-soft); span { color: #9aa7b6; } }
|
||||
.proof-input { display: block; font-size: 14px; }
|
||||
.proof-list {
|
||||
list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px;
|
||||
li {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||||
padding: 4px 10px; background: var(--kendo-color-base-subtle, #f1f4f8);
|
||||
border: 1px solid var(--line); border-radius: 8px; font-size: 13px;
|
||||
}
|
||||
}
|
||||
.proof-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.proof-warn { margin: 10px 0 0; font-size: 12px; line-height: 1.5; color: #b45309; span { color: #d97706; } }
|
||||
.proof-none { font-size: 14px; color: var(--ink-soft); }
|
||||
.proof-busy { font-size: 13px; color: var(--ink-soft); }
|
||||
.dialog-text { margin: 0; line-height: 1.55; span { color: var(--ink-soft); font-size: 13px; } }
|
||||
|
||||
.empty {
|
||||
|
||||
+81
-6
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user