add quick add entry.

This commit is contained in:
Chris Chen
2026-06-20 20:42:06 -07:00
parent 87425b3276
commit 8061a60fe5
18 changed files with 1050 additions and 5 deletions
@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Observable, from, of, map, switchMap } from 'rxjs';
import { Observable, Subject, from, of, map, switchMap, takeUntil } from 'rxjs';
import { buildProofPdf } from '../../services/proof-pdf.builder';
import { GridModule } from '@progress/kendo-angular-grid';
import { InputsModule } from '@progress/kendo-angular-inputs';
@@ -10,13 +10,14 @@ import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
import { DialogsModule } from '@progress/kendo-angular-dialog';
import { OfferingSessionApiService } from '../../services/offering-session-api.service';
import { OfferingEntrySignalrService } from '../../services/offering-entry-signalr.service';
import { GivingCategoryApiService } from '../../services/giving-category-api.service';
import { MemberApiService } from '../../../members/services/member-api.service';
import { MemberListItemDto, memberDisplayName } from '../../../members/models/member.model';
import { MemberQuickAddDialogComponent } from '../../components/member-quick-add-dialog/member-quick-add-dialog.component';
import {
GivingCategoryDto, PaymentMethod, OfferingBufferLine, CreateOfferingSessionRequest,
OfferingSessionListItemDto, OfferingSessionDto,
OfferingSessionListItemDto, OfferingSessionDto, OfferingGivingLineDto,
} from '../../models/giving.model';
import { PAYMENT_METHOD_OPTIONS } from '../../../../shared/i18n/option-lists';
@@ -34,9 +35,14 @@ type PageMode = 'landing' | 'workspace' | 'view';
templateUrl: './offering-session-page.component.html',
styleUrls: ['./offering-session-page.component.scss'],
})
export class OfferingSessionPageComponent implements OnInit {
export class OfferingSessionPageComponent implements OnInit, OnDestroy {
mode: PageMode = 'landing';
// The session date currently joined for live mobile-entry updates (yyyy-MM-dd),
// and a teardown signal for the SignalR subscription.
private liveDate: string | null = null;
private readonly destroy$ = new Subject<void>();
sessionDate: Date = new Date();
dateConflict = false;
categories: GivingCategoryDto[] = [];
@@ -72,6 +78,7 @@ export class OfferingSessionPageComponent implements OnInit {
private api: OfferingSessionApiService,
private categoryApi: GivingCategoryApiService,
private memberApi: MemberApiService,
private signalr: OfferingEntrySignalrService,
) {}
ngOnInit(): void {
@@ -81,6 +88,67 @@ export class OfferingSessionPageComponent implements OnInit {
});
this.checkDate();
this.loadSessions();
// Live updates: when a volunteer adds a line on the mobile page for the date
// we're currently viewing/editing, reflect it here without a manual refresh.
this.signalr.lineAdded$
.pipe(takeUntil(this.destroy$))
.subscribe(evt => this.onMobileLineAdded(evt));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.leaveLive();
this.signalr.stop();
}
// ── Live mobile-entry sync ─────────────────────────────────────────────────
private onMobileLineAdded(evt: { sessionDate: string; sessionId: number; line: OfferingGivingLineDto }): void {
if (evt.sessionDate !== this.liveDate) {
return;
}
if (this.mode === 'view' && this.viewSession && this.viewSession.id === evt.sessionId) {
// Re-fetch so the read-only Lines list (and totals/status) stays authoritative.
this.api.getById(this.viewSession.id).subscribe(dto => this.viewSession = dto);
} else if (this.mode === 'workspace' && this.editingSessionId === evt.sessionId) {
// Append to the open editor's buffer so it includes lines added from phones.
this.buffer = [...this.buffer, this.bufferLineFromDto(evt.line)];
}
}
private bufferLineFromDto(g: OfferingGivingLineDto): OfferingBufferLine {
return {
memberId: g.memberId, givingCategoryId: g.givingCategoryId, amount: g.amount,
paymentMethod: g.paymentMethod, checkNumber: g.checkNumber,
zelleReferenceCode: g.zelleReferenceCode, payPalTransactionId: g.payPalTransactionId,
isAnonymous: g.isAnonymous, notes: g.notes,
memberName: g.memberName, categoryName: g.categoryName,
};
}
/** Subscribe to live updates for one session date, leaving any previous one. */
private joinLive(date: string): void {
this.signalr.start()
.then(() => {
if (this.liveDate && this.liveDate !== date) {
return this.signalr.leaveDate(this.liveDate).then(() => undefined);
}
return undefined;
})
.then(() => {
this.liveDate = date;
return this.signalr.joinDate(date);
})
.catch(() => { /* offline is fine — the page still works without live sync */ });
}
private leaveLive(): void {
if (this.liveDate) {
this.signalr.leaveDate(this.liveDate).catch(() => undefined);
this.liveDate = null;
}
}
get systemTotal(): number { return this.buffer.reduce((s, l) => s + (l.amount || 0), 0); }
@@ -106,6 +174,7 @@ export class OfferingSessionPageComponent implements OnInit {
this.cashTotal = 0; this.checkTotal = 0; this.notes = null;
this.pendingProofFiles = [];
this.resetEntry();
this.joinLive(this.toIso(this.sessionDate));
this.mode = 'workspace';
}
@@ -125,7 +194,7 @@ export class OfferingSessionPageComponent implements OnInit {
/** Open a session read-only (from a Recent Sessions row or a resolved date). */
openView(s: OfferingSessionListItemDto): void {
this.api.getById(s.id).subscribe({
next: dto => { this.viewSession = dto; this.mode = 'view'; },
next: dto => { this.viewSession = dto; this.joinLive(dto.sessionDate); this.mode = 'view'; },
error: (err: { error?: { message?: string } }) => alert(err?.error?.message ?? 'Load failed.'),
});
}
@@ -161,6 +230,7 @@ export class OfferingSessionPageComponent implements OnInit {
/** Leave workspace/view and return to the date-first landing screen. */
backToLanding(): void {
this.leaveLive();
this.resetSession();
this.mode = 'landing';
this.loadSessions();
@@ -276,6 +346,7 @@ export class OfferingSessionPageComponent implements OnInit {
next: () => {
this.submitting = false;
alert(isEdit ? 'Offering session updated.' : 'Offering session submitted.');
this.leaveLive();
this.resetSession();
this.mode = 'landing';
this.loadSessions();