Expense - VeenaPD/Expense-Management-System GitHub Wiki
The users can add expense, edit expense, delete expense. Component used is expense-details.
The image below is the screen shot of UI component when user needs to create new expense item:
The image below is the screen shot of UI component when user needs to edit new expense item:
Data Models used in expense-details component
import { ExpenseCategory } from './ExpenseCategory';
export interface Expense {
id: string;
category: ExpenseCategory;
title: string;
amount: number;
date: Date;
isDeleted: boolean;
}
import { ExpenseCategory } from './ExpenseCategory';
export interface ExpenseByCategory {
category: ExpenseCategory;
total: number;
}
Actions expense-category.actions.ts
import { Action } from '@ngrx/store';
import { ExpenseCategory } from '../models/ExpenseCategory';
export const ADD_EXPENSE_CATEGORY = '[EXPENSE_CATEGORY] ADD_EXPENSE_CATEGORY';
export const REMOVE_EXPENSE_CATEGORY = '[EXPENSE_CATEGORY] REMOVE_EXPENSE_CATEGORY';
export class AddExpenseCategoryAction implements Action {
readonly type = ADD_EXPENSE_CATEGORY;
constructor(public payload: ExpenseCategory) { }
}
export class RemoveExpenseCategoryAction implements Action {
readonly type = REMOVE_EXPENSE_CATEGORY;
constructor(public payload: string) { }
}
export type ExpenseCategoryActionType =
AddExpenseCategoryAction |
RemoveExpenseCategoryAction;
Reducer expense-category.reducer.ts
import { createEntityAdapter, EntityState, Update } from '@ngrx/entity';
import * as expenseCategoryActions from '../actions/expense-category.actions';
import { ExpenseCategory } from '../models/ExpenseCategory';
export interface ExpenseCategoryState extends EntityState<ExpenseCategory> {};
export const expenseCategoryAdapter = createEntityAdapter<ExpenseCategory> ({
selectId: (e) => {
return e.id
}
})
const initialState: ExpenseCategoryState = expenseCategoryAdapter.getInitialState({});
export function expenseCategoryReducer(state = initialState, action: expenseCategoryActions.ExpenseCategoryActionType): ExpenseCategoryState {
switch(action.type){
case expenseCategoryActions.ADD_EXPENSE_CATEGORY: {
return expenseCategoryAdapter.addOne(action.payload, {...state})
}
case expenseCategoryActions.REMOVE_EXPENSE_CATEGORY: {
let cat = state.entities[action.payload];
let update: Update<ExpenseCategory> = {
id: action.payload,
changes: {isDeleted:!cat.isDeleted }
}
return expenseCategoryAdapter.updateOne(update,{...state})
}
default: {
return state;
}
}
}
Selector expense-category.selector.ts
import { createSelector } from '@ngrx/store';
import { State } from '../reducers';
import { expenseCategoryAdapter } from '../reducers/expense-category.reducer';
/**
*
* @param s State
*/
export const expenseCategorySelector = (s: State) => s.expenseCategories;
export const selectAllExpenseCategorySelector = createSelector(expenseCategorySelector,expenseCategoryAdapter.getSelectors().selectAll);
export const getAllExpenseCategoriesNotDeleted = createSelector(
selectAllExpenseCategorySelector,
categories => {
return categories.filter(e => !e.isDeleted);
}
);
export const getExpenseCategoryById = (id:string) => createSelector(expenseCategorySelector,state => state.entities[id]);
Service used expense-category.service.ts
import { v4 } from 'uuid';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import {
AddExpenseCategoryAction, RemoveExpenseCategoryAction
} from '../actions/expense-category.actions';
import { ExpenseCategory } from '../models/ExpenseCategory';
import { State } from '../reducers';
import { getAllExpenseCategoriesNotDeleted, selectAllExpenseCategorySelector, getExpenseCategoryById } from '../selectors/expense-category.selector';
@Injectable({
providedIn: 'root'
})
export class ExpenseCategoryService {
constructor(public store: Store<State>) { }
/**
* Add new expense category, by dispatching AddExpenseCategoryAction
* @param name expenseCategoryName
*/
addExpenseCategory(name){
let category: ExpenseCategory = {
name,
isDeleted: false,
id: v4()
}
this.store.dispatch(new AddExpenseCategoryAction(category));
}
/**
* Remove expense category, by dispatching RemoveExpenseCategoryAction
* @param id expenseCategoryId
*/
removeExpenseCategory(id) {
this.store.dispatch(new RemoveExpenseCategoryAction(id));
}
/**
* Get all expense categories, by using selector selectAllExpenseCategorySelector
*/
getAllExpenseCategories(){
return this.store.select(selectAllExpenseCategorySelector);
}
/**
* Get all expense categories not deleted by user, using selector getAllExpenseCategoriesNotDeleted
*/
getAllExpenseCategoriesNotDeleted(){
return this.store.select(getAllExpenseCategoriesNotDeleted);
}
/**
* fetch expense category by id, using selector getExpenseCategoryById
* @param id expenseCategoryId
*/
getExpenseCategoryById(id:string){
return this.store.select(getExpenseCategoryById(id))
}
}
expense-details.component.html
<div class="card">
<div class="card-body">
<h4 class="card-title">Expense details</h4>
<div class="container">
<form>
<div class="form-group row">
<label for="titleInput" class="col-sm-2 col-form-label">Title</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
id="titleInput"
placeholder="item 1"
[(ngModel)]="title"
name="title"
/>
</div>
</div>
<div class="form-group row">
<label for="categoryFormControlSelect" class="col-sm-2 col-form-label">Category select</label>
<div class="col-sm-10">
<select class="form-control" id="categoryFormControlSelect" (change)="onExpenseCategorySelected($event.target.value)" name="category">
<option [value]="cat.id" *ngFor="let cat of expenseCatService.getAllExpenseCategoriesNotDeleted() | async ">{{cat.name}}</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="amountInput" class="col-sm-2 col-form-label"> Amount </label>
<div class="col-sm-10">
<input
type="number"
class="form-control"
id="amountInput"
placeholder="100"
[(ngModel)]="amount"
name="amount"
/>
</div>
</div>
</form>
<div class="button-container">
<button type="button" class="btn btn-primary mr-2" *ngIf="isNewExpense" [disabled]="!isExpenseValid()" (click)="onClickSave()">Create</button>
<button type="button" class="btn btn-primary mr-2" *ngIf="!isNewExpense" (click)="onClickSave()">Save</button>
<button type="button" class="btn btn-secondary mr-2" data-toggle ="modal" data-target="#backConfirmModal"> Back </button>
<!-- Modal -->
<div class="modal fade" id="backConfirmModal" tabindex="-1" role="dialog" aria-labelledby="backConfirmModalId" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"> Unsaved Changes </h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
Are you sure want to discard the changes made?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" (click)="onClickClose()" >Close</button>
<button type="button" class="btn btn-primary" data-dismiss="modal" *ngIf="isExpenseValid" (click)="onClickSave()" >Save</button>
</div>
</div>
</div>
</div>
<button type="button" [disabled]="!isExpenseValid()" *ngIf="!isNewExpense" class="btn btn-danger" data-toggle="modal" data-target="#deleteConfirmModal">Delete</button>
<!-- Modal -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" role="dialog" aria-labelledby="deleteConfirmModalId" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Warning</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
Are you sure to delete the expense?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">No</button>
<button type="button" class="btn btn-primary" data-dismiss="modal" (click)="onClickDelete()">Yes, delete </button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
expense-details.component.css
.card-title{
margin: 16px;
}
.button-container{
float: right;
}
expense-details.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormGroup, FormControl } from '@angular/forms';
import { Expense } from 'src/app/models/Expense';
import { ExpenseCategory } from 'src/app/models/ExpenseCategory';
import { ExpenseCategoryService } from 'src/app/services/expense-category.service';
import { ExpenseService } from 'src/app/services/expense.service';
import { take } from 'rxjs/operators';
@Component({
selector: 'app-expense-details',
templateUrl: './expense-details.component.html',
styleUrls: ['./expense-details.component.scss']
})
export class ExpenseDetailsComponent implements OnInit {
form = new FormGroup({
dateYMD: new FormControl(new Date()),
dateFull: new FormControl(new Date()),
dateMDY: new FormControl(new Date()),
dateRange: new FormControl([new Date(), new Date()])
});
expense: Expense;
title:string;
amount:number;
category:ExpenseCategory;
id:string;
isNewExpense:boolean = false;
constructor(private route: ActivatedRoute, private router: Router,public expenseCatService:ExpenseCategoryService,public expenseService:ExpenseService) { }
/**
* component onInit
* creates new expense item or edit existing expense item by getting params route snapshot
* @param id expenseId
*/
ngOnInit() {
let {params} = this.route.snapshot;
let id = params['id'];
if(id === 'create'){
this.initialiseNewExpense();
this.isNewExpense = true;
} else {
this.id = id;
this.getExpenseById(id);
}
}
/**
* To Create new expense item using ExpenseCatService
*/
initialiseNewExpense(){
this.expenseCatService.getAllExpenseCategories().pipe(take(1)).subscribe(e => {
this.title = '';
this.amount = 0;
this.category = e[0];
});
}
/**
* Edit existing expense by subcribing for getExpenseById()
* @param id expenseId
*/
getExpenseById(id){
this.expenseService.getExpenseById(id).pipe(take(1)).subscribe(exp => {
if(exp){
this.title = exp.title;
this.amount = exp.amount;
this.category = exp.category;
}
});
}
/**
* Validates all expense inputs provided by user
*/
isExpenseValid(){
return this.title && this.amount && this.category && true;
}
/**
* Creates new expense item using addExpense()
*/
createExpense(){
this.expenseService.addExpense(this.title,this.amount,this.category);
}
/**
* Update edited expense item using updateExpense()
*/
updateExpense(){
let exp:Expense = <any>{
id:this.id,
amount:this.amount,
category:this.category,
title:this.title
}
this.expenseService.updateExpense(exp)
}
/**
* Deletes expense item(soft delete)
* @param id expenseId
*/
deleteExpense(){
this.expenseService.toggleExpense(this.id);
}
/**
* To select expense category during creation of expense
* @param id expenseId
*/
onExpenseCategorySelected(id:string){
this.expenseCatService.getExpenseCategoryById(id).pipe(take(1)).subscribe(cat => {
this.category = cat;
})
}
/**
* To save created new expense or updated new expense
*/
save(){
if(this.isNewExpense){
this.createExpense();
} else {
this.updateExpense();
}
}
/**
* Saves edit data of expense item
*/
onClickSave(){
this.save();
this.router.navigate(['/home']);
}
/**
* Closes expense details page, navigate back to home
*/
onClickClose(){
this.router.navigate(['/home']);
}
/**
* Delete expense item, navigate back to home
*/
onClickDelete(){
this.deleteExpense();
this.router.navigate(['/home']);
}
}