WIP
This commit is contained in:
+75
@@ -0,0 +1,75 @@
|
||||
<kendo-dialog title="Issue Checks / 開立支票" [width]="720" (close)="onClose()">
|
||||
<div class="p-2 flex flex-col gap-4" style="max-height: 70vh; overflow-y: auto;">
|
||||
|
||||
<label class="flex flex-col gap-1 w-60">
|
||||
Check Date / 支票日期
|
||||
<kendo-datepicker [(ngModel)]="checkDate"></kendo-datepicker>
|
||||
</label>
|
||||
|
||||
<div *ngFor="let f of forms; let i = index" class="border rounded p-3 flex flex-col gap-3"
|
||||
style="border-color:#e5e7eb;">
|
||||
<div class="font-semibold">
|
||||
Check #{{ i + 1 }} — {{ f.group.payeeType }} · Total {{ f.group.totalAmount | currency }}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
|
||||
<label class="flex flex-col gap-1 md:col-span-2">
|
||||
Payee Name / 收款人
|
||||
<kendo-textbox [(ngModel)]="f.payeeName"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 md:col-span-2">
|
||||
Address / 地址
|
||||
<kendo-textbox [(ngModel)]="f.address"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
City / 城市
|
||||
<kendo-textbox [(ngModel)]="f.city"></kendo-textbox>
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="flex flex-col gap-1">
|
||||
State / 州
|
||||
<kendo-textbox [(ngModel)]="f.state"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Zip / 郵遞區號
|
||||
<kendo-textbox [(ngModel)]="f.zip"></kendo-textbox>
|
||||
</label>
|
||||
</div>
|
||||
<label class="flex flex-col gap-1">
|
||||
Check # / 支票號碼
|
||||
<kendo-textbox [(ngModel)]="f.checkNumber"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Memo / 摘要
|
||||
<kendo-textbox [(ngModel)]="f.memo"></kendo-textbox>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left" style="border-bottom:1px solid #e5e7eb;">
|
||||
<th class="py-1">Date</th><th>Description</th><th class="text-right">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let l of f.group.lines" style="border-bottom:1px solid #f3f4f6;">
|
||||
<td class="py-1">{{ l.expenseDate }}</td>
|
||||
<td>{{ l.description }}</td>
|
||||
<td class="text-right">{{ l.amount | currency }}</td>
|
||||
</tr>
|
||||
<tr class="font-semibold">
|
||||
<td></td><td class="text-right pr-2">Total</td>
|
||||
<td class="text-right">{{ f.group.totalAmount | currency }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<kendo-dialog-actions>
|
||||
<button kendoButton (click)="onClose()">Cancel</button>
|
||||
<button kendoButton themeColor="primary" (click)="confirm()">
|
||||
Issue {{ forms.length }} Check(s)
|
||||
</button>
|
||||
</kendo-dialog-actions>
|
||||
</kendo-dialog>
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DialogsModule } from '@progress/kendo-angular-dialog';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
|
||||
import { GridModule } from '@progress/kendo-angular-grid';
|
||||
import { PayeeGroupDto, IssueChecksRequest, PayeeCheckInstruction } from '../../models/disbursement.model';
|
||||
|
||||
interface PayeeForm {
|
||||
group: PayeeGroupDto;
|
||||
payeeName: string;
|
||||
address: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
checkNumber: string;
|
||||
defaultCheckNumber: string;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-issue-check-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, DialogsModule, ButtonsModule, InputsModule, DateInputsModule, GridModule],
|
||||
templateUrl: './issue-check-dialog.component.html',
|
||||
})
|
||||
export class IssueCheckDialogComponent implements OnInit {
|
||||
/** The payee groups the user selected to pay. */
|
||||
@Input() groups: PayeeGroupDto[] = [];
|
||||
/** Next sequential check number from the church profile, used to prefill. */
|
||||
@Input() nextCheckNumber = 1001;
|
||||
|
||||
@Output() save = new EventEmitter<IssueChecksRequest>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
checkDate = new Date();
|
||||
forms: PayeeForm[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
let n = this.nextCheckNumber;
|
||||
this.forms = this.groups.map(g => {
|
||||
const cn = String(n++);
|
||||
return {
|
||||
group: g,
|
||||
payeeName: g.payeeName,
|
||||
address: g.address ?? '',
|
||||
city: g.city ?? '',
|
||||
state: g.state ?? '',
|
||||
zip: g.zip ?? '',
|
||||
checkNumber: cn,
|
||||
defaultCheckNumber: cn,
|
||||
memo: '',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
const d = this.checkDate;
|
||||
const checkDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
|
||||
const payees: PayeeCheckInstruction[] = this.forms.map(f => ({
|
||||
payeeType: f.group.payeeType,
|
||||
memberId: f.group.memberId,
|
||||
vendorKey: f.group.vendorKey,
|
||||
payeeName: f.payeeName.trim(),
|
||||
address: f.address.trim() || null,
|
||||
city: f.city.trim() || null,
|
||||
state: f.state.trim() || null,
|
||||
zip: f.zip.trim() || null,
|
||||
// Only send an override when the user changed the prefilled sequential number,
|
||||
// so the server's auto-allocation (and counter consumption) stays in sync.
|
||||
checkNumberOverride: f.checkNumber.trim() !== f.defaultCheckNumber ? f.checkNumber.trim() : null,
|
||||
memo: f.memo.trim() || null,
|
||||
expenseIds: f.group.lines.map(l => l.expenseId),
|
||||
}));
|
||||
|
||||
this.save.emit({ checkDate, payees });
|
||||
}
|
||||
|
||||
onClose(): void { this.cancel.emit(); }
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<kendo-dialog title="Receipt Acknowledgement / 簽收" [width]="480" (close)="onClose()">
|
||||
<div class="p-2 flex flex-col gap-3">
|
||||
<div class="text-sm" style="color:#374151;">
|
||||
Check #{{ check.checkNumber }} · {{ check.payeeName }} · {{ check.amount | currency }}
|
||||
</div>
|
||||
|
||||
<label class="flex flex-col gap-1">
|
||||
Signed Name / 簽收人姓名
|
||||
<kendo-textbox [(ngModel)]="signedName" placeholder="Recipient name"></kendo-textbox>
|
||||
</label>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-sm">Signature / 簽名</span>
|
||||
<canvas #pad width="440" height="180"
|
||||
class="border rounded touch-none"
|
||||
style="border-color:#9ca3af; background:#fff; touch-action:none;"
|
||||
(pointerdown)="onDown($event)"
|
||||
(pointermove)="onMove($event)"
|
||||
(pointerup)="onUp()"
|
||||
(pointerleave)="onUp()">
|
||||
</canvas>
|
||||
<button kendoButton fillMode="flat" class="self-start" (click)="clear()">Clear / 清除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<kendo-dialog-actions>
|
||||
<button kendoButton (click)="onClose()">Cancel</button>
|
||||
<button kendoButton themeColor="primary" [disabled]="!hasInk || !signedName.trim() || submitting"
|
||||
(click)="confirm()">Confirm / 確認簽收</button>
|
||||
</kendo-dialog-actions>
|
||||
</kendo-dialog>
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DialogsModule } from '@progress/kendo-angular-dialog';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
import { CheckListItemDto } from '../../models/disbursement.model';
|
||||
|
||||
export interface ReceiptSignResult { signature: Blob; signedName: string; }
|
||||
|
||||
@Component({
|
||||
selector: 'app-receipt-sign-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, DialogsModule, ButtonsModule, InputsModule],
|
||||
templateUrl: './receipt-sign-dialog.component.html',
|
||||
})
|
||||
export class ReceiptSignDialogComponent implements AfterViewInit {
|
||||
@Input() check!: CheckListItemDto;
|
||||
@Output() save = new EventEmitter<ReceiptSignResult>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
@ViewChild('pad', { static: false }) padRef!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
signedName = '';
|
||||
hasInk = false;
|
||||
submitting = false;
|
||||
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
private drawing = false;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const canvas = this.padRef.nativeElement;
|
||||
this.signedName = this.check.payeeName || '';
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.ctx.fillStyle = '#ffffff';
|
||||
this.ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
this.ctx.strokeStyle = '#111827';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.lineCap = 'round';
|
||||
this.ctx.lineJoin = 'round';
|
||||
}
|
||||
|
||||
private point(e: PointerEvent): { x: number; y: number } {
|
||||
const rect = this.padRef.nativeElement.getBoundingClientRect();
|
||||
return {
|
||||
x: (e.clientX - rect.left) * (this.padRef.nativeElement.width / rect.width),
|
||||
y: (e.clientY - rect.top) * (this.padRef.nativeElement.height / rect.height),
|
||||
};
|
||||
}
|
||||
|
||||
onDown(e: PointerEvent): void {
|
||||
e.preventDefault();
|
||||
this.drawing = true;
|
||||
const p = this.point(e);
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(p.x, p.y);
|
||||
this.padRef.nativeElement.setPointerCapture(e.pointerId);
|
||||
}
|
||||
|
||||
onMove(e: PointerEvent): void {
|
||||
if (!this.drawing) return;
|
||||
const p = this.point(e);
|
||||
this.ctx.lineTo(p.x, p.y);
|
||||
this.ctx.stroke();
|
||||
this.hasInk = true;
|
||||
}
|
||||
|
||||
onUp(): void { this.drawing = false; }
|
||||
|
||||
clear(): void {
|
||||
const canvas = this.padRef.nativeElement;
|
||||
this.ctx.fillStyle = '#ffffff';
|
||||
this.ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
this.hasInk = false;
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
if (!this.hasInk || !this.signedName.trim() || this.submitting) return;
|
||||
this.submitting = true;
|
||||
this.padRef.nativeElement.toBlob(blob => {
|
||||
if (!blob) { this.submitting = false; return; }
|
||||
this.save.emit({ signature: blob, signedName: this.signedName.trim() });
|
||||
}, 'image/png');
|
||||
}
|
||||
|
||||
onClose(): void { this.cancel.emit(); }
|
||||
}
|
||||
Reference in New Issue
Block a user