feat(1099): 1099 recipients master page with nav + route
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -228,6 +228,15 @@ export const routes: Routes = [
|
|||||||
title: 'Form 990 — Functional Expenses', titleZh: 'Form 990 功能性費用表', section: 'Finance',
|
title: 'Form 990 — Functional Expenses', titleZh: 'Form 990 功能性費用表', section: 'Finance',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'finance/payee-1099',
|
||||||
|
loadComponent: () => import('./features/payee1099/pages/payee-1099-page/payee-1099-page.component').then(m => m.Payee1099PageComponent),
|
||||||
|
canActivate: [PermissionGuard],
|
||||||
|
data: {
|
||||||
|
permission: { module: PermissionModules.Form1099, action: 'read' },
|
||||||
|
title: '1099 Recipients', titleZh: '1099 收款人', section: 'Finance',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<div class="page">
|
||||||
|
<ng-template appPageHeaderActions>
|
||||||
|
<label class="inactive-toggle">
|
||||||
|
<input type="checkbox" [(ngModel)]="includeInactive" (change)="load()" /> Show inactive / 顯示停用
|
||||||
|
</label>
|
||||||
|
<button kendoButton themeColor="primary"
|
||||||
|
*appHasPermission="{ module: 'Form1099', action: 'write' }"
|
||||||
|
(click)="openNew()">+ New Recipient / 新增收款人</button>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<div class="hint-text-sm">Click a name to edit · right-click a row for actions / 點選名稱編輯 · 右鍵顯示動作</div>
|
||||||
|
|
||||||
|
<!-- Desktop grid -->
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<kendo-grid class="clickable-rows" [data]="recipients" [loading]="loading"
|
||||||
|
(cellClick)="onCellClick($event)">
|
||||||
|
<kendo-grid-column field="legalName" title="Legal Name / 法定名稱">
|
||||||
|
<ng-template kendoGridCellTemplate let-r>
|
||||||
|
<span class="legal-name">{{ r.legalName }}</span>
|
||||||
|
<span *ngIf="r.displayName" class="display-name"> ({{ r.displayName }})</span>
|
||||||
|
</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
<kendo-grid-column field="memberName" title="Member / 會友">
|
||||||
|
<ng-template kendoGridCellTemplate let-r>{{ r.memberName || '—' }}</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
<kendo-grid-column field="taxClassification" title="Tax Class / 稅務分類" [width]="150"></kendo-grid-column>
|
||||||
|
<kendo-grid-column title="TIN" [width]="120">
|
||||||
|
<ng-template kendoGridCellTemplate let-r>{{ r.tinLast4 ? '***-**-' + r.tinLast4 : '—' }}</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
<kendo-grid-column field="w9Status" title="W-9" [width]="120">
|
||||||
|
<ng-template kendoGridCellTemplate let-r>
|
||||||
|
<span class="badge" [ngClass]="'badge-' + r.w9Status.toLowerCase()">{{ r.w9Status }}</span>
|
||||||
|
</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
<kendo-grid-column field="is1099Tracked" title="1099 Tracked" [width]="120">
|
||||||
|
<ng-template kendoGridCellTemplate let-r>{{ r.is1099Tracked ? 'Yes' : 'No' }}</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
<kendo-grid-column field="isActive" title="Active" [width]="90">
|
||||||
|
<ng-template kendoGridCellTemplate let-r>{{ r.isActive ? 'Yes' : 'No' }}</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 r of recipients" class="rounded border p-3" (click)="openEdit(r)">
|
||||||
|
<div class="flex justify-between items-start gap-2">
|
||||||
|
<div class="font-semibold">{{ r.legalName }}</div>
|
||||||
|
<span class="badge" [ngClass]="'badge-' + r.w9Status.toLowerCase()">{{ r.w9Status }}</span>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="r.displayName" class="text-sm text-gray-500">{{ r.displayName }}</div>
|
||||||
|
<div class="text-sm flex justify-between"><span>Member / 會友</span><span>{{ r.memberName || '—' }}</span></div>
|
||||||
|
<div class="text-sm flex justify-between"><span>Tax Class</span><span>{{ r.taxClassification }}</span></div>
|
||||||
|
<div class="text-sm flex justify-between"><span>TIN</span><span>{{ r.tinLast4 ? '***-**-' + r.tinLast4 : '—' }}</span></div>
|
||||||
|
<div class="text-sm flex justify-between"><span>1099 Tracked</span><span>{{ r.is1099Tracked ? 'Yes' : 'No' }}</span></div>
|
||||||
|
<div class="text-sm flex justify-between"><span>Active</span><span>{{ r.isActive ? 'Yes' : 'No' }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New / Edit dialog -->
|
||||||
|
<kendo-dialog *ngIf="dialogOpen"
|
||||||
|
[title]="editingId != null ? 'Edit Recipient / 編輯收款人' : 'New Recipient / 新增收款人'"
|
||||||
|
(close)="dialogOpen = false"
|
||||||
|
[width]="720" [maxWidth]="'95vw'">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Legal Name / 法定名稱 *
|
||||||
|
<kendo-textbox [(ngModel)]="form.legalName"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Display Name / 顯示名稱
|
||||||
|
<kendo-textbox [(ngModel)]="form.displayName"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1 md:col-span-2">
|
||||||
|
Linked Member / 連結會友
|
||||||
|
<kendo-dropdownlist
|
||||||
|
[data]="memberResults"
|
||||||
|
textField="displayName" valueField="id" [valuePrimitive]="true"
|
||||||
|
[filterable]="true" (filterChange)="onMemberFilter($event)"
|
||||||
|
[defaultItem]="{ id: null, displayName: '(None / 無)' }"
|
||||||
|
[(ngModel)]="form.memberId">
|
||||||
|
</kendo-dropdownlist>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Tax Classification / 稅務分類
|
||||||
|
<kendo-dropdownlist [data]="taxClassifications" [(ngModel)]="form.taxClassification"></kendo-dropdownlist>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 md:mt-6">
|
||||||
|
<kendo-switch [(ngModel)]="form.is1099Tracked"></kendo-switch>
|
||||||
|
<span>1099 Tracked / 列入 1099</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
TIN Type / 稅號類型
|
||||||
|
<kendo-dropdownlist [data]="tinTypes" [(ngModel)]="form.tinType"></kendo-dropdownlist>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
TIN / 稅號
|
||||||
|
<kendo-textbox [(ngModel)]="form.tin"
|
||||||
|
[placeholder]="editingId != null && editingTinLast4 ? '***-**-' + editingTinLast4 : ''"></kendo-textbox>
|
||||||
|
<span *ngIf="editingId != null" class="hint-text-sm">Leave blank to keep the existing TIN / 留空則保留現有稅號</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1 md:col-span-2">
|
||||||
|
Address Line 1 / 地址 1
|
||||||
|
<kendo-textbox [(ngModel)]="form.addressLine1"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1 md:col-span-2">
|
||||||
|
Address Line 2 / 地址 2
|
||||||
|
<kendo-textbox [(ngModel)]="form.addressLine2"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
City / 城市
|
||||||
|
<kendo-textbox [(ngModel)]="form.city"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
<div class="grid grid-cols-2 gap-x-4">
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
State / 州
|
||||||
|
<kendo-textbox [(ngModel)]="form.state"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Zip / 郵遞區號
|
||||||
|
<kendo-textbox [(ngModel)]="form.zip"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Email / 電郵
|
||||||
|
<kendo-textbox [(ngModel)]="form.email"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Phone / 電話
|
||||||
|
<kendo-textbox [(ngModel)]="form.phone"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
W-9 Status / W-9 狀態
|
||||||
|
<kendo-dropdownlist [data]="w9Statuses" [(ngModel)]="form.w9Status"></kendo-dropdownlist>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
W-9 Received / W-9 收到日期
|
||||||
|
<kendo-datepicker [(value)]="form.w9ReceivedDate"></kendo-datepicker>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1 md:col-span-2">
|
||||||
|
Notes / 備註
|
||||||
|
<kendo-textarea [(ngModel)]="form.notes" [rows]="3"></kendo-textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label *ngIf="editingId != null" class="flex items-center gap-2 md:col-span-2">
|
||||||
|
<input type="checkbox" [(ngModel)]="form.isActive" /> Active / 啟用
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<kendo-dialog-actions>
|
||||||
|
<button kendoButton (click)="dialogOpen = false">Cancel / 取消</button>
|
||||||
|
<button kendoButton themeColor="primary" [disabled]="!form.legalName" (click)="save()">Save / 儲存</button>
|
||||||
|
</kendo-dialog-actions>
|
||||||
|
</kendo-dialog>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.hint-text-sm {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid rows are clickable to open the editor.
|
||||||
|
.clickable-rows ::ng-deep .k-grid-content tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// W-9 status 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
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 { InputsModule } from '@progress/kendo-angular-inputs';
|
||||||
|
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
|
||||||
|
import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
|
||||||
|
import { ContextMenuModule, ContextMenuComponent, ContextMenuSelectEvent } from '@progress/kendo-angular-menu';
|
||||||
|
import { Payee1099ApiService } from '../../services/payee1099-api.service';
|
||||||
|
import { Payee1099ListItem, Payee1099, SavePayee1099Request } from '../../models/payee1099.model';
|
||||||
|
import { MemberApiService } from '../../../members/services/member-api.service';
|
||||||
|
import { MemberListItemDto, memberDisplayName } from '../../../members/models/member.model';
|
||||||
|
import { PageHeaderActionsDirective } from '../../../../shared/directives/page-header-actions.directive';
|
||||||
|
import { HasPermissionDirective } from '../../../../core/directives/has-permission.directive';
|
||||||
|
|
||||||
|
/** Flattened member item with a single displayName field for the picker. */
|
||||||
|
interface MemberOption { id: number; displayName: string; }
|
||||||
|
|
||||||
|
/** Editable form model for the New/Edit dialog. */
|
||||||
|
interface Payee1099Form {
|
||||||
|
legalName: string;
|
||||||
|
displayName: string;
|
||||||
|
memberId: number | null;
|
||||||
|
taxClassification: string;
|
||||||
|
is1099Tracked: boolean;
|
||||||
|
tinType: string;
|
||||||
|
tin: string;
|
||||||
|
addressLine1: string;
|
||||||
|
addressLine2: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
zip: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
w9Status: string;
|
||||||
|
w9ReceivedDate: Date | null;
|
||||||
|
isActive: boolean;
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-payee-1099-page',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule, FormsModule, GridModule, ButtonsModule, DialogsModule,
|
||||||
|
InputsModule, DropDownsModule, DateInputsModule, ContextMenuModule,
|
||||||
|
PageHeaderActionsDirective, HasPermissionDirective,
|
||||||
|
],
|
||||||
|
templateUrl: './payee-1099-page.component.html',
|
||||||
|
styleUrls: ['./payee-1099-page.component.scss'],
|
||||||
|
})
|
||||||
|
export class Payee1099PageComponent implements OnInit {
|
||||||
|
recipients: Payee1099ListItem[] = [];
|
||||||
|
loading = false;
|
||||||
|
includeInactive = false;
|
||||||
|
|
||||||
|
readonly taxClassifications = ['Individual', 'SoleProprietor', 'Partnership', 'CCorp', 'SCorp', 'LLC', 'Other'];
|
||||||
|
readonly tinTypes = ['SSN', 'EIN'];
|
||||||
|
readonly w9Statuses = ['Missing', 'Requested', 'OnFile', 'Expired'];
|
||||||
|
|
||||||
|
/** Member picker options, filled on demand from the members search. */
|
||||||
|
memberResults: MemberOption[] = [];
|
||||||
|
|
||||||
|
@ViewChild('rowMenu') rowMenu!: ContextMenuComponent;
|
||||||
|
rowMenuItems: { text: string }[] = [];
|
||||||
|
private contextRow: Payee1099ListItem | null = null;
|
||||||
|
|
||||||
|
dialogOpen = false;
|
||||||
|
editingId: number | null = null;
|
||||||
|
/** Last-4 of the existing TIN (edit mode), so the TIN box can show a masked placeholder. */
|
||||||
|
editingTinLast4: string | null = null;
|
||||||
|
form: Payee1099Form = this.blankForm();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private api: Payee1099ApiService,
|
||||||
|
private memberApi: MemberApiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
this.loading = true;
|
||||||
|
this.api.getAll(this.includeInactive).subscribe({
|
||||||
|
next: (rows) => {
|
||||||
|
this.recipients = rows;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: () => { this.loading = false; },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private blankForm(): Payee1099Form {
|
||||||
|
return {
|
||||||
|
legalName: '', displayName: '', memberId: null,
|
||||||
|
taxClassification: 'Individual', is1099Tracked: true,
|
||||||
|
tinType: 'SSN', tin: '',
|
||||||
|
addressLine1: '', addressLine2: '', city: '', state: '', zip: '',
|
||||||
|
email: '', phone: '',
|
||||||
|
w9Status: 'Missing', w9ReceivedDate: null,
|
||||||
|
isActive: true, notes: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Member picker (server-side search, same source as the expense form) ──────
|
||||||
|
onMemberFilter(term: string): void {
|
||||||
|
if (!term || term.length < 1) { this.memberResults = []; return; }
|
||||||
|
this.memberApi.getPaged({ search: term, pageSize: 10 }).subscribe((result) => {
|
||||||
|
this.memberResults = result.items.map((member: MemberListItemDto) => ({
|
||||||
|
id: member.id,
|
||||||
|
displayName: memberDisplayName(member),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Row interaction: primary click opens the editor; right-click shows actions ──
|
||||||
|
onCellClick(event: CellClickEvent): void {
|
||||||
|
if (event.type === 'contextmenu') {
|
||||||
|
event.originalEvent.preventDefault();
|
||||||
|
this.contextRow = event.dataItem;
|
||||||
|
this.rowMenuItems = this.buildMenuItems(event.dataItem.isActive);
|
||||||
|
this.rowMenu.show({ left: event.originalEvent.pageX, top: event.originalEvent.pageY });
|
||||||
|
} else {
|
||||||
|
this.openEdit(event.dataItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRowMenuSelect(event: ContextMenuSelectEvent): void {
|
||||||
|
if (!this.contextRow) return;
|
||||||
|
if (event.item.text === 'Edit') this.openEdit(this.contextRow);
|
||||||
|
else if (event.item.text === 'Deactivate') this.deactivate(this.contextRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildMenuItems(isActive: boolean): { text: string }[] {
|
||||||
|
const items: { text: string }[] = [{ text: 'Edit' }];
|
||||||
|
if (isActive) items.push({ text: 'Deactivate' });
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dialog open ──────────────────────────────────────────────────────────────
|
||||||
|
openNew(): void {
|
||||||
|
this.editingId = null;
|
||||||
|
this.editingTinLast4 = null;
|
||||||
|
this.form = this.blankForm();
|
||||||
|
this.dialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
openEdit(row: Payee1099ListItem): void {
|
||||||
|
this.editingId = row.id;
|
||||||
|
this.dialogOpen = true;
|
||||||
|
// Load the full record so the dialog can prefill the address/contact/notes fields.
|
||||||
|
this.api.getById(row.id).subscribe((payee: Payee1099) => {
|
||||||
|
this.editingTinLast4 = payee.tinLast4 ?? null;
|
||||||
|
this.form = {
|
||||||
|
legalName: payee.legalName,
|
||||||
|
displayName: payee.displayName ?? '',
|
||||||
|
memberId: payee.memberId ?? null,
|
||||||
|
taxClassification: payee.taxClassification,
|
||||||
|
is1099Tracked: payee.is1099Tracked,
|
||||||
|
tinType: payee.tinType ?? 'SSN',
|
||||||
|
tin: '',
|
||||||
|
addressLine1: payee.addressLine1 ?? '',
|
||||||
|
addressLine2: payee.addressLine2 ?? '',
|
||||||
|
city: payee.city ?? '',
|
||||||
|
state: payee.state ?? '',
|
||||||
|
zip: payee.zip ?? '',
|
||||||
|
email: payee.email ?? '',
|
||||||
|
phone: payee.phone ?? '',
|
||||||
|
w9Status: payee.w9Status,
|
||||||
|
w9ReceivedDate: this.parseDateOnly(payee.w9ReceivedDate),
|
||||||
|
isActive: payee.isActive,
|
||||||
|
notes: payee.notes ?? '',
|
||||||
|
};
|
||||||
|
// Seed the picker with the linked member so its name shows even before a search.
|
||||||
|
if (payee.memberId != null && payee.memberName) {
|
||||||
|
this.memberResults = [{ id: payee.memberId, displayName: payee.memberName }];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save ─────────────────────────────────────────────────────────────────────
|
||||||
|
save(): void {
|
||||||
|
if (!this.form.legalName.trim()) return;
|
||||||
|
const typedTin = this.form.tin.trim();
|
||||||
|
const request: SavePayee1099Request = {
|
||||||
|
legalName: this.form.legalName.trim(),
|
||||||
|
displayName: this.form.displayName.trim() || undefined,
|
||||||
|
memberId: this.form.memberId ?? null,
|
||||||
|
taxClassification: this.form.taxClassification,
|
||||||
|
is1099Tracked: this.form.is1099Tracked,
|
||||||
|
tinType: this.form.tinType,
|
||||||
|
// Send the typed TIN when present. On edit a blank leaves the stored value
|
||||||
|
// unchanged (null = no change); on new a blank simply means no TIN yet.
|
||||||
|
tin: typedTin || null,
|
||||||
|
addressLine1: this.form.addressLine1.trim() || undefined,
|
||||||
|
addressLine2: this.form.addressLine2.trim() || undefined,
|
||||||
|
city: this.form.city.trim() || undefined,
|
||||||
|
state: this.form.state.trim() || undefined,
|
||||||
|
zip: this.form.zip.trim() || undefined,
|
||||||
|
email: this.form.email.trim() || undefined,
|
||||||
|
phone: this.form.phone.trim() || undefined,
|
||||||
|
w9Status: this.form.w9Status,
|
||||||
|
w9ReceivedDate: this.toDateOnly(this.form.w9ReceivedDate),
|
||||||
|
isActive: this.form.isActive,
|
||||||
|
notes: this.form.notes.trim() || undefined,
|
||||||
|
};
|
||||||
|
const done = () => { this.dialogOpen = false; this.load(); };
|
||||||
|
if (this.editingId == null) this.api.create(request).subscribe(done);
|
||||||
|
else this.api.update(this.editingId, request).subscribe(done);
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate(row: Payee1099ListItem): void {
|
||||||
|
if (!confirm(`Deactivate "${row.legalName}"?`)) return;
|
||||||
|
this.api.delete(row.id).subscribe(() => this.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Date-only helpers: build/parse "yyyy-MM-dd" from LOCAL components ─────────
|
||||||
|
private parseDateOnly(value: string | undefined | null): Date | null {
|
||||||
|
if (!value) return null;
|
||||||
|
const [year, month, day] = value.split('-').map(Number);
|
||||||
|
return new Date(year, month - 1, day);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toDateOnly(date: Date | null): string | null {
|
||||||
|
if (!date) return null;
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,8 @@ export class UserPortalComponent implements OnInit, OnDestroy {
|
|||||||
permission: { module: PermissionModules.Disbursements, action: 'read' } },
|
permission: { module: PermissionModules.Disbursements, action: 'read' } },
|
||||||
{ text: 'Check Register', icon: walletOutlineIcon, path: '/user-portal/finance/check-register',
|
{ text: 'Check Register', icon: walletOutlineIcon, path: '/user-portal/finance/check-register',
|
||||||
permission: { module: PermissionModules.Disbursements, action: 'read' } },
|
permission: { module: PermissionModules.Disbursements, action: 'read' } },
|
||||||
|
{ text: '1099 Recipients', icon: fileReportIcon, path: '/user-portal/finance/payee-1099',
|
||||||
|
permission: { module: PermissionModules.Form1099, action: 'read' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user