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
Original file line number Diff line number Diff line change
@@ -1,77 +1 @@
<table mat-table #sort="matSort" [dataSource]="dataSource" multiTemplateDataRows class="mat-elevation-z8" matSort>

<!-- name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Budget Name </th>
<td mat-cell *matCellDef="let row">
{{row.name ? row.name : '-' }} <span *ngIf="row.childrenList?.length > 0" class="badge primary"
(click)="openChildBudgetDialog(row)"> Linked Budgets ({{ row?.childrenList?.length }}) </span>
</td>
</ng-container>

<!-- status Column -->
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Status </th>
<td mat-cell *matCellDef="let row">
<span class="badge" [ngClass]="row.status == 1 ? 'active': 'draft'">
{{translateStatus(row.status) | transloco}}
</span>
</td>
</ng-container>

<!-- Start year Column -->
<ng-container matColumnDef="startYear">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Start Year </th>
<td mat-cell *matCellDef="let row"> {{row.startYear ? row.startYear : '-'}} </td>
</ng-container>

<!-- Duration Column -->
<ng-container matColumnDef="duration">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Duration (Years) </th>
<td mat-cell *matCellDef="let row"> {{row.duration ? row.duration : '-' }} </td>
</ng-container>

<!-- actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Actions </th>
<td mat-cell *matCellDef="let row">
<div class="budget-btn-container" fxLayout="row" fxLayoutALign="start center" fxLayoutGap="10px">

<button color="primary" class="secondary-action" mat-mini-fab (click)="openCloneBudgetDialog(row)"
matTooltip="{{ 'BUDGET.VIEW-RECORD.ACTIONS.CLONE'| transloco }}" *ngIf="access('clone')">
<i class="fas fa-copy fa-sm"></i>
</button>

<button color="primary" class="secondary-action" mat-mini-fab
matTooltip="{{'BUDGET.VIEW-RECORD.ACTIONS.VIEW' | transloco}}" (click)="goToDetail(row.id!, 'view')"
*ngIf="access('view')">
<i class="fas fa-eye fa-sm"></i>
</button>

<button color="primary" mat-mini-fab matTooltip="{{ 'BUDGET.VIEW-RECORD.ACTIONS.EDIT' | transloco }}"
color="primary" (click)="goToDetail(row.id!, 'edit')" *ngIf="access('edit')">
<i class="fas fa-edit fa-sm"></i>
</button>

<!-- <button color="primary" class="secondary-action"
matTooltip="{{ 'BUDGET.VIEW-RECORD.ACTIONS.SHARE' | transloco }}" mat-mini-fab
(click)="openShareBudgetDialog(row)">
<i class="fas fa-share-alt fa-sm"></i>
</button> -->

<button color="primary" class="delete-btn" matTooltip="Delete" mat-mini-fab
(click)="openShareBudgetDialog(row)">
<i class="fas fa-trash-alt fa-sm"></i>
</button>

<!-- Will only be true on parent since does not get cascaded down below. -->
<!-- <div class="budget-btn-container" style="float: right; text-align: right">
<button class="btn-promote" (click)="promote()">{{ 'BUDGET.VIEW-RECORD.PROMOTE' | transloco }}</button>
</div> -->
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 25, 100]" aria-label="Select page of users"></mat-paginator>
<div *ngFor="let item of allBudgets()">
Original file line number Diff line number Diff line change
@@ -1,140 +1,13 @@
import { Component, EventEmitter, Input, Output, ViewChild } 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 { Budget, BudgetRecord } from '@app/model/finance/planning/budgets';

import { ShareBudgetModalComponent } from '../share-budget-modal/share-budget-modal.component';
import { CreateBudgetModalComponent } from '../create-budget-modal/create-budget-modal.component';
import { ChildBudgetsModalComponent } from '../../modals/child-budgets-modal/child-budgets-modal.component';
import { Component, input } from '@angular/core';

@Component({
selector: 'app-budget-table',
templateUrl: './budget-table.component.html',
styleUrls: ['./budget-table.component.scss'],
selector: 'kujali-budget-table',
standalone: true,
templateUrl: './budget-table.component.html'
changeDetection: ChangeDetectionStrategy.OnPush,
})

export class BudgetTableComponent {

private _sbS = new SubSink();

@Input() budgets$: Observable<{overview: BudgetRecord[], budgets: any[]}>;
@Input() canPromote = false;

@Output() doPromote: EventEmitter<void> = new EventEmitter();

dataSource = new MatTableDataSource();

displayedColumns: string[] = ['name', 'status', 'startYear', 'duration', 'actions'];

@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild('sort', { static: true }) sort: MatSort;

overviewBudgets: BudgetRecord[] = [];

constructor(private _router$$: Router,
private _dialog: MatDialog,
) { }

ngOnInit(): void {
this._sbS.sink = this.budgets$.pipe(tap((o) => {
this.overviewBudgets = o.overview;
this.dataSource.data = o.budgets;
})).subscribe();
}

/**
* Checks whether the user has access to a certain feature.
*
* @TODO @IanOdhiambo9 - Please put proper access control architecture in place.
*/
access(requested:any)
{
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;
}
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();

if (this.dataSource.paginator) {
this.dataSource.paginator.firstPage();
}
}

promote() {
if (this.canPromote)
this.doPromote.emit();
}

/** Open share screen to configure budget access. */
openShareBudgetDialog(parent: Budget | false): void
{
this._dialog.open(ShareBudgetModalComponent, {
panelClass: 'no-pad-dialog',
width: '600px',
data: parent != null ? parent : false
});
}

/** Open clone screen to clone and reconfigure budget. */
openCloneBudgetDialog(parent: Budget | false): void {
this._dialog.open(CreateBudgetModalComponent, {
height: 'fit-content',
width: '600px',
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)
this._dialog.open(ChildBudgetsModalComponent, {
height: 'fit-content',
minWidth: '600px',
data: {parent: parent, budgets: children}
});
}

goToDetail(budgetId: string, action: string) {
this._router$$.navigate(['budgets', budgetId, action]).then(() => this._dialog.closeAll());
}

deleteBudget(budget: Budget) {

}

translateStatus(status: number) {
switch (status) {
case 1:
return 'BUDGET.STATUS.ACTIVE';
case 0:
return 'BUDGET.STATUS.DESIGN';
case 9:
return 'BUDGET.STATUS.NO-USE';
case -1:
return 'BUDGET.STATUS.DELETED';
default:
return '';
}
}
overview = input<any>();
sharedBudgets = input<any[]>();
allBudgets = input<any[]>();
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,5 @@
<app-page>
<div>
<kujali-finance-toolbar>
<kujali-finance-search-header-card header (toogleFilterEvent)='toogleFilter($event)'
(searchTableEvent)='applyFilter($event)' [tableData]="[]">
<div add-new>
<button mat-flat-button (click)="openDialog(false)" class="add-new">
<i class="bi bi-plus-circle-fill"></i>
{{'HEADER-ADD-NEW' | transloco}}
</button>
</div>
</kujali-finance-search-header-card>
</kujali-finance-toolbar>

<div *ngIf="showFilter" class="filter-section">
<!-- <kujali-invoices-filter (filterChanged)='fieldsFilter($event)'>
</kujali-invoices-filter> -->
</div>

<div class="table-container">
<app-budget-table [budgets$]="allBudgets$"></app-budget-table>
</div>
</div>
</app-page>
<kujali-budget-table
[overview]="overviewSignal"
[sharedBudgets]="sharedBudgetsSignal"
[allBudgets]="allBudgetsSignal">
</kujali-budget-table>
Original file line number Diff line number Diff line change
@@ -1,108 +1,34 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';

import { cloneDeep as ___cloneDeep, flatMap as __flatMap } from 'lodash';
import { Observable, combineLatest, map, tap } from 'rxjs';

import { Logger } from '@iote/bricks-angular';

import { Budget, BudgetRecord, BudgetStatus, OrgBudgetsOverview } from '@app/model/finance/planning/budgets';

import { BudgetsStore, OrgBudgetsStore } from '@app/state/finance/budgetting/budgets';

import { CreateBudgetModalComponent } from '../../components/create-budget-modal/create-budget-modal.component';

import { Component, signal, computed, effect, inject } from '@angular/core';
import { SelectBudgetService } from '../services/select-budget.service';

@Component({
selector: 'app-select-budget',
templateUrl: './select-budget.component.html',
styleUrls: ['./select-budget.component.scss',
'../../components/budget-view-styles.scss'],
selector: 'kujali-select-budget-page',
standalone: true,
templateUrl: './select-budget-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
/** List of all active budgets on the system. */
export class SelectBudgetPageComponent implements OnInit
{
/** Overview which contains all budgets of an organisation */
overview$!: Observable<OrgBudgetsOverview>;
sharedBudgets$: Observable<any[]>;

showFilter = false;

// budgetsLoaded: boolean = false;

allBudgets$: Observable<{overview: BudgetRecord[], budgets: any[]}>;

constructor(private _orgBudgets$$: OrgBudgetsStore,
private _budgets$$: BudgetsStore,
private _dialog: MatDialog,
private _logger: Logger)
{ }

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}
}));
}

applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
// this.dataSource.filter = filterValue.trim().toLowerCase();
}
export class SelectBudgetPageComponent {

fieldsFilter(value: (Invoice) => boolean) {
// this.filter$$.next(value);
}
private service = inject(SelectBudgetService);

toogleFilter(value) {
// this.showFilter = value
}
// Convert Observables → Signals
private overviewSignal = signal(null);
private sharedBudgetsSignal = signal([]);
private allBudgetsSignal = signal([]);

openDialog(parent : Budget | false): void
{
const dialog = this._dialog.open(CreateBudgetModalComponent, {
height: 'fit-content',
width: '600px',
data: parent != null ? parent : false
constructor() {
// Replace subscriptions with effect()
effect(() => {
this.service.overview$.subscribe(v => this.overviewSignal.set(v));
this.service.sharedBudgets$.subscribe(v => this.sharedBudgetsSignal.set(v));
this.service.allBudgets$.subscribe(v => this.allBudgetsSignal.set(v));
});

dialog.afterClosed().subscribe(() => {
// Dialog after action
})
}

/**
* @TODO - Review and fix
* Returns true if the budget can be activated */
canPromote(record: BudgetRecord) {
// 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)
{
const toSave = ___cloneDeep(record.budget);

// Clean up budget record values.
delete (toSave as any).canBeActivated;
delete (toSave as any).access;

// Set Active
toSave.status = BudgetStatus.InUse;

(<any> record).updating = true;
// Fire update
this._budgets$$.update(toSave)
.subscribe(() => {
(<any> record).updating = false;
this._logger.log(() => `Updated Budget with id ${toSave.id}. Set as an active budget for this org.`)
});
}
}
// Computed combination example
budgetsViewModel = computed(() => ({
overview: this.overviewSignal(),
shared: this.sharedBudgetsSignal(),
all: this.allBudgetsSignal()
}));
}