Files
ROLAC/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html
T
Chris Chen 773d38d838
ci-cd-vm / ci-cd (push) Successful in 1m59s
update view.
2026-06-25 21:55:16 -07:00

200 lines
9.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="page">
<!-- Filter toolbar -->
<div class="flex flex-wrap gap-3 items-end mb-4">
<label class="flex flex-col gap-1">
Search
<kendo-textbox placeholder="Search description / vendor / member / check #" [(ngModel)]="filter.search"
(keydown.enter)="applyFilter()">
</kendo-textbox>
</label>
<label class="flex flex-col gap-1">
Ministry
<kendo-dropdownlist [data]="ministries" textField="label" valueField="id" [valuePrimitive]="true"
[(ngModel)]="filter.ministryId" [defaultItem]="{ id: null, label: 'All Ministries/全部事工' }">
</kendo-dropdownlist>
</label>
<label class="flex flex-col gap-1">
Status
<kendo-dropdownlist [data]="statuses" textField="label" valueField="value" [valuePrimitive]="true"
[(ngModel)]="filter.status" [defaultItem]="{ value: null, label: 'All Status/全部狀態' }">
</kendo-dropdownlist>
</label>
<button kendoButton (click)="applyFilter()">Apply</button>
<div class="ml-auto flex gap-2">
<button kendoButton themeColor="primary" (click)="vendorDialogOpen = true">+ Vendor Payment</button>
<button kendoButton themeColor="primary" (click)="reimbDialogOpen = true">+ Reimbursement</button>
</div>
</div>
<!-- Desktop grid -->
<div class="hidden md:block">
<div class="hint-text-sm mb-2">Right-click a row for actions / 右鍵顯示動作</div>
<kendo-grid [data]="{ data: rows, total: total }" [loading]="loading" [pageable]="true" [skip]="skip"
[pageSize]="pageSize" (pageChange)="onPageChange($event)" (cellClick)="onCellClick($event)">
<kendo-grid-column field="expenseDate" title="Date" [width]="110"></kendo-grid-column>
<!-- <kendo-grid-column field="type" title="Type" [width]="140"></kendo-grid-column> -->
<kendo-grid-column title="Ministry / Category" [width]="240">
<ng-template kendoGridCellTemplate let-dataItem>
<div>{{ dataItem.ministryName }}</div>
<div class="text-gray-500 text-xs">
{{ dataItem.primaryCategoryName }}<span *ngIf="dataItem.lineCount > 1"> +{{ dataItem.lineCount - 1 }}</span>
</div>
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="description" title="Description"></kendo-grid-column>
<kendo-grid-column title="Payee" [width]="180">
<ng-template kendoGridCellTemplate let-dataItem>
<ng-container *ngIf="dataItem.vendorName; else memberPayee">{{ dataItem.vendorName }}</ng-container>
<ng-template #memberPayee>
<ng-container *ngIf="dataItem.memberName; else dash">
<div *ngIf="dataItem.memberNickName">{{ dataItem.memberNickName }}</div>
<div [class.text-gray-500]="dataItem.memberNickName" [class.text-xs]="dataItem.memberNickName">{{ dataItem.memberName }}</div>
</ng-container>
<ng-template #dash></ng-template>
</ng-template>
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="amount" title="Amount" [width]="110" format="c2"></kendo-grid-column>
<kendo-grid-column title="Check #" [width]="90">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.status === 'Paid' && dataItem.checkNumber ? dataItem.checkNumber : '—' }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column title="Status" [width]="200">
<ng-template kendoGridCellTemplate let-dataItem>
<span [class]="statusClass(dataItem.status)">{{ dataItem.status }}</span>
<div *ngIf="dataItem.reviewedByName && (dataItem.status === 'Approved' || dataItem.status === 'Paid')"
class="review-meta">✓ Approved by {{ dataItem.reviewedByName }}<br>{{ dataItem.reviewedAt | date:'yyyy-MM-dd HH:mm' }}</div>
<div *ngIf="dataItem.reviewedByName && dataItem.status === 'Rejected'" class="review-meta review-meta-reject">
✗ Rejected by {{ dataItem.reviewedByName }}<br>{{ dataItem.reviewedAt | date:'yyyy-MM-dd HH:mm' }}
<div *ngIf="dataItem.reviewNotes" class="review-reason">{{ dataItem.reviewNotes }}</div>
</div>
</ng-template>
</kendo-grid-column>
</kendo-grid>
<kendo-contextmenu #rowMenu [items]="rowMenuItems" (select)="onRowMenuSelect($event)"></kendo-contextmenu>
</div>
<!-- Mobile cards -->
<div class="md:hidden flex flex-col gap-3">
<div *ngFor="let dataItem of rows" class="rounded border p-3 flex flex-col gap-2">
<div class="flex justify-between items-start gap-2">
<div class="text-sm text-gray-500">{{ dataItem.expenseDate }}</div>
<div class="font-semibold">{{ dataItem.amount | currency }}</div>
</div>
<div>
<div class="font-medium">{{ dataItem.ministryName }}</div>
<div class="text-gray-500 text-xs">
{{ dataItem.primaryCategoryName }}<span *ngIf="dataItem.lineCount > 1"> +{{ dataItem.lineCount - 1 }}</span>
</div>
</div>
<div *ngIf="dataItem.description" class="text-sm">{{ dataItem.description }}</div>
<div class="text-sm flex justify-between gap-2">
<span class="text-gray-500">Payee / 收款人</span>
<span class="text-right">
<ng-container *ngIf="dataItem.vendorName; else mobileMemberPayee">{{ dataItem.vendorName }}</ng-container>
<ng-template #mobileMemberPayee>
<ng-container *ngIf="dataItem.memberName; else mobileDash">
<span *ngIf="dataItem.memberNickName">{{ dataItem.memberNickName }} </span>
<span [class.text-gray-500]="dataItem.memberNickName">{{ dataItem.memberName }}</span>
</ng-container>
<ng-template #mobileDash></ng-template>
</ng-template>
</span>
</div>
<div *ngIf="dataItem.status === 'Paid' && dataItem.checkNumber" class="text-sm flex justify-between gap-2">
<span class="text-gray-500">Check # / 支票號</span>
<span>{{ dataItem.checkNumber }}</span>
</div>
<div>
<span [class]="statusClass(dataItem.status)">{{ dataItem.status }}</span>
<div *ngIf="dataItem.reviewedByName && (dataItem.status === 'Approved' || dataItem.status === 'Paid')"
class="review-meta">✓ Approved by {{ dataItem.reviewedByName }}<br>{{ dataItem.reviewedAt | date:'yyyy-MM-dd HH:mm' }}</div>
<div *ngIf="dataItem.reviewedByName && dataItem.status === 'Rejected'" class="review-meta review-meta-reject">
✗ Rejected by {{ dataItem.reviewedByName }}<br>{{ dataItem.reviewedAt | date:'yyyy-MM-dd HH:mm' }}
<div *ngIf="dataItem.reviewNotes" class="review-reason">{{ dataItem.reviewNotes }}</div>
</div>
</div>
<div class="flex flex-wrap gap-2 pt-1">
<button *ngIf="canEdit(dataItem)" kendoButton size="small" (click)="openEdit(dataItem)">Edit</button>
<button *ngIf="canApproveOrReject(dataItem)" kendoButton size="small" themeColor="primary"
(click)="openReview(dataItem)">Review</button>
<button *ngIf="canPay(dataItem)" kendoButton size="small" themeColor="primary"
(click)="openPay(dataItem)">Pay</button>
<button *ngIf="dataItem.hasReceipt" kendoButton size="small" fillMode="outline"
(click)="openReceipt(dataItem.id)">Receipt</button>
</div>
</div>
<div *ngIf="!loading && rows.length === 0" class="text-center text-gray-500 py-6">No expenses / 無支出資料</div>
<div *ngIf="rows.length > 0" class="flex items-center justify-between gap-2 pt-1">
<button kendoButton size="small" [disabled]="page <= 1" (click)="prevPage()"> Prev</button>
<span class="text-sm text-gray-500">{{ page }} / {{ totalPages }}</span>
<button kendoButton size="small" [disabled]="page >= totalPages" (click)="nextPage()">Next </button>
</div>
</div>
<!-- Vendor Payment dialog -->
<app-expense-form-dialog *ngIf="vendorDialogOpen" mode="vendor" title="Vendor Payment" (save)="onVendorSave($event)"
(cancel)="vendorDialogOpen = false">
</app-expense-form-dialog>
<!-- Reimbursement (on behalf) dialog -->
<app-expense-form-dialog *ngIf="reimbDialogOpen" mode="reimbursement" [allowMemberPick]="true"
title="Reimbursement (on behalf)" (save)="onReimbSave($event)" (cancel)="reimbDialogOpen = false">
</app-expense-form-dialog>
<!-- Edit dialog -->
<app-expense-form-dialog *ngIf="editRow" [mode]="editMode" [expense]="editRow"
[title]="editMode === 'vendor' ? 'Edit Vendor Payment' : 'Edit Reimbursement'"
(save)="onEditSave($event)" (cancel)="closeEdit()">
</app-expense-form-dialog>
<!-- Mark Paid dialog -->
<kendo-dialog *ngIf="payRow" title="Mark Paid" [width]="400" [maxWidth]="'95vw'" (close)="payRow = null">
<div class="grid grid-cols-1 gap-3 p-2">
<label class="flex flex-col gap-1">
Check #
<kendo-textbox [(ngModel)]="payCheckNumber" placeholder="Optional"></kendo-textbox>
</label>
<label class="flex flex-col gap-1">
Payment Date
<kendo-datepicker [(ngModel)]="payDate"></kendo-datepicker>
</label>
</div>
<kendo-dialog-actions>
<button kendoButton (click)="payRow = null">Cancel</button>
<button kendoButton themeColor="primary" (click)="confirmPay()">Confirm</button>
</kendo-dialog-actions>
</kendo-dialog>
<!-- Review dialog: detail + receipt preview, with Approve / Reject(reason) -->
<app-expense-review-dialog *ngIf="reviewRow" [expenseId]="reviewRow.id"
(approve)="onReviewApprove()" (reject)="onReviewReject($event)" (cancel)="closeReview()">
</app-expense-review-dialog>
<!-- Transient save confirmation (sits above the open dialog during continuous entry) -->
<div *ngIf="toast" class="save-toast">{{ toast }}</div>
</div>