TU Tran

Technologies should serve for business purpose.

NAVIGATION - SEARCH

[TinyERP: SPA for Enterprise Application]Add new staff

Overview

As mentioned in "Manage Staffs - Part 2" part, It was easy to query the list of staffs as they were prepared before hand.

All necessary preparation steps occurred in "Add/ update staff information".

Let see "Add staff" first.

Add the following line into staffs.html as child element of page-header (place before closing tag of page-header):

<page-actions class="pull-right" [actions]=model.actions></page-actions>

This is the component defines the list of action for each page. We also have actions property for model property. So let add this into StaffsModel class:

 public actions:Array<PageAction>=[];

and also add addAction method:

public addAction(action: PageAction):void{
        this.actions.push(action);
    }

This function was called from Staffs.ts file to add "Create Staff" button, add this line right after creating new instance of StaffsModel:

self.model.addAction(new PageAction("",()=>{self.onAddNewStaffClicked();}).setText("Add new Staff"));

This code means that we will add new page action item for current page, when this item was clicked, it will trigger onAddNewStaffClicked method.

Then we navigate to addNewStaff page:

private onAddNewStaffClicked():void{
        this.navigate(routes.addNewStaff.name);
    }

Do not worry much about routes. this is predefined object from "<modules>/hrm/_share/config/routes.ts". In this file, we defined the list of route and appropriated name. So we use it later rather than using magic number. I always agree that using "routes.staffs.addNew.path" was better than "/hrm/addNewStaff" string.

With newly PageAction added, the UI for "Staffs" has new button on the right of "Manage Staffs":

Register route and add "AddStaff" page into HRMRoute module:

import {AddNewStaff} from "./pages/addNewStaff";
let routeConfigs = [
    { path: "", redirectTo: routes.staffs.path, pathMatch: "full" },
    { path: routes.staffs.path, component: Staffs},
    { path: routes.addNewStaff.path, component: AddNewStaff}
];
@NgModule({
    imports: [CommonModule, FormsModule, RouterModule.forChild(routeConfigs), AppCommon],
    exports: [RouterModule, Staffs],
    declarations: [Staffs, AddNewStaff],
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HrmRoute { }

This code was used as reference. You will get error if run it. Please use source-code from github instead.

And define UI for addStaff page:

<page>
    <page-header>Add new Staff</page-header>
    <page-content>
        <horizontal-form>
            <form-text-input [labelText]="'First Name'" [(model)]=model.firstName></form-text-input>
            <form-text-input [labelText]="'Last Name'" [(model)]=model.lastName></form-text-input>
            <form-primary-button [label]="'Save'" (onClick)="onSaveClicked($event)"></form-primary-button>
            <form-default-button [label]="'Cancel'" (onClick)="onCancelClicked($event)"></form-default-button>
        </horizontal-form>
    </page-content>
</page>

This was traighforward, there is a form with 2 text-input fields. There are also 2 buttons, 1 for cancel "AddStaff" process and 1 for creating new staff (Save button).

The ts file for this page as:

import { Component } from "@angular/core";
import { BasePage } from "@app/common";
import { IStaffService } from "../_share/services/istaffService";
import {AddNewStaffModel} from "./addNewStaffModel";
import {LocalIoCNames} from "../_share/enum";
import routes from "../_share/config/routes";
@Component({
    templateUrl:"src/modules/hrm/pages/addNewStaff.html"
})
export class AddNewStaff extends BasePage<AddNewStaffModel>{
    public model: AddNewStaffModel= new AddNewStaffModel();
    public onSaveClicked():void{
        let service: IStaffService = window.ioc.resolve(LocalIoCNames.IStaffService);
        let self=this;
        service.create(self.model).then(()=>{
            self.navigate(routes.staffs.name);
        });
    }
    public onCancelClicked():void{
        this.navigate(routes.staffs.name);
    }
}

The typescript file for this page quite simple. Just creates new staff and navigates back to "Manage Staffs" page.

Inside create function of StaffService, we make the REST call to remote api and send necessary information to api:

public create(staff: any):Promise{
        let uri="http://localhost:86/api/hrm/staffs";
        let iconnector: IConnector = window.ioc.resolve(IoCNames.IConnector);
        return iconnector.post(uri, staff);
    }

Note: Please replace "localhost:86" by your api host at runtime.

See the API for "Add Staff":

Everything was started from StaffHandler class in TinyERP.HRM project:

[Route("")]
[HttpPost()]
[ResponseWrapper()]
public void CreateStaff(CreateStaffRequest request) {
	this.Execute<CreateStaffRequest, CreateStaffResponse>(request);
}

In TinyERP, All operations (such as: create staff, delete .....) should be converted to appropriated request. Each request needs to contains necessary information for handling that request (such as authentication, ...). This was specified in stateless rule of RESTful web-service.

 what we need to do is calling to "Execute" method. This method will redirect CreateStaffRequest request to IBaseCommandHandler<CreateStaffRequest> handler where will handle this request. Currently, we register handler for this request in "TinyERP.HRM/Command/Bootstrap.cs" class:

container.RegisterTransient<IBaseCommandHandler<CreateStaffRequest, CreateStaffResponse>, StaffCommandHandler>();

This mean that, StaffCommandHandler class subscribes for incomming CreateStaffRequest and return back CreateStaffRespone.

By default, there is not Execute method for normal api controller, we need to let StaffHandler inherits from CommandHandlerController<AggregateType>, AggregateType was "Staff" class in this case.

 public class StaffHandler: CommandHandlerController<TinyERP.HRM.Aggregate.Staff>

Inside StaffCommandHandler, we have Handle method which receives CreateStaffRequest as its parameter. This is where CreateStaffRequest was actually processed:

public CreateStaffResponse Handle(CreateStaffRequest command)
{
	this.Validate(command);
	using (IUnitOfWork uow = this.CreateUnitOfWork<TinyERP.HRM.Aggregate.Staff>()) {
		TinyERP.HRM.Aggregate.Staff staff = new Aggregate.Staff();
		staff.UpdateBasicInfo(command);
		IStaffRepository repository = IoC.Container.Resolve<IStaffRepository>(uow);
		repository.Add(staff);
		uow.Commit();
		staff.PublishEvents();
		return ObjectHelper.Cast<CreateStaffResponse>(staff);
	}
}

Look at the code, there are some points you may not understand or confused, such as: validation, ....

Ok, do not worry, I was not intended to talk everything in 1 single article. Just ignore for now, I mainly want to show you the basic flow for creating new staff. In later articles, we will go in detail each aspect, such as: error handling, ...

In this case, we just create new instance of Staff aggregate and call appropriated method to update basic information and save into MSSQL database.

Why don't we set properties for Staff instance directly, I mean without calling to UpdateBasicInfo.

This was related to DDD (Domain Drivent Design). I will not go beyond the scope of this article. Please search other article about this on my blog (http://www.tranthanhtu.vn) for more information.

So Staff we can consider as a domain, beside the basic staff information, we can have other information (such as: working history, salary, department, working status, ...). This was belong how you break you app into smaller domain.

When we want to change information of Staff domain, let prepare appropriated request (or command as called in CQRS) and send that request to system. Aggregate of Staff domain (it was Staff class in this case) will handle that request and update appropriated information and raise necessary event.

staff.PublishEvents();

Why do we need those events and what are they?

In CQRS, we have seperated Write and Read side. Write side is where we store data for future validation and read-side, we store de-normalized data in necessary format, This will reduce amount of time for reading this data in the future. Please search for CQRS article on my blog (http://www.tranthanhtu.vn) as I will not go beyond in this article.

So, it means that when we modify data in write-side, we also need to update those changes into read-side.

Look at Staff class, we can see that, there is new event added for every-time we change the properties of Staff class.

public Staff()
{
	this.AddEvent(new OnStaffCreated(this.Id));
}

and

internal void UpdateBasicInfo(CreateStaffRequest command)
{
	//....
	this.AddEvent(new OnStaffBasicInforChanged(this.Id, this.FirstName, this.LastName, this.Email));
}

 

Those events will be handled in StaffEventHandler. We did in the similar way as StaddCommandHandler, so please read the code yourself.

The result for running "Add Staff" feature as:

Enter information for new staff:

Data for this staff was stored on MSSQL (Write side) database:

And also stored on read-side (using mongodb):

Then, we can see the list of staffs on "Manage Staffs" page:

Summary for this article:

  • The application should be broken into smaller isolated domain.
  • Each operations (modify data of domain) need to be converted as request (or command as called in CQRS).
  • Aggregate of domain will subscribe and handle appropriated request then also raises necessary events.
  • We can listen on those events and update data on read-side or perform other actions (such as: send activation email for new registered member, ....)
  • Data was stored in de-normalized format as need and can be read quickly.

For more information about source-code for this part, please have a look at https://github.com/tranthanhtu0vn/TinyERP (in feature/add_staff branch).

 

Other articles in series

Thank you for reading,

 

Comments are closed