diff --git a/APP/src/app/app.routes.ts b/APP/src/app/app.routes.ts
index e07e601..81a079e 100644
--- a/APP/src/app/app.routes.ts
+++ b/APP/src/app/app.routes.ts
@@ -22,6 +22,8 @@ import { DisbursementPageComponent } from './features/disbursement/pages/disburs
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 { Form1099ReportPageComponent } from './features/finance-report/pages/form1099-report-page/form1099-report-page.component';
+import { Payee1099PageComponent } from './features/payee1099/pages/payee-1099-page/payee-1099-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';
@@ -230,13 +232,22 @@ export const routes: Routes = [
},
{
path: 'finance/payee-1099',
- loadComponent: () => import('./features/payee1099/pages/payee-1099-page/payee-1099-page.component').then(m => m.Payee1099PageComponent),
+ component: Payee1099PageComponent,
canActivate: [PermissionGuard],
data: {
permission: { module: PermissionModules.Form1099, action: 'read' },
title: '1099 Recipients', titleZh: '1099 收款人', section: 'Finance',
},
},
+ {
+ path: 'finance/form1099-report',
+ component: Form1099ReportPageComponent,
+ canActivate: [PermissionGuard],
+ data: {
+ permission: { module: PermissionModules.Form1099, action: 'read' },
+ title: '1099 Year-End Report', titleZh: '1099 年度報表', section: 'Finance',
+ },
+ },
]
},
diff --git a/APP/src/app/features/finance-report/pages/form1099-report-page/form1099-report-page.component.html b/APP/src/app/features/finance-report/pages/form1099-report-page/form1099-report-page.component.html
new file mode 100644
index 0000000..8ff7580
--- /dev/null
+++ b/APP/src/app/features/finance-report/pages/form1099-report-page/form1099-report-page.component.html
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Total Reportable / 應申報總額
+
{{ summary.totalReportable | currency }}
+
+
+
Recipients ≥ $600 / 達門檻收款人
+
{{ summary.recipientsAtThreshold }}
+
+
0">
+
Missing W-9 / 缺少 W-9
+
{{ summary.recipientsMissingW9 }}
+
+
+
+
Click a name for payment detail · right-click a row for Copy B / 點選名稱檢視明細 · 右鍵下載 Copy B
+
+
+
+
+
+
+ {{ r.legalName }}
+
+
+
+ {{ r.tinLast4 ? '***-**-' + r.tinLast4 : '—' }}
+
+
+
+
+ {{ r.w9Status }}
+
+
+
+
+
+
+
+
+ ≥ $600
+ —
+
+
+
+
+
+
+
+
+
+
+
{{ r.legalName }}
+
+ {{ r.w9Status }}
+
+
+
TIN{{ r.tinLast4 ? '***-**-' + r.tinLast4 : '—' }}
+
NEC / 非雇員報酬{{ r.necTotal | currency }}
+
Rents / 租金{{ r.rentsTotal | currency }}
+
Total / 總計{{ r.grandTotal | currency }}
+
+ Threshold / 門檻
+ ≥ $600—
+
+
+
+
+
+
+
+ Loading… / 載入中…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/APP/src/app/features/finance-report/pages/form1099-report-page/form1099-report-page.component.scss b/APP/src/app/features/finance-report/pages/form1099-report-page/form1099-report-page.component.scss
new file mode 100644
index 0000000..6f9da0f
--- /dev/null
+++ b/APP/src/app/features/finance-report/pages/form1099-report-page/form1099-report-page.component.scss
@@ -0,0 +1,100 @@
+.hint-text-sm {
+ margin-bottom: 0.5rem;
+ font-size: 0.8rem;
+ color: #999;
+}
+
+.legal-name {
+ font-weight: 600;
+}
+
+// Grid rows are clickable to open the recipient detail.
+.clickable-rows ::ng-deep .k-grid-content tr {
+ cursor: pointer;
+}
+
+// Summary chips.
+.summary-chip {
+ flex: 1 1 200px;
+ min-width: 180px;
+ padding: 0.75rem 1rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ background-color: #f9fafb;
+}
+
+.summary-label {
+ font-size: 0.75rem;
+ color: #6b7280;
+}
+
+.summary-value {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #111827;
+}
+
+// Missing-W-9 chip is a governance flag — make it stand out.
+.summary-chip-flag {
+ border-color: #fca5a5;
+ background-color: #fef2f2;
+
+ .summary-value {
+ color: #991b1b;
+ }
+}
+
+// Recipient detail header.
+.detail-header {
+ margin-bottom: 0.75rem;
+}
+
+.detail-name {
+ font-size: 1.1rem;
+ font-weight: 700;
+}
+
+.detail-meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.75rem;
+ margin-top: 0.25rem;
+ font-size: 0.85rem;
+ color: #555;
+}
+
+// Status / threshold badges.
+.badge {
+ display: inline-block;
+ padding: 0.1rem 0.5rem;
+ border-radius: 9999px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ line-height: 1.2;
+}
+
+.badge-onfile {
+ background-color: #dcfce7;
+ color: #166534;
+}
+
+.badge-requested {
+ background-color: #fef9c3;
+ color: #854d0e;
+}
+
+.badge-missing {
+ background-color: #fee2e2;
+ color: #991b1b;
+}
+
+.badge-expired {
+ background-color: #fed7aa;
+ color: #9a3412;
+}
+
+.badge-threshold {
+ background-color: #dbeafe;
+ color: #1e40af;
+}
diff --git a/APP/src/app/features/finance-report/pages/form1099-report-page/form1099-report-page.component.ts b/APP/src/app/features/finance-report/pages/form1099-report-page/form1099-report-page.component.ts
new file mode 100644
index 0000000..bc63424
--- /dev/null
+++ b/APP/src/app/features/finance-report/pages/form1099-report-page/form1099-report-page.component.ts
@@ -0,0 +1,122 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { GridModule, CellClickEvent } from '@progress/kendo-angular-grid';
+import { ButtonsModule } from '@progress/kendo-angular-buttons';
+import { DialogsModule } from '@progress/kendo-angular-dialog';
+import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
+import { ContextMenuModule, ContextMenuComponent, ContextMenuSelectEvent } from '@progress/kendo-angular-menu';
+import { Form1099ReportApiService } from '../../../payee1099/services/form1099-report-api.service';
+import {
+ Form1099Summary, Form1099RecipientRow, Form1099RecipientDetail,
+} from '../../../payee1099/models/payee1099.model';
+import { PageHeaderActionsDirective } from '../../../../shared/directives/page-header-actions.directive';
+
+@Component({
+ selector: 'app-form1099-report-page',
+ standalone: true,
+ imports: [
+ CommonModule, FormsModule, GridModule, ButtonsModule, DialogsModule,
+ DropDownsModule, ContextMenuModule, PageHeaderActionsDirective,
+ ],
+ templateUrl: './form1099-report-page.component.html',
+ styleUrls: ['./form1099-report-page.component.scss'],
+})
+export class Form1099ReportPageComponent implements OnInit {
+ /** Recent years offered in the selector: current year and the prior four. */
+ readonly years: number[] = [];
+ taxYear: number = new Date().getFullYear();
+
+ summary: Form1099Summary | null = null;
+ loading = false;
+
+ // Per-row "Copy B" action, surfaced through a right-click context menu (matches
+ // the recipients page convention of putting row actions in a context menu).
+ @ViewChild('rowMenu') rowMenu!: ContextMenuComponent;
+ rowMenuItems: { text: string }[] = [];
+ private contextRow: Form1099RecipientRow | null = null;
+
+ detail: Form1099RecipientDetail | null = null;
+ detailLoading = false;
+
+ constructor(private api: Form1099ReportApiService) {
+ const currentYear = new Date().getFullYear();
+ for (let offset = 0; offset < 5; offset++) {
+ this.years.push(currentYear - offset);
+ }
+ }
+
+ ngOnInit(): void {
+ this.load();
+ }
+
+ load(): void {
+ this.loading = true;
+ this.api.getSummary(this.taxYear).subscribe({
+ next: (summary) => {
+ this.summary = summary;
+ this.loading = false;
+ },
+ error: () => { this.loading = false; },
+ });
+ }
+
+ // ── Row interaction: primary click opens the detail; right-click shows actions ──
+ onCellClick(event: CellClickEvent): void {
+ if (event.type === 'contextmenu') {
+ event.originalEvent.preventDefault();
+ this.contextRow = event.dataItem;
+ this.rowMenuItems = [{ text: 'Copy B PDF' }];
+ this.rowMenu.show({ left: event.originalEvent.pageX, top: event.originalEvent.pageY });
+ } else {
+ this.openDetail(event.dataItem);
+ }
+ }
+
+ onRowMenuSelect(event: ContextMenuSelectEvent): void {
+ if (!this.contextRow) return;
+ if (event.item.text === 'Copy B PDF') this.copyB(this.contextRow);
+ }
+
+ openDetail(row: Form1099RecipientRow): void {
+ this.detail = null;
+ this.detailLoading = true;
+ this.api.getRecipient(row.payeeId, this.taxYear).subscribe({
+ next: (detail) => {
+ this.detail = detail;
+ this.detailLoading = false;
+ },
+ error: () => { this.detailLoading = false; },
+ });
+ }
+
+ closeDetail(): void {
+ this.detail = null;
+ this.detailLoading = false;
+ }
+
+ // ── Downloads: fetched as blobs so the auth interceptor attaches the token ──────
+ exportCsv(): void {
+ this.api.downloadCsv(this.taxYear).subscribe((blob) => {
+ this.saveBlob(blob, `1099-filing-${this.taxYear}.csv`);
+ });
+ }
+
+ copyB(row: Form1099RecipientRow): void {
+ this.api.downloadCopyB(row.payeeId, this.taxYear).subscribe((blob) => {
+ this.saveBlob(blob, `1099-NEC-${row.payeeId}-${this.taxYear}.pdf`);
+ });
+ }
+
+ /** Trigger a browser save of a downloaded blob via a temporary anchor. */
+ private saveBlob(blob: Blob, fileName: string): void {
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = fileName;
+ document.body.appendChild(anchor);
+ anchor.click();
+ document.body.removeChild(anchor);
+ setTimeout(() => URL.revokeObjectURL(url), 60_000);
+ }
+}
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 7f3ac55..dd73e28 100644
--- a/APP/src/app/portals/user-portal/user-portal.component.ts
+++ b/APP/src/app/portals/user-portal/user-portal.component.ts
@@ -140,6 +140,8 @@ export class UserPortalComponent implements OnInit, OnDestroy {
permission: { module: PermissionModules.Disbursements, action: 'read' } },
{ text: '1099 Recipients', icon: fileReportIcon, path: '/user-portal/finance/payee-1099',
permission: { module: PermissionModules.Form1099, action: 'read' } },
+ { text: '1099 Report', icon: fileReportIcon, path: '/user-portal/finance/form1099-report',
+ permission: { module: PermissionModules.Form1099, action: 'read' } },
],
},
{