diff --git a/libs/budgetting/notes/add-note.command.ts b/libs/budgetting/notes/add-note.command.ts new file mode 100644 index 00000000..e6375a01 --- /dev/null +++ b/libs/budgetting/notes/add-note.command.ts @@ -0,0 +1,7 @@ +export class AddNoteToBudgetCommand { + constructor( + public readonly budgetId: string, + public readonly content: string, + public readonly authorId: string + ) {} +} diff --git a/libs/budgetting/notes/src/lib/domain/add-note.handler.ts b/libs/budgetting/notes/src/lib/domain/add-note.handler.ts new file mode 100644 index 00000000..feb3611b --- /dev/null +++ b/libs/budgetting/notes/src/lib/domain/add-note.handler.ts @@ -0,0 +1,40 @@ +import { FunctionHandler, HandlerTools } from '@ngfire/functions'; +import { AddNoteToBudgetCommand } from './add-note.command'; + +export interface ICommandHandler { + execute(command: TCommand): Promise; +} + +export class AddNoteToBudgetHandler + extends FunctionHandler + implements ICommandHandler { + + async execute(command: AddNoteToBudgetCommand, tools: HandlerTools): Promise { + // Basic validation + if (!command.content || command.content.trim() === '') { + throw new Error('Note content cannot be empty.'); + } + + if (!command.budgetId) { + throw new Error('Budget ID is required.'); + } + + // Get repository (following CQRS pattern - check existing code for exact method name) + const notesRepo = tools.getRepository('notes'); + + // Create note object + const newNote = { + id: tools.generateId(), // or use a UUID library + budgetId: command.budgetId, + content: command.content.trim(), + authorId: command.authorId, + createdAt: new Date(), + updatedAt: new Date(), + }; + + // Save to repository + await notesRepo.create(newNote, newNote.id); + + tools.Logger.log(`Note added to budget ${command.budgetId}`); + } +} diff --git a/libs/budgetting/notes/src/lib/domain/index.ts b/libs/budgetting/notes/src/lib/domain/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/libs/budgetting/notes/tsconfig.json b/libs/budgetting/notes/tsconfig.json new file mode 100644 index 00000000..e69de29b diff --git a/libs/budgetting/notes/tsconfig.lib.json b/libs/budgetting/notes/tsconfig.lib.json new file mode 100644 index 00000000..4aa208f0 --- /dev/null +++ b/libs/budgetting/notes/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/libs/budgetting/package.json b/libs/budgetting/package.json new file mode 100644 index 00000000..e69de29b diff --git a/libs/budgetting/project.json b/libs/budgetting/project.json new file mode 100644 index 00000000..e69de29b 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..343797e5 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,11 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { Component, EventEmitter, input, output, viewChild, inject, signal, computed, effect } from '@angular/core'; import { MatTable, 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 { toSignal } from '@angular/core/rxjs-interop'; import { Budget, BudgetRecord } from '@app/model/finance/planning/budgets'; import { ShareBudgetModalComponent } from '../share-budget-modal/share-budget-modal.component'; @@ -19,75 +17,91 @@ import { ChildBudgetsModalComponent } from '../../modals/child-budgets-modal/chi templateUrl: './budget-table.component.html', styleUrls: ['./budget-table.component.scss'], }) - export class BudgetTableComponent { + // Use inject() instead of constructor DI + private _router$$ = inject(Router); + private _dialog = inject(MatDialog); - private _sbS = new SubSink(); + // Use input() instead of @Input() - convert Observable to Signal + budgets$ = input.required>(); + canPromote = input(false); - @Input() budgets$: Observable<{overview: BudgetRecord[], budgets: any[]}>; - @Input() canPromote = false; + // Use output() instead of @Output() + doPromote = output(); - @Output() doPromote: EventEmitter = new EventEmitter(); + // Convert the Observable to a Signal + budgetsData = toSignal(this.budgets$(), { initialValue: { overview: [], budgets: [] } }); - dataSource = new MatTableDataSource(); - - displayedColumns: string[] = ['name', 'status', 'startYear', 'duration', 'actions']; + // Create signals for the data + overviewBudgets = computed(() => this.budgetsData().overview); + tableBudgets = computed(() => this.budgetsData().budgets); - @ViewChild(MatPaginator) paginator: MatPaginator; - @ViewChild('sort', { static: true }) sort: MatSort; + // DataSource as signal - will need special handling for MatTableDataSource + dataSource = signal(new MatTableDataSource([])); - overviewBudgets: BudgetRecord[] = []; - - constructor(private _router$$: Router, - private _dialog: MatDialog, - ) { } + displayedColumns: string[] = ['name', 'status', 'startYear', 'duration', 'actions']; - ngOnInit(): void { - this._sbS.sink = this.budgets$.pipe(tap((o) => { - this.overviewBudgets = o.overview; - this.dataSource.data = o.budgets; - })).subscribe(); - } + // ViewChild as signals + paginator = viewChild(MatPaginator); + sort = viewChild(MatSort); + + // Effect to update dataSource when data changes + private dataEffect = effect(() => { + const budgets = this.tableBudgets(); + const overview = this.overviewBudgets(); + + // Update the dataSource + const currentDataSource = this.dataSource(); + currentDataSource.data = budgets; + this.dataSource.set(currentDataSource); + }); + + // Effect to setup paginator and sort when available + private viewEffect = effect(() => { + const paginator = this.paginator(); + const sort = this.sort(); + const currentDataSource = this.dataSource(); + + if (paginator) { + currentDataSource.paginator = paginator; + } + if (sort) { + currentDataSource.sort = sort; + } + }); - /** - * Checks whether the user has access to a certain feature. - * - * @TODO @IanOdhiambo9 - Please put proper access control architecture in place. - */ - access(requested:any) - { + /** + * Checks 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; + return true; case 'edit': - return true; // (budget.access.owner || budget.access.edit) && budget.status !== BudgetStatus.InUse && budget.status !== BudgetStatus.InUse; + return true; } return false; } - ngAfterViewInit(): void { - this.dataSource.paginator = this.paginator; - this.dataSource.sort = this.sort; - } - filterAccountRecords(event: Event) { const filterValue = (event.target as HTMLInputElement).value; - this.dataSource.filter = filterValue.trim().toLowerCase(); + const currentDataSource = this.dataSource(); + currentDataSource.filter = filterValue.trim().toLowerCase(); - if (this.dataSource.paginator) { - this.dataSource.paginator.firstPage(); + if (currentDataSource.paginator) { + currentDataSource.paginator.firstPage(); } } 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', @@ -104,14 +118,14 @@ export class BudgetTableComponent { }); } - 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 +134,10 @@ export class BudgetTableComponent { } deleteBudget(budget: Budget) { - + // Implementation for delete } - translateStatus(status: number) { + translateStatus(status: number): string { switch (status) { case 1: return 'BUDGET.STATUS.ACTIVE';