Expense Summary - VeenaPD/Expense-Management-System GitHub Wiki
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
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))
}
}
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();
}
}