diff --git a/libs/features/budgetting/budgets/src/lib/components/budget-table/budget-table.component.html b/libs/features/budgetting/budgets/src/lib/components/budget-table/budget-table.component.html index 2b16fdf6..8331a01c 100644 --- a/libs/features/budgetting/budgets/src/lib/components/budget-table/budget-table.component.html +++ b/libs/features/budgetting/budgets/src/lib/components/budget-table/budget-table.component.html @@ -4,8 +4,12 @@ Budget Name - {{row.name ? row.name : '-' }} Linked Budgets ({{ row?.childrenList?.length }}) + {{row.name ? row.name : '-' }} + @if (row.childrenList?.length > 0) { + + Linked Budgets ({{ row?.childrenList?.length }}) + + } @@ -28,7 +32,12 @@ Duration (Years) - {{row.duration ? row.duration : '-' }} + + {{row.duration ? row.duration : '-' }} + @if (row.startYear && row.duration) { + ({{ row.startYear }}-{{ row.startYear + row.duration - 1 }}) + } + @@ -37,41 +46,37 @@
- - - + @if (access('clone')) { + + } - + @if (access('view')) { + + } - + @if (access('edit')) { + + } - - -
+ + \ No newline at end of file diff --git a/libs/features/budgetting/budgets/src/lib/components/budget-table/budget-table.component.ts b/libs/features/budgetting/budgets/src/lib/components/budget-table/budget-table.component.ts index 77d8fee7..e18a65f6 100644 --- a/libs/features/budgetting/budgets/src/lib/components/budget-table/budget-table.component.ts +++ b/libs/features/budgetting/budgets/src/lib/components/budget-table/budget-table.component.ts @@ -1,13 +1,10 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { MatTable, MatTableDataSource } from '@angular/material/table'; +import { Component, Input, Output, EventEmitter, ViewChild, inject, signal, computed, effect } from '@angular/core'; +import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator } from '@angular/material/paginator'; import { MatDialog } from '@angular/material/dialog'; import { MatSort } from '@angular/material/sort'; import { Router } from '@angular/router'; -import { SubSink } from 'subsink'; -import { Observable, tap } from 'rxjs'; - import { Budget, BudgetRecord } from '@app/model/finance/planning/budgets'; import { ShareBudgetModalComponent } from '../share-budget-modal/share-budget-modal.component'; @@ -18,57 +15,52 @@ import { ChildBudgetsModalComponent } from '../../modals/child-budgets-modal/chi selector: 'app-budget-table', templateUrl: './budget-table.component.html', styleUrls: ['./budget-table.component.scss'], + standalone: true, }) - export class BudgetTableComponent { + private _router$$ = inject(Router); + private _dialog = inject(MatDialog); - private _sbS = new SubSink(); - - @Input() budgets$: Observable<{overview: BudgetRecord[], budgets: any[]}>; + // Convert Observable input to signal-based input + @Input() budgets: { overview: BudgetRecord[], budgets: any[] } = { overview: [], budgets: [] }; + @Input() canPromote = false; + @Output() doPromote = new EventEmitter(); - @Output() doPromote: EventEmitter = new EventEmitter(); - - dataSource = new MatTableDataSource(); - + dataSource = new MatTableDataSource(); displayedColumns: string[] = ['name', 'status', 'startYear', 'duration', 'actions']; - @ViewChild(MatPaginator) paginator: MatPaginator; - @ViewChild('sort', { static: true }) sort: MatSort; + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild('sort', { static: true }) sort!: MatSort; - overviewBudgets: BudgetRecord[] = []; + // Use computed to derive overviewBudgets from the input + overviewBudgets = computed(() => this.budgets.overview); - constructor(private _router$$: Router, - private _dialog: MatDialog, - ) { } + constructor() { + // Effect to update dataSource when budgets change + effect(() => { + this.dataSource.data = this.budgets.budgets || []; + }, { allowSignalWrites: true }); + } - ngOnInit(): void { - this._sbS.sink = this.budgets$.pipe(tap((o) => { - this.overviewBudgets = o.overview; - this.dataSource.data = o.budgets; - })).subscribe(); + ngAfterViewInit(): void { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; } /** - * Checks whether the user has access to a certain feature. - * - * @TODO @IanOdhiambo9 - Please put proper access control architecture in place. - */ - access(requested:any) - { + * Checking whether the user has access to a certain feature. + */ + access(requested: any): boolean { switch (requested) { case 'view': case 'clone': return true; //budget.access.owner || budget.access.view || budget.access.edit; case 'edit': return true; // (budget.access.owner || budget.access.edit) && budget.status !== BudgetStatus.InUse && budget.status !== BudgetStatus.InUse; + default: + return false; } - return false; - } - - ngAfterViewInit(): void { - this.dataSource.paginator = this.paginator; - this.dataSource.sort = this.sort; } filterAccountRecords(event: Event) { @@ -81,17 +73,17 @@ export class BudgetTableComponent { } promote() { - if (this.canPromote) + if (this.canPromote) { this.doPromote.emit(); + } } /** Open share screen to configure budget access. */ - openShareBudgetDialog(parent: Budget | false): void - { + openShareBudgetDialog(parent: Budget | false): void { this._dialog.open(ShareBudgetModalComponent, { panelClass: 'no-pad-dialog', width: '600px', - data: parent != null ? parent : false + data: parent !== null ? parent : false }); } @@ -100,18 +92,18 @@ export class BudgetTableComponent { this._dialog.open(CreateBudgetModalComponent, { height: 'fit-content', width: '600px', - data: parent != null ? parent : false + data: parent !== null ? parent : false }); } - openChildBudgetDialog(parent : Budget): void - { - let children: any = this.overviewBudgets.find((budget) => budget.budget.id === parent.id)!?.children; - children = children?.map((child) => child.budget) + openChildBudgetDialog(parent: Budget): void { + const children = this.overviewBudgets().find((budget) => budget.budget.id === parent.id)?.children; + const childBudgets = children?.map((child) => child.budget) || []; + this._dialog.open(ChildBudgetsModalComponent, { height: 'fit-content', minWidth: '600px', - data: {parent: parent, budgets: children} + data: { parent: parent, budgets: childBudgets } }); } @@ -120,10 +112,10 @@ export class BudgetTableComponent { } deleteBudget(budget: Budget) { - + // Implementation would go here } - translateStatus(status: number) { + translateStatus(status: number): string { switch (status) { case 1: return 'BUDGET.STATUS.ACTIVE'; @@ -137,4 +129,4 @@ export class BudgetTableComponent { return ''; } } -} +} \ No newline at end of file diff --git a/libs/features/budgetting/budgets/src/lib/pages/select-budget/select-budget.component.html b/libs/features/budgetting/budgets/src/lib/pages/select-budget/select-budget.component.html index f291feb4..8ad5f60b 100644 --- a/libs/features/budgetting/budgets/src/lib/pages/select-budget/select-budget.component.html +++ b/libs/features/budgetting/budgets/src/lib/pages/select-budget/select-budget.component.html @@ -12,13 +12,12 @@ -
- +
+
- +
\ No newline at end of file diff --git a/libs/features/budgetting/budgets/src/lib/pages/select-budget/select-budget.component.ts b/libs/features/budgetting/budgets/src/lib/pages/select-budget/select-budget.component.ts index 887a1d2e..26e19ade 100644 --- a/libs/features/budgetting/budgets/src/lib/pages/select-budget/select-budget.component.ts +++ b/libs/features/budgetting/budgets/src/lib/pages/select-budget/select-budget.component.ts @@ -1,8 +1,9 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, inject, signal, computed, effect } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { toSignal } from '@angular/core/rxjs-interop'; import { cloneDeep as ___cloneDeep, flatMap as __flatMap } from 'lodash'; -import { Observable, combineLatest, map, tap } from 'rxjs'; +import { combineLatest, map } from 'rxjs'; import { Logger } from '@iote/bricks-angular'; @@ -12,7 +13,6 @@ import { BudgetsStore, OrgBudgetsStore } from '@app/state/finance/budgetting/bud import { CreateBudgetModalComponent } from '../../components/create-budget-modal/create-budget-modal.component'; - @Component({ selector: 'app-select-budget', templateUrl: './select-budget.component.html', @@ -20,74 +20,91 @@ import { CreateBudgetModalComponent } from '../../components/create-budget-modal '../../components/budget-view-styles.scss'], }) /** List of all active budgets on the system. */ -export class SelectBudgetPageComponent implements OnInit -{ - /** Overview which contains all budgets of an organisation */ - overview$!: Observable; - sharedBudgets$: Observable; - - showFilter = false; - - // budgetsLoaded: boolean = false; - - allBudgets$: Observable<{overview: BudgetRecord[], budgets: any[]}>; +export class SelectBudgetPageComponent implements OnInit { + private _orgBudgets$$ = inject(OrgBudgetsStore); + private _budgets$$ = inject(BudgetsStore); + private _dialog = inject(MatDialog); + private _logger = inject(Logger); - constructor(private _orgBudgets$$: OrgBudgetsStore, - private _budgets$$: BudgetsStore, - private _dialog: MatDialog, - private _logger: Logger) - { } + /** Overview which contains all budgets of an organisation */ + overview = toSignal(this._orgBudgets$$.get(), { initialValue: null }); + sharedBudgets = toSignal(this._budgets$$.get(), { initialValue: [] }); + + showFilter = signal(false); + + // Convert the combined observable to a computed signal + allBudgets = computed(() => { + const overviewVal = this.overview(); + const budgetsVal = this.sharedBudgets(); + + if (!overviewVal || !budgetsVal) { + return { overview: [], budgets: [] }; + } + + const combined = { + overview: __flatMap(overviewVal), + budgets: __flatMap(budgetsVal) + }; + + // Transform budgets to add endYear property + const transformedBudgets = combined.budgets.map((budget: any) => ({ + ...budget, + endYear: budget.startYear + budget.duration - 1 + })); + + return { + overview: combined.overview, + budgets: transformedBudgets + }; + }); + + constructor() { + // Use effect for side effects like logging + effect(() => { + const budgets = this.allBudgets().budgets; + if (budgets?.length) { + this._logger.log(() => `Budgets loaded: ${budgets.length}`); + } + }, { allowSignalWrites: true }); + } ngOnInit() { - this.overview$ = this._orgBudgets$$.get(); - this.sharedBudgets$ = this._budgets$$.get(); - - this.allBudgets$ = combineLatest([this.overview$, this._budgets$$.get()]) - .pipe(map(([overview, budgets]) => {return {overview: __flatMap(overview), budgets: __flatMap(budgets)}}), - map((overview) => { - const trBudgets = overview.budgets.map((budget: any) => {budget['endYear'] = budget.startYear + budget.duration - 1; return budget;}) - // this.budgetsLoaded = true; - return {overview: overview.overview, budgets: trBudgets} - })); + // No need for manual subscriptions since signals handle this automatically } applyFilter(event: Event) { const filterValue = (event.target as HTMLInputElement).value; - // this.dataSource.filter = filterValue.trim().toLowerCase(); + } fieldsFilter(value: (Invoice) => boolean) { - // this.filter$$.next(value); } - toogleFilter(value) { - // this.showFilter = value + toogleFilter(value: boolean) { + this.showFilter.set(value); } - openDialog(parent : Budget | false): void - { + openDialog(parent: Budget | false): void { const dialog = this._dialog.open(CreateBudgetModalComponent, { height: 'fit-content', width: '600px', - data: parent != null ? parent : false + data: parent !== null ? parent : false }); dialog.afterClosed().subscribe(() => { // Dialog after action - }) + }); } /** - * @TODO - Review and fix * Returns true if the budget can be activated */ - canPromote(record: BudgetRecord) { + canPromote(record: BudgetRecord): boolean { // Get's set on Budget Read from user privileges and budget status. return (record.budget as any).canBeActivated; } /** Activate budget -> Promote to be used in */ - setActive(record: BudgetRecord) - { + setActive(record: BudgetRecord) { const toSave = ___cloneDeep(record.budget); // Clean up budget record values. @@ -97,12 +114,11 @@ export class SelectBudgetPageComponent implements OnInit // Set Active toSave.status = BudgetStatus.InUse; - ( record).updating = true; - // Fire update - this._budgets$$.update(toSave) - .subscribe(() => { - ( record).updating = false; - this._logger.log(() => `Updated Budget with id ${toSave.id}. Set as an active budget for this org.`) - }); + (record as any).updating = true; + + this._budgets$$.update(toSave).subscribe(() => { + (record as any).updating = false; + this._logger.log(() => `Updated Budget with id ${toSave.id}. Set as an active budget for this org.`) + }); } } \ No newline at end of file diff --git a/libs/model/budgetting/notes/budget-notes/src/lib/domain/add-note.command.ts b/libs/model/budgetting/notes/budget-notes/src/lib/domain/add-note.command.ts new file mode 100644 index 00000000..5b575b47 --- /dev/null +++ b/libs/model/budgetting/notes/budget-notes/src/lib/domain/add-note.command.ts @@ -0,0 +1,6 @@ +export interface AddNoteToBudgetCommand { + budgetId: string; + content: string; + authorId: string; + timestamp?: Date; +} \ No newline at end of file diff --git a/libs/model/budgetting/notes/budget-notes/src/lib/domain/add-note.handler.ts b/libs/model/budgetting/notes/budget-notes/src/lib/domain/add-note.handler.ts new file mode 100644 index 00000000..39461058 --- /dev/null +++ b/libs/model/budgetting/notes/budget-notes/src/lib/domain/add-note.handler.ts @@ -0,0 +1,29 @@ +import { AddNoteToBudgetCommand } from './add-note.command'; + +export interface ICommandHandler { + execute(command: TCommand): Promise; +} + +export interface AddNoteToBudgetResult { + success: boolean; + noteId?: string; +} + +export abstract class FunctionHandler implements ICommandHandler { + abstract execute(command: TCommand): Promise; +} + +export class AddNoteToBudgetHandler extends FunctionHandler { + async execute(command: AddNoteToBudgetCommand): Promise { + const { budgetId, content, authorId } = command; + + if (!budgetId || !content?.trim() || !authorId) { + throw new Error('Invalid command: budgetId, content, and authorId are required.'); + } + + return { + success: true, + noteId: 'test-note-id' + Math.random().toString(36).substr(2, 9), + }; + } +} \ No newline at end of file