feat(employee): Matched models to the API and adjusted mock-api. (TV-346)

Squashed commit of the following:

commit 0f10a7864960cae47694212042f9d58053b05a0c
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Aug 18 14:13:15 2021 +0200

    Changed tjanst and utforandeVerksamhet to plural

commit 3ffe861d8721692d0b49b0f333f2f52186b23560
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Aug 18 12:26:43 2021 +0200

    Updated fetching single employee in mock-api

commit ae101885a90367b86b77faadaa171816aa2ffcaa
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Aug 18 12:23:28 2021 +0200

    Changed models for employees and fixed mock-api
This commit is contained in:
Erik Tiekstra
2021-08-18 14:16:17 +02:00
parent b7c7b6b6b0
commit 5bb81c3bd4
9 changed files with 158 additions and 115 deletions

View File

@@ -31,13 +31,17 @@
<a [routerLink]="employee.id" class="employees-list__link">{{ employee.fullName }}</a> <a [routerLink]="employee.id" class="employees-list__link">{{ employee.fullName }}</a>
</th> </th>
<td> <td>
<ng-container *ngFor="let service of employee.services; let last = last"> <ng-container *ngIf="employee.tjanster.length">
{{ service.name }}<ng-container *ngIf="!last">, </ng-container> {{ employee.tjanster[0] }}<ng-container *ngIf="employee.tjanster.length > 1">
(+{{employee.tjanster.length - 1}})</ng-container
>
</ng-container> </ng-container>
</td> </td>
<td> <td>
<ng-container *ngFor="let organization of employee.organizations; let last = last"> <ng-container *ngIf="employee.utforandeVerksamheter.length">
{{ organization.address.city }}<ng-container *ngIf="!last">, </ng-container> {{ employee.utforandeVerksamheter[0] }}<ng-container *ngIf="employee.utforandeVerksamheter.length > 1">
(+{{employee.utforandeVerksamheter.length - 1}})</ng-container
>
</ng-container> </ng-container>
</td> </td>
<td> <td>

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { SortOrder } from '@msfa-enums/sort-order.enum'; import { SortOrder } from '@msfa-enums/sort-order.enum';
import { Employee } from '@msfa-models/employee.model'; import { EmployeeCompact } from '@msfa-models/employee.model';
import { PaginationMeta } from '@msfa-models/pagination-meta.model'; import { PaginationMeta } from '@msfa-models/pagination-meta.model';
import { Sort } from '@msfa-models/sort.model'; import { Sort } from '@msfa-models/sort.model';
@@ -11,21 +11,21 @@ import { Sort } from '@msfa-models/sort.model';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class EmployeesListComponent { export class EmployeesListComponent {
@Input() employees: Employee[]; @Input() employees: EmployeeCompact[];
@Input() paginationMeta: PaginationMeta; @Input() paginationMeta: PaginationMeta;
@Input() sort: Sort<keyof Employee>; @Input() sort: Sort<keyof EmployeeCompact>;
@Output() sorted = new EventEmitter<keyof Employee>(); @Output() sorted = new EventEmitter<keyof EmployeeCompact>();
@Output() paginated = new EventEmitter<number>(); @Output() paginated = new EventEmitter<number>();
columnHeaders: { label: string; key: keyof Employee }[] = [ columnHeaders: { label: string; key: keyof EmployeeCompact }[] = [
{ label: 'Namn', key: 'fullName' }, { label: 'Namn', key: 'fullName' },
{ {
label: 'Tjänst', label: 'Tjänst',
key: 'services', key: 'tjanster',
}, },
{ {
label: 'Utförandeverksamheter', label: 'Utförandeverksamheter',
key: 'organizations', key: 'utforandeVerksamheter',
}, },
]; ];
@@ -52,7 +52,7 @@ export class EmployeesListComponent {
return end < this.count ? end : this.count; return end < this.count ? end : this.count;
} }
handleSort(key: keyof Employee): void { handleSort(key: keyof EmployeeCompact): void {
this.sorted.emit(key); this.sorted.emit(key);
} }

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IconType } from '@msfa-enums/icon-type.enum'; import { IconType } from '@msfa-enums/icon-type.enum';
import { Employee, EmployeesData } from '@msfa-models/employee.model'; import { EmployeeCompact, EmployeesData } from '@msfa-models/employee.model';
import { Sort } from '@msfa-models/sort.model'; import { Sort } from '@msfa-models/sort.model';
import { EmployeeService } from '@msfa-services/api/employee.service'; import { EmployeeService } from '@msfa-services/api/employee.service';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
@@ -15,7 +15,7 @@ export class EmployeesComponent {
private _searchValue$ = new BehaviorSubject<string>(''); private _searchValue$ = new BehaviorSubject<string>('');
private _onlyEmployeesWithoutAuthorization$ = new BehaviorSubject<boolean>(false); private _onlyEmployeesWithoutAuthorization$ = new BehaviorSubject<boolean>(false);
employeesData$: Observable<EmployeesData> = this.employeeService.employeesData$; employeesData$: Observable<EmployeesData> = this.employeeService.employeesData$;
sort$: Observable<Sort<keyof Employee>> = this.employeeService.sort$; sort$: Observable<Sort<keyof EmployeeCompact>> = this.employeeService.sort$;
iconType = IconType; iconType = IconType;
constructor(private employeeService: EmployeeService) {} constructor(private employeeService: EmployeeService) {}
@@ -32,7 +32,7 @@ export class EmployeesComponent {
this._searchValue$.next($event.detail.target.value); this._searchValue$.next($event.detail.target.value);
} }
handleEmployeesSort(key: keyof Employee): void { handleEmployeesSort(key: keyof EmployeeCompact): void {
this.employeeService.setSort(key); this.employeeService.setSort(key);
} }

View File

@@ -0,0 +1,25 @@
import { PaginationMeta } from '@msfa-models/pagination-meta.model';
export interface EmployeeCompactResponse {
ciamUserId: string;
name: string;
tjanst: string[];
utforandeVerksamhet: string[];
}
export interface EmployeeResponse {
ciamUserId: string;
firstName: string;
lastName: string;
email: string;
personnummer: string;
roles: string[];
tjansteKoder: string[];
utforandeVerksamhetIds: string[];
adressIds: string[];
}
export interface EmployeesApiResponse {
data: EmployeeCompactResponse[];
meta: PaginationMeta;
}

View File

@@ -1,75 +1,81 @@
import { Authorization } from '@msfa-enums/authorization.enum'; import { EmployeeCompactResponse, EmployeeResponse } from './api/employee.response.model';
import { Organization } from './organization.model';
import { PaginationMeta } from './pagination-meta.model'; import { PaginationMeta } from './pagination-meta.model';
import { Service } from './service.model';
export interface EmployeeCompact {
id: string;
fullName: string;
tjanster: string[];
utforandeVerksamheter: string[];
}
export interface Employee { export interface Employee {
id: string; id: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
fullName?: string; email: string;
ssn: string; ssn: string;
organizations: Organization[]; roles: string[];
services: Service[]; tjanstCodes: string[];
authorizations: Authorization[]; utforandeVerksamhetIds: string[];
createdAt?: number; utforandeAdressIds: string[];
}
export interface EmployeesApiResponse {
data: Employee[];
meta: PaginationMeta;
} }
export interface EmployeesData { export interface EmployeesData {
data: Employee[]; data: EmployeeCompact[];
meta: PaginationMeta; meta: PaginationMeta;
} }
export interface EmployeeApiResponse { export interface EmployeeRequestData {
data: Employee; email: string;
roles: string[];
tjansteKoder: string[];
utforandeVerksamhetIds: string[];
adressIds: string[];
} }
export interface EmployeeApiResponseData { export function mapEmployeeToRequestData(data: Employee): EmployeeRequestData {
id: string; const { email, roles, tjanstCodes, utforandeVerksamhetIds, utforandeAdressIds } = data;
firstName: string;
lastName: string;
ssn: string;
organizations: Organization[];
authorizations: Authorization[];
services: Service[];
}
export interface EmployeeApiRequestData {
firstName: string;
lastName: string;
ssn: string;
organizations: Organization[];
services: Service[];
authorizations: Authorization[];
}
export function mapEmployeeToEmployeeApiRequestData(data: Employee): EmployeeApiRequestData {
const { firstName, lastName, ssn, services, organizations, authorizations } = data;
return { return {
firstName, email,
lastName, roles,
ssn, tjansteKoder: tjanstCodes,
services, utforandeVerksamhetIds,
organizations, adressIds: utforandeAdressIds,
authorizations,
}; };
} }
export function mapEmployeeReponseToEmployee(data: EmployeeApiResponseData): Employee { export function mapResponseToEmployeeCompact(data: EmployeeCompactResponse): EmployeeCompact {
const { id, firstName, lastName, ssn, services, organizations, authorizations } = data; const { ciamUserId, name, tjanst, utforandeVerksamhet } = data;
return { return {
id, id: ciamUserId,
firstName, fullName: name,
lastName, tjanster: tjanst || [],
fullName: `${firstName} ${lastName}`, utforandeVerksamheter: utforandeVerksamhet || [],
organizations, };
authorizations, }
services,
ssn, export function mapResponseToEmployee(data: EmployeeResponse): Employee {
const {
ciamUserId,
firstName,
lastName,
email,
personnummer,
roles,
tjansteKoder,
utforandeVerksamhetIds,
adressIds,
} = data;
return {
id: ciamUserId,
firstName,
lastName,
email,
ssn: personnummer,
roles,
tjanstCodes: tjansteKoder,
utforandeVerksamhetIds,
utforandeAdressIds: adressIds,
}; };
} }

View File

@@ -4,14 +4,15 @@ import { ErrorType } from '@msfa-enums/error-type.enum';
import { SortOrder } from '@msfa-enums/sort-order.enum'; import { SortOrder } from '@msfa-enums/sort-order.enum';
import { environment } from '@msfa-environment'; import { environment } from '@msfa-environment';
import { EmployeeInviteMockApiResponse } from '@msfa-models/api/employee-invite.response.model'; import { EmployeeInviteMockApiResponse } from '@msfa-models/api/employee-invite.response.model';
import { EmployeeResponse, EmployeesApiResponse } from '@msfa-models/api/employee.response.model';
import { EmployeeInviteMockaData } from '@msfa-models/employee-invite-mock-data.model'; import { EmployeeInviteMockaData } from '@msfa-models/employee-invite-mock-data.model';
import { import {
Employee, Employee,
EmployeeApiResponse, EmployeeCompact,
EmployeesApiResponse,
EmployeesData, EmployeesData,
mapEmployeeReponseToEmployee, mapEmployeeToRequestData,
mapEmployeeToEmployeeApiRequestData, mapResponseToEmployee,
mapResponseToEmployeeCompact,
} from '@msfa-models/employee.model'; } from '@msfa-models/employee.model';
import { Sort } from '@msfa-models/sort.model'; import { Sort } from '@msfa-models/sort.model';
import { BehaviorSubject, combineLatest, Observable, throwError } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, throwError } from 'rxjs';
@@ -23,11 +24,11 @@ const API_HEADERS = { headers: environment.api.headers };
providedIn: 'root', providedIn: 'root',
}) })
export class EmployeeService { export class EmployeeService {
private _apiUrl = `${environment.api.url}/employee`; private _apiUrl = `${environment.api.url}/users`;
private _limit$ = new BehaviorSubject<number>(20); private _limit$ = new BehaviorSubject<number>(20);
private _page$ = new BehaviorSubject<number>(1); private _page$ = new BehaviorSubject<number>(1);
private _sort$ = new BehaviorSubject<Sort<keyof Employee>>({ key: 'fullName', order: SortOrder.ASC }); private _sort$ = new BehaviorSubject<Sort<keyof EmployeeCompact>>({ key: 'fullName', order: SortOrder.ASC });
public sort$: Observable<Sort<keyof Employee>> = this._sort$.asObservable(); public sort$: Observable<Sort<keyof EmployeeCompact>> = this._sort$.asObservable();
private _searchFilter$ = new BehaviorSubject<string>(''); private _searchFilter$ = new BehaviorSubject<string>('');
private _onlyEmployeesWithoutAuthorization$ = new BehaviorSubject<boolean>(false); private _onlyEmployeesWithoutAuthorization$ = new BehaviorSubject<boolean>(false);
@@ -46,7 +47,7 @@ export class EmployeeService {
private _fetchEmployees$( private _fetchEmployees$(
limit: number, limit: number,
page: number, page: number,
sort: Sort<keyof Employee>, sort: Sort<keyof EmployeeCompact>,
searchFilter: string, searchFilter: string,
onlyEmployeesWithoutAuthorization?: boolean onlyEmployeesWithoutAuthorization?: boolean
): Observable<EmployeesData> { ): Observable<EmployeesData> {
@@ -72,15 +73,15 @@ export class EmployeeService {
}) })
.pipe( .pipe(
map(({ data, meta }) => { map(({ data, meta }) => {
return { data: data.map(employee => mapEmployeeReponseToEmployee(employee)), meta }; return { data: data.map(employee => mapResponseToEmployeeCompact(employee)), meta };
}) })
); );
} }
public fetchDetailedEmployeeData$(id: string): Observable<Employee> { public fetchDetailedEmployeeData$(id: string): Observable<Employee> {
return this.httpClient return this.httpClient
.get<EmployeeApiResponse>(`${this._apiUrl}/${id}`, { ...API_HEADERS }) .get<{ data: EmployeeResponse }>(`${this._apiUrl}/${id}`, { ...API_HEADERS })
.pipe(map(result => mapEmployeeReponseToEmployee(result.data))); .pipe(map(result => mapResponseToEmployee(result.data)));
} }
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}
@@ -93,7 +94,7 @@ export class EmployeeService {
this._onlyEmployeesWithoutAuthorization$.next(value); this._onlyEmployeesWithoutAuthorization$.next(value);
} }
public setSort(newSortKey: keyof Employee): void { public setSort(newSortKey: keyof EmployeeCompact): void {
const currentSort = this._sort$.getValue(); const currentSort = this._sort$.getValue();
const order = const order =
currentSort.key === newSortKey && currentSort.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; currentSort.key === newSortKey && currentSort.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
@@ -106,17 +107,15 @@ export class EmployeeService {
} }
public postNewEmployee(employeeData: Employee): Observable<string> { public postNewEmployee(employeeData: Employee): Observable<string> {
return this.httpClient return this.httpClient.post<{ id: string }>(this._apiUrl, mapEmployeeToRequestData(employeeData), API_HEADERS).pipe(
.post<{ id: string }>(this._apiUrl, mapEmployeeToEmployeeApiRequestData(employeeData), API_HEADERS) map(({ id }) => id),
.pipe( catchError(error => throwError({ message: error as string, type: ErrorType.API }))
map(({ id }) => id), );
catchError(error => throwError({ message: error as string, type: ErrorType.API }))
);
} }
postEmployeeInvitation(email: string): Observable<EmployeeInviteMockaData> { public postEmployeeInvitation(email: string): Observable<EmployeeInviteMockaData> {
return this.httpClient return this.httpClient
.post<{ data: EmployeeInviteMockApiResponse }>('http://localhost:8000/invites', { email }, API_HEADERS) .post<{ data: EmployeeInviteMockApiResponse }>(`${this._apiUrl}/invite`, { email }, API_HEADERS)
.pipe( .pipe(
take(1), take(1),
map(res => res.data), map(res => res.data),

View File

@@ -1,7 +1,7 @@
import faker from 'faker'; import faker from 'faker';
import authorizations from './authorizations.js';
import organizations from './organizations.js'; import organizations from './organizations.js';
import tjanster from './tjanster.js'; import tjanster from './tjanster.js';
import chooseRandom from './utils/choose-random.util.js';
faker.locale = 'sv'; faker.locale = 'sv';
@@ -12,24 +12,31 @@ function generateEmployees(amount = 10) {
const employees = []; const employees = [];
for (let i = 1; i <= amount; ++i) { for (let i = 1; i <= amount; ++i) {
const person = { const firstName = faker.name.firstName();
id: faker.datatype.uuid(), const lastName = faker.name.lastName();
firstName: faker.name.firstName(), const currentTjanster = chooseRandom(TJANSTER, faker.datatype.number({ min: 1, max: TJANSTER.length }));
lastName: faker.name.lastName(), const currentOrganizations = chooseRandom(ORGANIZATIONS, faker.datatype.number({ min: 1, max: 5 }));
ssn: `${faker.date.between('1950', '2000').toISOString().split('T')[0].replace(/-/g, '')}-${faker.datatype.number(
{ const employee = {
min: 1000, ciamUserId: faker.datatype.uuid(),
max: 9999, firstName,
} lastName,
)}`, name: `${firstName} ${lastName}`,
organizations: [ORGANIZATIONS[Math.floor(Math.random() * ORGANIZATIONS.length)]], personnummer: `${faker.date
services: [TJANSTER[Math.floor(Math.random() * TJANSTER.length)]], .between('1950', '2000')
authorizations: i % 2 === 0 ? authorizations.generate() : [], .toISOString()
createdAt: Date.now(), .split('T')[0]
.replace(/-/g, '')}-${faker.datatype.number({
min: 1000,
max: 9999,
})}`,
tjanst: currentTjanster.map(tjanst => tjanst.name),
tjansteKoder: currentTjanster.map(tjanst => tjanst.code),
utforandeVerksamhet: currentOrganizations.map(organization => organization.name),
utforandeVerksamhetIds: currentOrganizations.map(organization => organization.id),
}; };
person.fullName = `${person.firstName} ${person.lastName}`; employees.push(employee);
employees.push(person);
} }
console.info('Employees generated...'); console.info('Employees generated...');

View File

@@ -8,10 +8,10 @@ function generateTjanster() {
code: faker.datatype.uuid(), code: faker.datatype.uuid(),
name: 'Kundval Rusta och matcha', name: 'Kundval Rusta och matcha',
}, },
{ // {
code: faker.datatype.uuid(), // code: faker.datatype.uuid(),
name: 'Karriärvägledning', // name: 'Karriärvägledning',
}, // },
// { // {
// code: faker.datatype.uuid(), // code: faker.datatype.uuid(),
// name: 'STOM', // name: 'STOM',

View File

@@ -9,13 +9,15 @@ server.use(middlewares);
server.use( server.use(
jsonServer.rewriter({ jsonServer.rewriter({
'/api/*': '/$1', '/api/*': '/$1',
'*sort=services*': '$1sort=services[0].name$2', '*sort=fullName*': '$1sort=name$2',
'*sort=organizations*': '$1sort=organizations[0].address.city$2', '*sort=utforandeVerksamheter*': '$1sort=utforandeVerksamhet[0]$2',
'*sort=utforandeVerksamhet*': '$1sort=utforandeverksamhet$2', '*sort=tjanster*': '$1sort=tjanst[0]$2',
'*sort=tjanst*': '$1sort=tjansteNamn$2', '/users/:id': '/employees?ciamUserId=:id',
'/employee*search=*': '/employee$1fullName_like=$2', '/users*': '/employees$1',
'/employee*onlyEmployeesWithoutAuthorization=*': '/employee$1authorizations.length_gte=1', '/employees*search=*': '/employees$1fullName_like=$2',
'/employee*': '/employees$1', '/employees*onlyEmployeesWithoutAuthorization=*': '/employees$1authorizations.length_gte=1',
'/employees/invite': '/invites',
'/employees*': '/employees$1',
'/participants': '/participants?_embed=employees', '/participants': '/participants?_embed=employees',
'/participant/:id': '/participants/:id?_embed=employees', '/participant/:id': '/participants/:id?_embed=employees',
'/auth/userinfo': '/currentUser', '/auth/userinfo': '/currentUser',