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 new file mode 100644 index 0000000..2067eaf --- /dev/null +++ b/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.html @@ -0,0 +1,112 @@ +
+ + +
+ + +
+
+

Groups / 組別

+ +
+ + + + + + {{ g.isActive ? 'Yes' : 'No' }} + + + + + + + + + +
+ + +
+
+

Subcategories / 子類別 — {{ selectedGroup.name_en }}

+ +
+
Select a group on the left to view its subcategories.
+ + + + + + {{ s.isActive ? 'Yes' : 'No' }} + + + + + + + + +
+ +
+ + + +
+ + + + +
+ + + + +
+ + + +
+ + + + +
+ + + + +
+ +
diff --git a/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.scss b/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.scss new file mode 100644 index 0000000..0bc81e8 --- /dev/null +++ b/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.scss @@ -0,0 +1,30 @@ +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + + h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; + } +} + +.selected-label { + font-weight: 400; + color: #555; +} + +.hint-text { + padding: 1.5rem 0; + color: #888; + font-style: italic; +} 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 new file mode 100644 index 0000000..d309cde --- /dev/null +++ b/APP/src/app/features/expense/pages/expense-categories-page/expense-categories-page.component.ts @@ -0,0 +1,93 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { GridModule } from '@progress/kendo-angular-grid'; +import { ButtonsModule } from '@progress/kendo-angular-buttons'; +import { DialogsModule } from '@progress/kendo-angular-dialog'; +import { InputsModule } from '@progress/kendo-angular-inputs'; +import { ExpenseCategoryApiService } from '../../services/expense-category-api.service'; +import { ExpenseCategoryGroupDto, ExpenseSubCategoryDto } from '../../models/expense.model'; + +@Component({ + selector: 'app-expense-categories-page', + standalone: true, + imports: [CommonModule, FormsModule, GridModule, ButtonsModule, DialogsModule, InputsModule], + templateUrl: './expense-categories-page.component.html', + styleUrls: ['./expense-categories-page.component.scss'], +}) +export class ExpenseCategoriesPageComponent implements OnInit { + groups: ExpenseCategoryGroupDto[] = []; + selectedGroup: ExpenseCategoryGroupDto | null = null; + loading = false; + + groupDialogOpen = false; + editingGroupId: number | null = null; + groupForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true }; + + subDialogOpen = false; + editingSubId: number | null = null; + subForm = { name_en: '', name_zh: '', sortOrder: 0, isActive: true }; + + constructor(private api: ExpenseCategoryApiService) {} + + ngOnInit(): void { this.load(); } + + load(): void { + this.loading = true; + this.api.getAll(true).subscribe({ + next: g => { + this.groups = g; + if (this.selectedGroup) this.selectedGroup = g.find(x => x.id === this.selectedGroup!.id) ?? null; + this.loading = false; + }, + error: () => { this.loading = false; }, + }); + } + + selectGroup(g: ExpenseCategoryGroupDto): void { this.selectedGroup = g; } + get subCategories(): ExpenseSubCategoryDto[] { return this.selectedGroup?.subCategories ?? []; } + + openNewGroup(): void { + this.editingGroupId = null; + this.groupForm = { name_en: '', name_zh: '', sortOrder: this.groups.length + 1, isActive: true }; + 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.groupDialogOpen = true; + } + saveGroup(): void { + const body = { name_en: this.groupForm.name_en, name_zh: this.groupForm.name_zh || null, sortOrder: this.groupForm.sortOrder }; + 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); + } + deactivateGroup(g: ExpenseCategoryGroupDto): void { + if (!confirm(`Deactivate "${g.name_en}"?`)) return; + this.api.deactivateGroup(g.id).subscribe(() => this.load()); + } + + openNewSub(): void { + if (!this.selectedGroup) return; + this.editingSubId = null; + this.subForm = { name_en: '', name_zh: '', sortOrder: this.subCategories.length + 1, isActive: true }; + 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.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 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); + } + deactivateSub(s: ExpenseSubCategoryDto): void { + if (!confirm(`Deactivate "${s.name_en}"?`)) return; + this.api.deactivateSub(s.id).subscribe(() => this.load()); + } +}