Design center module
diff --git a/src/app/centers/center-exists.guard.ts b/src/app/centers/center-exists.guard.ts
new file mode 100644
index 0000000..6936b08
--- /dev/null
+++ b/src/app/centers/center-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import {OfficeService} from '../services/office/office.service';
+import * as fromEmployees from './store';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from './store/employee.actions';
+import {of} from 'rxjs/observable/of';
+import {EmployeesStore} from './store/index';
+import {ExistsGuardService} from '../common/guards/exists-guard';
+
+@Injectable()
+export class CenterExistsGuard implements CanActivate {
+
+ constructor(private store: EmployeesStore,
+ private officeService: OfficeService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasEmployeeInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromEmployees.getEmployeesLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasEmployeeInApi(id: string): Observable<boolean> {
+ const getEmployee$ = this.officeService.getEmployee(id)
+ .map(employeeEntity => new LoadAction({
+ resource: employeeEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(employee => !!employee);
+
+ return this.existsGuardService.routeTo404OnError(getEmployee$);
+ }
+
+ hasEmployee(id: string): Observable<boolean> {
+ return this.hasEmployeeInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasEmployeeInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasEmployee(route.params['id']);
+ }
+}
diff --git a/src/app/centers/center.component.html b/src/app/centers/center.component.html
new file mode 100644
index 0000000..bd1065a
--- /dev/null
+++ b/src/app/centers/center.component.html
@@ -0,0 +1,15 @@
+<fims-layout-card-over title="{{'Manage centers' | translate}}">
+ <fims-layout-card-over-header-menu>
+ <td-search-box #searchBox placeholder="{{'Search' | translate}}" (search)="search($event)" [alwaysVisible]="false"></td-search-box>
+ </fims-layout-card-over-header-menu>
+ <fims-data-table flex
+ (onFetch)="fetchEmployees($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="employeeData$ | async"
+ [loading]="loading$ | async"
+ [sortable]="true"
+ [pageable]="true">
+ </fims-data-table>
+ </fims-layout-card-over>
+ <fims-fab-button title="{{'Create new center' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'office_employees', accessLevel: 'CHANGE'}"></fims-fab-button>
\ No newline at end of file
diff --git a/src/app/centers/center.component.scss b/src/app/centers/center.component.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/app/centers/center.component.scss
diff --git a/src/app/centers/center.component.spec.ts b/src/app/centers/center.component.spec.ts
new file mode 100644
index 0000000..da7698d
--- /dev/null
+++ b/src/app/centers/center.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CenterComponent } from './center.component';
+
+describe('CenterComponent', () => {
+ let component: CenterComponent;
+ let fixture: ComponentFixture<CenterComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ CenterComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CenterComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/centers/center.component.ts b/src/app/centers/center.component.ts
new file mode 100644
index 0000000..ab58913
--- /dev/null
+++ b/src/app/centers/center.component.ts
@@ -0,0 +1,72 @@
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Params, Router} from '@angular/router';
+import {Employee} from '../services/office/domain/employee.model';
+import {FetchRequest} from '../services/domain/paging/fetch-request.model';
+import {TableData} from '../common/data-table/data-table.component';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../store';
+import {Observable} from 'rxjs/Observable';
+import {SEARCH} from '../store/employee/employee.actions';
+
+
+
+@Component({
+ selector: 'fims-center',
+ templateUrl: './center.component.html',
+ styleUrls: ['./center.component.scss']
+})
+export class CenterComponent implements OnInit {
+
+ employeeData$: Observable<TableData>;
+
+ loading$: Observable<boolean>;
+
+ columns: any[] = [
+ { name: 'name', label: 'Name' },
+ { name: 'accountNumber', label: 'Account #' },
+ { name: 'id', label: 'External Id' },
+ { name: 'status', label: 'Status' },
+ { name: 'office', label: 'Office' }
+ ];
+
+ searchTerm: string;
+
+ private lastFetchRequest: FetchRequest = {};
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: Store<fromRoot.State>) {}
+
+ ngOnInit(): void {
+
+ this.employeeData$ = this.store.select(fromRoot.getEmployeeSearchResults)
+ .map(employeePage => ({
+ data: employeePage.employees,
+ totalElements: employeePage.totalElements,
+ totalPages: employeePage.totalPages
+ }));
+
+ this.loading$ = this.store.select(fromRoot.getEmployeeSearchLoading);
+
+ this.route.queryParams.subscribe((params: Params) => {
+ this.search(params['term']);
+ });
+ }
+
+ search(searchTerm: string): void {
+ this.searchTerm = searchTerm;
+ this.fetchEmployees();
+ }
+
+ rowSelect(row: Employee): void {
+ this.router.navigate(['detail', row.identifier], { relativeTo: this.route });
+ }
+
+ fetchEmployees(fetchRequest?: FetchRequest) {
+ if (fetchRequest) {
+ this.lastFetchRequest = fetchRequest;
+ }
+
+ this.lastFetchRequest.searchTerm = this.searchTerm;
+
+ this.store.dispatch({ type: SEARCH, payload: this.lastFetchRequest });
+ }
+}
diff --git a/src/app/centers/center.module.ts b/src/app/centers/center.module.ts
new file mode 100644
index 0000000..10ded43
--- /dev/null
+++ b/src/app/centers/center.module.ts
@@ -0,0 +1,66 @@
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {CenterComponent} from './center.component';
+import {CenterRoutes} from './center.routing';
+import {CenterFormComponent} from './form/form.component';
+import {CreateCenterFormComponent} from './form/create/create.form.component';
+import {EmployeeDetailComponent} from './detail/employee.detail.component';
+import {EditEmployeeFormComponent} from './form/edit/edit.form.component';
+import {UserResolver} from './user.resolver';
+import {FimsSharedModule} from '../common/common.module';
+import {CenterExistsGuard} from './center-exists.guard';
+import {Store} from '@ngrx/store';
+import {EmployeesStore, employeeStoreFactory} from './store/index';
+import {EmployeeNotificationEffects} from './store/effects/notification.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {EmployeeApiEffects} from './store/effects/service.effects';
+import {EmployeeRouteEffects} from './store/effects/route.effects';
+import {
+ MatButtonModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatOptionModule,
+ MatSelectModule,
+ MatToolbarModule
+} from '@angular/material';
+import {CovalentSearchModule, CovalentStepsModule} from '@covalent/core';
+import {TranslateModule} from '@ngx-translate/core';
+import {CommonModule} from '@angular/common';
+import {ReactiveFormsModule} from '@angular/forms';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(CenterRoutes),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ CommonModule,
+ TranslateModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatOptionModule,
+ MatInputModule,
+ MatButtonModule,
+ MatSelectModule,
+ CovalentSearchModule,
+ CovalentStepsModule,
+
+ EffectsModule.run(EmployeeApiEffects),
+ EffectsModule.run(EmployeeRouteEffects),
+ EffectsModule.run(EmployeeNotificationEffects)
+ ],
+ declarations: [
+ CenterComponent,
+ CenterFormComponent,
+ CreateCenterFormComponent,
+ EditEmployeeFormComponent,
+ EmployeeDetailComponent
+ ],
+ providers: [
+ UserResolver,
+ CenterExistsGuard,
+ { provide: EmployeesStore, useFactory: employeeStoreFactory, deps: [Store]}
+ ]
+})
+ export class CenterModule {}
\ No newline at end of file
diff --git a/src/app/centers/center.routing.ts b/src/app/centers/center.routing.ts
new file mode 100644
index 0000000..3b7718a
--- /dev/null
+++ b/src/app/centers/center.routing.ts
@@ -0,0 +1,34 @@
+import {Routes} from '@angular/router';
+import {CenterComponent} from './center.component';
+import {CreateCenterFormComponent} from './form/create/create.form.component';
+import {EmployeeDetailComponent} from './detail/employee.detail.component';
+import {EditEmployeeFormComponent} from './form/edit/edit.form.component';
+import {UserResolver} from './user.resolver';
+import {CenterExistsGuard} from './center-exists.guard';
+
+export const CenterRoutes: Routes = [
+ {
+ path: '',
+ component: CenterComponent,
+ data: {title: 'Manage Employees', hasPermission: {id: 'office_employees', accessLevel: 'READ'}}
+ },
+ {
+ path: 'create',
+ component: CreateCenterFormComponent,
+ data: {title: 'Create Employee', hasPermission: {id: 'office_employees', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'detail/:id/edit',
+ component: EditEmployeeFormComponent,
+ canActivate: [CenterExistsGuard],
+ resolve: {user: UserResolver},
+ data: {title: 'Edit Employee', hasPermission: {id: 'office_employees', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'detail/:id',
+ component: EmployeeDetailComponent,
+ canActivate: [CenterExistsGuard],
+ resolve: {user: UserResolver},
+ data: {title: 'View Employee', hasPermission: {id: 'office_employees', accessLevel: 'READ'}}
+ }
+];
diff --git a/src/app/centers/detail/employee.detail.component.html b/src/app/centers/detail/employee.detail.component.html
new file mode 100644
index 0000000..f4d6824
--- /dev/null
+++ b/src/app/centers/detail/employee.detail.component.html
@@ -0,0 +1,47 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{employee.givenName}} {{employee.surname}}" [navigateBackTo]="'/employees'">
+ <fims-layout-card-over-header-menu layout="row" layout-align="end center">
+ <td-search-box placeholder="{{'Search' | translate}}" (search)="searchEmployee($event)" [alwaysVisible]="false"></td-search-box>
+ <button mat-icon-button (click)="deleteEmployee()" title="{{'Delete this employee' | translate}}" *hasPermission="{ id: 'office_employees', accessLevel: 'DELETE' }"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <mat-list>
+ <h3 mat-subheader translate>Assigned office</h3>
+ <mat-list-item>
+ <mat-icon matListAvatar>store</mat-icon>
+ <h3 matLine *ngIf="employee.assignedOffice">{{employee.assignedOffice}}</h3>
+ <h3 matLine *ngIf="!employee.assignedOffice" translate>No office assigned</h3>
+ </mat-list-item>
+ <h3 mat-subheader translate>Assigned role</h3>
+ <mat-list-item>
+ <mat-icon matListAvatar>lock</mat-icon>
+ <h3 matLine>{{user.role}}</h3>
+ </mat-list-item>
+ <h3 mat-subheader translate>Contact Information</h3>
+ <mat-list-item [ngSwitch]="detail.type" *ngFor="let detail of employee.contactDetails">
+ <mat-icon *ngSwitchCase="'EMAIL'" matListAvatar>email</mat-icon>
+ <mat-icon *ngSwitchCase="'PHONE'" matListAvatar>phone</mat-icon>
+ <mat-icon *ngSwitchCase="'MOBILE'" matListAvatar>smartphone</mat-icon>
+ <h3 matLine>{{detail.value}}</h3>
+ </mat-list-item>
+ <mat-list-item *ngIf="!employee.contactDetails.length">
+ <h3 matLine translate>No contact details available</h3>
+ </mat-list-item>
+ </mat-list>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit member ' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'office_employees', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/centers/detail/employee.detail.component.ts b/src/app/centers/detail/employee.detail.component.ts
new file mode 100644
index 0000000..c019833
--- /dev/null
+++ b/src/app/centers/detail/employee.detail.component.ts
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRoute, Router} from '@angular/router';
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Center} from '../../services/office/domain/employee.model';
+import {TdDialogService} from '@covalent/core';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {User} from '../../services/identity/domain/user.model';
+import * as fromEmployee from '../store';
+import {DELETE, SelectAction} from '../store/employee.actions';
+import {EmployeesStore} from '../store/index';
+
+@Component({
+ selector: 'fims-employee-detail',
+ templateUrl: './employee.detail.component.html'
+})
+export class EmployeeDetailComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ private employeeSubscription: Subscription;
+
+ employee: Center;
+
+ user: User;
+
+ constructor(private route: ActivatedRoute, private router: Router, private dialogService: TdDialogService,
+ private store: EmployeesStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+
+ this.employeeSubscription = this.store.select(fromEmployee.getSelectedEmployee)
+ .filter(employee => !!employee)
+ .subscribe(employee => this.employee = employee);
+
+ // TODO load user via store
+ this.route.data.subscribe(( data: { user: User }) => {
+ this.user = data.user;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ this.employeeSubscription.unsubscribe();
+ }
+
+ searchEmployee(term): void {
+ if (!term) {
+ return;
+ }
+ this.goToOverviewPage(term);
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete employee "' + this.employee.givenName + ' ' + this.employee.surname + '"?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE EMPLOYEE',
+ }).afterClosed();
+ }
+
+ deleteEmployee(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch({ type: DELETE, payload: {
+ employee: this.employee,
+ activatedRoute: this.route
+ } });
+ });
+ }
+
+ goToOverviewPage(term?: string): void {
+ this.router.navigate(['../../'], { queryParams: { term: term }, relativeTo: this.route });
+ }
+}
diff --git a/src/app/centers/form/create/create.form.component.html b/src/app/centers/form/create/create.form.component.html
new file mode 100644
index 0000000..16a62e0
--- /dev/null
+++ b/src/app/centers/form/create/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new center' | translate}}">
+ <fims-center-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [formData]="centerFormData">
+ </fims-center-form-component>
+</fims-layout-card-over>
diff --git a/src/app/centers/form/create/create.form.component.spec.ts b/src/app/centers/form/create/create.form.component.spec.ts
new file mode 100644
index 0000000..0475674
--- /dev/null
+++ b/src/app/centers/form/create/create.form.component.spec.ts
@@ -0,0 +1,130 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {CenterFormComponent, CenterSaveEvent} from '../form.component';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {CreateCenterFormComponent} from './create.form.component';
+import {mapEmployee, mapUser} from '../form.mapper';
+import {EmployeesStore} from '../../store/index';
+import {CREATE} from '../../store/employee.actions';
+import {Store} from '@ngrx/store';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {MatCardModule, MatInputModule, MatOptionModule, MatSelectModule} from '@angular/material';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {FimsSharedModule} from '../../../common/common.module';
+
+const eventMock: CenterSaveEvent = {
+ detailForm: {
+ identifier: 'test',
+ firstName: 'test',
+ middleName: 'test',
+ lastName: 'test',
+ password: 'test',
+ role: 'test'
+ },
+ contactForm: {
+ email: 'test',
+ mobile: 'test',
+ phone: 'test'
+ },
+
+ officeForm: {
+ assignedOffice: 'test'
+ }
+};
+
+let router: Router;
+
+describe('Test employee form component', () => {
+
+ let fixture: ComponentFixture<CreateCenterFormComponent>;
+
+ let testComponent: CreateCenterFormComponent;
+
+ beforeEach(() => {
+ router = jasmine.createSpyObj('Router', ['navigate']);
+
+ TestBed.configureTestingModule({
+ declarations: [
+ CenterFormComponent,
+ CreateCenterFormComponent,
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatInputModule,
+ MatCardModule,
+ MatSelectModule,
+ MatOptionModule,
+ CovalentStepsModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ { provide: Router, useValue: router},
+ { provide: ActivatedRoute, useValue: {} },
+ {
+ provide: Store, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }},
+ {
+ provide: EmployeesStore, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }
+ }
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ });
+
+ fixture = TestBed.createComponent(CreateCenterFormComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should test if employee is created', async(inject([EmployeesStore], (store: EmployeesStore) => {
+ fixture.detectChanges();
+
+ testComponent.onSave(eventMock);
+
+ fixture.whenStable().then(() => {
+ const employee = mapEmployee(eventMock);
+ const user = mapUser(eventMock);
+
+ expect(store.dispatch).toHaveBeenCalledWith({ type: CREATE, payload: {
+ employee: employee,
+ user: user,
+ activatedRoute: {}
+ }});
+ });
+ })));
+
+ xit('should test if error is set on 409', async(() => {
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(testComponent.formComponent.detailForm.get('identifier').errors).toBeDefined();
+ expect(testComponent.formComponent.detailForm.get('identifier').errors['unique']).toBeTruthy();
+ });
+ }));
+});
diff --git a/src/app/centers/form/create/create.form.component.ts b/src/app/centers/form/create/create.form.component.ts
new file mode 100644
index 0000000..f5c1d0a
--- /dev/null
+++ b/src/app/centers/form/create/create.form.component.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {CenterFormComponent, CenterFormData, CenterSaveEvent} from '../form.component';
+import {mapEmployee, mapUser} from '../form.mapper';
+import {Center} from '../../../services/office/domain/employee.model';
+import {UserWithPassword} from '../../../services/identity/domain/user-with-password.model';
+import * as fromEmployees from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+import {CREATE, RESET_FORM} from '../../store/employee.actions';
+import {Error} from '../../../services/domain/error.model';
+import {EmployeesStore} from '../../store/index';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateCenterFormComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ @ViewChild('form') formComponent: CenterFormComponent;
+
+ centerFormData: CenterFormData = {
+ user: { identifier: '', role: ''},
+ employee: { identifier: '', givenName: '', surname: '', contactDetails: [] }
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: EmployeesStore) {}
+
+ ngOnInit(): void {
+ this.formStateSubscription = this.store.select(fromEmployees.getEmployeeFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => {
+ const detailForm = this.formComponent.detailForm;
+ const errors = detailForm.get('identifier').errors || {};
+ errors['unique'] = true;
+ detailForm.get('identifier').setErrors(errors);
+ this.formComponent.step.open();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(event: CenterSaveEvent): void {
+ const employee: Center = mapEmployee(event);
+ const user: UserWithPassword = mapUser(event);
+
+ this.store.dispatch({ type: CREATE, payload: {
+ employee: employee,
+ user: user,
+ activatedRoute: this.route
+ }});
+
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/centers/form/edit/edit.form.component.html b/src/app/centers/form/edit/edit.form.component.html
new file mode 100644
index 0000000..5750425
--- /dev/null
+++ b/src/app/centers/form/edit/edit.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit employee' | translate}}">
+ <fims-center-form-component #form
+ [editMode]="true"
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [formData]="formData | async">
+ </fims-center-form-component>
+</fims-layout-card-over>
diff --git a/src/app/centers/form/edit/edit.form.component.spec.ts b/src/app/centers/form/edit/edit.form.component.spec.ts
new file mode 100644
index 0000000..c3dd78b
--- /dev/null
+++ b/src/app/centers/form/edit/edit.form.component.spec.ts
@@ -0,0 +1,141 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {EditEmployeeFormComponent} from './edit.form.component';
+import {CenterFormComponent} from '../form.component';
+import {ActivatedRoute, Router} from '@angular/router';
+import {User} from '../../../services/identity/domain/user.model';
+import {Center} from '../../../services/office/domain/employee.model';
+import {Observable} from 'rxjs/Observable';
+import {EmployeesStore} from '../../store/index';
+import {Store} from '@ngrx/store';
+import {UPDATE} from '../../store/employee.actions';
+import * as fromEmployees from '../../store';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {MatCardModule, MatInputModule, MatOptionModule, MatSelectModule} from '@angular/material';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {FimsSharedModule} from '../../../common/common.module';
+
+const userMock: User = {
+ identifier: 'test',
+ role: 'test'
+};
+
+const employeeMock: Center = {
+ identifier: 'test',
+ assignedOffice: 'test',
+ givenName: 'test',
+ middleName: 'test',
+ surname: 'test',
+ contactDetails: [
+ {
+ group: 'BUSINESS',
+ type: 'EMAIL',
+ value: 'test',
+ preferenceLevel: 1
+ }
+ ]
+
+};
+
+const activatedRoute = {
+ data: Observable.of({
+ user: userMock
+ })
+};
+let router: Router;
+
+describe('Test employee form component', () => {
+
+ let fixture: ComponentFixture<EditEmployeeFormComponent>;
+
+ let testComponent: EditEmployeeFormComponent;
+
+ beforeEach(() => {
+ router = jasmine.createSpyObj('Router', ['navigate']);
+
+ TestBed.configureTestingModule({
+ declarations: [
+ CenterFormComponent,
+ EditEmployeeFormComponent,
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatInputModule,
+ MatCardModule,
+ MatSelectModule,
+ MatOptionModule,
+ CovalentStepsModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ { provide: Router, useValue: router},
+ { provide: ActivatedRoute, useValue: activatedRoute },
+ {
+ provide: Store, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }
+ },
+ {
+ provide: EmployeesStore, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.callFake(selector => {
+ if (selector === fromEmployees.getSelectedEmployee) {
+ return Observable.of(employeeMock);
+ }
+
+ return Observable.empty();
+ });
+ }
+ }
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ });
+
+ fixture = TestBed.createComponent(EditEmployeeFormComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should test if employee is updated', async(inject([EmployeesStore], (store: EmployeesStore) => {
+ fixture.detectChanges();
+
+ testComponent.formComponent.detailForm.get('password').setValue('newPassword');
+
+ fixture.detectChanges();
+
+ testComponent.formComponent.save();
+
+ fixture.whenStable().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith({ type: UPDATE, payload: {
+ employee: employeeMock,
+ contactDetails: employeeMock.contactDetails,
+ role: userMock.role,
+ password: 'newPassword',
+ activatedRoute: activatedRoute
+ }});
+ });
+
+ })));
+});
diff --git a/src/app/centers/form/edit/edit.form.component.ts b/src/app/centers/form/edit/edit.form.component.ts
new file mode 100644
index 0000000..f0ab243
--- /dev/null
+++ b/src/app/centers/form/edit/edit.form.component.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {CenterFormComponent, CenterFormData, CenterSaveEvent} from '../form.component';
+import {mapContactDetails, mapEmployee} from '../form.mapper';
+import {Center} from '../../../services/office/domain/employee.model';
+import {User} from '../../../services/identity/domain/user.model';
+import {UPDATE} from '../../store/employee.actions';
+import {Observable} from 'rxjs/Observable';
+import {EmployeesStore, getSelectedEmployee} from '../../store/index';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditEmployeeFormComponent implements OnInit {
+
+ @ViewChild('form') formComponent: CenterFormComponent;
+
+ formData: Observable<CenterFormData>;
+
+ employee: Center;
+
+ user: User;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: EmployeesStore) {}
+
+ ngOnInit() {
+ this.formData = Observable.combineLatest(
+ this.store.select(getSelectedEmployee),
+ this.route.data,
+ (employee: Center, data: { user: User }) => {
+ return {
+ user: data.user,
+ employee: employee
+ };
+ }
+ );
+ }
+
+ onSave(event: CenterSaveEvent) {
+ const employee: Center = mapEmployee(event);
+
+ this.store.dispatch({ type: UPDATE, payload: {
+ employee: employee,
+ contactDetails: mapContactDetails(event.contactForm),
+ role: event.detailForm.role,
+ password: event.detailForm.password,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.navigateToOffice();
+ }
+
+ private navigateToOffice() {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/centers/form/form.component.html b/src/app/centers/form/form.component.html
new file mode 100644
index 0000000..43ff2d4
--- /dev/null
+++ b/src/app/centers/form/form.component.html
@@ -0,0 +1,88 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Center details' | translate}}" [state]="detailForm.valid ? 'complete' : detailForm.pristine ? 'none' : 'required'">
+ <form [formGroup]="detailForm" layout="column">
+ <fims-id-input [form]="detailForm" placeholder="External id" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="detailForm" controlName="firstName" placeholder="{{'Name' | translate}}"></fims-text-input>
+
+
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="officeStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step #officeStep label="{{'Assign office to center' | translate}}"
+ [state]="officeForm.get('assignedOffice').value ? 'complete' : 'none'">
+
+ <fims-select-list #officeList flex
+ [data]="offices"
+ id="identifier"
+ listIcon="store"
+ [preSelection]="officeForm.get('assignedOffice').value ? [officeForm.get('assignedOffice').value] : []"
+ (onSearch)="searchOffice($event)"
+ (onSelectionChange)="assignOffice($event)"
+ title="{{'Assigned Office' | translate}}"
+ noResultsMessage="{{'No office was found.' | translate}}"
+ noSelectionMessage="{{'No office assigned to employee, yet.' | translate}}">
+ </fims-select-list>
+<br/>
+ <mat-form-field>
+ <mat-select placeholder="Select Staff">
+ <mat-option *ngFor="let staff of staffs" [value]="staff.value">
+ {{staff.viewValue}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+
+
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="contactStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step #officeStep label="{{'Assign group to center' | translate}}"
+ [state]="officeForm.get('assignedOffice').value ? 'complete' : 'none'">
+
+ <fims-select-list #officeList flex
+
+ title="{{'Assigned Group' | translate}}"
+ noResultsMessage="{{'No group was found.' | translate}}"
+ noSelectionMessage="{{'No group assigned to center, yet.' | translate}}">
+ </fims-select-list>
+
+
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="contactStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'CENTER'"
+ [editMode]="editMode"
+ [disabled]="formsInvalid()"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+
+</td-steps>
diff --git a/src/app/centers/form/form.component.spec.ts b/src/app/centers/form/form.component.spec.ts
new file mode 100644
index 0000000..621eefa
--- /dev/null
+++ b/src/app/centers/form/form.component.spec.ts
@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {Component, EventEmitter, ViewChild} from '@angular/core';
+import {Center} from '../../services/office/domain/employee.model';
+import {CenterFormComponent, CenterFormData, CenterSaveEvent} from './form.component';
+import {User} from '../../services/identity/domain/user.model';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {Observable} from 'rxjs/Observable';
+import {Store} from '@ngrx/store';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {FimsSharedModule} from '../../common/common.module';
+import {MatIconModule, MatInputModule, MatOptionModule, MatSelectModule} from '@angular/material';
+
+const employeeTemplate: Center = {
+ identifier: 'test',
+ givenName: 'test',
+ middleName: 'test',
+ surname: 'test',
+ contactDetails: [{
+ type: 'EMAIL',
+ group: 'BUSINESS',
+ value: 'test@test.de',
+ preferenceLevel: 0
+ }],
+ assignedOffice: 'test'
+};
+
+const userTemplate: User = {
+ identifier: 'test',
+ role: 'test'
+};
+
+describe('Test employee form component', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TestComponent,
+ CenterFormComponent
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatSelectModule,
+ MatOptionModule,
+ MatIconModule,
+ MatInputModule,
+ CovalentStepsModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ {
+ provide: Store, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }}
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should test if the form save the original values', () => {
+ fixture.detectChanges();
+
+ testComponent.saveEmitter.subscribe((saveEvent: CenterSaveEvent) => {
+ expect(employeeTemplate.identifier).toEqual(saveEvent.detailForm.identifier);
+ expect(employeeTemplate.givenName).toEqual(saveEvent.detailForm.firstName);
+ expect(employeeTemplate.middleName).toEqual(saveEvent.detailForm.middleName);
+ expect(employeeTemplate.surname).toEqual(saveEvent.detailForm.lastName);
+ expect(saveEvent.detailForm.password).toEqual('');
+
+ expect(employeeTemplate.assignedOffice).toEqual(saveEvent.officeForm.assignedOffice);
+
+ expect(employeeTemplate.contactDetails.length).toEqual(1);
+ expect(employeeTemplate.contactDetails[0].value).toEqual(saveEvent.contactForm.email);
+
+ expect(userTemplate.role).toEqual(saveEvent.detailForm.role);
+ });
+
+ testComponent.triggerSave();
+
+ });
+});
+
+@Component({
+ template: `
+ <fims-employee-form-component #form (onSave)="onSave($event)" (onCancel)="onCancel($event)" [formData]="employeeFormData">
+ </fims-employee-form-component>`
+})
+class TestComponent {
+
+ saveEmitter = new EventEmitter<CenterSaveEvent>();
+
+ @ViewChild('form') formComponent: CenterFormComponent;
+
+ employeeFormData: CenterFormData = {
+ employee: employeeTemplate,
+ user: userTemplate
+ };
+
+ triggerSave(): void {
+ this.formComponent.save();
+ }
+
+ onSave(event: CenterSaveEvent): void {
+ this.saveEmitter.emit(event);
+ }
+
+}
diff --git a/src/app/centers/form/form.component.ts b/src/app/centers/form/form.component.ts
new file mode 100644
index 0000000..6f1e6cc
--- /dev/null
+++ b/src/app/centers/form/form.component.ts
@@ -0,0 +1,189 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormBuilder, FormGroup, ValidatorFn, Validators} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+import {TdStepComponent} from '@covalent/core';
+import {Office} from '../../services/office/domain/office.model';
+import {Center} from '../../services/office/domain/employee.model';
+import {BUSINESS, ContactDetail, EMAIL, MOBILE, PHONE} from '../../services/domain/contact/contact-detail.model';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Role} from '../../services/identity/domain/role.model';
+import {User} from '../../services/identity/domain/user.model';
+import {FimsValidators} from '../../common/validator/validators';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../../store';
+import {SEARCH as SEARCH_OFFICE} from '../../store/office/office.actions';
+import {SEARCH as SEARCH_ROLE} from '../../store/role/role.actions';
+
+export interface CenterFormData {
+ user: User;
+ employee: Center;
+}
+
+export interface CenterSaveEvent {
+ detailForm: {
+ identifier: string;
+ firstName: string;
+ middleName: string;
+ lastName: string;
+ password: string;
+ role: string;
+ };
+ contactForm: {
+ email: string;
+ phone: string;
+ mobile: string;
+ };
+
+ officeForm: {
+ assignedOffice: string;
+ };
+}
+
+@Component({
+ selector: 'fims-center-form-component',
+ templateUrl: './form.component.html'
+})
+export class CenterFormComponent implements OnInit {
+
+ offices: Observable<Office[]>;
+
+ roles: Observable<Role[]>;
+
+ detailForm: FormGroup;
+ contactForm: FormGroup;
+ officeForm: FormGroup;
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @Input('editMode') editMode: boolean;
+
+ @Input('formData') set formData(formData: CenterFormData) {
+ this.prepareDetailForm(formData.employee, formData.user);
+ this.prepareOfficeForm(formData.employee);
+ this.prepareContactForm(formData.employee.contactDetails);
+ }
+
+ @Output('onSave') onSave = new EventEmitter<CenterSaveEvent>();
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder, private store: Store<fromRoot.State>) {}
+
+ ngOnInit(): void {
+ this.offices = this.store.select(fromRoot.getOfficeSearchResults)
+ .map(officePage => officePage.offices);
+
+ this.roles = this.store.select(fromRoot.getRoleSearchResults)
+ .map(rolesPage => rolesPage.roles);
+
+ this.fetchRoles();
+
+ this.step.open();
+ }
+
+ prepareDetailForm(employee: Center, user: User): void {
+ const passwordValidators: ValidatorFn[] = [Validators.minLength(8)];
+
+ if (!this.editMode) {
+ passwordValidators.push(Validators.required);
+ }
+
+ this.detailForm = this.formBuilder.group({
+ identifier: [employee.identifier, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ firstName: [employee.givenName, [Validators.required, Validators.maxLength(256)]],
+ middleName: [employee.middleName, Validators.maxLength(256)],
+ lastName: [employee.surname, [Validators.required, Validators.maxLength(256)]],
+ password: ['', passwordValidators],
+ role: [user ? user.role : '', Validators.required]
+ });
+ }
+
+ private prepareOfficeForm(employee: Center) {
+ this.officeForm = this.formBuilder.group({
+ assignedOffice: [employee.assignedOffice]
+ });
+ }
+
+ private prepareContactForm(contactDetails: ContactDetail[]): void {
+ let phone = '';
+ let mobile = '';
+ let email = '';
+
+ const businessContacts: ContactDetail[] = contactDetails.filter(contactDetail => contactDetail.group === BUSINESS);
+
+ if (businessContacts.length) {
+ phone = this.getFirstItemByType(businessContacts, PHONE);
+ mobile = this.getFirstItemByType(businessContacts, MOBILE);
+ email = this.getFirstItemByType(businessContacts, EMAIL);
+ }
+
+ this.contactForm = this.formBuilder.group({
+ email: [email, [Validators.maxLength(256), FimsValidators.email]],
+ phone: [phone, Validators.maxLength(256)],
+ mobile: [mobile, Validators.maxLength(256)]
+ });
+ }
+
+
+ getFirstItemByType(contactDetails: ContactDetail[], type: string): string {
+ const items = contactDetails.filter(contact => contact.type === type);
+ return items.length ? items[0].value : '';
+ }
+
+ formsInvalid(): boolean {
+ return (!this.officeForm.pristine && this.officeForm.invalid) ||
+ (!this.contactForm.pristine && this.contactForm.invalid)
+ || this.detailForm.invalid;
+ }
+
+
+
+ save(): void {
+ this.onSave.emit({
+ detailForm: this.detailForm.value,
+ contactForm: this.contactForm.value,
+ officeForm: this.officeForm.value
+ });
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ searchOffice(searchTerm: string): void {
+ const fetchRequest: FetchRequest = {
+ searchTerm
+ };
+ this.store.dispatch({ type: SEARCH_OFFICE, payload: fetchRequest });
+ }
+
+ assignOffice(selections: string[]): void {
+ this.setFormValue(this.officeForm, {'assignedOffice': selections && selections.length > 0 ? selections[0] : undefined});
+ }
+
+ fetchRoles(): void {
+ this.store.dispatch({ type: SEARCH_ROLE });
+ }
+
+ private setFormValue(form: FormGroup, value: any) {
+ form.setValue(value);
+ form.markAsDirty();
+ }
+}
diff --git a/src/app/centers/form/form.mapper.ts b/src/app/centers/form/form.mapper.ts
new file mode 100644
index 0000000..c74aab3
--- /dev/null
+++ b/src/app/centers/form/form.mapper.ts
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {CenterSaveEvent} from './form.component';
+import {ContactDetail, ContactDetailType} from '../../services/domain/contact/contact-detail.model';
+import {Center} from '../../services/office/domain/employee.model';
+import {UserWithPassword} from '../../services/identity/domain/user-with-password.model';
+
+function buildContactDetail(type: ContactDetailType, value: string): ContactDetail {
+ return {
+ group: 'BUSINESS',
+ type: type,
+ value: value,
+ preferenceLevel: 1
+ };
+}
+
+export function mapContactDetails(contactForm: any): ContactDetail[] {
+ const contactDetails: ContactDetail[] = [];
+
+ if (contactForm.phone) {
+ contactDetails.push(buildContactDetail('PHONE', contactForm.phone));
+ }
+
+ if (contactForm.mobile) {
+ contactDetails.push(buildContactDetail('MOBILE', contactForm.mobile));
+ }
+
+ if (contactForm.email) {
+ contactDetails.push(buildContactDetail('EMAIL', contactForm.email));
+ }
+
+ return contactDetails;
+}
+
+
+export function mapEmployee(event: CenterSaveEvent): Center {
+ const assignedOffice = event.officeForm.assignedOffice;
+
+ const contactDetails: ContactDetail[] = mapContactDetails(event.contactForm);
+
+ const employee: Center = {
+ identifier: event.detailForm.identifier,
+ givenName: event.detailForm.firstName,
+ middleName: event.detailForm.middleName,
+ surname: event.detailForm.lastName,
+ contactDetails: contactDetails,
+ assignedOffice: assignedOffice ? assignedOffice : undefined
+ };
+
+ return employee;
+}
+
+export function mapUser(event: CenterSaveEvent): UserWithPassword {
+ const userWithPassword: UserWithPassword = {
+ identifier: event.detailForm.identifier,
+ password: event.detailForm.password,
+ role: event.detailForm.role
+ };
+
+ return userWithPassword;
+}
diff --git a/src/app/centers/store/effects/notification.effects.ts b/src/app/centers/store/effects/notification.effects.ts
new file mode 100644
index 0000000..3de745e
--- /dev/null
+++ b/src/app/centers/store/effects/notification.effects.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as employeeActions from '../employee.actions';
+import {NotificationService, NotificationType} from '../../../services/notification/notification.service';
+
+@Injectable()
+export class EmployeeNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createEmployeeSuccess$: Observable<Action> = this.actions$
+ .ofType(employeeActions.CREATE_SUCCESS, employeeActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Employee is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteEmployeeSuccess$: Observable<Action> = this.actions$
+ .ofType(employeeActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Employee is going to be deleted'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
+
diff --git a/src/app/centers/store/effects/route.effects.ts b/src/app/centers/store/effects/route.effects.ts
new file mode 100644
index 0000000..3e35dd1
--- /dev/null
+++ b/src/app/centers/store/effects/route.effects.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as employeeActions from '../employee.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class EmployeeRouteEffects {
+
+ @Effect({ dispatch: false })
+ createEmployeeSuccess$: Observable<Action> = this.actions$
+ .ofType(employeeActions.CREATE_SUCCESS, employeeActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute} ));
+
+ @Effect({ dispatch: false })
+ deleteEmployeeSuccess$: Observable<Action> = this.actions$
+ .ofType(employeeActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/centers/store/effects/service.effects.spec.ts b/src/app/centers/store/effects/service.effects.spec.ts
new file mode 100644
index 0000000..5afcd87
--- /dev/null
+++ b/src/app/centers/store/effects/service.effects.spec.ts
@@ -0,0 +1,201 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {fakeAsync, TestBed, tick} from '@angular/core/testing';
+import {EffectsRunner, EffectsTestingModule} from '@ngrx/effects/testing';
+import {EmployeeApiEffects} from './service.effects';
+import {OfficeService} from '../../../services/office/office.service';
+import {IdentityService} from '../../../services/identity/identity.service';
+import {Observable} from 'rxjs/Observable';
+import {UpdateEmployeeAction, UpdateEmployeeSuccessAction} from '../employee.actions';
+import {Employee} from '../../../services/office/domain/employee.model';
+
+describe('Account Search Api Effects', () => {
+ beforeEach(() => {
+
+ TestBed.configureTestingModule({
+ imports: [
+ EffectsTestingModule
+ ],
+ providers: [
+ EmployeeApiEffects,
+ {
+ provide: OfficeService,
+ useValue: jasmine.createSpyObj('officeService', ['createEmployee', 'updateEmployee', 'setContactDetails'])
+ },
+ {
+ provide: IdentityService,
+ useValue: jasmine.createSpyObj('identityService', ['createUser', 'changeUserRole', 'changePassword'])
+ }
+ ]
+ });
+
+ });
+
+ describe('updateEmployee$', () => {
+
+ function setup() {
+ const officeService = TestBed.get(OfficeService);
+
+ officeService.updateEmployee.and.returnValue(Observable.of({}));
+
+ return {
+ runner: TestBed.get(EffectsRunner),
+ officeService: officeService,
+ identityService: TestBed.get(IdentityService),
+ employeeEffects: TestBed.get(EmployeeApiEffects)
+ };
+ }
+
+ it('should update employee', fakeAsync(() => {
+ const { runner, officeService, employeeEffects } = setup();
+
+ const employee: Employee = {
+ identifier: '',
+ givenName: '',
+ surname: '',
+ contactDetails: []
+ };
+
+ const expectedResult = new UpdateEmployeeSuccessAction({
+ resource: employee,
+ activatedRoute: null
+ });
+
+ runner.queue(new UpdateEmployeeAction({
+ employee: employee,
+ activatedRoute: null
+ }));
+
+ let result = null;
+ employeeEffects.updateEmployee$.subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual(expectedResult);
+
+ expect(officeService.updateEmployee).toHaveBeenCalled();
+ }));
+
+ it('should update contact details', fakeAsync(() => {
+ const { runner, officeService, employeeEffects } = setup();
+
+ officeService.setContactDetails.and.returnValue(Observable.of({}));
+
+ const employee: Employee = {
+ identifier: '',
+ givenName: '',
+ surname: '',
+ contactDetails: []
+ };
+
+ const expectedResult = new UpdateEmployeeSuccessAction({
+ resource: employee,
+ activatedRoute: null
+ });
+
+ runner.queue(new UpdateEmployeeAction({
+ employee: employee,
+ contactDetails: [
+ {
+ type: 'EMAIL',
+ group: 'BUSINESS',
+ value: 'dont@call.me',
+ preferenceLevel: 0
+ }
+ ],
+ activatedRoute: null
+ }));
+
+ let result = null;
+ employeeEffects.updateEmployee$.subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual(expectedResult);
+
+ expect(officeService.setContactDetails).toHaveBeenCalled();
+ }));
+
+ it('should update password', fakeAsync(() => {
+ const { runner, identityService, employeeEffects } = setup();
+
+ identityService.changePassword.and.returnValue(Observable.of({}));
+
+ const employee: Employee = {
+ identifier: '',
+ givenName: '',
+ surname: '',
+ contactDetails: []
+ };
+
+ const expectedResult = new UpdateEmployeeSuccessAction({
+ resource: employee,
+ activatedRoute: null
+ });
+
+ runner.queue(new UpdateEmployeeAction({
+ employee: employee,
+ password: 'test',
+ activatedRoute: null
+ }));
+
+ let result = null;
+ employeeEffects.updateEmployee$.subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual(expectedResult);
+
+ expect(identityService.changePassword).toHaveBeenCalled();
+ }));
+
+ it('should update role', fakeAsync(() => {
+ const { runner, identityService, employeeEffects } = setup();
+
+ identityService.changeUserRole.and.returnValue(Observable.of({}));
+
+ const employee: Employee = {
+ identifier: '',
+ givenName: '',
+ surname: '',
+ contactDetails: []
+ };
+
+ const expectedResult = new UpdateEmployeeSuccessAction({
+ resource: employee,
+ activatedRoute: null
+ });
+
+ runner.queue(new UpdateEmployeeAction({
+ employee: employee,
+ role: 'test',
+ activatedRoute: null
+ }));
+
+ let result = null;
+ employeeEffects.updateEmployee$.subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual(expectedResult);
+
+ expect(identityService.changeUserRole).toHaveBeenCalled();
+ }));
+ });
+});
diff --git a/src/app/centers/store/effects/service.effects.ts b/src/app/centers/store/effects/service.effects.ts
new file mode 100644
index 0000000..0a17834
--- /dev/null
+++ b/src/app/centers/store/effects/service.effects.ts
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {OfficeService} from '../../../services/office/office.service';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as employeeActions from '../employee.actions';
+import {IdentityService} from '../../../services/identity/identity.service';
+import {RoleIdentifier} from '../../../services/identity/domain/role-identifier.model';
+import {Password} from '../../../services/identity/domain/password.model';
+
+@Injectable()
+export class EmployeeApiEffects {
+
+ @Effect()
+ createEmployee$: Observable<Action> = this.actions$
+ .ofType(employeeActions.CREATE)
+ .map((action: employeeActions.CreateEmployeeAction) => action.payload)
+ .mergeMap(payload =>
+ this.identityService.createUser(payload.user)
+ .mergeMap(() => this.officeService.createEmployee(payload.employee) )
+ .map(() => new employeeActions.CreateEmployeeSuccessAction({
+ resource: payload.employee,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new employeeActions.CreateEmployeeFailAction(error)))
+ );
+
+ @Effect()
+ updateEmployee$: Observable<Action> = this.actions$
+ .ofType(employeeActions.UPDATE)
+ .map((action: employeeActions.UpdateEmployeeAction) => action.payload)
+ .mergeMap(payload => {
+ const employee = payload.employee;
+ const httpCalls: Observable<any>[] = [];
+
+ httpCalls.push(this.officeService.updateEmployee(employee));
+
+ if (payload.contactDetails) {
+ httpCalls.push(this.officeService.setContactDetails(employee.identifier, payload.contactDetails));
+ }
+
+ if (payload.role) {
+ httpCalls.push(this.identityService.changeUserRole(employee.identifier, new RoleIdentifier(payload.role)));
+ }
+
+ if (payload.password) {
+ httpCalls.push(this.identityService.changePassword(employee.identifier, new Password(payload.password)));
+ }
+
+ return Observable.forkJoin(httpCalls)
+ .map(() => new employeeActions.UpdateEmployeeSuccessAction({
+ resource: payload.employee,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new employeeActions.UpdateEmployeeFailAction(error)));
+ });
+
+ @Effect()
+ deleteEmployee$: Observable<Action> = this.actions$
+ .ofType(employeeActions.DELETE)
+ .map((action: employeeActions.DeleteEmployeeAction) => action.payload)
+ .mergeMap(payload => {
+ return Observable.forkJoin(
+ this.officeService.deleteEmployee(payload.employee.identifier),
+ this.identityService.changeUserRole(payload.employee.identifier, new RoleIdentifier('deactivated'))
+ )
+ .map(() => new employeeActions.DeleteEmployeeSuccessAction({
+ resource: payload.employee,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new employeeActions.DeleteEmployeeFailAction(error)));
+ }
+ );
+
+ constructor(private actions$: Actions, private officeService: OfficeService, private identityService: IdentityService) { }
+}
diff --git a/src/app/centers/store/employee.actions.ts b/src/app/centers/store/employee.actions.ts
new file mode 100644
index 0000000..75c3a99
--- /dev/null
+++ b/src/app/centers/store/employee.actions.ts
@@ -0,0 +1,149 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../store/util';
+import {Employee} from '../../services/office/domain/employee.model';
+import {Error} from '../../services/domain/error.model';
+import {ContactDetail} from '../../services/domain/contact/contact-detail.model';
+import {UserWithPassword} from '../../services/identity/domain/user-with-password.model';
+import {RoutePayload} from '../../common/store/route-payload';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../common/store/resource.reducer';
+
+export const LOAD = type('[Center] Load');
+export const SELECT = type('[Center] Select');
+
+export const CREATE = type('[Center] Create');
+export const CREATE_SUCCESS = type('[Center] Create Success');
+export const CREATE_FAIL = type('[Center] Create Fail');
+
+export const UPDATE = type('[Center] Update');
+export const UPDATE_SUCCESS = type('[Center] Update Success');
+export const UPDATE_FAIL = type('[Center] Update Fail');
+
+export const DELETE = type('[Center] Delete');
+export const DELETE_SUCCESS = type('[Center] Delete Success');
+export const DELETE_FAIL = type('[Center] Delete Fail');
+
+export const RESET_FORM = type('[Center] Reset Form');
+
+export interface EmployeeRoutePayload extends RoutePayload {
+ employee: Employee;
+}
+
+export interface CreateEmployeePayload extends EmployeeRoutePayload {
+ user: UserWithPassword;
+}
+
+export interface UpdateEmployeePayload extends EmployeeRoutePayload {
+ contactDetails?: ContactDetail[];
+ password?: string;
+ role?: string;
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateEmployeeAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: CreateEmployeePayload) { }
+}
+
+export class CreateEmployeeSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateEmployeeFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateEmployeeAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: UpdateEmployeePayload) { }
+}
+
+export class UpdateEmployeeSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateEmployeeFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteEmployeeAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: EmployeeRoutePayload) { }
+}
+
+export class DeleteEmployeeSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteEmployeeFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetEmployeeFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() { }
+}
+
+export type Actions
+ = LoadAction
+ | SelectAction
+ | CreateEmployeeAction
+ | CreateEmployeeSuccessAction
+ | CreateEmployeeFailAction
+ | UpdateEmployeeAction
+ | UpdateEmployeeSuccessAction
+ | UpdateEmployeeFailAction
+ | DeleteEmployeeAction
+ | DeleteEmployeeSuccessAction
+ | DeleteEmployeeFailAction
+ | ResetEmployeeFormAction;
diff --git a/src/app/centers/store/index.ts b/src/app/centers/store/index.ts
new file mode 100644
index 0000000..6b4ea4e
--- /dev/null
+++ b/src/app/centers/store/index.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as fromRoot from '../../store';
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../store/index';
+import {createSelector} from 'reselect';
+import {createResourceReducer, getResourceLoadedAt, getResourceSelected, ResourceState} from '../../common/store/resource.reducer';
+import {createFormReducer, FormState, getFormError} from '../../common/store/form.reducer';
+
+export interface State extends fromRoot.State {
+ employees: ResourceState;
+ employeeForm: FormState;
+}
+
+const reducers = {
+ employees: createResourceReducer('Employee'),
+ employeeForm: createFormReducer('Employee')
+};
+
+export const employeeModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export const getEmployeesState = (state: State) => state.employees;
+
+export const getEmployeeFormState = (state: State) => state.employeeForm;
+export const getEmployeeFormError = createSelector(getEmployeeFormState, getFormError);
+
+export const getEmployeesLoadedAt = createSelector(getEmployeesState, getResourceLoadedAt);
+export const getSelectedEmployee = createSelector(getEmployeesState, getResourceSelected);
+
+export class EmployeesStore extends Store<State> {}
+
+export function employeeStoreFactory(appStore: Store<fromRoot.State>) {
+ appStore.replaceReducer(employeeModuleReducer);
+ return appStore;
+}
diff --git a/src/app/centers/user.resolver.ts b/src/app/centers/user.resolver.ts
new file mode 100644
index 0000000..34f29cd
--- /dev/null
+++ b/src/app/centers/user.resolver.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, Resolve} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {IdentityService} from '../services/identity/identity.service';
+import {User} from '../services/identity/domain/user.model';
+
+@Injectable()
+export class UserResolver implements Resolve<User> {
+
+ constructor(private identityService: IdentityService) {}
+
+ resolve(route: ActivatedRouteSnapshot): Observable<User> {
+ return this.identityService.getUser(route.params['id']);
+ }
+}
diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html
index ed0ab27..758205a 100644
--- a/src/app/main/main.component.html
+++ b/src/app/main/main.component.html
@@ -38,7 +38,7 @@
</button>
<mat-menu #client="matMenu">
<a mat-menu-item [routerLink]="['/customers']">Client</a>
- <a mat-menu-item [routerLink]="['/']">Group</a>
+ <a mat-menu-item [routerLink]="['/centers']">Group</a>
<a mat-menu-item [routerLink]="['/']">Center</a>
</mat-menu>
</li>
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts
index cbe9a42..9e74fa9 100644
--- a/src/app/main/main.component.ts
+++ b/src/app/main/main.component.ts
@@ -122,6 +122,15 @@
permission: {id: 'accounting_ledgers', accessLevel: 'READ'}
},
+ {
+ title: 'Centers',
+ description: 'View centers',
+ icon: 'event_note',
+ routerLink: '/centers',
+ permission: {id: 'office_employees', accessLevel: 'READ'}
+
+ },
+
];
isLoading$: Observable<boolean>;
diff --git a/src/app/main/main.routing.ts b/src/app/main/main.routing.ts
index 1f81b96..d94d87b 100644
--- a/src/app/main/main.routing.ts
+++ b/src/app/main/main.routing.ts
@@ -38,7 +38,9 @@
{ path: 'deposits', loadChildren: './../depositAccount/deposit-account.module#DepositAccountModule' },
{ path: 'teller', loadChildren: './../teller/teller.module#TellerModule' },
{ path: 'reports', loadChildren: './../reporting/reporting.module#ReportingModule' },
- { path: 'denied', component: AccessDeniedComponent, data: { title: 'Not allowed' } }
+ { path: 'denied', component: AccessDeniedComponent, data: { title: 'Not allowed' }},
+ { path: 'centers', loadChildren: './../centers/center.module#CenterModule' }
+
]
},
{
diff --git a/src/app/services/office/domain/employee.model.ts b/src/app/services/office/domain/employee.model.ts
index 485e515..8350439 100644
--- a/src/app/services/office/domain/employee.model.ts
+++ b/src/app/services/office/domain/employee.model.ts
@@ -26,3 +26,14 @@
assignedOffice?: string;
contactDetails: ContactDetail[];
}
+
+export interface Center {
+ identifier: string;
+ givenName: string;
+ middleName?: string;
+ surname: string;
+ assignedOffice?: string;
+ contactDetails: ContactDetail[];
+}
+
+
diff --git a/src/app/services/security/authz/permission-id.type.ts b/src/app/services/security/authz/permission-id.type.ts
index efbe082..dde53b3 100644
--- a/src/app/services/security/authz/permission-id.type.ts
+++ b/src/app/services/security/authz/permission-id.type.ts
@@ -30,3 +30,4 @@
'reporting_management' |
'cheque_management' | 'cheque_transaction' |
'payroll_configuration' | 'payroll_distribution';
+
diff --git a/src/app/services/security/authz/permittable-group-id-mapper.ts b/src/app/services/security/authz/permittable-group-id-mapper.ts
index c81b30e..ac127e5 100644
--- a/src/app/services/security/authz/permittable-group-id-mapper.ts
+++ b/src/app/services/security/authz/permittable-group-id-mapper.ts
@@ -51,6 +51,7 @@
label: 'User created resources(Offices & Employees)'
};
+
this._permittableGroupMap[IdentityPermittableGroupIds.IDENTITY_MANAGEMENT] = {id: 'identity_identities', label: 'Identities'};
this._permittableGroupMap[IdentityPermittableGroupIds.ROLE_MANAGEMENT] = {id: 'identity_roles', label: 'Roles'};
this._permittableGroupMap[IdentityPermittableGroupIds.SELF_MANAGEMENT] = {