From 5aaac3246da13b414ed6779289327fea0c290b9d Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Wed, 24 Jun 2026 19:49:02 -0700 Subject: [PATCH] feat(web): Form 990 functional-expenses report page, route, and nav Co-Authored-By: Claude Opus 4.8 --- APP/src/app/app.routes.ts | 10 ++++ .../models/form990-report.model.ts | 19 ++++++++ .../form990-report-page.component.html | 39 ++++++++++++++++ .../form990-report-page.component.ts | 46 +++++++++++++++++++ .../services/form990-report-api.service.ts | 24 ++++++++++ .../user-portal/user-portal.component.ts | 2 + 6 files changed, 140 insertions(+) create mode 100644 APP/src/app/features/finance-report/pages/form990-report-page/form990-report-page.component.html create mode 100644 APP/src/app/features/finance-report/pages/form990-report-page/form990-report-page.component.ts create mode 100644 APP/src/app/features/finance-report/services/form990-report-api.service.ts diff --git a/APP/src/app/app.routes.ts b/APP/src/app/app.routes.ts index e99500b..3835603 100644 --- a/APP/src/app/app.routes.ts +++ b/APP/src/app/app.routes.ts @@ -20,6 +20,7 @@ import { FinanceDashboardPageComponent } from './features/finance-dashboard/page import { DisbursementPageComponent } from './features/disbursement/pages/disbursement-page/disbursement-page.component'; import { CheckRegisterPageComponent } from './features/disbursement/pages/check-register-page/check-register-page.component'; import { ChurchProfilePageComponent } from './features/disbursement/pages/church-profile-page/church-profile-page.component'; +import { Form990ReportPageComponent } from './features/finance-report/pages/form990-report-page/form990-report-page.component'; import { AttendanceCounterPageComponent } from './features/meal-attendance/pages/attendance-counter-page/attendance-counter-page.component'; import { OfferingEntryMobilePageComponent } from './features/giving/pages/offering-entry-mobile-page/offering-entry-mobile-page.component'; import { SystemLogsPageComponent } from './features/logging/pages/system-logs-page/system-logs-page.component'; @@ -206,6 +207,15 @@ export const routes: Routes = [ title: 'Church Profile', titleZh: '教會資料', section: 'Finance', }, }, + { + path: 'finance/form-990-report', + component: Form990ReportPageComponent, + canActivate: [PermissionGuard], + data: { + permission: { module: PermissionModules.Form990Report, action: 'read' }, + title: 'Form 990 — Functional Expenses', titleZh: 'Form 990 功能性費用表', section: 'Finance', + }, + }, ] }, diff --git a/APP/src/app/features/finance-report/models/form990-report.model.ts b/APP/src/app/features/finance-report/models/form990-report.model.ts index d214d5f..2479b56 100644 --- a/APP/src/app/features/finance-report/models/form990-report.model.ts +++ b/APP/src/app/features/finance-report/models/form990-report.model.ts @@ -6,3 +6,22 @@ export interface Form990ExpenseLineDto { sortOrder: number; label?: string; // bilingual "code — name", filled by service } + +export interface FunctionalExpenseRowDto { + lineCode: string; + name_en: string; + name_zh: string | null; + program: number; + managementGeneral: number; + fundraising: number; + total: number; +} + +export interface FunctionalExpenseStatementDto { + rows: FunctionalExpenseRowDto[]; + programTotal: number; + managementGeneralTotal: number; + fundraisingTotal: number; + grandTotal: number; + unmappedExpenseCount: number; +} diff --git a/APP/src/app/features/finance-report/pages/form990-report-page/form990-report-page.component.html b/APP/src/app/features/finance-report/pages/form990-report-page/form990-report-page.component.html new file mode 100644 index 0000000..db32c77 --- /dev/null +++ b/APP/src/app/features/finance-report/pages/form990-report-page/form990-report-page.component.html @@ -0,0 +1,39 @@ +
+ + + +
+ +
+ {{ statement?.unmappedExpenseCount }} expense(s) have no Form 990 mapping — counted under line 24. + 尚有支出未對應 990 行,已暫計入 line 24。 +
+ + + +
+
+
{{ row.lineCode }} — {{ row.name_en }}
+
Program{{ row.program | currency }}
+
M&G{{ row.managementGeneral | currency }}
+
Fundraising{{ row.fundraising | currency }}
+
Total{{ row.total | currency }}
+
+
diff --git a/APP/src/app/features/finance-report/pages/form990-report-page/form990-report-page.component.ts b/APP/src/app/features/finance-report/pages/form990-report-page/form990-report-page.component.ts new file mode 100644 index 0000000..4558058 --- /dev/null +++ b/APP/src/app/features/finance-report/pages/form990-report-page/form990-report-page.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { GridModule } from '@progress/kendo-angular-grid'; +import { DatePickerModule } from '@progress/kendo-angular-dateinputs'; +import { ButtonsModule } from '@progress/kendo-angular-buttons'; +import { Form990ReportApiService } from '../../services/form990-report-api.service'; +import { FunctionalExpenseStatementDto } from '../../models/form990-report.model'; + +@Component({ + selector: 'app-form990-report-page', + standalone: true, + imports: [CommonModule, FormsModule, GridModule, DatePickerModule, ButtonsModule], + templateUrl: './form990-report-page.component.html', +}) +export class Form990ReportPageComponent implements OnInit { + from: Date = new Date(new Date().getFullYear(), 0, 1); + to: Date = new Date(new Date().getFullYear(), 11, 31); + statement: FunctionalExpenseStatementDto | null = null; + loading = false; + + constructor(private api: Form990ReportApiService) {} + + ngOnInit(): void { + this.load(); + } + + load(): void { + this.loading = true; + const fmt = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }; + this.api.getFunctionalExpenses(fmt(this.from), fmt(this.to)).subscribe({ + next: (statement) => { + this.statement = statement; + this.loading = false; + }, + error: () => { + this.loading = false; + }, + }); + } +} diff --git a/APP/src/app/features/finance-report/services/form990-report-api.service.ts b/APP/src/app/features/finance-report/services/form990-report-api.service.ts new file mode 100644 index 0000000..200935d --- /dev/null +++ b/APP/src/app/features/finance-report/services/form990-report-api.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ApiConfigService } from '../../../core/services/api-config.service'; +import { FunctionalExpenseStatementDto } from '../models/form990-report.model'; + +@Injectable({ providedIn: 'root' }) +export class Form990ReportApiService { + private readonly endpoint: string; + + constructor(private http: HttpClient, apiConfig: ApiConfigService) { + this.endpoint = apiConfig.getApiUrl('form990-report'); + } + + getFunctionalExpenses(from?: string, to?: string): Observable { + let params = new HttpParams(); + if (from) { params = params.set('from', from); } + if (to) { params = params.set('to', to); } + return this.http.get( + `${this.endpoint}/functional-expenses`, + { params } + ); + } +} diff --git a/APP/src/app/portals/user-portal/user-portal.component.ts b/APP/src/app/portals/user-portal/user-portal.component.ts index 6e77afc..4399781 100644 --- a/APP/src/app/portals/user-portal/user-portal.component.ts +++ b/APP/src/app/portals/user-portal/user-portal.component.ts @@ -108,6 +108,8 @@ export class UserPortalComponent implements OnInit, OnDestroy { permission: { module: PermissionModules.FinanceDashboard, action: 'read' } }, { text: 'Monthly Statement', icon: fileReportIcon, path: '/user-portal/finance/monthly-statement', permission: { module: PermissionModules.MonthlyStatements, action: 'read' } }, + { text: 'Form 990 Report', icon: fileReportIcon, path: '/user-portal/finance/form-990-report', + permission: { module: PermissionModules.Form990Report, action: 'read' } }, ], }, {