update mobile view.

This commit is contained in:
Chris Chen
2026-06-23 14:15:20 -07:00
parent 9d7c224ad2
commit 68649223d9
11 changed files with 304 additions and 52 deletions
@@ -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)"
@@ -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',
@@ -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>
@@ -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;