diff --git a/APP/src/app/features/giving/pages/givings-page/givings-page.component.html b/APP/src/app/features/giving/pages/givings-page/givings-page.component.html new file mode 100644 index 0000000..b04f2d0 --- /dev/null +++ b/APP/src/app/features/giving/pages/givings-page/givings-page.component.html @@ -0,0 +1,85 @@ +
+ + +
+ + + +
+ + + + + {{ g.isAnonymous ? '(Anonymous)' : g.memberName }} + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + + +
+
diff --git a/APP/src/app/features/giving/pages/givings-page/givings-page.component.scss b/APP/src/app/features/giving/pages/givings-page/givings-page.component.scss new file mode 100644 index 0000000..2663f68 --- /dev/null +++ b/APP/src/app/features/giving/pages/givings-page/givings-page.component.scss @@ -0,0 +1,4 @@ +.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } +.filters { display: flex; gap: 0.5rem; margin-bottom: 1rem; } +.form-grid { display: flex; flex-direction: column; gap: 0.75rem; } +.form-grid label { display: flex; flex-direction: column; gap: 0.25rem; } diff --git a/APP/src/app/features/giving/pages/givings-page/givings-page.component.ts b/APP/src/app/features/giving/pages/givings-page/givings-page.component.ts new file mode 100644 index 0000000..a17c289 --- /dev/null +++ b/APP/src/app/features/giving/pages/givings-page/givings-page.component.ts @@ -0,0 +1,158 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { GridModule, PageChangeEvent } 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 { DialogsModule } from '@progress/kendo-angular-dialog'; +import { DateInputsModule } from '@progress/kendo-angular-dateinputs'; +import { GivingApiService } from '../../services/giving-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 { + GivingListItemDto, GivingCategoryDto, CreateGivingRequest, PaymentMethod, PagedResult, +} from '../../models/giving.model'; + +/** Flattened member item with a single displayName field for the dropdown. */ +interface MemberOption { + id: number; + displayName: string; +} + +@Component({ + selector: 'app-givings-page', + standalone: true, + imports: [ + CommonModule, FormsModule, GridModule, InputsModule, ButtonsModule, + DropDownsModule, DialogsModule, DateInputsModule, + ], + templateUrl: './givings-page.component.html', + styleUrls: ['./givings-page.component.scss'], +}) +export class GivingsPageComponent implements OnInit { + data: GivingListItemDto[] = []; + totalCount = 0; + page = 1; + pageSize = 20; + isLoading = false; + + search = ''; + filterCategoryId: number | null = null; + categories: GivingCategoryDto[] = []; + + readonly paymentMethods: PaymentMethod[] = ['Cash', 'Check', 'Zelle', 'PayPal', 'Other']; + + memberResults: MemberOption[] = []; + + showDialog = false; + editingId: number | null = null; + form: CreateGivingRequest = this.blankForm(); + selectedMemberId: number | null = null; + + /** Separate Date field for kendo-datepicker (which binds Date, not string). */ + givingDateValue: Date = new Date(); + + constructor( + private api: GivingApiService, + private categoryApi: GivingCategoryApiService, + private memberApi: MemberApiService, + ) {} + + ngOnInit(): void { + this.categoryApi.getAll(false).subscribe(c => this.categories = c); + this.load(); + } + + load(): void { + this.isLoading = true; + this.api.getPaged({ + page: this.page, pageSize: this.pageSize, + search: this.search || undefined, + categoryId: this.filterCategoryId ?? undefined, + }).subscribe({ + next: (r: PagedResult) => { + this.data = r.items; this.totalCount = r.totalCount; this.isLoading = false; + }, + error: () => { this.isLoading = false; }, + }); + } + + onPageChange(e: PageChangeEvent): void { + this.page = e.skip / this.pageSize + 1; this.pageSize = e.take; this.load(); + } + + onSearch(): void { this.page = 1; this.load(); } + + onMemberFilter(term: string): void { + if (!term || term.length < 1) { 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), + })); + }); + } + + openAdd(): void { + this.editingId = null; + this.form = this.blankForm(); + this.selectedMemberId = null; + this.givingDateValue = new Date(); + this.memberResults = []; + this.showDialog = true; + } + + /** Called from template valueChange — receives the primitive id (number | null). */ + onMemberIdSelected(id: number | null): void { + this.selectedMemberId = id ?? null; + this.form.memberId = this.selectedMemberId; + } + + toggleAnonymous(): void { + this.form.isAnonymous = !this.form.isAnonymous; + if (this.form.isAnonymous) { + this.form.memberId = null; + this.selectedMemberId = null; + } + } + + save(): void { + // Sync the datepicker Date back to the ISO string field. + this.form.givingDate = this.givingDateValue + ? this.givingDateValue.toISOString().slice(0, 10) + : new Date().toISOString().slice(0, 10); + + if (this.editingId) { + this.api.update(this.editingId, this.form).subscribe(() => { this.showDialog = false; this.load(); }); + } else { + this.api.create(this.form).subscribe(() => { this.showDialog = false; this.load(); }); + } + } + + delete(g: GivingListItemDto): void { + if (!confirm('Delete this giving record?')) return; + this.api.delete(g.id).subscribe({ + next: () => this.load(), + error: (err: { error?: { message?: string } }) => + alert(err?.error?.message ?? 'Delete failed (record may belong to a locked session).'), + }); + } + + private blankForm(): CreateGivingRequest { + return { + memberId: null, + givingCategoryId: this.categories[0]?.id ?? 0, + amount: 0, + paymentMethod: 'Cash', + checkNumber: null, + zelleReferenceCode: null, + payPalTransactionId: null, + givingDate: new Date().toISOString().slice(0, 10), + isAnonymous: false, + notes: null, + }; + } +}