This commit is contained in:
Chris Chen
2026-05-28 22:29:13 -07:00
parent a2d394029a
commit 0639d1fe83
23 changed files with 896 additions and 158 deletions
@@ -1,92 +1,142 @@
<div class="page">
<header class="page-header">
<h2>Sunday Offering Entry / 主日奉獻錄入</h2>
<label>Date
<kendo-datepicker [(ngModel)]="sessionDate" (valueChange)="checkDate()" [disabled]="editingSessionId != null"></kendo-datepicker>
</label>
</header>
<!-- Card A — Session header -->
<section class="card">
<header class="page-header">
<h2 style="margin:0">Sunday Offering Entry / 主日奉獻錄入</h2>
<label class="flex flex-col gap-1">Date
<kendo-datepicker [(ngModel)]="sessionDate" (valueChange)="checkDate()"
[disabled]="editingSessionId != null"></kendo-datepicker>
</label>
</header>
<div *ngIf="editingSessionId != null" class="edit-banner">
Editing submitted session — make changes and click "Update Session".
<button kendoButton fillMode="flat" (click)="cancelEdit()">Cancel edit</button>
</div>
<div *ngIf="editingSessionId != null" class="edit-banner">
<span class="badge badge--draft">Editing</span>
Editing session — make changes and click "Update Session".
<button kendoButton fillMode="flat" (click)="cancelEdit()">Cancel edit</button>
</div>
<div *ngIf="dateConflict && editingSessionId == null" class="warn">
An offering session for this date already exists. Pick another date, or reopen the existing session to edit.
</div>
<section class="entry-row">
<label *ngIf="!entry.isAnonymous">Giver
<kendo-dropdownlist [data]="memberResults" textField="displayName" valueField="id"
[valuePrimitive]="true" [filterable]="true"
(filterChange)="onMemberFilter($event)" [(ngModel)]="selectedMemberId"
(valueChange)="onMemberSelected($event)" placeholder="Search by name"></kendo-dropdownlist>
</label>
<span *ngIf="entry.isAnonymous" class="anon-chip">Anonymous</span>
<label>Type
<kendo-dropdownlist [data]="categories" textField="name_en" valueField="id"
[valuePrimitive]="true" [(ngModel)]="entry.givingCategoryId"></kendo-dropdownlist>
</label>
<label>Method
<kendo-dropdownlist [data]="paymentMethods" [(ngModel)]="entry.paymentMethod"></kendo-dropdownlist>
</label>
<label *ngIf="entry.paymentMethod === 'Check'">Check #<kendo-textbox [(ngModel)]="entry.checkNumber"></kendo-textbox></label>
<label>Amount
<kendo-numerictextbox [(ngModel)]="entry.amount" [min]="0" [format]="'c2'" (keydown.enter)="addLine()"></kendo-numerictextbox>
</label>
<label>Notes<kendo-textbox [(ngModel)]="entry.notes" (keydown.enter)="addLine()"></kendo-textbox></label>
<div class="entry-actions">
<button kendoButton (click)="markAnonymous()">Anonymous</button>
<button kendoButton (click)="showQuickAdd = true">+ Quick add member</button>
<button kendoButton themeColor="primary" (click)="addLine()">+ Add (Enter)</button>
<div *ngIf="dateConflict && editingSessionId == null" class="warn">
An offering session for this date already exists. Pick another date, or reopen the existing session to edit.
</div>
</section>
<kendo-grid [data]="buffer">
<kendo-grid-column title="Giver">
<ng-template kendoGridCellTemplate let-l>{{ l.isAnonymous ? '(Anonymous)' : l.memberName }}</ng-template>
</kendo-grid-column>
<kendo-grid-column field="categoryName" title="Type"></kendo-grid-column>
<kendo-grid-column field="paymentMethod" title="Method" [width]="90"></kendo-grid-column>
<kendo-grid-column field="checkNumber" title="Check #" [width]="90"></kendo-grid-column>
<kendo-grid-column field="amount" title="Amount" [width]="110" format="c2"></kendo-grid-column>
<kendo-grid-column title="" [width]="120">
<ng-template kendoGridCellTemplate let-l let-i="rowIndex">
<button kendoButton fillMode="flat" (click)="editLine(i)">Edit</button>
<button kendoButton fillMode="flat" (click)="removeLine(i)">×</button>
</ng-template>
</kendo-grid-column>
</kendo-grid>
<!-- Card B — Add giving -->
<section class="card">
<h3 class="section-title">Add Giving / 錄入奉獻</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-x-4 gap-y-3">
<label class="flex flex-col gap-1 md:col-span-2">Giver
<kendo-dropdownlist *ngIf="!entry.isAnonymous" [data]="memberResults" textField="displayName" valueField="id"
[valuePrimitive]="true" [filterable]="true" (filterChange)="onMemberFilter($event)"
[(ngModel)]="selectedMemberId" (valueChange)="onMemberSelected($event)"
placeholder="Search by name"></kendo-dropdownlist>
<span *ngIf="entry.isAnonymous" class="flex items-center gap-2">
<span class="anon-chip">Anonymous</span>
<button kendoButton fillMode="flat" size="small" (click)="clearAnonymous()">Clear</button>
</span>
</label>
<section class="reconcile">
<div>Lines: {{ buffer.length }} | System total: {{ systemTotal | currency }}</div>
<label>Cash counted<kendo-numerictextbox [(ngModel)]="cashTotal" [min]="0" [format]="'c2'"></kendo-numerictextbox></label>
<label>Check counted<kendo-numerictextbox [(ngModel)]="checkTotal" [min]="0" [format]="'c2'"></kendo-numerictextbox></label>
<div [class.ok]="difference === 0" [class.bad]="difference !== 0">Difference: {{ difference | currency }}</div>
<button kendoButton themeColor="primary"
[disabled]="buffer.length === 0 || (editingSessionId == null && dateConflict) || submitting"
(click)="submit()">{{ editingSessionId != null ? 'Update Session' : 'Submit' }}</button>
<label class="flex flex-col gap-1">Type
<kendo-dropdownlist [data]="categories" textField="name_en" valueField="id" [valuePrimitive]="true"
[(ngModel)]="entry.givingCategoryId"></kendo-dropdownlist>
</label>
<label class="flex flex-col gap-1">Method
<kendo-dropdownlist [data]="paymentMethods" [(ngModel)]="entry.paymentMethod"></kendo-dropdownlist>
</label>
<label *ngIf="entry.paymentMethod === 'Check'" class="flex flex-col gap-1">Check #
<kendo-textbox [(ngModel)]="entry.checkNumber"></kendo-textbox>
</label>
<label class="flex flex-col gap-1">Amount
<kendo-numerictextbox [(ngModel)]="entry.amount" [min]="0" [format]="'c2'"
(keydown.enter)="addLine()"></kendo-numerictextbox>
</label>
<label class="flex flex-col gap-1 md:col-span-2">Notes
<kendo-textbox [(ngModel)]="entry.notes" (keydown.enter)="addLine()"></kendo-textbox>
</label>
<div class="flex flex-wrap gap-2 md:col-span-2">
<button kendoButton (click)="markAnonymous()">Anonymous</button>
<button kendoButton (click)="showQuickAdd = true">+ Quick add member</button>
<button kendoButton themeColor="primary" (click)="addLine()">{{ editingIndex !== null ? 'Update line' : '+ Add
(Enter)' }}</button>
</div>
</div>
</section>
<section class="sessions-list">
<h3>Recent Sessions</h3>
<!-- Card C — Lines -->
<section class="card">
<h3 class="section-title">Lines / 已加入明細</h3>
<div *ngIf="buffer.length === 0" class="empty">No lines yet — add givings above.</div>
<kendo-grid *ngIf="buffer.length > 0" [data]="buffer">
<kendo-grid-column title="Giver">
<ng-template kendoGridCellTemplate let-l>{{ l.isAnonymous ? '(Anonymous)' : l.memberName }}</ng-template>
</kendo-grid-column>
<kendo-grid-column field="categoryName" title="Type"></kendo-grid-column>
<kendo-grid-column field="paymentMethod" title="Method" [width]="90"></kendo-grid-column>
<kendo-grid-column field="checkNumber" title="Check #" [width]="90"></kendo-grid-column>
<kendo-grid-column field="amount" title="Amount" [width]="110" format="c2"></kendo-grid-column>
<kendo-grid-column title="" [width]="120">
<ng-template kendoGridCellTemplate let-l let-i="rowIndex">
<button kendoButton fillMode="flat" (click)="editLine(i)">Edit</button>
<button kendoButton fillMode="flat" (click)="removeLine(i)">×</button>
</ng-template>
</kendo-grid-column>
</kendo-grid>
<div *ngIf="buffer.length > 0" class="lines-footer">Lines: {{ buffer.length }} · System total: {{ systemTotal |
currency }}</div>
</section>
<!-- Card D — Reconcile & submit -->
<section class="card">
<h3 class="section-title">Reconcile &amp; Submit / 對帳</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<label class="flex flex-col gap-1">Cash counted
<kendo-numerictextbox [(ngModel)]="cashTotal" [min]="0" [format]="'c2'"></kendo-numerictextbox>
</label>
<label class="flex flex-col gap-1">Check counted
<kendo-numerictextbox [(ngModel)]="checkTotal" [min]="0" [format]="'c2'"></kendo-numerictextbox>
</label>
<label class="flex flex-col gap-1 md:col-span-2">Session notes
<kendo-textarea [(ngModel)]="notes" [rows]="2"></kendo-textarea>
</label>
</div>
<div class="flex flex-wrap items-center gap-4 mt-4">
<span>System total: {{ systemTotal | currency }}</span>
<span [class.ok]="difference === 0" [class.bad]="difference !== 0">Difference: {{ difference | currency }}</span>
<button kendoButton themeColor="primary"
[disabled]="buffer.length === 0 || (editingSessionId == null && dateConflict) || submitting"
(click)="submit()">{{ editingSessionId != null ? 'Update Session' : 'Submit' }}</button>
</div>
</section>
<!-- Card E — Recent sessions -->
<section class="card">
<h3 class="section-title">Recent Sessions</h3>
<kendo-grid [data]="sessions">
<kendo-grid-column field="sessionDate" title="Date" [width]="120"></kendo-grid-column>
<kendo-grid-column field="status" title="Status" [width]="110"></kendo-grid-column>
<kendo-grid-column title="Status" [width]="120">
<ng-template kendoGridCellTemplate let-s>
<span class="badge" [class.badge--draft]="s.status === 'Draft'"
[class.badge--submitted]="s.status === 'Submitted'" [class.badge--reconciled]="s.status === 'Reconciled'">{{
s.status }}</span>
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="lineCount" title="Lines" [width]="80"></kendo-grid-column>
<kendo-grid-column field="systemTotal" title="System" [width]="110" format="c2"></kendo-grid-column>
<kendo-grid-column field="difference" title="Diff" [width]="100" format="c2"></kendo-grid-column>
<kendo-grid-column title="" [width]="140">
<kendo-grid-column title="" [width]="150">
<ng-template kendoGridCellTemplate let-s>
<button kendoButton fillMode="flat" *ngIf="s.status === 'Submitted'" (click)="reopenAndEdit(s)">Reopen &amp; Edit</button>
<button kendoButton fillMode="flat" *ngIf="s.status === 'Submitted'" (click)="reopenAndEdit(s)">Reopen &amp;
Edit</button>
<button kendoButton fillMode="flat" *ngIf="s.status === 'Draft'" (click)="continueEditDraft(s)">Continue
editing</button>
</ng-template>
</kendo-grid-column>
</kendo-grid>
</section>
<app-member-quick-add-dialog *ngIf="showQuickAdd"
(created)="onMemberQuickCreated($event)"
(cancelled)="showQuickAdd = false"></app-member-quick-add-dialog>
</div>
<app-member-quick-add-dialog *ngIf="showQuickAdd" (created)="onMemberQuickCreated($event)"
(cancelled)="showQuickAdd = false"></app-member-quick-add-dialog>
</div>