feat(1099): add 1099 box dropdowns to category admin page
Mirror the 990-line dropdown in both the group and subcategory edit dialogs: add form1099BoxId to the frontend group/subcategory DTOs and request interfaces, load boxes via a new getForm1099Boxes() method on ExpenseCategoryApiService (same label pattern as getForm990Lines: "boxCode — name_en / name_zh"), wire form1099BoxId into all open/edit/save paths, and render a side-by-side "1099 Box / 1099 框" Kendo DropdownList with [valuePrimitive]="true" and "— none —" default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,11 @@ export interface PagedResult<T> {
|
|||||||
|
|
||||||
export interface MinistryDto { id: number; name_en: string; name_zh: string | null; sortOrder: number; isActive: boolean; label?: string; }
|
export interface MinistryDto { id: number; name_en: string; name_zh: string | null; sortOrder: number; isActive: boolean; label?: string; }
|
||||||
|
|
||||||
export interface ExpenseSubCategoryDto { id: number; groupId: number; name_en: string; name_zh: string | null; sortOrder: number; isActive: boolean; label?: string; form990LineId: number | null; form990LineCode: string | null; }
|
export interface ExpenseSubCategoryDto { id: number; groupId: number; name_en: string; name_zh: string | null; sortOrder: number; isActive: boolean; label?: string; form990LineId: number | null; form990LineCode: string | null; form1099BoxId: number | null; form1099BoxCode: string | null; }
|
||||||
export interface ExpenseCategoryGroupDto { id: number; name_en: string; name_zh: string | null; sortOrder: number; isActive: boolean; subCategories: ExpenseSubCategoryDto[]; label?: string; form990LineId: number | null; form990LineCode: string | null; }
|
export interface ExpenseCategoryGroupDto { id: number; name_en: string; name_zh: string | null; sortOrder: number; isActive: boolean; subCategories: ExpenseSubCategoryDto[]; label?: string; form990LineId: number | null; form990LineCode: string | null; form1099BoxId: number | null; form1099BoxCode: string | null; }
|
||||||
export interface CreateExpenseGroupRequest { name_en: string; name_zh: string | null; sortOrder: number; form990LineId: number | null; }
|
export interface CreateExpenseGroupRequest { name_en: string; name_zh: string | null; sortOrder: number; form990LineId: number | null; form1099BoxId: number | null; }
|
||||||
export interface UpdateExpenseGroupRequest extends CreateExpenseGroupRequest { isActive: boolean; }
|
export interface UpdateExpenseGroupRequest extends CreateExpenseGroupRequest { isActive: boolean; }
|
||||||
export interface CreateExpenseSubCategoryRequest { groupId: number; name_en: string; name_zh: string | null; sortOrder: number; form990LineId: number | null; }
|
export interface CreateExpenseSubCategoryRequest { groupId: number; name_en: string; name_zh: string | null; sortOrder: number; form990LineId: number | null; form1099BoxId: number | null; }
|
||||||
export interface UpdateExpenseSubCategoryRequest extends CreateExpenseSubCategoryRequest { isActive: boolean; }
|
export interface UpdateExpenseSubCategoryRequest extends CreateExpenseSubCategoryRequest { isActive: boolean; }
|
||||||
|
|
||||||
export interface ExpenseLineItemDto {
|
export interface ExpenseLineItemDto {
|
||||||
@@ -28,6 +28,7 @@ export interface ExpenseListItemDto {
|
|||||||
expenseDate: string; hasReceipt: boolean;
|
expenseDate: string; hasReceipt: boolean;
|
||||||
checkNumber: string | null;
|
checkNumber: string | null;
|
||||||
reviewedByName: string | null; reviewedAt: string | null; reviewNotes: string | null;
|
reviewedByName: string | null; reviewedAt: string | null; reviewNotes: string | null;
|
||||||
|
payeeId: number | null;
|
||||||
}
|
}
|
||||||
export interface ExpenseDto extends ExpenseListItemDto {
|
export interface ExpenseDto extends ExpenseListItemDto {
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
@@ -70,6 +71,7 @@ export interface CreateExpenseRequest {
|
|||||||
type: ExpenseType; ministryId: number; lines: ExpenseLineInput[];
|
type: ExpenseType; ministryId: number; lines: ExpenseLineInput[];
|
||||||
description: string; vendorName: string | null; memberId: number | null;
|
description: string; vendorName: string | null; memberId: number | null;
|
||||||
checkNumber: string | null; expenseDate: string; notes: string | null;
|
checkNumber: string | null; expenseDate: string; notes: string | null;
|
||||||
|
payeeId: number | null;
|
||||||
}
|
}
|
||||||
export type UpdateExpenseRequest = CreateExpenseRequest;
|
export type UpdateExpenseRequest = CreateExpenseRequest;
|
||||||
export interface RejectExpenseRequest { reviewNotes: string | null; }
|
export interface RejectExpenseRequest { reviewNotes: string | null; }
|
||||||
|
|||||||
+20
-2
@@ -91,7 +91,7 @@
|
|||||||
Sort order
|
Sort order
|
||||||
<kendo-numerictextbox [(ngModel)]="groupForm.sortOrder" [format]="'n0'" [decimals]="0" [min]="0"></kendo-numerictextbox>
|
<kendo-numerictextbox [(ngModel)]="groupForm.sortOrder" [format]="'n0'" [decimals]="0" [min]="0"></kendo-numerictextbox>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 md:col-span-2">
|
<label class="flex flex-col gap-1">
|
||||||
<span>Form 990 Line / 990 行</span>
|
<span>Form 990 Line / 990 行</span>
|
||||||
<kendo-dropdownlist
|
<kendo-dropdownlist
|
||||||
[data]="form990Lines"
|
[data]="form990Lines"
|
||||||
@@ -100,6 +100,15 @@
|
|||||||
[(ngModel)]="groupForm.form990LineId">
|
[(ngModel)]="groupForm.form990LineId">
|
||||||
</kendo-dropdownlist>
|
</kendo-dropdownlist>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span>1099 Box / 1099 框</span>
|
||||||
|
<kendo-dropdownlist
|
||||||
|
[data]="form1099Boxes"
|
||||||
|
textField="label" valueField="id" [valuePrimitive]="true"
|
||||||
|
[defaultItem]="{ id: null, label: '— none —' }"
|
||||||
|
[(ngModel)]="groupForm.form1099BoxId">
|
||||||
|
</kendo-dropdownlist>
|
||||||
|
</label>
|
||||||
<label *ngIf="editingGroupId != null" class="flex items-center gap-2 md:col-span-2">
|
<label *ngIf="editingGroupId != null" class="flex items-center gap-2 md:col-span-2">
|
||||||
<input type="checkbox" [(ngModel)]="groupForm.isActive" /> Active
|
<input type="checkbox" [(ngModel)]="groupForm.isActive" /> Active
|
||||||
</label>
|
</label>
|
||||||
@@ -158,7 +167,7 @@
|
|||||||
Sort order
|
Sort order
|
||||||
<kendo-numerictextbox [(ngModel)]="subForm.sortOrder" [format]="'n0'" [decimals]="0" [min]="0"></kendo-numerictextbox>
|
<kendo-numerictextbox [(ngModel)]="subForm.sortOrder" [format]="'n0'" [decimals]="0" [min]="0"></kendo-numerictextbox>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 md:col-span-2">
|
<label class="flex flex-col gap-1">
|
||||||
<span>Form 990 Line / 990 行</span>
|
<span>Form 990 Line / 990 行</span>
|
||||||
<kendo-dropdownlist
|
<kendo-dropdownlist
|
||||||
[data]="form990Lines"
|
[data]="form990Lines"
|
||||||
@@ -167,6 +176,15 @@
|
|||||||
[(ngModel)]="subForm.form990LineId">
|
[(ngModel)]="subForm.form990LineId">
|
||||||
</kendo-dropdownlist>
|
</kendo-dropdownlist>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span>1099 Box / 1099 框</span>
|
||||||
|
<kendo-dropdownlist
|
||||||
|
[data]="form1099Boxes"
|
||||||
|
textField="label" valueField="id" [valuePrimitive]="true"
|
||||||
|
[defaultItem]="{ id: null, label: '— none —' }"
|
||||||
|
[(ngModel)]="subForm.form1099BoxId">
|
||||||
|
</kendo-dropdownlist>
|
||||||
|
</label>
|
||||||
<label *ngIf="editingSubId != null" class="flex items-center gap-2 md:col-span-2">
|
<label *ngIf="editingSubId != null" class="flex items-center gap-2 md:col-span-2">
|
||||||
<input type="checkbox" [(ngModel)]="subForm.isActive" /> Active
|
<input type="checkbox" [(ngModel)]="subForm.isActive" /> Active
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
+11
-8
@@ -10,6 +10,7 @@ import { ContextMenuModule, ContextMenuComponent, ContextMenuSelectEvent } from
|
|||||||
import { ExpenseCategoryApiService } from '../../services/expense-category-api.service';
|
import { ExpenseCategoryApiService } from '../../services/expense-category-api.service';
|
||||||
import { ExpenseCategoryGroupDto, ExpenseSubCategoryDto, CategoryAiSuggestion } from '../../models/expense.model';
|
import { ExpenseCategoryGroupDto, ExpenseSubCategoryDto, CategoryAiSuggestion } from '../../models/expense.model';
|
||||||
import { Form990ExpenseLineDto } from '../../../finance-report/models/form990-report.model';
|
import { Form990ExpenseLineDto } from '../../../finance-report/models/form990-report.model';
|
||||||
|
import { Form1099Box } from '../../../payee1099/models/payee1099.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-expense-categories-page',
|
selector: 'app-expense-categories-page',
|
||||||
@@ -23,6 +24,7 @@ export class ExpenseCategoriesPageComponent implements OnInit {
|
|||||||
selectedGroup: ExpenseCategoryGroupDto | null = null;
|
selectedGroup: ExpenseCategoryGroupDto | null = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
form990Lines: Form990ExpenseLineDto[] = [];
|
form990Lines: Form990ExpenseLineDto[] = [];
|
||||||
|
form1099Boxes: (Form1099Box & { label: string })[] = [];
|
||||||
|
|
||||||
@ViewChild('groupMenu') groupMenu!: ContextMenuComponent;
|
@ViewChild('groupMenu') groupMenu!: ContextMenuComponent;
|
||||||
@ViewChild('subMenu') subMenu!: ContextMenuComponent;
|
@ViewChild('subMenu') subMenu!: ContextMenuComponent;
|
||||||
@@ -33,13 +35,13 @@ export class ExpenseCategoriesPageComponent implements OnInit {
|
|||||||
|
|
||||||
groupDialogOpen = false;
|
groupDialogOpen = false;
|
||||||
editingGroupId: number | null = null;
|
editingGroupId: number | null = null;
|
||||||
groupForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true, form990LineId: null as number | null };
|
groupForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true, form990LineId: null as number | null, form1099BoxId: null as number | null };
|
||||||
groupAiLoading = false;
|
groupAiLoading = false;
|
||||||
groupAiSuggestion: CategoryAiSuggestion | null = null;
|
groupAiSuggestion: CategoryAiSuggestion | null = null;
|
||||||
|
|
||||||
subDialogOpen = false;
|
subDialogOpen = false;
|
||||||
editingSubId: number | null = null;
|
editingSubId: number | null = null;
|
||||||
subForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true, form990LineId: null as number | null };
|
subForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true, form990LineId: null as number | null, form1099BoxId: null as number | null };
|
||||||
subAiLoading = false;
|
subAiLoading = false;
|
||||||
subAiSuggestion: CategoryAiSuggestion | null = null;
|
subAiSuggestion: CategoryAiSuggestion | null = null;
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ export class ExpenseCategoriesPageComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.load();
|
this.load();
|
||||||
this.api.getForm990Lines().subscribe(lines => { this.form990Lines = lines; });
|
this.api.getForm990Lines().subscribe(lines => { this.form990Lines = lines; });
|
||||||
|
this.api.getForm1099Boxes().subscribe(boxes => { this.form1099Boxes = boxes; });
|
||||||
}
|
}
|
||||||
|
|
||||||
load(): void {
|
load(): void {
|
||||||
@@ -111,13 +114,13 @@ export class ExpenseCategoriesPageComponent implements OnInit {
|
|||||||
|
|
||||||
openNewGroup(): void {
|
openNewGroup(): void {
|
||||||
this.editingGroupId = null;
|
this.editingGroupId = null;
|
||||||
this.groupForm = { name_en: '', name_zh: '', sortOrder: this.groups.length + 1, isActive: true, form990LineId: null };
|
this.groupForm = { name_en: '', name_zh: '', sortOrder: this.groups.length + 1, isActive: true, form990LineId: null, form1099BoxId: null };
|
||||||
this.resetGroupAi();
|
this.resetGroupAi();
|
||||||
this.groupDialogOpen = true;
|
this.groupDialogOpen = true;
|
||||||
}
|
}
|
||||||
openEditGroup(g: ExpenseCategoryGroupDto): void {
|
openEditGroup(g: ExpenseCategoryGroupDto): void {
|
||||||
this.editingGroupId = g.id;
|
this.editingGroupId = g.id;
|
||||||
this.groupForm = { name_en: g.name_en, name_zh: g.name_zh ?? '', sortOrder: g.sortOrder, isActive: g.isActive, form990LineId: g.form990LineId };
|
this.groupForm = { name_en: g.name_en, name_zh: g.name_zh ?? '', sortOrder: g.sortOrder, isActive: g.isActive, form990LineId: g.form990LineId, form1099BoxId: g.form1099BoxId };
|
||||||
this.resetGroupAi();
|
this.resetGroupAi();
|
||||||
this.groupDialogOpen = true;
|
this.groupDialogOpen = true;
|
||||||
}
|
}
|
||||||
@@ -143,7 +146,7 @@ export class ExpenseCategoriesPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
dismissGroupAiSuggestion(): void { this.groupAiSuggestion = null; }
|
dismissGroupAiSuggestion(): void { this.groupAiSuggestion = null; }
|
||||||
saveGroup(): void {
|
saveGroup(): void {
|
||||||
const body = { name_en: this.groupForm.name_en, name_zh: this.groupForm.name_zh || null, sortOrder: this.groupForm.sortOrder, form990LineId: this.groupForm.form990LineId };
|
const body = { name_en: this.groupForm.name_en, name_zh: this.groupForm.name_zh || null, sortOrder: this.groupForm.sortOrder, form990LineId: this.groupForm.form990LineId, form1099BoxId: this.groupForm.form1099BoxId };
|
||||||
const done = () => { this.groupDialogOpen = false; this.load(); };
|
const done = () => { this.groupDialogOpen = false; this.load(); };
|
||||||
if (this.editingGroupId == null) this.api.createGroup(body).subscribe(done);
|
if (this.editingGroupId == null) this.api.createGroup(body).subscribe(done);
|
||||||
else this.api.updateGroup(this.editingGroupId, { ...body, isActive: this.groupForm.isActive }).subscribe(done);
|
else this.api.updateGroup(this.editingGroupId, { ...body, isActive: this.groupForm.isActive }).subscribe(done);
|
||||||
@@ -156,13 +159,13 @@ export class ExpenseCategoriesPageComponent implements OnInit {
|
|||||||
openNewSub(): void {
|
openNewSub(): void {
|
||||||
if (!this.selectedGroup) return;
|
if (!this.selectedGroup) return;
|
||||||
this.editingSubId = null;
|
this.editingSubId = null;
|
||||||
this.subForm = { name_en: '', name_zh: '', sortOrder: this.subCategories.length + 1, isActive: true, form990LineId: null };
|
this.subForm = { name_en: '', name_zh: '', sortOrder: this.subCategories.length + 1, isActive: true, form990LineId: null, form1099BoxId: null };
|
||||||
this.resetSubAi();
|
this.resetSubAi();
|
||||||
this.subDialogOpen = true;
|
this.subDialogOpen = true;
|
||||||
}
|
}
|
||||||
openEditSub(s: ExpenseSubCategoryDto): void {
|
openEditSub(s: ExpenseSubCategoryDto): void {
|
||||||
this.editingSubId = s.id;
|
this.editingSubId = s.id;
|
||||||
this.subForm = { name_en: s.name_en, name_zh: s.name_zh ?? '', sortOrder: s.sortOrder, isActive: s.isActive, form990LineId: s.form990LineId };
|
this.subForm = { name_en: s.name_en, name_zh: s.name_zh ?? '', sortOrder: s.sortOrder, isActive: s.isActive, form990LineId: s.form990LineId, form1099BoxId: s.form1099BoxId };
|
||||||
this.resetSubAi();
|
this.resetSubAi();
|
||||||
this.subDialogOpen = true;
|
this.subDialogOpen = true;
|
||||||
}
|
}
|
||||||
@@ -195,7 +198,7 @@ export class ExpenseCategoriesPageComponent implements OnInit {
|
|||||||
dismissSubAiSuggestion(): void { this.subAiSuggestion = null; }
|
dismissSubAiSuggestion(): void { this.subAiSuggestion = null; }
|
||||||
saveSub(): void {
|
saveSub(): void {
|
||||||
if (!this.selectedGroup) return;
|
if (!this.selectedGroup) return;
|
||||||
const body = { groupId: this.selectedGroup.id, name_en: this.subForm.name_en, name_zh: this.subForm.name_zh || null, sortOrder: this.subForm.sortOrder, form990LineId: this.subForm.form990LineId };
|
const body = { groupId: this.selectedGroup.id, name_en: this.subForm.name_en, name_zh: this.subForm.name_zh || null, sortOrder: this.subForm.sortOrder, form990LineId: this.subForm.form990LineId, form1099BoxId: this.subForm.form1099BoxId };
|
||||||
const done = () => { this.subDialogOpen = false; this.load(); };
|
const done = () => { this.subDialogOpen = false; this.load(); };
|
||||||
if (this.editingSubId == null) this.api.createSub(body).subscribe(done);
|
if (this.editingSubId == null) this.api.createSub(body).subscribe(done);
|
||||||
else this.api.updateSub(this.editingSubId, { ...body, isActive: this.subForm.isActive }).subscribe(done);
|
else this.api.updateSub(this.editingSubId, { ...body, isActive: this.subForm.isActive }).subscribe(done);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ExpenseCategoryAiRequest, CategoryAiSuggestion,
|
ExpenseCategoryAiRequest, CategoryAiSuggestion,
|
||||||
} from '../models/expense.model';
|
} from '../models/expense.model';
|
||||||
import { Form990ExpenseLineDto } from '../../finance-report/models/form990-report.model';
|
import { Form990ExpenseLineDto } from '../../finance-report/models/form990-report.model';
|
||||||
|
import { Form1099Box } from '../../payee1099/models/payee1099.model';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ExpenseCategoryApiService {
|
export class ExpenseCategoryApiService {
|
||||||
@@ -38,4 +39,9 @@ export class ExpenseCategoryApiService {
|
|||||||
return this.http.get<Form990ExpenseLineDto[]>(this.apiConfig.getApiUrl('form990-report') + '/lines')
|
return this.http.get<Form990ExpenseLineDto[]>(this.apiConfig.getApiUrl('form990-report') + '/lines')
|
||||||
.pipe(map(rows => rows.map(r => ({ ...r, label: `${r.lineCode} — ${r.name_en}${r.name_zh ? ' / ' + r.name_zh : ''}` }))));
|
.pipe(map(rows => rows.map(r => ({ ...r, label: `${r.lineCode} — ${r.name_en}${r.name_zh ? ' / ' + r.name_zh : ''}` }))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getForm1099Boxes(): Observable<(Form1099Box & { label: string })[]> {
|
||||||
|
return this.http.get<Form1099Box[]>(this.apiConfig.getApiUrl('form1099-report') + '/boxes')
|
||||||
|
.pipe(map(rows => rows.map(b => ({ ...b, label: `${b.boxCode} — ${b.name_en}${b.name_zh ? ' / ' + b.name_zh : ''}` }))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user