248 lines
9.3 KiB
TypeScript
248 lines
9.3 KiB
TypeScript
import { Component, OnInit } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { Observable } from 'rxjs';
|
|
import { GridModule } from '@progress/kendo-angular-grid';
|
|
import { InputsModule } from '@progress/kendo-angular-inputs';
|
|
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
|
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
|
|
import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
|
|
import { OfferingSessionApiService } from '../../services/offering-session-api.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,
|
|
} from '../../models/giving.model';
|
|
|
|
interface MemberOption { id: number; displayName: string; }
|
|
|
|
@Component({
|
|
selector: 'app-offering-session-page',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule, FormsModule, GridModule, InputsModule, ButtonsModule,
|
|
DropDownsModule, DateInputsModule, MemberQuickAddDialogComponent,
|
|
],
|
|
templateUrl: './offering-session-page.component.html',
|
|
styleUrls: ['./offering-session-page.component.scss'],
|
|
})
|
|
export class OfferingSessionPageComponent implements OnInit {
|
|
sessionDate: Date = new Date();
|
|
dateConflict = false;
|
|
categories: GivingCategoryDto[] = [];
|
|
readonly paymentMethods: PaymentMethod[] = ['Cash', 'Check', 'Zelle', 'PayPal', 'Other'];
|
|
|
|
memberResults: MemberOption[] = [];
|
|
selectedMemberId: number | null = null;
|
|
selectedMemberName: string | null = null;
|
|
entry = this.blankEntry();
|
|
|
|
buffer: OfferingBufferLine[] = [];
|
|
editingIndex: number | null = null;
|
|
|
|
cashTotal = 0;
|
|
checkTotal = 0;
|
|
notes: string | null = null;
|
|
|
|
showQuickAdd = false;
|
|
submitting = false;
|
|
|
|
sessions: OfferingSessionListItemDto[] = [];
|
|
editingSessionId: number | null = null;
|
|
|
|
constructor(
|
|
private api: OfferingSessionApiService,
|
|
private categoryApi: GivingCategoryApiService,
|
|
private memberApi: MemberApiService,
|
|
) {}
|
|
|
|
ngOnInit(): void {
|
|
this.categoryApi.getAll(false).subscribe(c => {
|
|
this.categories = c;
|
|
this.entry.givingCategoryId = c[0]?.id ?? 0;
|
|
});
|
|
this.checkDate();
|
|
this.loadSessions();
|
|
}
|
|
|
|
get systemTotal(): number { return this.buffer.reduce((s, l) => s + (l.amount || 0), 0); }
|
|
get difference(): number { return (this.cashTotal + this.checkTotal) - this.systemTotal; }
|
|
|
|
checkDate(): void {
|
|
this.api.checkDate(this.toIso(this.sessionDate)).subscribe(r => this.dateConflict = r.exists);
|
|
}
|
|
|
|
loadSessions(): void {
|
|
this.api.getPaged(1, 20).subscribe(r => this.sessions = r.items);
|
|
}
|
|
|
|
reopenAndEdit(s: OfferingSessionListItemDto): void {
|
|
if (s.status !== 'Submitted') return;
|
|
this.api.reopen(s.id).subscribe({
|
|
next: () => this.api.getById(s.id).subscribe(dto => this.loadIntoBuffer(dto)),
|
|
error: (err: { error?: { message?: string } }) => alert(err?.error?.message ?? 'Reopen failed.'),
|
|
});
|
|
}
|
|
|
|
// Already a Draft (e.g. a session reopened then left) — load it straight back in, no reopen.
|
|
continueEditDraft(s: OfferingSessionListItemDto): void {
|
|
if (s.status !== 'Draft') return;
|
|
this.api.getById(s.id).subscribe({
|
|
next: dto => this.loadIntoBuffer(dto),
|
|
error: (err: { error?: { message?: string } }) => alert(err?.error?.message ?? 'Load failed.'),
|
|
});
|
|
}
|
|
|
|
private loadIntoBuffer(dto: OfferingSessionDto): void {
|
|
this.editingSessionId = dto.id;
|
|
this.sessionDate = new Date(dto.sessionDate + 'T00:00:00');
|
|
this.dateConflict = false;
|
|
this.cashTotal = dto.cashTotal;
|
|
this.checkTotal = dto.checkTotal;
|
|
this.notes = dto.notes;
|
|
this.buffer = dto.givings.map(g => ({
|
|
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,
|
|
}));
|
|
this.resetEntry();
|
|
}
|
|
|
|
cancelEdit(): void {
|
|
this.editingSessionId = null;
|
|
this.editingIndex = null;
|
|
this.buffer = []; this.cashTotal = 0; this.checkTotal = 0; this.notes = null;
|
|
this.sessionDate = new Date();
|
|
this.checkDate();
|
|
// The reopened session is now a server-side Draft — refresh so its "Continue editing" appears.
|
|
this.loadSessions();
|
|
}
|
|
|
|
onMemberFilter(term: string): void {
|
|
if (!term) { this.memberResults = []; return; }
|
|
this.memberApi.getPaged({ search: term, pageSize: 10 }).subscribe(r =>
|
|
this.memberResults = r.items.map((m: MemberListItemDto) => ({ id: m.id, displayName: memberDisplayName(m) })));
|
|
}
|
|
|
|
onMemberSelected(id: number | null): void {
|
|
this.selectedMemberId = id ?? null;
|
|
this.entry.memberId = this.selectedMemberId;
|
|
this.selectedMemberName = this.memberResults.find(m => m.id === id)?.displayName ?? null;
|
|
if (id != null) this.entry.isAnonymous = false;
|
|
}
|
|
|
|
markAnonymous(): void {
|
|
this.entry.isAnonymous = true; this.entry.memberId = null;
|
|
this.selectedMemberId = null; this.selectedMemberName = null;
|
|
}
|
|
|
|
clearAnonymous(): void {
|
|
this.entry.isAnonymous = false;
|
|
}
|
|
|
|
addLine(): void {
|
|
if (this.entry.amount <= 0) return;
|
|
if (this.entry.paymentMethod === 'Check' && !this.entry.checkNumber) return;
|
|
const cat = this.categories.find(c => c.id === this.entry.givingCategoryId);
|
|
const line: OfferingBufferLine = {
|
|
...this.entry,
|
|
memberName: this.entry.isAnonymous ? null : this.selectedMemberName,
|
|
categoryName: cat?.name_en ?? '',
|
|
};
|
|
if (this.editingIndex !== null) { this.buffer[this.editingIndex] = line; this.editingIndex = null; }
|
|
else { this.buffer = [...this.buffer, line]; }
|
|
this.resetEntry();
|
|
}
|
|
|
|
editLine(i: number): void {
|
|
const l = this.buffer[i];
|
|
this.entry = { ...l };
|
|
this.selectedMemberId = l.memberId;
|
|
this.selectedMemberName = l.memberName;
|
|
// Seed the dropdown so the giver name stays visible while editing the line.
|
|
this.memberResults = (l.memberId && l.memberName)
|
|
? [{ id: l.memberId, displayName: l.memberName }]
|
|
: [];
|
|
this.editingIndex = i;
|
|
}
|
|
|
|
removeLine(i: number): void { this.buffer = this.buffer.filter((_, idx) => idx !== i); }
|
|
|
|
onMemberQuickCreated(m: MemberListItemDto): void {
|
|
this.showQuickAdd = false;
|
|
const opt: MemberOption = { id: m.id, displayName: memberDisplayName(m) };
|
|
this.memberResults = [opt, ...this.memberResults.filter(x => x.id !== m.id)];
|
|
this.onMemberSelected(m.id);
|
|
}
|
|
|
|
submit(): void {
|
|
if (this.buffer.length === 0 || (this.editingSessionId == null && this.dateConflict)) return;
|
|
this.submitting = true;
|
|
const req: CreateOfferingSessionRequest = {
|
|
sessionDate: this.toIso(this.sessionDate),
|
|
cashTotal: this.cashTotal,
|
|
checkTotal: this.checkTotal,
|
|
notes: this.notes,
|
|
givings: this.buffer.map(l => ({
|
|
memberId: l.memberId,
|
|
givingCategoryId: l.givingCategoryId,
|
|
amount: l.amount,
|
|
paymentMethod: l.paymentMethod,
|
|
checkNumber: l.checkNumber,
|
|
zelleReferenceCode: l.zelleReferenceCode,
|
|
payPalTransactionId: l.payPalTransactionId,
|
|
isAnonymous: l.isAnonymous,
|
|
notes: l.notes,
|
|
})),
|
|
};
|
|
const obs: Observable<unknown> = this.editingSessionId != null
|
|
? this.api.replace(this.editingSessionId, req)
|
|
: this.api.create(req);
|
|
obs.subscribe({
|
|
next: () => {
|
|
this.submitting = false;
|
|
alert(this.editingSessionId != null ? 'Offering session updated.' : 'Offering session submitted.');
|
|
this.editingSessionId = null;
|
|
this.editingIndex = null;
|
|
this.buffer = []; this.cashTotal = 0; this.checkTotal = 0; this.notes = null;
|
|
this.sessionDate = new Date();
|
|
this.checkDate();
|
|
this.loadSessions();
|
|
},
|
|
error: (err: { error?: { message?: string } }) => {
|
|
this.submitting = false;
|
|
alert(err?.error?.message ?? 'Submit failed.');
|
|
},
|
|
});
|
|
}
|
|
|
|
private resetEntry(): void {
|
|
this.editingIndex = null;
|
|
this.selectedMemberId = null; this.selectedMemberName = null; this.memberResults = [];
|
|
this.entry = this.blankEntry();
|
|
this.entry.givingCategoryId = this.categories[0]?.id ?? 0;
|
|
}
|
|
|
|
private blankEntry(): OfferingBufferLine {
|
|
return {
|
|
memberId: null, givingCategoryId: 0, amount: 0, paymentMethod: 'Cash',
|
|
checkNumber: null, zelleReferenceCode: null, payPalTransactionId: null,
|
|
isAnonymous: false, notes: null, memberName: null, categoryName: '',
|
|
};
|
|
}
|
|
|
|
// Format using LOCAL date components — NOT toISOString(), which converts to UTC and
|
|
// rolls the date forward a day for behind-UTC users when the Date carries an evening time.
|
|
private toIso(d: Date): string {
|
|
const y = d.getFullYear();
|
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
return `${y}-${m}-${day}`;
|
|
}
|
|
}
|