diff --git a/APP/src/app/features/expense/models/expense.model.ts b/APP/src/app/features/expense/models/expense.model.ts new file mode 100644 index 0000000..c701d89 --- /dev/null +++ b/APP/src/app/features/expense/models/expense.model.ts @@ -0,0 +1,46 @@ +export type ExpenseType = 'VendorPayment' | 'StaffReimbursement'; +export type ExpenseStatus = 'Draft' | 'PendingApproval' | 'Approved' | 'Paid' | 'Rejected'; + +export interface PagedResult { + items: T[]; totalCount: number; page: number; pageSize: number; totalPages: number; +} + +export interface MinistryDto { id: number; name_en: string; name_zh: string | null; sortOrder: number; isActive: boolean; } + +export interface ExpenseSubCategoryDto { id: number; groupId: number; name_en: string; name_zh: string | null; sortOrder: number; isActive: boolean; } +export interface ExpenseCategoryGroupDto { id: number; name_en: string; name_zh: string | null; sortOrder: number; isActive: boolean; subCategories: ExpenseSubCategoryDto[]; } +export interface CreateExpenseGroupRequest { name_en: string; name_zh: string | null; sortOrder: number; } +export interface UpdateExpenseGroupRequest extends CreateExpenseGroupRequest { isActive: boolean; } +export interface CreateExpenseSubCategoryRequest { groupId: number; name_en: string; name_zh: string | null; sortOrder: number; } +export interface UpdateExpenseSubCategoryRequest extends CreateExpenseSubCategoryRequest { isActive: boolean; } + +export interface ExpenseListItemDto { + id: number; type: ExpenseType; status: ExpenseStatus; amount: number; description: string; + ministryId: number; ministryName: string; categoryGroupId: number; categoryGroupName: string; + subCategoryId: number; subCategoryName: string; vendorName: string | null; + memberId: number | null; memberName: string | null; expenseDate: string; hasReceipt: boolean; +} +export interface ExpenseDto extends ExpenseListItemDto { + checkNumber: string | null; notes: string | null; reviewNotes: string | null; + submittedBy: string | null; submittedAt: string | null; reviewedAt: string | null; paidAt: string | null; +} +export interface CreateExpenseRequest { + type: ExpenseType; ministryId: number; categoryGroupId: number; subCategoryId: number; + amount: number; description: string; vendorName: string | null; memberId: number | null; + checkNumber: string | null; expenseDate: string; notes: string | null; +} +export type UpdateExpenseRequest = CreateExpenseRequest; +export interface RejectExpenseRequest { reviewNotes: string | null; } +export interface PayExpenseRequest { checkNumber: string | null; paidAt: string | null; } + +export interface MonthlyStatementDto { + id: number; year: number; month: number; openingBalance: number; totalGiving: number; + totalOtherIncome: number; totalExpenses: number; calculatedClosingBalance: number; + bankStatementBalance: number; difference: number; notes: string | null; isFinalized: boolean; +} +export interface CreateMonthlyStatementRequest { + year: number; month: number; openingBalance: number; totalOtherIncome: number; bankStatementBalance: number; notes: string | null; +} +export interface UpdateMonthlyStatementRequest { + openingBalance: number; totalOtherIncome: number; bankStatementBalance: number; notes: string | null; +} diff --git a/APP/src/app/features/expense/services/expense-api.service.ts b/APP/src/app/features/expense/services/expense-api.service.ts new file mode 100644 index 0000000..de4c40a --- /dev/null +++ b/APP/src/app/features/expense/services/expense-api.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ApiConfigService } from '../../../core/services/api-config.service'; +import { + PagedResult, ExpenseListItemDto, ExpenseDto, CreateExpenseRequest, UpdateExpenseRequest, + RejectExpenseRequest, PayExpenseRequest, +} from '../models/expense.model'; + +export interface ExpenseQuery { + page?: number; pageSize?: number; search?: string; ministryId?: number; + categoryGroupId?: number; status?: string; from?: string; to?: string; +} + +@Injectable({ providedIn: 'root' }) +export class ExpenseApiService { + private readonly endpoint: string; + constructor(private http: HttpClient, apiConfig: ApiConfigService) { + this.endpoint = apiConfig.getApiUrl('expenses'); + } + private toParams(q: Record): HttpParams { + let p = new HttpParams(); + for (const [k, v] of Object.entries(q)) if (v !== undefined && v !== null && v !== '') p = p.set(k, String(v)); + return p; + } + getPaged(q: ExpenseQuery): Observable> { + return this.http.get>(this.endpoint, { params: this.toParams(q as Record) }); + } + getMine(status?: string, page = 1, pageSize = 50): Observable> { + return this.http.get>(`${this.endpoint}/mine`, { params: this.toParams({ status, page, pageSize }) }); + } + getById(id: number): Observable { return this.http.get(`${this.endpoint}/${id}`); } + create(r: CreateExpenseRequest): Observable<{ id: number }> { return this.http.post<{ id: number }>(this.endpoint, r); } + update(id: number, r: UpdateExpenseRequest): Observable { return this.http.put(`${this.endpoint}/${id}`, r); } + delete(id: number): Observable { return this.http.delete(`${this.endpoint}/${id}`); } + submit(id: number): Observable { return this.http.post(`${this.endpoint}/${id}/submit`, {}); } + approve(id: number): Observable { return this.http.post(`${this.endpoint}/${id}/approve`, {}); } + reject(id: number, r: RejectExpenseRequest): Observable { return this.http.post(`${this.endpoint}/${id}/reject`, r); } + pay(id: number, r: PayExpenseRequest): Observable { return this.http.post(`${this.endpoint}/${id}/pay`, r); } + uploadReceipt(id: number, file: File): Observable { + const form = new FormData(); form.append('file', file); + return this.http.post(`${this.endpoint}/${id}/receipt`, form); + } + receiptUrl(id: number): string { return `${this.endpoint}/${id}/receipt`; } +} 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 new file mode 100644 index 0000000..6b21bf5 --- /dev/null +++ b/APP/src/app/features/expense/services/expense-category-api.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ApiConfigService } from '../../../core/services/api-config.service'; +import { + ExpenseCategoryGroupDto, CreateExpenseGroupRequest, UpdateExpenseGroupRequest, + CreateExpenseSubCategoryRequest, UpdateExpenseSubCategoryRequest, +} from '../models/expense.model'; + +@Injectable({ providedIn: 'root' }) +export class ExpenseCategoryApiService { + private readonly endpoint: string; + constructor(private http: HttpClient, apiConfig: ApiConfigService) { + this.endpoint = apiConfig.getApiUrl('expense-categories'); + } + getAll(includeInactive = false): Observable { + return this.http.get(this.endpoint, { params: new HttpParams().set('includeInactive', includeInactive) }); + } + createGroup(r: CreateExpenseGroupRequest): Observable<{ id: number }> { return this.http.post<{ id: number }>(`${this.endpoint}/groups`, r); } + updateGroup(id: number, r: UpdateExpenseGroupRequest): Observable { return this.http.put(`${this.endpoint}/groups/${id}`, r); } + deactivateGroup(id: number): Observable { return this.http.delete(`${this.endpoint}/groups/${id}`); } + 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}`); } +} diff --git a/APP/src/app/features/expense/services/ministry-api.service.ts b/APP/src/app/features/expense/services/ministry-api.service.ts new file mode 100644 index 0000000..d67794a --- /dev/null +++ b/APP/src/app/features/expense/services/ministry-api.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ApiConfigService } from '../../../core/services/api-config.service'; +import { MinistryDto } from '../models/expense.model'; + +@Injectable({ providedIn: 'root' }) +export class MinistryApiService { + private readonly endpoint: string; + constructor(private http: HttpClient, apiConfig: ApiConfigService) { + this.endpoint = apiConfig.getApiUrl('ministries'); + } + getAll(includeInactive = false): Observable { + return this.http.get(this.endpoint, { params: new HttpParams().set('includeInactive', includeInactive) }); + } +} diff --git a/APP/src/app/features/expense/services/monthly-statement-api.service.ts b/APP/src/app/features/expense/services/monthly-statement-api.service.ts new file mode 100644 index 0000000..009c54a --- /dev/null +++ b/APP/src/app/features/expense/services/monthly-statement-api.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ApiConfigService } from '../../../core/services/api-config.service'; +import { MonthlyStatementDto, CreateMonthlyStatementRequest, UpdateMonthlyStatementRequest } from '../models/expense.model'; + +@Injectable({ providedIn: 'root' }) +export class MonthlyStatementApiService { + private readonly endpoint: string; + constructor(private http: HttpClient, apiConfig: ApiConfigService) { + this.endpoint = apiConfig.getApiUrl('monthly-statements'); + } + getAll(year?: number): Observable { + let p = new HttpParams(); if (year) p = p.set('year', year); + return this.http.get(this.endpoint, { params: p }); + } + getById(id: number): Observable { return this.http.get(`${this.endpoint}/${id}`); } + create(r: CreateMonthlyStatementRequest): Observable<{ id: number }> { return this.http.post<{ id: number }>(this.endpoint, r); } + update(id: number, r: UpdateMonthlyStatementRequest): Observable { return this.http.put(`${this.endpoint}/${id}`, r); } + finalize(id: number): Observable { return this.http.post(`${this.endpoint}/${id}/finalize`, {}); } +}