This commit is contained in:
Chris Chen
2026-06-20 21:06:24 -07:00
parent aaaae09bd2
commit 7ab8e9703b
7 changed files with 189 additions and 6 deletions
@@ -40,8 +40,12 @@
(click)="clearAnonymous()">改回填寫 · Clear</button>
</div>
<button *ngIf="!entry.isAnonymous" kendoButton fillMode="outline" size="large"
class="oe__anon-btn" (click)="markAnonymous()">設為匿名 · Anonymous</button>
<div *ngIf="!entry.isAnonymous" class="oe__giver-actions">
<button kendoButton fillMode="outline" size="large"
(click)="openQuickAdd()"> 快速新增 · Quick add</button>
<button kendoButton fillMode="outline" size="large"
(click)="markAnonymous()">設為匿名 · Anonymous</button>
</div>
</div>
<div class="oe__field">
@@ -93,4 +97,41 @@
</div>
<div *ngIf="toast" class="oe__toast">{{ toast }}</div>
<!-- Quick-add member -->
<kendo-dialog *ngIf="showQuickAdd" title="快速新增會友 · Quick add member"
(close)="cancelQuickAdd()" [minWidth]="280" [width]="360">
<div class="oe__qa">
<div class="oe__field">
<label class="oe__label">英文名 · Legal first name *</label>
<kendo-textbox class="oe__control" [(ngModel)]="quickAdd.firstName_en" size="large"></kendo-textbox>
</div>
<div class="oe__field">
<label class="oe__label">英文姓 · Last name *</label>
<kendo-textbox class="oe__control" [(ngModel)]="quickAdd.lastName_en" size="large"></kendo-textbox>
</div>
<div class="oe__field">
<label class="oe__label">暱稱 · Nick name</label>
<kendo-textbox class="oe__control" [(ngModel)]="quickAdd.nickName" size="large"></kendo-textbox>
</div>
<div class="oe__field">
<label class="oe__label">中文名 · Chinese first name</label>
<kendo-textbox class="oe__control" [(ngModel)]="quickAdd.firstName_zh" size="large"></kendo-textbox>
</div>
<div class="oe__field">
<label class="oe__label">中文姓 · Chinese last name</label>
<kendo-textbox class="oe__control" [(ngModel)]="quickAdd.lastName_zh" size="large"></kendo-textbox>
</div>
<div class="oe__field">
<label class="oe__label">手機 · Cell phone</label>
<kendo-textbox class="oe__control" [(ngModel)]="quickAdd.phoneCell" size="large"></kendo-textbox>
</div>
</div>
<kendo-dialog-actions>
<button kendoButton (click)="cancelQuickAdd()">取消 · Cancel</button>
<button kendoButton themeColor="primary" [disabled]="!canSaveQuickAdd" (click)="saveQuickAdd()">
{{ quickAddSaving ? '新增中… · Saving' : '新增 · Add' }}
</button>
</kendo-dialog-actions>
</kendo-dialog>
</div>
@@ -170,9 +170,19 @@
background: #e0e7ff;
}
.oe__anon-btn {
.oe__giver-actions {
margin-top: 0.1rem;
align-self: flex-start;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
/* Quick-add dialog body: stacked fields, scrollable on short screens. */
.oe__qa {
display: flex;
flex-direction: column;
gap: 0.85rem;
padding: 0.25rem 0.1rem;
}
/* ── Sticky submit bar ──────────────────────────────────────────────────── */
@@ -5,9 +5,12 @@ import { Subject, takeUntil } from 'rxjs';
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 { OfferingEntryApiService } from '../../services/offering-entry-api.service';
import { OfferingEntrySignalrService } from '../../services/offering-entry-signalr.service';
import { GivingCategoryDto, OfferingGivingLineRequest, MemberTypeaheadDto } from '../../models/giving.model';
import {
GivingCategoryDto, OfferingGivingLineRequest, MemberTypeaheadDto, QuickAddMemberRequest,
} from '../../models/giving.model';
import { PAYMENT_METHOD_OPTIONS } from '../../../../shared/i18n/option-lists';
interface MemberOption { id: number; displayName: string; }
@@ -21,7 +24,7 @@ interface MemberOption { id: number; displayName: string; }
@Component({
selector: 'app-offering-entry-mobile-page',
standalone: true,
imports: [CommonModule, FormsModule, InputsModule, ButtonsModule, DropDownsModule],
imports: [CommonModule, FormsModule, InputsModule, ButtonsModule, DropDownsModule, DialogsModule],
templateUrl: './offering-entry-mobile-page.component.html',
styleUrls: ['./offering-entry-mobile-page.component.scss'],
})
@@ -48,6 +51,11 @@ export class OfferingEntryMobilePageComponent implements OnInit, OnDestroy {
toast: string | null = null;
connected = false;
// Quick-add dialog for a giver who isn't on file yet.
showQuickAdd = false;
quickAddSaving = false;
quickAdd: QuickAddMemberRequest = this.blankQuickAdd();
private toastTimer?: ReturnType<typeof setTimeout>;
private readonly destroy$ = new Subject<void>();
@@ -144,6 +152,66 @@ export class OfferingEntryMobilePageComponent implements OnInit, OnDestroy {
this.entry.isAnonymous = false;
}
// ── Quick-add member ────────────────────────────────────────────────────────
openQuickAdd(): void {
this.quickAdd = this.blankQuickAdd();
this.showQuickAdd = true;
}
cancelQuickAdd(): void {
this.showQuickAdd = false;
}
get canSaveQuickAdd(): boolean {
return !this.quickAddSaving
&& !!this.quickAdd.firstName_en?.trim()
&& !!this.quickAdd.lastName_en?.trim();
}
saveQuickAdd(): void {
if (!this.canSaveQuickAdd) {
return;
}
this.quickAddSaving = true;
const request: QuickAddMemberRequest = {
firstName_en: this.quickAdd.firstName_en.trim(),
lastName_en: this.quickAdd.lastName_en.trim(),
nickName: this.trimToNull(this.quickAdd.nickName),
firstName_zh: this.trimToNull(this.quickAdd.firstName_zh),
lastName_zh: this.trimToNull(this.quickAdd.lastName_zh),
phoneCell: this.trimToNull(this.quickAdd.phoneCell),
};
this.api.quickAddMember(request).subscribe({
next: created => {
this.quickAddSaving = false;
this.showQuickAdd = false;
// Seed the new member into the typeahead and select them immediately.
const option = { id: created.id, displayName: this.giverLabel(created) };
this.memberResults = [option, ...this.memberResults.filter(m => m.id !== created.id)];
this.entry.isAnonymous = false;
this.onMemberSelected(created.id);
this.showToast('已新增會友 ✓ Member added');
},
error: (err: { error?: { message?: string } }) => {
this.quickAddSaving = false;
this.showToast(err?.error?.message ?? '新增失敗 Add failed');
},
});
}
private blankQuickAdd(): QuickAddMemberRequest {
return {
firstName_en: '', lastName_en: '', nickName: null,
firstName_zh: null, lastName_zh: null, phoneCell: null,
};
}
private trimToNull(value: string | null): string | null {
const trimmed = value?.trim();
return trimmed ? trimmed : null;
}
submit(): void {
if (!this.canSubmit) {
return;
@@ -185,8 +253,13 @@ export class OfferingEntryMobilePageComponent implements OnInit, OnDestroy {
private resetForm(): void {
const defaultCategory = this.categories[0]?.id ?? 0;
// Keep the last-used payment method — a counter usually enters several gifts
// of the same method in a row, so they don't have to re-pick it each time.
// The per-gift reference fields (check #, Zelle, PayPal) still clear below.
const keepMethod = this.entry.paymentMethod;
this.entry = this.blankEntry();
this.entry.givingCategoryId = defaultCategory;
this.entry.paymentMethod = keepMethod;
this.selectedMemberId = null;
this.selectedMemberName = null;
this.memberResults = [];