diff --git a/APP/src/app/features/expense/models/expense.model.ts b/APP/src/app/features/expense/models/expense.model.ts index 0a9ce2c..7c785d3 100644 --- a/APP/src/app/features/expense/models/expense.model.ts +++ b/APP/src/app/features/expense/models/expense.model.ts @@ -10,9 +10,9 @@ export interface MinistryDto { id: number; name_en: string; name_zh: string | nu 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 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 CreateExpenseGroupRequest { name_en: string; name_zh: string | null; sortOrder: number; } +export interface CreateExpenseGroupRequest { name_en: string; name_zh: string | null; sortOrder: number; form990LineId: number | null; } export interface UpdateExpenseGroupRequest extends CreateExpenseGroupRequest { isActive: boolean; } -export interface CreateExpenseSubCategoryRequest { groupId: number; name_en: string; name_zh: string | null; sortOrder: number; } +export interface CreateExpenseSubCategoryRequest { groupId: number; name_en: string; name_zh: string | null; sortOrder: number; form990LineId: number | null; } export interface UpdateExpenseSubCategoryRequest extends CreateExpenseSubCategoryRequest { isActive: boolean; } export interface ExpenseListItemDto { diff --git a/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.html b/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.html index 7c7ff7b..90be8fa 100644 --- a/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.html +++ b/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.html @@ -61,6 +61,15 @@ Sort order + @@ -89,6 +98,15 @@ Sort order + diff --git a/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.ts b/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.ts index 994fc51..3a395f7 100644 --- a/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.ts +++ b/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.ts @@ -5,14 +5,16 @@ import { GridModule, CellClickEvent, RowClassArgs } from '@progress/kendo-angula 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 { ContextMenuModule, ContextMenuComponent, ContextMenuSelectEvent } from '@progress/kendo-angular-menu'; import { ExpenseCategoryApiService } from '../../services/expense-category-api.service'; import { ExpenseCategoryGroupDto, ExpenseSubCategoryDto } from '../../models/expense.model'; +import { Form990ExpenseLineDto } from '../../../finance-report/models/form990-report.model'; @Component({ selector: 'app-expense-categories-page', standalone: true, - imports: [CommonModule, FormsModule, GridModule, ButtonsModule, DialogsModule, InputsModule, ContextMenuModule], + imports: [CommonModule, FormsModule, GridModule, ButtonsModule, DialogsModule, InputsModule, DropDownsModule, ContextMenuModule], templateUrl: './expense-categories-page.component.html', styleUrls: ['./expense-categories-page.component.scss'], }) @@ -20,6 +22,7 @@ export class ExpenseCategoriesPageComponent implements OnInit { groups: ExpenseCategoryGroupDto[] = []; selectedGroup: ExpenseCategoryGroupDto | null = null; loading = false; + form990Lines: Form990ExpenseLineDto[] = []; @ViewChild('groupMenu') groupMenu!: ContextMenuComponent; @ViewChild('subMenu') subMenu!: ContextMenuComponent; @@ -30,15 +33,18 @@ export class ExpenseCategoriesPageComponent implements OnInit { groupDialogOpen = false; editingGroupId: number | null = null; - groupForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true }; + groupForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true, form990LineId: null as number | null }; subDialogOpen = false; editingSubId: number | null = null; - subForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true }; + subForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true, form990LineId: null as number | null }; constructor(private api: ExpenseCategoryApiService) {} - ngOnInit(): void { this.load(); } + ngOnInit(): void { + this.load(); + this.api.getForm990Lines().subscribe(lines => { this.form990Lines = lines; }); + } load(): void { this.loading = true; @@ -101,16 +107,16 @@ export class ExpenseCategoriesPageComponent implements OnInit { openNewGroup(): void { this.editingGroupId = null; - this.groupForm = { name_en: '', name_zh: '', sortOrder: this.groups.length + 1, isActive: true }; + this.groupForm = { name_en: '', name_zh: '', sortOrder: this.groups.length + 1, isActive: true, form990LineId: null }; this.groupDialogOpen = true; } openEditGroup(g: ExpenseCategoryGroupDto): void { this.editingGroupId = g.id; - this.groupForm = { name_en: g.name_en, name_zh: g.name_zh ?? '', sortOrder: g.sortOrder, isActive: g.isActive }; + this.groupForm = { name_en: g.name_en, name_zh: g.name_zh ?? '', sortOrder: g.sortOrder, isActive: g.isActive, form990LineId: g.form990LineId }; this.groupDialogOpen = true; } saveGroup(): void { - const body = { name_en: this.groupForm.name_en, name_zh: this.groupForm.name_zh || null, sortOrder: this.groupForm.sortOrder }; + const body = { name_en: this.groupForm.name_en, name_zh: this.groupForm.name_zh || null, sortOrder: this.groupForm.sortOrder, form990LineId: this.groupForm.form990LineId }; const done = () => { this.groupDialogOpen = false; this.load(); }; if (this.editingGroupId == null) this.api.createGroup(body).subscribe(done); else this.api.updateGroup(this.editingGroupId, { ...body, isActive: this.groupForm.isActive }).subscribe(done); @@ -123,17 +129,17 @@ export class ExpenseCategoriesPageComponent implements OnInit { openNewSub(): void { if (!this.selectedGroup) return; this.editingSubId = null; - this.subForm = { name_en: '', name_zh: '', sortOrder: this.subCategories.length + 1, isActive: true }; + this.subForm = { name_en: '', name_zh: '', sortOrder: this.subCategories.length + 1, isActive: true, form990LineId: null }; this.subDialogOpen = true; } openEditSub(s: ExpenseSubCategoryDto): void { this.editingSubId = s.id; - this.subForm = { name_en: s.name_en, name_zh: s.name_zh ?? '', sortOrder: s.sortOrder, isActive: s.isActive }; + this.subForm = { name_en: s.name_en, name_zh: s.name_zh ?? '', sortOrder: s.sortOrder, isActive: s.isActive, form990LineId: s.form990LineId }; this.subDialogOpen = true; } saveSub(): void { 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 }; + 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 done = () => { this.subDialogOpen = false; this.load(); }; if (this.editingSubId == null) this.api.createSub(body).subscribe(done); else this.api.updateSub(this.editingSubId, { ...body, isActive: this.subForm.isActive }).subscribe(done); diff --git a/APP/src/app/features/expense/services/expense-category-api.service.ts b/APP/src/app/features/expense/services/expense-category-api.service.ts index d204878..4052f57 100644 --- a/APP/src/app/features/expense/services/expense-category-api.service.ts +++ b/APP/src/app/features/expense/services/expense-category-api.service.ts @@ -7,11 +7,12 @@ import { ExpenseCategoryGroupDto, CreateExpenseGroupRequest, UpdateExpenseGroupRequest, CreateExpenseSubCategoryRequest, UpdateExpenseSubCategoryRequest, } from '../models/expense.model'; +import { Form990ExpenseLineDto } from '../../finance-report/models/form990-report.model'; @Injectable({ providedIn: 'root' }) export class ExpenseCategoryApiService { private readonly endpoint: string; - constructor(private http: HttpClient, apiConfig: ApiConfigService) { + constructor(private http: HttpClient, private apiConfig: ApiConfigService) { this.endpoint = apiConfig.getApiUrl('expense-categories'); } getAll(includeInactive = false): Observable { @@ -29,4 +30,8 @@ export class ExpenseCategoryApiService { createSub(r: CreateExpenseSubCategoryRequest): Observable<{ id: number }> { return this.http.post<{ id: number }>(`${this.endpoint}/subcategories`, r); } updateSub(id: number, r: UpdateExpenseSubCategoryRequest): Observable { return this.http.put(`${this.endpoint}/subcategories/${id}`, r); } deactivateSub(id: number): Observable { return this.http.delete(`${this.endpoint}/subcategories/${id}`); } + getForm990Lines(): Observable { + return this.http.get(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 : ''}` })))); + } } diff --git a/APP/src/app/features/finance-report/models/form990-report.model.ts b/APP/src/app/features/finance-report/models/form990-report.model.ts new file mode 100644 index 0000000..d214d5f --- /dev/null +++ b/APP/src/app/features/finance-report/models/form990-report.model.ts @@ -0,0 +1,8 @@ +export interface Form990ExpenseLineDto { + id: number; + lineCode: string; + name_en: string; + name_zh: string | null; + sortOrder: number; + label?: string; // bilingual "code — name", filled by service +}