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 = 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}`; } }