Expense - VeenaPD/Expense-Management-System GitHub Wiki

Expense CRUD operations

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:

new expense details

The image below is the screen shot of UI component when user needs to edit new expense item:

edit 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">&times;</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">&times;</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']);
  }
}
⚠️ **GitHub.com Fallback** ⚠️