diff --git a/APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.html b/APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.html new file mode 100644 index 0000000..afb07ff --- /dev/null +++ b/APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.html @@ -0,0 +1,102 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
diff --git a/APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.ts b/APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.ts new file mode 100644 index 0000000..afef898 --- /dev/null +++ b/APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.ts @@ -0,0 +1,110 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { InputsModule } from '@progress/kendo-angular-inputs'; +import { ButtonsModule } from '@progress/kendo-angular-buttons'; +import { DialogsModule } from '@progress/kendo-angular-dialog'; +import { DropDownsModule } from '@progress/kendo-angular-dropdowns'; +import { DateInputsModule } from '@progress/kendo-angular-dateinputs'; +import { MinistryApiService } from '../../services/ministry-api.service'; +import { ExpenseCategoryApiService } from '../../services/expense-category-api.service'; +import { MemberApiService } from '../../../members/services/member-api.service'; +import { MemberListItemDto, memberDisplayName } from '../../../members/models/member.model'; +import { + MinistryDto, ExpenseCategoryGroupDto, ExpenseSubCategoryDto, ExpenseType, CreateExpenseRequest, +} from '../../models/expense.model'; + +export interface ExpenseFormResult { request: CreateExpenseRequest; receipt: File | null; } + +/** Flattened member item with a single displayName field for the dropdown. */ +interface MemberOption { id: number; displayName: string; } + +@Component({ + selector: 'app-expense-form-dialog', + standalone: true, + imports: [CommonModule, FormsModule, InputsModule, ButtonsModule, DialogsModule, DropDownsModule, DateInputsModule], + templateUrl: './expense-form-dialog.component.html', +}) +export class ExpenseFormDialogComponent implements OnInit { + @Input() mode: 'vendor' | 'reimbursement' = 'reimbursement'; + @Input() allowMemberPick = false; + @Input() title = 'New Expense'; + @Output() save = new EventEmitter(); + @Output() cancel = new EventEmitter(); + + ministries: MinistryDto[] = []; + groups: ExpenseCategoryGroupDto[] = []; + subs: ExpenseSubCategoryDto[] = []; + + memberResults: MemberOption[] = []; + + form = { + ministryId: null as number | null, + categoryGroupId: null as number | null, + subCategoryId: null as number | null, + amount: 0, + description: '', + vendorName: '', + checkNumber: '', + memberId: null as number | null, + expenseDate: new Date(), + }; + receipt: File | null = null; + + constructor( + private ministryApi: MinistryApiService, + private catApi: ExpenseCategoryApiService, + private memberApi: MemberApiService, + ) {} + + ngOnInit(): void { + this.ministryApi.getAll().subscribe(m => (this.ministries = m)); + this.catApi.getAll(false).subscribe(g => (this.groups = g)); + } + + onGroupChange(groupId: number | null): void { + this.form.subCategoryId = null; + this.subs = this.groups.find(g => g.id === groupId)?.subCategories ?? []; + } + + onMemberFilter(term: string): void { + if (!term || term.length < 1) { this.memberResults = []; return; } + this.memberApi.getPaged({ search: term, pageSize: 10 }) + .subscribe(r => { + this.memberResults = r.items.map((m: MemberListItemDto) => ({ + id: m.id, + displayName: memberDisplayName(m), + })); + }); + } + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + this.receipt = input.files?.[0] ?? null; + } + + get isValid(): boolean { + return !!this.form.ministryId && !!this.form.categoryGroupId && !!this.form.subCategoryId + && this.form.amount > 0 && this.form.description.trim().length > 0; + } + + emitSave(): void { + if (!this.isValid) return; + const d = this.form.expenseDate; + const expenseDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + const request: CreateExpenseRequest = { + type: (this.mode === 'vendor' ? 'VendorPayment' : 'StaffReimbursement') as ExpenseType, + ministryId: this.form.ministryId!, + categoryGroupId: this.form.categoryGroupId!, + subCategoryId: this.form.subCategoryId!, + amount: this.form.amount, + description: this.form.description.trim(), + vendorName: this.mode === 'vendor' ? (this.form.vendorName || null) : null, + memberId: this.allowMemberPick ? this.form.memberId : null, + checkNumber: this.mode === 'vendor' ? (this.form.checkNumber || null) : null, + expenseDate, + notes: null, + }; + this.save.emit({ request, receipt: this.receipt }); + } +}