feat(giving): keyboard-first Sunday offering batch entry page + routes

- Add MemberQuickAddDialogComponent for fast in-session member creation
- Add OfferingSessionPageComponent: client-side buffer, reconcile totals, date-conflict check, submit to API
- Wire finance/giving-categories, finance/givings, finance/offering-session routes (RoleGuard: finance + super_admin)
- Fix givings-page: replace [total] + data[] with GridDataResult for Kendo v20 server-side paging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Chen
2026-05-28 17:14:56 -07:00
parent 81a0b5a038
commit 001db35cef
8 changed files with 364 additions and 6 deletions
@@ -0,0 +1,13 @@
<kendo-dialog title="Quick add member" (close)="cancelled.emit()" [width]="420">
<div style="display:flex;flex-direction:column;gap:0.75rem;">
<label>First name (EN) *<kendo-textbox [(ngModel)]="firstName_en"></kendo-textbox></label>
<label>Last name (EN) *<kendo-textbox [(ngModel)]="lastName_en"></kendo-textbox></label>
<label>名 (中)<kendo-textbox [(ngModel)]="firstName_zh"></kendo-textbox></label>
<label>姓 (中)<kendo-textbox [(ngModel)]="lastName_zh"></kendo-textbox></label>
<label>Cell phone<kendo-textbox [(ngModel)]="phoneCell"></kendo-textbox></label>
</div>
<kendo-dialog-actions>
<button kendoButton (click)="cancelled.emit()">Cancel</button>
<button kendoButton themeColor="primary" [disabled]="!firstName_en || !lastName_en || saving" (click)="save()">Create</button>
</kendo-dialog-actions>
</kendo-dialog>
@@ -0,0 +1,76 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { DialogsModule } from '@progress/kendo-angular-dialog';
import { MemberApiService } from '../../../members/services/member-api.service';
import { CreateMemberRequest, MemberListItemDto } from '../../../members/models/member.model';
@Component({
selector: 'app-member-quick-add-dialog',
standalone: true,
imports: [CommonModule, FormsModule, InputsModule, ButtonsModule, DialogsModule],
templateUrl: './member-quick-add-dialog.component.html',
})
export class MemberQuickAddDialogComponent {
@Output() created = new EventEmitter<MemberListItemDto>();
@Output() cancelled = new EventEmitter<void>();
firstName_en = '';
lastName_en = '';
firstName_zh: string | null = null;
lastName_zh: string | null = null;
phoneCell: string | null = null;
saving = false;
constructor(private memberApi: MemberApiService) {}
save(): void {
if (!this.firstName_en || !this.lastName_en) return;
this.saving = true;
const req: CreateMemberRequest = {
firstName_en: this.firstName_en,
lastName_en: this.lastName_en,
nickName: null,
firstName_zh: this.firstName_zh,
lastName_zh: this.lastName_zh,
gender: null,
dateOfBirth: null,
baptismDate: null,
baptismChurch: null,
email: null,
phoneCell: this.phoneCell,
phoneHome: null,
address: null,
city: null,
state: null,
zipCode: null,
country: 'USA',
status: 'Visitor',
languagePreference: 'en',
joinDate: null,
notes: null,
familyUnitId: null,
};
this.memberApi.create(req).subscribe({
next: ({ id }) => {
this.saving = false;
this.created.emit({
id,
firstName_en: this.firstName_en,
lastName_en: this.lastName_en,
nickName: null,
firstName_zh: this.firstName_zh,
lastName_zh: this.lastName_zh,
status: 'Visitor',
email: null,
phoneCell: this.phoneCell,
joinDate: null,
linkedUserId: null,
});
},
error: () => { this.saving = false; },
});
}
}