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 : '-' }} 0" class="badge primary"
- (click)="openChildBudgetDialog(row)"> 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