Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions libs/budgetting/notes/add-note.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class AddNoteToBudgetCommand {
constructor(
public readonly budgetId: string,
public readonly content: string,
public readonly authorId: string
) {}
}
40 changes: 40 additions & 0 deletions libs/budgetting/notes/src/lib/domain/add-note.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { FunctionHandler, HandlerTools } from '@ngfire/functions';
import { AddNoteToBudgetCommand } from './add-note.command';

export interface ICommandHandler<TCommand> {
execute(command: TCommand): Promise<void>;
}

export class AddNoteToBudgetHandler
extends FunctionHandler<AddNoteToBudgetCommand, void>
implements ICommandHandler<AddNoteToBudgetCommand> {

async execute(command: AddNoteToBudgetCommand, tools: HandlerTools): Promise<void> {
// 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}`);
}
}
Empty file.
Empty file.
10 changes: 10 additions & 0 deletions libs/budgetting/notes/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["**/*.ts"],
"exclude": ["**/*.spec.ts"]
}
Empty file added libs/budgetting/package.json
Empty file.
Empty file added libs/budgetting/project.json
Empty file.
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<Observable<{overview: BudgetRecord[], budgets: any[]}>>();
canPromote = input<boolean>(false);

@Input() budgets$: Observable<{overview: BudgetRecord[], budgets: any[]}>;
@Input() canPromote = false;
// Use output() instead of @Output()
doPromote = output<void>();

@Output() doPromote: EventEmitter<void> = 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<any>([]));

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',
Expand All @@ -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 }
});
}

Expand All @@ -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';
Expand Down