You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

549 lines
19 KiB

import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
Output
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
// ── Field types ──────────────────────────────────────────────────────────
type FieldType = 'currency' | 'percent' | 'text' | 'checkbox';
interface K1FieldDef {
boxNumber: string;
label: string;
type: FieldType;
}
interface K1Section {
title: string;
description?: string;
fields: K1FieldDef[];
collapsed?: boolean;
}
// ── Section definitions matching the real IRS Schedule K-1 ───────────────
const K1_SECTIONS: K1Section[] = [
{
title: 'Header / Metadata',
fields: [
{ boxNumber: 'K1_DOCUMENT_ID', label: 'K-1 Document ID', type: 'text' },
{ boxNumber: 'TAX_YEAR', label: 'Tax Year', type: 'text' },
{ boxNumber: 'FINAL_K1', label: 'Final K-1', type: 'checkbox' },
{ boxNumber: 'AMENDED_K1', label: 'Amended K-1', type: 'checkbox' }
],
collapsed: true
},
{
title: 'Part I — Partnership Information',
fields: [
{ boxNumber: 'A', label: "A — Partnership's EIN", type: 'text' },
{ boxNumber: 'B', label: "B — Partnership's name / address", type: 'text' },
{ boxNumber: 'C', label: 'C — IRS center where return filed', type: 'text' },
{ boxNumber: 'D', label: 'D — Publicly traded partnership', type: 'checkbox' }
],
collapsed: true
},
{
title: 'Part II — Partner Information',
fields: [
{ boxNumber: 'E', label: "E — Partner's identifying number", type: 'text' },
{ boxNumber: 'F', label: "F — Partner's name / address", type: 'text' },
{ boxNumber: 'G_GENERAL', label: 'G — General partner / LLC member-manager', type: 'checkbox' },
{ boxNumber: 'G_LIMITED', label: 'G — Limited partner / other LLC member', type: 'checkbox' },
{ boxNumber: 'H1_DOMESTIC', label: 'H1 — Domestic partner', type: 'checkbox' },
{ boxNumber: 'H1_FOREIGN', label: 'H1 — Foreign partner', type: 'checkbox' },
{ boxNumber: 'H2', label: 'H2 — Disregarded entity', type: 'checkbox' },
{ boxNumber: 'H2_TIN', label: 'H2 — DE taxpayer ID', type: 'text' },
{ boxNumber: 'I1', label: 'I1 — Type of entity', type: 'text' },
{ boxNumber: 'I2', label: 'I2 — IRA / SEP / Keogh', type: 'checkbox' }
],
collapsed: true
},
{
title: "Section J — Partner's Share of Profit, Loss & Capital",
fields: [
{ boxNumber: 'J_PROFIT_BEGIN', label: 'Profit — Beginning', type: 'percent' },
{ boxNumber: 'J_PROFIT_END', label: 'Profit — Ending', type: 'percent' },
{ boxNumber: 'J_LOSS_BEGIN', label: 'Loss — Beginning', type: 'percent' },
{ boxNumber: 'J_LOSS_END', label: 'Loss — Ending', type: 'percent' },
{ boxNumber: 'J_CAPITAL_BEGIN', label: 'Capital — Beginning', type: 'percent' },
{ boxNumber: 'J_CAPITAL_END', label: 'Capital — Ending', type: 'percent' },
{ boxNumber: 'J_SALE', label: 'Decrease due to sale', type: 'checkbox' },
{ boxNumber: 'J_EXCHANGE', label: 'Exchange of partnership interest', type: 'checkbox' }
]
},
{
title: "Section K — Partner's Share of Liabilities",
fields: [
{ boxNumber: 'K_NONRECOURSE_BEGIN', label: 'Nonrecourse — Beginning', type: 'currency' },
{ boxNumber: 'K_NONRECOURSE_END', label: 'Nonrecourse — Ending', type: 'currency' },
{ boxNumber: 'K_QUAL_NONRECOURSE_BEGIN', label: 'Qualified nonrecourse — Beginning', type: 'currency' },
{ boxNumber: 'K_QUAL_NONRECOURSE_END', label: 'Qualified nonrecourse — Ending', type: 'currency' },
{ boxNumber: 'K_RECOURSE_BEGIN', label: 'Recourse — Beginning', type: 'currency' },
{ boxNumber: 'K_RECOURSE_END', label: 'Recourse — Ending', type: 'currency' },
{ boxNumber: 'K2', label: 'Includes lower-tier partnership liabilities', type: 'checkbox' },
{ boxNumber: 'K3', label: 'Liability subject to guarantees', type: 'checkbox' }
]
},
{
title: "Section L — Partner's Capital Account",
fields: [
{ boxNumber: 'L_BEG_CAPITAL', label: 'Beginning capital account', type: 'currency' },
{ boxNumber: 'L_CONTRIBUTED', label: 'Capital contributed during year', type: 'currency' },
{ boxNumber: 'L_CURR_YR_INCOME', label: 'Current year net income (loss)', type: 'currency' },
{ boxNumber: 'L_OTHER', label: 'Other increase (decrease)', type: 'currency' },
{ boxNumber: 'L_WITHDRAWALS', label: 'Withdrawals & distributions', type: 'currency' },
{ boxNumber: 'L_END_CAPITAL', label: 'Ending capital account', type: 'currency' }
]
},
{
title: 'Sections M & N',
fields: [
{ boxNumber: 'M_YES', label: 'M — Contributed property: Yes', type: 'checkbox' },
{ boxNumber: 'M_NO', label: 'M — Contributed property: No', type: 'checkbox' },
{ boxNumber: 'N_BEGINNING', label: 'N — Net 704(c) gain/loss: Beginning', type: 'currency' },
{ boxNumber: 'N_ENDING', label: 'N — Net 704(c) gain/loss: Ending', type: 'currency' }
]
},
{
title: 'Part III — Income & Gains (Boxes 1–11)',
fields: [
{ boxNumber: '1', label: '1 — Ordinary business income (loss)', type: 'currency' },
{ boxNumber: '2', label: '2 — Net rental real estate income (loss)', type: 'currency' },
{ boxNumber: '3', label: '3 — Other net rental income (loss)', type: 'currency' },
{ boxNumber: '4', label: '4 — Guaranteed payments for services', type: 'currency' },
{ boxNumber: '4a', label: '4a — Guaranteed payments for capital', type: 'currency' },
{ boxNumber: '4b', label: '4b — Total guaranteed payments', type: 'currency' },
{ boxNumber: '5', label: '5 — Interest income', type: 'currency' },
{ boxNumber: '6a', label: '6a — Ordinary dividends', type: 'currency' },
{ boxNumber: '6b', label: '6b — Qualified dividends', type: 'currency' },
{ boxNumber: '6c', label: '6c — Dividend equivalents', type: 'currency' },
{ boxNumber: '7', label: '7 — Royalties', type: 'currency' },
{ boxNumber: '8', label: '8 — Net short-term capital gain (loss)', type: 'currency' },
{ boxNumber: '9a', label: '9a — Net long-term capital gain (loss)', type: 'currency' },
{ boxNumber: '9b', label: '9b — Collectibles (28%) gain (loss)', type: 'currency' },
{ boxNumber: '9c', label: '9c — Unrecaptured §1250 gain', type: 'currency' },
{ boxNumber: '10', label: '10 — Net §1231 gain (loss)', type: 'currency' },
{ boxNumber: '11', label: '11 — Other income (loss)', type: 'currency' }
]
},
{
title: 'Part III — Deductions & Credits (Boxes 12–18)',
fields: [
{ boxNumber: '12', label: '12 — §179 deduction', type: 'currency' },
{ boxNumber: '13', label: '13 — Other deductions', type: 'currency' },
{ boxNumber: '14', label: '14 — Self-employment earnings (loss)', type: 'currency' },
{ boxNumber: '15', label: '15 — Credits', type: 'currency' },
{ boxNumber: '16', label: '16 — Foreign transactions', type: 'currency' },
{ boxNumber: '16_K3', label: '16 — Schedule K-3 attached', type: 'checkbox' },
{ boxNumber: '17', label: '17 — AMT items', type: 'currency' },
{ boxNumber: '18', label: '18 — Tax-exempt income / nondeductible expenses', type: 'currency' }
]
},
{
title: 'Part III — Distributions & Other (Boxes 19–23)',
fields: [
{ boxNumber: '19', label: '19 — Distributions', type: 'currency' },
{ boxNumber: '19a', label: '19a — Cash & marketable securities', type: 'currency' },
{ boxNumber: '19b', label: '19b — Other property', type: 'currency' },
{ boxNumber: '20A', label: '20A — Other information: Code A', type: 'currency' },
{ boxNumber: '20B', label: '20B — Other information: Code B', type: 'currency' },
{ boxNumber: '20V', label: '20V — Other information: Code V', type: 'currency' },
{ boxNumber: '20_WILDCARD', label: '20 — Other information: Other codes', type: 'currency' },
{ boxNumber: '21', label: '21 — Foreign taxes paid or accrued', type: 'currency' },
{ boxNumber: '22', label: '22 — At-risk: more than one activity', type: 'checkbox' },
{ boxNumber: '23', label: '23 — Passive: more than one activity', type: 'checkbox' }
]
}
];
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatCheckboxModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatTooltipModule
],
selector: 'gf-k-document-form',
standalone: true,
styles: [
`
:host {
display: block;
}
.form-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 24px;
}
/* Collapsible sections */
.k1-section {
margin-bottom: 8px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 10px;
background: #fff;
transition: box-shadow 0.2s;
}
.k1-section:hover {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
background: transparent;
cursor: pointer;
user-select: none;
font-weight: 500;
font-size: 15px;
letter-spacing: -0.01em;
color: rgba(0, 0, 0, 0.82);
transition: background 0.15s;
}
.section-header:hover {
background: rgba(0, 0, 0, 0.025);
}
.section-header mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
line-height: 20px;
flex-shrink: 0;
color: rgba(0, 0, 0, 0.45);
transition: transform 0.2s ease;
}
.section-header mat-icon.expanded {
transform: rotate(90deg);
}
.section-header .section-title {
flex: 1;
min-width: 0;
}
.section-header .section-desc {
font-weight: 400;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-left: auto;
flex-shrink: 0;
}
.section-body {
padding: 4px 20px 16px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
/* Two-column grid */
.fields-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2px 24px;
}
@media (max-width: 700px) {
.fields-grid {
grid-template-columns: 1fr;
}
}
/* Field rows */
.field-row {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 0;
min-height: 36px;
}
.field-label {
flex: 1 1 auto;
font-size: 13px;
color: rgba(0, 0, 0, 0.7);
line-height: 1.35;
min-width: 0;
}
.field-input {
flex: 0 0 140px;
display: flex;
align-items: center;
}
.field-input input {
width: 100%;
box-sizing: border-box;
padding: 6px 10px;
font-size: 13px;
font-family: 'Roboto Mono', monospace;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 6px;
background: rgba(0, 0, 0, 0.015);
outline: none;
text-align: right;
transition: border-color 0.15s, box-shadow 0.15s;
}
.field-input input:focus {
border-color: #1976d2;
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.12);
background: #fff;
}
.field-input input.text-input {
text-align: left;
font-family: inherit;
}
.field-input .unit-suffix {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
margin-left: 4px;
flex-shrink: 0;
}
.field-input .unit-prefix {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
margin-right: 4px;
flex-shrink: 0;
}
.field-input input.is-zero {
color: rgba(0, 0, 0, 0.28);
}
/* Checkbox row */
.field-row-checkbox {
cursor: pointer;
padding: 4px 0;
}
.field-row-checkbox .cb-label {
font-size: 13px;
color: rgba(0, 0, 0, 0.7);
}
/* Footer */
.form-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
}
`
],
template: `
<div class="form-header">
<mat-form-field style="min-width: 180px">
<mat-label>Filing Status</mat-label>
<mat-select [(ngModel)]="filingStatusValue">
<mat-option value="DRAFT">Draft</mat-option>
<mat-option value="ESTIMATED">Estimated</mat-option>
<mat-option value="FINAL">Final</mat-option>
</mat-select>
</mat-form-field>
</div>
@for (section of sections; track section.title) {
<div class="k1-section">
<div class="section-header" (click)="section.collapsed = !section.collapsed">
<mat-icon [class.expanded]="!section.collapsed">chevron_right</mat-icon>
<span class="section-title">{{ section.title }}</span>
@if (section.description) {
<span class="section-desc">{{ section.description }}</span>
}
</div>
@if (!section.collapsed) {
<div class="section-body">
<div class="fields-grid">
@for (field of section.fields; track field.boxNumber) {
@if (field.type === 'checkbox') {
<div class="field-row field-row-checkbox">
<mat-checkbox
[checked]="isChecked(field.boxNumber)"
(change)="setCheckbox(field.boxNumber, $event.checked)">
<span class="cb-label">{{ field.label }}</span>
</mat-checkbox>
</div>
} @else if (field.type === 'text') {
<div class="field-row">
<span class="field-label">{{ field.label }}</span>
<div class="field-input">
<input class="text-input"
[value]="getTextValue(field.boxNumber)"
(input)="setTextValue(field.boxNumber, $event)"
placeholder="—" />
</div>
</div>
} @else if (field.type === 'percent') {
<div class="field-row">
<span class="field-label">{{ field.label }}</span>
<div class="field-input">
<input type="number" step="any"
[value]="getNumericDisplay(field.boxNumber)"
[class.is-zero]="isZero(field.boxNumber)"
(input)="setNumericValue(field.boxNumber, $event)"
placeholder="0" />
<span class="unit-suffix">%</span>
</div>
</div>
} @else {
<div class="field-row">
<span class="field-label">{{ field.label }}</span>
<div class="field-input">
<span class="unit-prefix">$</span>
<input type="number" step="any"
[value]="getNumericDisplay(field.boxNumber)"
[class.is-zero]="isZero(field.boxNumber)"
(input)="setNumericValue(field.boxNumber, $event)"
placeholder="0" />
</div>
</div>
}
}
</div>
</div>
}
</div>
}
<div class="form-footer">
<button mat-button type="button" (click)="cancelled.emit()">Cancel</button>
<button mat-flat-button color="primary" (click)="onSubmit()">
{{ isEditMode ? 'Update' : 'Create' }}
</button>
</div>
`
})
export class GfKDocumentFormComponent implements OnChanges {
@Input() public data: Record<string, number | string | null> | null = null;
@Input() public filingStatus: string = 'DRAFT';
@Input() public isEditMode: boolean = false;
@Output() public cancelled = new EventEmitter<void>();
@Output() public submitted = new EventEmitter<{
filingStatus: string;
data: Record<string, number | string | null>;
}>();
public filingStatusValue = 'DRAFT';
public sections: K1Section[] = [];
/** Internal data store keyed by boxNumber */
private values: Record<string, number | string | null> = {};
public constructor() {
this.sections = K1_SECTIONS.map((s) => ({
...s,
fields: [...s.fields],
collapsed: s.collapsed ?? false
}));
}
public ngOnChanges(): void {
this.filingStatusValue = this.filingStatus || 'DRAFT';
if (this.data) {
this.values = { ...this.data };
} else {
this.values = {};
}
}
// ── Value accessors ────────────────────────────────────────────────────
public isChecked(boxNumber: string): boolean {
const v = this.values[boxNumber];
return v === 'true' || v === 1 || v === '1';
}
public setCheckbox(boxNumber: string, checked: boolean): void {
this.values[boxNumber] = checked ? 'true' : 'false';
}
public getTextValue(boxNumber: string): string {
const v = this.values[boxNumber];
return v != null ? String(v) : '';
}
public setTextValue(boxNumber: string, event: Event): void {
const input = event.target as HTMLInputElement;
this.values[boxNumber] = input.value || null;
}
public getNumericDisplay(boxNumber: string): string {
const v = this.values[boxNumber];
if (v == null || v === '') {
return '';
}
const n = Number(v);
return isNaN(n) ? '' : String(n);
}
public isZero(boxNumber: string): boolean {
const v = this.values[boxNumber];
return v === 0 || v === '0';
}
public setNumericValue(boxNumber: string, event: Event): void {
const input = event.target as HTMLInputElement;
const raw = input.value;
if (raw === '' || raw == null) {
this.values[boxNumber] = null;
} else {
const n = parseFloat(raw);
this.values[boxNumber] = isNaN(n) ? null : n;
}
}
// ── Submit ─────────────────────────────────────────────────────────────
public onSubmit(): void {
const data: Record<string, number | string | null> = {};
for (const section of this.sections) {
for (const field of section.fields) {
const v = this.values[field.boxNumber];
if (v != null && v !== '') {
data[field.boxNumber] = v;
}
}
}
this.submitted.emit({
data,
filingStatus: this.filingStatusValue
});
}
}