Expense Summary - VeenaPD/Expense-Management-System GitHub Wiki

Expense Summary Dashboard

Expense summary dashboard represents the expense trends as well as list of expense items

The image below shows the dashboard of expense trend wrapped in home component

dashboard

Home component nests following components

  • BudgetOverviewComponent: To show total budget and total expenses using pie chart
  • CategorySplitComponent: To show category wise split of expenses using Pie chart
  • ExpenseComponent: To show category wise expense list, with Add, Edit, Undo action buttons. Uses ngx-datatable

ngx-datatable is a Angular component for presenting large and complex data. It has all the features you would expect from any other table but in a light package with no external dependencies. The table was designed to be extremely flexible and light; it doesn't make any assumptions about data or how to filter etc.

Data models used in these components:

export interface Settings {
  budget: number;
  updatedAt: Date;
}

import { ExpenseCategory } from './ExpenseCategory';

export interface ExpenseByCategory {
  category: ExpenseCategory;
  total: number;
}

import { ExpenseCategory } from './ExpenseCategory';

export interface Expense {
  id: string;
  category: ExpenseCategory;
  title: string;
  amount: number;
  date: Date;
  isDeleted: boolean;
}
/**
Random color generation for Charts
*/
export const getRandomColor = () => {
    var r = Math.floor(Math.random() * 255);
    var g = Math.floor(Math.random() * 255);
    var b = Math.floor(Math.random() * 255);
    return "rgb(" + r + "," + g + "," + b + ")";
 };

Actions used UpdateBudgetAction, AddExpenseAction, ToggleExpenseAction, UpdateExpenseAction

Service used expense.service.ts

import { v4 } from 'uuid';

import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';

import {
    AddExpenseAction, ToggleExpenseAction, UpdateExpenseAction
} from '../actions/expense.actions';
import { Expense } from '../models/Expense';
import { ExpenseCategory } from '../models/ExpenseCategory';
import { State } from '../reducers';
import {
    getAllExpensesNotDeleted, getExpensesByCategory, getTotalExpenses, getExpenseById, getAllExpensesSelector
} from '../selectors/expense.selector';

@Injectable({
  providedIn: 'root'
})
export class ExpenseService {

  constructor(private store: Store<State>) { }
  /**
   * Fetch all user expenses using getAllExpenseSelector 
   */
  getAllExpenses() {
    return this.store.select(getAllExpensesSelector);
  }
  /**
   * Fetch all user expenses group by category using getExpenseByCategory selector 
   */
  getAllExpensesGroupByCategory(){
    return this.store.select(getExpensesByCategory)
  }
  /**
  * Fetch total expenses, using getTotalExpenses selector
   */
  getTotalExpenses(){
    return this.store.select(getTotalExpenses);
  }
  /**
   * Add expense method, addExpenseAction is dispatched
   * @params title, amount, category, date
   */
  addExpense(title: string, amount: number, category: ExpenseCategory, date: Date = new Date()) {
    let e: Expense = {
      title,
      amount,
      category,
      date,
      isDeleted: false,
      id: v4()
    }
    let addExpenseAction = new AddExpenseAction(e);
    this.store.dispatch(addExpenseAction);
  }
    /**
   * Update expense method, updateExpenseAction is dispatched
   * @param e Expense 
   */
  updateExpense(e: Expense) {
    let updateExpenseAction = new UpdateExpenseAction(e);
    this.store.dispatch(updateExpenseAction);
  }
  /**
   * Toggle expense method toggles expense state, removeExpenseAction is dispatched
   * @param exp string
   */
  toggleExpense(exp: string){
    let removeExpenseAction = new ToggleExpenseAction(exp);
    this.store.dispatch(removeExpenseAction);
  }
  /**
   * Fetch expense by Id using getExpenseById selector
   * @param id expenseId
   */
  getExpenseById(id:string){
    return this.store.select(getExpenseById(id))
  }
}

Home component

Contains 3 different components

budgetOverview.component.html

<div class="card">
  <div class="card-body">
    <h5 class="card-title">Budget Overview</h5>
    <hr />
    <div class="row">
      <div class="col-6">
        <div class="chart">
          <canvas
            baseChart
            [data]="budgetChartData"
            [options]="budgetChartOptions"
            [chartType]="budgetChartType"
            [colors]="budgetChartColors"
            [legend]="budgetChartLegend"
            [labels]="lables"
          ></canvas>
        </div>
      </div>
      <div class="col-6">
        <h6>Total budget</h6>
        <p>{{budget}}</p>
        <h6>Total Expense</h6>
        <p>{{expenses}}</p>
      </div>
    </div>
  </div>
</div>

budgetOverview.component.ts

import { Component, OnInit, Input } from '@angular/core';
import * as pluginDataLabels from 'chartjs-plugin-datalabels';
import { ChartOptions, ChartType } from 'chart.js';

@Component({
  selector: 'app-budget-overview',
  templateUrl: './budget-overview.component.html',
  styleUrls: ['./budget-overview.component.scss']
})
export class BudgetOverviewComponent implements OnInit {
  @Input('budget') budget:number;
  @Input('expenses') expenses:number;
  public budgetChartOptions: ChartOptions = {
    responsive: true,
    plugins: {
      dataLabels: {
        formatter: (value, ctx) => {
          const label = ctx.chart.data.labels[ctx.dataIndex];
          return label;
        }
      }
    }
  };
  budgetChartData: number[] = [];
  budgetChartType: ChartType = 'pie';
  budgetChartLegend = false;
  budgetChartPlugins = [pluginDataLabels];
  budgetChartColors = [
    {
      backgroundColor: ['rgba(255,0,0,0.3)', 'rgba(0,255,0,0.3)'],
    },
  ];
  lables = ['Expenses','Remaining Budget'];
  constructor() { }
  /**
   * Durong Budget data changes
   */
  ngOnChanges(): void {
    this.onChanged();
  }
  /**
   * onInit fetch for latest changes in budget data
   */
  ngOnInit() {
    this.onChanged();
  }
  /**
   * Changes in budgetChartData is reflected on chart 
   */
  private onChanged() {
    this.budgetChartData = [this.expenses, this.budget - this.expenses];
  }
}

category-split.component.html

<div class="card">
  <div class="card-body">
    <h5 class="card-title">Category wise split</h5>
    <hr />
    <div class="row" *ngIf="isDataPresent">
      <div class="col-6">
        <div class="chart">
          <canvas
            baseChart
            [data]="categorySplitChartData"
            [options]="categorySplitChartOptions"
            [chartType]="categorySplitChartType"
            [colors]="categorySplitChartColors"
            [legend]="categorySplitChartLegend"
            [labels]="labels"
          ></canvas>
        </div>
      </div>
    </div>
    <div class="row" *ngIf="!isDataPresent">
      <div class="col-6">
          <span style="text-align: center;margin: 0 auto;" >No Data to Display...</span> 
      </div>
      
    </div>
  </div>
</div>

category.component.html

import { Component, Input, OnInit } from '@angular/core';
import { ChartOptions, ChartType } from 'chart.js';
import * as pluginDataLabels from 'chartjs-plugin-datalabels';
import { ExpenseByCategory } from 'src/app/models/ExpenseByCategory';
import {getRandomColor} from '../../models/RandomColor'
@Component({
  selector: 'app-category-split',
  templateUrl: './category-split.component.html',
  styleUrls: ['./category-split.component.scss']
})
export class CategorySplitComponent implements OnInit {
  @Input('data') data:ExpenseByCategory[]
  isDataPresent:boolean;
   categorySplitChartOptions: ChartOptions = {
    responsive: true,
    legend: {
      position: 'right'
    },
    plugins: {
      dataLabels: {
        formatter: (value, ctx) => {
          const label = this.labels[ctx.dataIndex];
          console.log(ctx.dataIndex);
          return label;
        }
      }
    }
  };
   categorySplitChartData: number[] = [100];
   categorySplitChartType: ChartType = 'pie';
   categorySplitChartLegend = true;
   categorySplitChartPlugins = [pluginDataLabels];
   categorySplitChartColors = [
    {
      backgroundColor: [],
    },
  ];
  constructor() { }
  labels = [];
  /**
   * Initializes pie chart setup for expense data
   */
  ngOnInit() {
    console.log(this.data);
    this.setUpPieChart();
  }
  /**
   * Changes in expense data reflected on chart
   */
  ngOnChanges(): void {
    this.setUpPieChart();
  }
  /**
   * Pie chart setup to changed expense data
   */
  setUpPieChart(){
    if(this.data.length  === 0){
      this.isDataPresent = false;
      return;
    }
    this.isDataPresent = true;

    this.labels = [];
    this.categorySplitChartData = [];
    let colors = [{backgroundColor:[]}];
    this.data.forEach(ec => {
      this.labels.push(ec.category.name);
      this.categorySplitChartData.push(ec.total);
      colors[0].backgroundColor.push(getRandomColor());
    })
    this.categorySplitChartColors = colors;
  }

}

expenses.component.html

<ngx-datatable
  [rowHeight]="'auto'"
  [rows]="expenseItems"
  [columnMode]="'flex'"
  [headerHeight]="50"
  class="material"
  [footerHeight]="50"
  [count]="true"
  [limit]="3"
>
  <ngx-datatable-column
    name="Category"
    [flexGrow]="2"
    prop="category.name"
  ></ngx-datatable-column>
  <ngx-datatable-column name="Item name" [flexGrow]="2" prop="title">
    <ng-template ngx-datatable-cell-template let-value="value" let-row="row">
      <span [ngClass]="!row.isDeleted ? 'null' : 'strikethrough'">{{row.title}}</span>
    </ng-template>
  </ngx-datatable-column>

  <ngx-datatable-column
    name="Amount"
    [flexGrow]="1"
    prop="amount"
  ></ngx-datatable-column>
  <ngx-datatable-column name="Expense Date" [flexGrow]="1" prop="date">
    <ng-template ngx-datatable-cell-template let-value="value" let-row="row">
      {{ value | amLocal | amDateFormat: "YYYY-MM-DD HH:mm" }}
    </ng-template>
  </ngx-datatable-column>
  <ngx-datatable-column name="Options" [flexGrow]="1">
    <ng-template ngx-datatable-cell-template let-value="value" let-row="row">
      <button
        type="button"
        *ngIf="!row.isDeleted"
        class="btn btn-secondary"
        [routerLink]="['/expense', row.id]"
      >
        Edit
      </button>
      <button
        type="button"
        *ngIf="row.isDeleted"
        class="btn btn-secondary"
        (click)="undoDelete(row.id)"
      >
        Undo
      </button>
    </ng-template>
  </ngx-datatable-column>
</ngx-datatable>

expenses.component.ts

import { Component, OnInit, EventEmitter, Input, Output } from '@angular/core';
import { Expense } from 'src/app/models/Expense';
import { ExpenseService } from 'src/app/services/expense.service';


@Component({
  selector: 'app-expenses',
  templateUrl: './expenses.component.html',
  styleUrls: ['./expenses.component.scss']
})
export class ExpensesComponent implements OnInit {
  @Input('expenseItems') expenseItems : Expense[];
  @Output('onEdit')onEdit: EventEmitter<any> = new EventEmitter<any>();
  constructor(public expenseService:ExpenseService) { }
  ngOnInit() {
  }
  
  /**
   * Undo the deleted expense
   * @param id expenseId
   */
  undoDelete(id:string){
    this.expenseService.toggleExpense(id)
  }

}

home.component.html

<div class="container">
  <div class="row">
    <div class="col-md-6">
      <app-budget-overview [budget]="getCurrentBudget() | async" [expenses]="getTotalExpenses() | async"></app-budget-overview>
    </div>
    <div class="col-md-6">
      <app-category-split [data]="getExpenseCategorised()| async"></app-category-split>
    </div>
  </div>

  <div class="container">
    <button
      type="button"
      class="btn btn-primary"
      [routerLink]="['/expense', 'create']">
      Add expense
    </button>
  </div>
  <div class="container">
    <app-expenses [expenseItems]="getAllExpenses() | async"></app-expenses>
  </div>
</div>

home.component.css

.chart {
  width: 300px;
  height: 150px;
}
.container {
  padding-top: 16px;
}

home.component.ts

import { Component, OnInit } from '@angular/core';
import { ExpenseService } from 'src/app/services/expense.service';
import { Expense } from 'src/app/models/Expense';
import { ExpenseCategoryService } from 'src/app/services/expense-category.service';
import {take} from 'rxjs/operators'
import { SettingsService } from 'src/app/services/settings.service';
@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {

  expenseList: Expense[] = [];

  constructor(public expenseService: ExpenseService,public expenseCat:ExpenseCategoryService,public settingsService:SettingsService) { }

  ngOnInit() {
        
  }
  /**
   * Fetch list of all expenses
   */
  getAllExpenses(){
    return this.expenseService.getAllExpenses();
  }
  /**
   * Fetch current budget
   */
  getCurrentBudget(){
    return this.settingsService.getTotalBudget();
  }
  /**
   * Get total expenses
   */
  getTotalExpenses(){
    return this.expenseService.getTotalExpenses();
  }
  /**
   * Fetch all expenses group by category
   */
  getExpenseCategorised(){
    return this.expenseService.getAllExpensesGroupByCategory();
  }

}
⚠️ **GitHub.com Fallback** ⚠️