feat(web): Form 990 functional-expenses report page, route, and nav
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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',
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<div class="flex flex-wrap items-end gap-3 mb-4">
|
||||
<label class="flex flex-col gap-1"><span>From / 起</span>
|
||||
<kendo-datepicker [(value)]="from"></kendo-datepicker></label>
|
||||
<label class="flex flex-col gap-1"><span>To / 迄</span>
|
||||
<kendo-datepicker [(value)]="to"></kendo-datepicker></label>
|
||||
<button kendoButton themeColor="primary" (click)="load()">Apply / 套用</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="statement?.unmappedExpenseCount" class="mb-3 p-2 rounded bg-amber-50 text-amber-800 text-sm">
|
||||
{{ statement?.unmappedExpenseCount }} expense(s) have no Form 990 mapping — counted under line 24.
|
||||
尚有支出未對應 990 行,已暫計入 line 24。
|
||||
</div>
|
||||
|
||||
<div class="hidden md:block">
|
||||
<kendo-grid [data]="statement?.rows ?? []">
|
||||
<kendo-grid-column field="lineCode" title="Line" [width]="80"></kendo-grid-column>
|
||||
<kendo-grid-column field="name_en" title="Description / 說明"></kendo-grid-column>
|
||||
<kendo-grid-column field="program" title="Program" format="{0:c2}" [width]="140"></kendo-grid-column>
|
||||
<kendo-grid-column field="managementGeneral" title="Mgmt & General" format="{0:c2}" [width]="150"></kendo-grid-column>
|
||||
<kendo-grid-column field="fundraising" title="Fundraising" format="{0:c2}" [width]="140"></kendo-grid-column>
|
||||
<kendo-grid-column field="total" title="Total" format="{0:c2}" [width]="140"></kendo-grid-column>
|
||||
</kendo-grid>
|
||||
<div class="flex justify-end gap-8 mt-2 font-semibold" *ngIf="statement">
|
||||
<span>Program: {{ statement.programTotal | currency }}</span>
|
||||
<span>M&G: {{ statement.managementGeneralTotal | currency }}</span>
|
||||
<span>Fundraising: {{ statement.fundraisingTotal | currency }}</span>
|
||||
<span>Total: {{ statement.grandTotal | currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex flex-col gap-3">
|
||||
<div *ngFor="let row of statement?.rows ?? []" class="rounded border p-3">
|
||||
<div class="font-semibold">{{ row.lineCode }} — {{ row.name_en }}</div>
|
||||
<div class="text-sm flex justify-between"><span>Program</span><span>{{ row.program | currency }}</span></div>
|
||||
<div class="text-sm flex justify-between"><span>M&G</span><span>{{ row.managementGeneral | currency }}</span></div>
|
||||
<div class="text-sm flex justify-between"><span>Fundraising</span><span>{{ row.fundraising | currency }}</span></div>
|
||||
<div class="text-sm flex justify-between font-semibold"><span>Total</span><span>{{ row.total | currency }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
+46
@@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<FunctionalExpenseStatementDto> {
|
||||
let params = new HttpParams();
|
||||
if (from) { params = params.set('from', from); }
|
||||
if (to) { params = params.set('to', to); }
|
||||
return this.http.get<FunctionalExpenseStatementDto>(
|
||||
`${this.endpoint}/functional-expenses`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user