update mobile view.
This commit is contained in:
+3
-3
@@ -1,4 +1,4 @@
|
||||
<kendo-dialog title="Receipt Acknowledgement / 簽收" [width]="480" (close)="onClose()">
|
||||
<kendo-dialog title="Receipt Acknowledgement / 簽收" [width]="'95vw'" [maxWidth]="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 }}
|
||||
@@ -12,8 +12,8 @@
|
||||
<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;"
|
||||
class="border rounded touch-none w-full"
|
||||
style="border-color:#9ca3af; background:#fff; touch-action:none; height:auto; aspect-ratio:440 / 180;"
|
||||
(pointerdown)="onDown($event)"
|
||||
(pointermove)="onMove($event)"
|
||||
(pointerup)="onUp()"
|
||||
|
||||
+96
-39
@@ -14,47 +14,104 @@
|
||||
<button kendoButton (click)="applyFilter()">Apply</button>
|
||||
</div>
|
||||
|
||||
<kendo-grid [data]="{ data: rows, total: total }" [loading]="loading" [pageable]="true" [skip]="skip"
|
||||
[pageSize]="pageSize" (pageChange)="onPageChange($event)">
|
||||
<!-- Desktop / tablet: full data grid -->
|
||||
<div class="hidden md:block">
|
||||
<kendo-grid [data]="{ data: rows, total: total }" [loading]="loading" [pageable]="true" [skip]="skip"
|
||||
[pageSize]="pageSize" (pageChange)="onPageChange($event)">
|
||||
|
||||
<kendo-grid-column field="checkNumber" title="Check #" [width]="100"></kendo-grid-column>
|
||||
<kendo-grid-column field="checkDate" title="Date" [width]="110"></kendo-grid-column>
|
||||
<kendo-grid-column field="payeeName" title="Payee"></kendo-grid-column>
|
||||
<kendo-grid-column field="amount" title="Amount" [width]="120" format="c2"></kendo-grid-column>
|
||||
<kendo-grid-column title="Lines" [width]="80">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>{{ dataItem.lineCount }}</ng-template>
|
||||
</kendo-grid-column>
|
||||
<kendo-grid-column title="Status" [width]="110">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<span [class]="statusClass(dataItem.status)">{{ dataItem.status }}</span>
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
<kendo-grid-column title="Receipt / 簽收" [width]="180">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<ng-container *ngIf="dataItem.signed; else notSigned">
|
||||
<span class="badge-paid">Signed</span>
|
||||
<div class="text-xs" style="color:#6b7280;">
|
||||
{{ dataItem.receiptSignedName }} · {{ dataItem.receiptSignedAt | date:'short' }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #notSigned><span style="color:#9ca3af;">—</span></ng-template>
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
<kendo-grid-column field="checkNumber" title="Check #" [width]="100"></kendo-grid-column>
|
||||
<kendo-grid-column field="checkDate" title="Date" [width]="110"></kendo-grid-column>
|
||||
<kendo-grid-column field="payeeName" title="Payee"></kendo-grid-column>
|
||||
<kendo-grid-column field="amount" title="Amount" [width]="120" format="c2"></kendo-grid-column>
|
||||
<kendo-grid-column title="Lines" [width]="80">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>{{ dataItem.lineCount }}</ng-template>
|
||||
</kendo-grid-column>
|
||||
<kendo-grid-column title="Status" [width]="110">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<span [class]="statusClass(dataItem.status)">{{ dataItem.status }}</span>
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
<kendo-grid-column title="Receipt / 簽收" [width]="180">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<ng-container *ngIf="dataItem.signed; else notSigned">
|
||||
<span class="badge-paid">Signed</span>
|
||||
<div class="text-xs" style="color:#6b7280;">
|
||||
{{ dataItem.receiptSignedName }} · {{ dataItem.receiptSignedAt | date:'short' }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #notSigned><span style="color:#9ca3af;">—</span></ng-template>
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
|
||||
<kendo-grid-column title="Actions" [width]="600">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<button kendoButton fillMode="flat" (click)="view(dataItem)">View</button>
|
||||
<button kendoButton fillMode="flat" themeColor="primary" (click)="print(dataItem)">Print</button>
|
||||
<button *ngIf="canSign(dataItem)" kendoButton fillMode="flat" themeColor="success"
|
||||
(click)="openSign(dataItem)">簽收</button>
|
||||
<!-- <button *ngIf="dataItem.signed" kendoButton fillMode="flat" (click)="viewSignature(dataItem)">Signature</button> -->
|
||||
<button *ngIf="dataItem.signed" kendoButton fillMode="flat" themeColor="primary"
|
||||
(click)="printReceipt(dataItem)">收據</button>
|
||||
<button *ngIf="canVoid(dataItem)" kendoButton fillMode="flat" themeColor="error"
|
||||
(click)="openVoid(dataItem)">Void</button>
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
</kendo-grid>
|
||||
<kendo-grid-column title="Actions" [width]="600">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<button kendoButton fillMode="flat" (click)="view(dataItem)">View</button>
|
||||
<button kendoButton fillMode="flat" themeColor="primary" (click)="print(dataItem)">Print</button>
|
||||
<button *ngIf="canSign(dataItem)" kendoButton fillMode="flat" themeColor="success"
|
||||
(click)="openSign(dataItem)">簽收</button>
|
||||
<!-- <button *ngIf="dataItem.signed" kendoButton fillMode="flat" (click)="viewSignature(dataItem)">Signature</button> -->
|
||||
<button *ngIf="dataItem.signed" kendoButton fillMode="flat" themeColor="primary"
|
||||
(click)="printReceipt(dataItem)">收據</button>
|
||||
<button *ngIf="canVoid(dataItem)" kendoButton fillMode="flat" themeColor="error"
|
||||
(click)="openVoid(dataItem)">Void</button>
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
</kendo-grid>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: tappable card list -->
|
||||
<div class="md:hidden chk-cards">
|
||||
<div *ngIf="loading" class="chk-empty">Loading…</div>
|
||||
<div *ngIf="!loading && rows.length === 0" class="chk-empty">No checks found.</div>
|
||||
|
||||
<div class="chk-card" *ngFor="let row of rows">
|
||||
<div class="chk-card__top">
|
||||
<span class="chk-card__number">Check #{{ row.checkNumber }}</span>
|
||||
<span class="chk-card__amount">{{ row.amount | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="chk-card__payee">{{ row.payeeName }}</div>
|
||||
|
||||
<dl class="chk-card__meta">
|
||||
<div>
|
||||
<dt>Date</dt>
|
||||
<dd>{{ row.checkDate }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Lines</dt>
|
||||
<dd>{{ row.lineCount }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Receipt / 簽收</dt>
|
||||
<dd>
|
||||
<ng-container *ngIf="row.signed; else notSignedCard">
|
||||
{{ row.receiptSignedName }} · {{ row.receiptSignedAt | date:'short' }}
|
||||
</ng-container>
|
||||
<ng-template #notSignedCard>—</ng-template>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="chk-card__footer">
|
||||
<span [class]="statusClass(row.status)">{{ row.status }}</span>
|
||||
<span *ngIf="row.signed" class="badge-paid">Signed</span>
|
||||
</div>
|
||||
|
||||
<div class="chk-card__actions">
|
||||
<button kendoButton fillMode="outline" (click)="view(row)">View</button>
|
||||
<button kendoButton themeColor="primary" (click)="print(row)">Print</button>
|
||||
<button *ngIf="canSign(row)" kendoButton themeColor="success" (click)="openSign(row)">簽收</button>
|
||||
<button *ngIf="row.signed" kendoButton fillMode="outline" (click)="printReceipt(row)">收據</button>
|
||||
<button *ngIf="canVoid(row)" kendoButton themeColor="error" fillMode="outline" (click)="openVoid(row)">Void</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chk-pager" *ngIf="!loading && rows.length > 0">
|
||||
<button kendoButton fillMode="outline" [disabled]="page <= 1" (click)="prevPage()">Prev</button>
|
||||
<span class="chk-pager__info">Page {{ page }} of {{ totalPages }}</span>
|
||||
<button kendoButton fillMode="outline" [disabled]="page >= totalPages" (click)="nextPage()">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail dialog -->
|
||||
<kendo-dialog *ngIf="detail" title="Check #{{ detail.checkNumber }}" [width]="560" (close)="detail = null">
|
||||
|
||||
+109
@@ -24,3 +24,112 @@
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
// Mobile card list
|
||||
// NOTE: display/flex layout lives on the element via Tailwind (flex flex-col gap-3)
|
||||
// so the responsive `md:hidden` utility wins on desktop. Setting `display: flex`
|
||||
// here would override `md:hidden` and leak the card list onto the desktop view.
|
||||
|
||||
.chk-empty {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.chk-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
|
||||
&__top {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__number {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
&__payee {
|
||||
margin-top: 4px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin: 10px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
color: #374151;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
// Comfortable tap targets; let buttons share the row evenly
|
||||
.k-button {
|
||||
flex: 1 1 auto;
|
||||
min-height: 40px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chk-pager {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
&__info {
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.k-button {
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -49,9 +49,24 @@ export class CheckRegisterPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
get skip(): number { return (this.page - 1) * this.pageSize; }
|
||||
get totalPages(): number { return Math.max(1, Math.ceil(this.total / this.pageSize)); }
|
||||
applyFilter(): void { this.page = 1; this.load(); }
|
||||
onPageChange(e: PageChangeEvent): void { this.page = Math.floor(e.skip / this.pageSize) + 1; this.load(); }
|
||||
|
||||
prevPage(): void {
|
||||
if (this.page > 1) {
|
||||
this.page--;
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
nextPage(): void {
|
||||
if (this.page < this.totalPages) {
|
||||
this.page++;
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
view(row: CheckListItemDto): void {
|
||||
this.api.getCheck(row.id).subscribe(d => (this.detail = d));
|
||||
}
|
||||
|
||||
+7
@@ -1,6 +1,12 @@
|
||||
<kendo-dialog [title]="title" (close)="cancel.emit()" [width]="560" [maxWidth]="'95vw'" [maxHeight]="'90vh'">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
|
||||
|
||||
<!-- Continuous entry: keep member/ministry/category/date after each save (on-behalf reimbursement only) -->
|
||||
<label *ngIf="showContinueEntry" class="flex items-center gap-2 md:col-span-2">
|
||||
<kendo-switch [(ngModel)]="continueEntry"></kendo-switch>
|
||||
<span>連續登打 / Continuous Entry</span>
|
||||
</label>
|
||||
|
||||
<!-- Member picker (finance creating on behalf of a member) -->
|
||||
<label *ngIf="allowMemberPick" class="flex flex-col gap-1 md:col-span-2">Member
|
||||
<kendo-dropdownlist
|
||||
@@ -91,6 +97,7 @@
|
||||
@Output() cancel and wrongly close the dialog. See Angular issues #50556 / #13997.
|
||||
-->
|
||||
<input
|
||||
#receiptInput
|
||||
type="file"
|
||||
accept="image/*,application/pdf"
|
||||
(change)="onFileSelected($event)"
|
||||
|
||||
+34
-3
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
@@ -15,7 +15,12 @@ import {
|
||||
ExpenseListItemDto,
|
||||
} from '../../models/expense.model';
|
||||
|
||||
export interface ExpenseFormResult { request: CreateExpenseRequest; receipt: File | null; }
|
||||
export interface ExpenseFormResult {
|
||||
request: CreateExpenseRequest;
|
||||
receipt: File | null;
|
||||
/** When true (continuous-entry mode), the parent should keep the dialog open after saving. */
|
||||
continueEntry: boolean;
|
||||
}
|
||||
|
||||
/** Flattened member item with a single displayName field for the dropdown. */
|
||||
interface MemberOption { id: number; displayName: string; }
|
||||
@@ -35,12 +40,23 @@ export class ExpenseFormDialogComponent implements OnInit {
|
||||
@Output() save = new EventEmitter<ExpenseFormResult>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
/** Native receipt file input, cleared between continuous-entry saves. */
|
||||
@ViewChild('receiptInput') receiptInput?: ElementRef<HTMLInputElement>;
|
||||
|
||||
ministries: MinistryDto[] = [];
|
||||
groups: ExpenseCategoryGroupDto[] = [];
|
||||
subs: ExpenseSubCategoryDto[] = [];
|
||||
|
||||
memberResults: MemberOption[] = [];
|
||||
|
||||
/** Continuous-entry toggle: keep member/ministry/category/date and the dialog open after each save. */
|
||||
continueEntry = false;
|
||||
|
||||
/** The on-behalf reimbursement create flow is the only place continuous entry applies. */
|
||||
get showContinueEntry(): boolean {
|
||||
return this.mode === 'reimbursement' && this.allowMemberPick && !this.expense;
|
||||
}
|
||||
|
||||
form = {
|
||||
ministryId: null as number | null,
|
||||
categoryGroupId: null as number | null,
|
||||
@@ -131,6 +147,21 @@ export class ExpenseFormDialogComponent implements OnInit {
|
||||
expenseDate,
|
||||
notes: null,
|
||||
};
|
||||
this.save.emit({ request, receipt: this.receipt });
|
||||
// The request and receipt are snapshotted here, so resetting the form right
|
||||
// after emitting is safe even though the parent saves asynchronously.
|
||||
this.save.emit({ request, receipt: this.receipt, continueEntry: this.continueEntry });
|
||||
if (this.continueEntry) this.resetForNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear only the per-entry fields, keeping Member, Ministry, Category Group,
|
||||
* Sub-Category and Expense Date (plus the loaded sub-category list) so the
|
||||
* user can immediately log the next reimbursement.
|
||||
*/
|
||||
private resetForNext(): void {
|
||||
this.form.amount = 0;
|
||||
this.form.description = '';
|
||||
this.receipt = null;
|
||||
if (this.receiptInput) this.receiptInput.nativeElement.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,4 +132,7 @@
|
||||
</kendo-dialog-actions>
|
||||
</kendo-dialog>
|
||||
|
||||
<!-- Transient save confirmation (sits above the open dialog during continuous entry) -->
|
||||
<div *ngIf="toast" class="save-toast">{{ toast }}</div>
|
||||
|
||||
</div>
|
||||
@@ -44,3 +44,20 @@
|
||||
color: #1d4ed8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
// Save confirmation pill. z-index sits above the Kendo dialog overlay so it
|
||||
// stays visible while the continuous-entry dialog remains open.
|
||||
.save-toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 2rem;
|
||||
transform: translateX(-50%);
|
||||
z-index: 20000;
|
||||
padding: 0.7rem 1.2rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #f0fdf4;
|
||||
background: #16a34a;
|
||||
box-shadow: 0 12px 30px -12px rgba(22, 163, 74, 0.7);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ export class ExpensesPageComponent implements OnInit {
|
||||
rejectRow: ExpenseListItemDto | null = null;
|
||||
rejectNotes = '';
|
||||
|
||||
/** Transient confirmation pill, used so the user gets feedback during continuous entry. */
|
||||
toast: string | null = null;
|
||||
private toastTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(private api: ExpenseApiService, private ministryApi: MinistryApiService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -82,7 +86,12 @@ export class ExpensesPageComponent implements OnInit {
|
||||
switchMap(c => result.receipt
|
||||
? this.api.uploadReceipt(c.id, result.receipt).pipe(switchMap(() => of(c)))
|
||||
: of(c)),
|
||||
).subscribe(() => { this.reimbDialogOpen = false; this.load(); });
|
||||
).subscribe(() => {
|
||||
// In continuous-entry mode the dialog resets itself and stays open for the next entry.
|
||||
if (!result.continueEntry) this.reimbDialogOpen = false;
|
||||
this.showToast('已儲存 ✓ Saved');
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
|
||||
openEdit(row: ExpenseListItemDto): void {
|
||||
@@ -150,6 +159,12 @@ export class ExpensesPageComponent implements OnInit {
|
||||
//should be pay by disbursement
|
||||
}
|
||||
|
||||
private showToast(message: string): void {
|
||||
this.toast = message;
|
||||
if (this.toastTimer) clearTimeout(this.toastTimer);
|
||||
this.toastTimer = setTimeout(() => (this.toast = null), 2200);
|
||||
}
|
||||
|
||||
statusClass(status: string): string {
|
||||
return ({
|
||||
Draft: 'badge-draft',
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Mobile: tappable card list -->
|
||||
<div class="md:hidden rmb-cards">
|
||||
<div class="md:hidden flex flex-col gap-3 rmb-cards">
|
||||
<div *ngIf="loading" class="rmb-empty">Loading…</div>
|
||||
<div *ngIf="!loading && rows.length === 0" class="rmb-empty">No reimbursements yet.</div>
|
||||
|
||||
|
||||
+3
-5
@@ -46,11 +46,9 @@
|
||||
}
|
||||
|
||||
// Mobile card list
|
||||
.rmb-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
// NOTE: display/flex layout lives on the element via Tailwind (flex flex-col gap-3)
|
||||
// so the responsive `md:hidden` utility wins on desktop. Setting `display: flex`
|
||||
// here would override `md:hidden` and leak the card list onto the desktop view.
|
||||
|
||||
.rmb-empty {
|
||||
padding: 24px 0;
|
||||
|
||||
Reference in New Issue
Block a user