diff --git a/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html b/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html
new file mode 100644
index 0000000..639263b
--- /dev/null
+++ b/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dataItem.categoryGroupName }} / {{ dataItem.subCategoryName }}
+
+
+
+
+
+ {{ dataItem.vendorName || dataItem.memberName || '—' }}
+
+
+
+
+
+
+
+ {{ dataItem.status }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.scss b/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.scss
new file mode 100644
index 0000000..cba4215
--- /dev/null
+++ b/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.scss
@@ -0,0 +1,46 @@
+// Status badge pill styles
+%badge-base {
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 9999px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.badge-draft {
+ @extend %badge-base;
+ background-color: #e5e7eb;
+ color: #374151;
+}
+
+.badge-pending {
+ @extend %badge-base;
+ background-color: #fef3c7;
+ color: #92400e;
+}
+
+.badge-approved {
+ @extend %badge-base;
+ background-color: #dbeafe;
+ color: #1e40af;
+}
+
+.badge-paid {
+ @extend %badge-base;
+ background-color: #d1fae5;
+ color: #065f46;
+}
+
+.badge-rejected {
+ @extend %badge-base;
+ background-color: #fee2e2;
+ color: #991b1b;
+}
+
+.receipt-link {
+ font-size: 0.8rem;
+ margin-left: 4px;
+ color: #1d4ed8;
+ text-decoration: underline;
+}
diff --git a/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.ts b/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.ts
new file mode 100644
index 0000000..6ffb827
--- /dev/null
+++ b/APP/src/app/features/expense/pages/expenses-page/expenses-page.component.ts
@@ -0,0 +1,137 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { GridModule, PageChangeEvent } from '@progress/kendo-angular-grid';
+import { ButtonsModule } from '@progress/kendo-angular-buttons';
+import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
+import { InputsModule } from '@progress/kendo-angular-inputs';
+import { DialogsModule } from '@progress/kendo-angular-dialog';
+import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
+import { ExpenseApiService, ExpenseQuery } from '../../services/expense-api.service';
+import { MinistryApiService } from '../../services/ministry-api.service';
+import { ExpenseFormDialogComponent, ExpenseFormResult } from '../../components/expense-form-dialog/expense-form-dialog.component';
+import { ExpenseListItemDto, MinistryDto, ExpenseStatus } from '../../models/expense.model';
+import { switchMap, of } from 'rxjs';
+
+@Component({
+ selector: 'app-expenses-page',
+ standalone: true,
+ imports: [
+ CommonModule, FormsModule, GridModule, ButtonsModule, DropDownsModule,
+ InputsModule, DialogsModule, DateInputsModule, ExpenseFormDialogComponent,
+ ],
+ templateUrl: './expenses-page.component.html',
+ styleUrls: ['./expenses-page.component.scss'],
+})
+export class ExpensesPageComponent implements OnInit {
+ rows: ExpenseListItemDto[] = [];
+ total = 0;
+ page = 1;
+ pageSize = 20;
+ loading = false;
+
+ ministries: MinistryDto[] = [];
+ readonly statuses: ExpenseStatus[] = ['Draft', 'PendingApproval', 'Approved', 'Paid', 'Rejected'];
+
+ filter: ExpenseQuery = {};
+
+ vendorDialogOpen = false;
+ reimbDialogOpen = false;
+
+ payRow: ExpenseListItemDto | null = null;
+ payCheckNumber = '';
+ payDate = new Date();
+
+ rejectRow: ExpenseListItemDto | null = null;
+ rejectNotes = '';
+
+ constructor(private api: ExpenseApiService, private ministryApi: MinistryApiService) {}
+
+ ngOnInit(): void {
+ this.ministryApi.getAll().subscribe(m => (this.ministries = m));
+ this.load();
+ }
+
+ load(): void {
+ this.loading = true;
+ this.api.getPaged({ ...this.filter, page: this.page, pageSize: this.pageSize }).subscribe({
+ next: r => { this.rows = r.items; this.total = r.totalCount; this.loading = false; },
+ error: () => { this.loading = false; },
+ });
+ }
+
+ get skip(): number { return (this.page - 1) * this.pageSize; }
+
+ applyFilter(): void { this.page = 1; this.load(); }
+
+ onPageChange(e: PageChangeEvent): void {
+ this.page = Math.floor(e.skip / this.pageSize) + 1;
+ this.load();
+ }
+
+ onVendorSave(result: ExpenseFormResult): void {
+ this.api.create(result.request).subscribe(() => { this.vendorDialogOpen = false; this.load(); });
+ }
+
+ onReimbSave(result: ExpenseFormResult): void {
+ this.api.create(result.request).pipe(
+ switchMap(c => result.receipt
+ ? this.api.uploadReceipt(c.id, result.receipt).pipe(switchMap(() => of(c)))
+ : of(c)),
+ ).subscribe(() => { this.reimbDialogOpen = false; this.load(); });
+ }
+
+ approve(row: ExpenseListItemDto): void {
+ this.api.approve(row.id).subscribe(() => this.load());
+ }
+
+ openReject(row: ExpenseListItemDto): void {
+ this.rejectRow = row;
+ this.rejectNotes = '';
+ }
+
+ confirmReject(): void {
+ if (!this.rejectRow) return;
+ this.api.reject(this.rejectRow.id, { reviewNotes: this.rejectNotes || null }).subscribe(() => {
+ this.rejectRow = null;
+ this.load();
+ });
+ }
+
+ openPay(row: ExpenseListItemDto): void {
+ this.payRow = row;
+ this.payCheckNumber = '';
+ this.payDate = new Date();
+ }
+
+ confirmPay(): void {
+ if (!this.payRow) return;
+ const d = this.payDate;
+ const paidAt = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
+ this.api.pay(this.payRow.id, { checkNumber: this.payCheckNumber || null, paidAt }).subscribe(() => {
+ this.payRow = null;
+ this.load();
+ });
+ }
+
+ openReceipt(id: number): void {
+ this.api.downloadReceipt(id).subscribe(blob => {
+ const url = URL.createObjectURL(blob);
+ window.open(url, '_blank');
+ setTimeout(() => URL.revokeObjectURL(url), 60_000);
+ });
+ }
+
+ canApproveOrReject(row: ExpenseListItemDto): boolean { return row.status === 'PendingApproval'; }
+ canPay(row: ExpenseListItemDto): boolean { return row.status === 'Approved'; }
+
+ statusClass(status: string): string {
+ return ({
+ Draft: 'badge-draft',
+ PendingApproval: 'badge-pending',
+ Approved: 'badge-approved',
+ Paid: 'badge-paid',
+ Rejected: 'badge-rejected',
+ } as Record