From 4a2b1420615b151dfb92d1ee81ac5645a7715a86 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Thu, 28 May 2026 16:57:15 -0700 Subject: [PATCH] feat(giving): frontend models + API services Co-Authored-By: Claude Sonnet 4.6 --- .../features/giving/models/giving.model.ts | 119 ++++++++++++++++++ .../giving/services/giving-api.service.ts | 38 ++++++ .../services/giving-category-api.service.ts | 29 +++++ .../services/offering-session-api.service.ts | 37 ++++++ 4 files changed, 223 insertions(+) create mode 100644 APP/src/app/features/giving/models/giving.model.ts create mode 100644 APP/src/app/features/giving/services/giving-api.service.ts create mode 100644 APP/src/app/features/giving/services/giving-category-api.service.ts create mode 100644 APP/src/app/features/giving/services/offering-session-api.service.ts diff --git a/APP/src/app/features/giving/models/giving.model.ts b/APP/src/app/features/giving/models/giving.model.ts new file mode 100644 index 0000000..c595c64 --- /dev/null +++ b/APP/src/app/features/giving/models/giving.model.ts @@ -0,0 +1,119 @@ +export type PaymentMethod = 'Cash' | 'Check' | 'Zelle' | 'PayPal' | 'Other'; +export type SessionStatus = 'Draft' | 'Submitted' | 'Reconciled'; + +export interface PagedResult { + items: T[]; + totalCount: number; + page: number; + pageSize: number; + totalPages: number; +} + +// ── Giving categories ───────────────────────────────────────────── +export interface GivingCategoryDto { + id: number; + name_en: string; + name_zh: string | null; + description_en: string | null; + description_zh: string | null; + isActive: boolean; + sortOrder: number; +} +export interface CreateGivingCategoryRequest { + name_en: string; + name_zh: string | null; + description_en: string | null; + description_zh: string | null; + sortOrder: number; +} +export interface UpdateGivingCategoryRequest extends CreateGivingCategoryRequest { + isActive: boolean; +} + +// ── Single giving ───────────────────────────────────────────────── +export interface GivingListItemDto { + id: number; + memberId: number | null; + memberName: string | null; + givingCategoryId: number; + categoryName: string; + amount: number; + paymentMethod: PaymentMethod; + givingDate: string; // yyyy-MM-dd + isAnonymous: boolean; + offeringSessionId: number | null; +} +export interface CreateGivingRequest { + memberId: number | null; + givingCategoryId: number; + amount: number; + paymentMethod: PaymentMethod; + checkNumber: string | null; + zelleReferenceCode: string | null; + payPalTransactionId: string | null; + givingDate: string; // yyyy-MM-dd + isAnonymous: boolean; + notes: string | null; +} +export type UpdateGivingRequest = CreateGivingRequest; + +// ── Offering session (batch) ────────────────────────────────────── +export interface OfferingGivingLineRequest { + memberId: number | null; + givingCategoryId: number; + amount: number; + paymentMethod: PaymentMethod; + checkNumber: string | null; + zelleReferenceCode: string | null; + payPalTransactionId: string | null; + isAnonymous: boolean; + notes: string | null; +} +export interface CreateOfferingSessionRequest { + sessionDate: string; // yyyy-MM-dd + cashTotal: number; + checkTotal: number; + notes: string | null; + givings: OfferingGivingLineRequest[]; +} +export interface OfferingGivingLineDto { + id: number; + memberId: number | null; + memberName: string | null; + givingCategoryId: number; + categoryName: string; + amount: number; + paymentMethod: PaymentMethod; + checkNumber: string | null; + zelleReferenceCode: string | null; + payPalTransactionId: string | null; + isAnonymous: boolean; + notes: string | null; +} +export interface OfferingSessionDto { + id: number; + sessionDate: string; + status: SessionStatus; + cashTotal: number; + checkTotal: number; + systemTotal: number; + difference: number; + notes: string | null; + givings: OfferingGivingLineDto[]; +} +export interface OfferingSessionListItemDto { + id: number; + sessionDate: string; + status: SessionStatus; + cashTotal: number; + checkTotal: number; + systemTotal: number; + difference: number; + lineCount: number; +} + +/** A row held in the client-side batch buffer before submit. */ +export interface OfferingBufferLine extends OfferingGivingLineRequest { + memberName: string | null; // for display only + categoryName: string; // for display only +} diff --git a/APP/src/app/features/giving/services/giving-api.service.ts b/APP/src/app/features/giving/services/giving-api.service.ts new file mode 100644 index 0000000..8800706 --- /dev/null +++ b/APP/src/app/features/giving/services/giving-api.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ApiConfigService } from '../../../core/services/api-config.service'; +import { + GivingListItemDto, CreateGivingRequest, UpdateGivingRequest, PagedResult, +} from '../models/giving.model'; + +export interface GivingQuery { + page?: number; pageSize?: number; search?: string; + categoryId?: number; from?: string; to?: string; +} + +@Injectable({ providedIn: 'root' }) +export class GivingApiService { + private readonly endpoint: string; + constructor(private http: HttpClient, apiConfig: ApiConfigService) { + this.endpoint = apiConfig.getApiUrl('givings'); + } + + getPaged(q: GivingQuery = {}): Observable> { + let p = new HttpParams().set('page', q.page ?? 1).set('pageSize', q.pageSize ?? 20); + if (q.search) p = p.set('search', q.search); + if (q.categoryId != null) p = p.set('categoryId', q.categoryId); + if (q.from) p = p.set('from', q.from); + if (q.to) p = p.set('to', q.to); + return this.http.get>(this.endpoint, { params: p }); + } + create(request: CreateGivingRequest): Observable<{ id: number }> { + return this.http.post<{ id: number }>(this.endpoint, request); + } + update(id: number, request: UpdateGivingRequest): Observable { + return this.http.put(`${this.endpoint}/${id}`, request); + } + delete(id: number): Observable { + return this.http.delete(`${this.endpoint}/${id}`); + } +} diff --git a/APP/src/app/features/giving/services/giving-category-api.service.ts b/APP/src/app/features/giving/services/giving-category-api.service.ts new file mode 100644 index 0000000..e8029ab --- /dev/null +++ b/APP/src/app/features/giving/services/giving-category-api.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ApiConfigService } from '../../../core/services/api-config.service'; +import { + GivingCategoryDto, CreateGivingCategoryRequest, UpdateGivingCategoryRequest, +} from '../models/giving.model'; + +@Injectable({ providedIn: 'root' }) +export class GivingCategoryApiService { + private readonly endpoint: string; + constructor(private http: HttpClient, apiConfig: ApiConfigService) { + this.endpoint = apiConfig.getApiUrl('giving-categories'); + } + + getAll(includeInactive = false): Observable { + const params = new HttpParams().set('includeInactive', includeInactive); + return this.http.get(this.endpoint, { params }); + } + create(request: CreateGivingCategoryRequest): Observable<{ id: number }> { + return this.http.post<{ id: number }>(this.endpoint, request); + } + update(id: number, request: UpdateGivingCategoryRequest): Observable { + return this.http.put(`${this.endpoint}/${id}`, request); + } + deactivate(id: number): Observable { + return this.http.delete(`${this.endpoint}/${id}`); + } +} diff --git a/APP/src/app/features/giving/services/offering-session-api.service.ts b/APP/src/app/features/giving/services/offering-session-api.service.ts new file mode 100644 index 0000000..8b54a80 --- /dev/null +++ b/APP/src/app/features/giving/services/offering-session-api.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ApiConfigService } from '../../../core/services/api-config.service'; +import { + OfferingSessionDto, OfferingSessionListItemDto, + CreateOfferingSessionRequest, PagedResult, +} from '../models/giving.model'; + +@Injectable({ providedIn: 'root' }) +export class OfferingSessionApiService { + private readonly endpoint: string; + constructor(private http: HttpClient, apiConfig: ApiConfigService) { + this.endpoint = apiConfig.getApiUrl('offering-sessions'); + } + + getPaged(page = 1, pageSize = 20): Observable> { + const params = new HttpParams().set('page', page).set('pageSize', pageSize); + return this.http.get>(this.endpoint, { params }); + } + getById(id: number): Observable { + return this.http.get(`${this.endpoint}/${id}`); + } + checkDate(date: string): Observable<{ exists: boolean }> { + const params = new HttpParams().set('date', date); + return this.http.get<{ exists: boolean }>(`${this.endpoint}/check-date`, { params }); + } + create(request: CreateOfferingSessionRequest): Observable<{ id: number }> { + return this.http.post<{ id: number }>(this.endpoint, request); + } + reopen(id: number): Observable { + return this.http.post(`${this.endpoint}/${id}/reopen`, {}); + } + replace(id: number, request: CreateOfferingSessionRequest): Observable { + return this.http.put(`${this.endpoint}/${id}`, request); + } +}