feat(employee-list): Added possibility to sort and paginate inside the list of employees (TV-217) (TV-222)

Squashed commit of the following:

commit f13b52a134693bb6237b2df6408020504c3fbe1c
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Jun 9 07:47:56 2021 +0200

    Updated after PR

commit 4055d3a14eda9737ef76ed5e85ea35480e19e71c
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Jun 7 15:20:26 2021 +0200

    updates after PR comments

commit f515ed6d06087f62de7522745691429d7ca91153
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Jun 7 12:07:50 2021 +0200

    Now using Sort interface again

commit 5c793a5a7579a520c3792bb3d13c00bb68dbfcd4
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Jun 7 11:54:27 2021 +0200

    Fixed bug showing wrong amount in pagination component

commit 7c55751147e05c1279b75356dc143b069e63e6b2
Merge: 11eab63 a701888
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Jun 7 11:42:59 2021 +0200

    Updated after merge with develop

commit 11eab6330191a140c2cfd7094838495793e02719
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Jun 7 11:23:52 2021 +0200

    Added functionality to sort on different columns

commit f13422a2aa53a69a243f32f9cd0b7ed6bd3441fc
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Jun 4 11:40:22 2021 +0200

    Fixed other mappings after changes in the mock-api

commit ba2d3200167281422354f5e3cfdf7720444a9c4c
Merge: 6232b32 d91b3e6
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Jun 4 10:00:00 2021 +0200

    Added paging functionality

commit 6232b3274ff73f2da929342a244fbc87430b796f
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Jun 4 09:25:01 2021 +0200

    Added meta model and changed services to adapt new API data structure

commit 3ea0046bb713a6ee13d2a2cb2983e92d10559aa3
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Thu Jun 3 13:02:44 2021 +0200

    Adjusted mock-api functionality
This commit is contained in:
Erik Tiekstra
2021-06-09 07:51:22 +02:00
parent a70188863c
commit 48801a93a0
31 changed files with 392 additions and 173 deletions

View File

@@ -0,0 +1,4 @@
export enum SortOrder {
ASC = 'asc',
DESC = 'desc',
}

View File

@@ -6,11 +6,15 @@ export interface Authorization {
} }
export interface AuthorizationApiResponse { export interface AuthorizationApiResponse {
data: AuthorizationApiResponseData[];
}
export interface AuthorizationApiResponseData {
id: string; id: string;
name: AuthorizationEnum; name: AuthorizationEnum;
} }
export function mapAuthorizationApiResponseToAuthorization(data: AuthorizationApiResponse): Authorization { export function mapAuthorizationApiResponseToAuthorization(data: AuthorizationApiResponseData): Authorization {
const { id, name } = data; const { id, name } = data;
return { return {
id, id,

View File

@@ -1,22 +1,43 @@
import { Authorization } from './authorization.model'; import { Authorization } from '@dafa-enums/authorization.enum';
import { Organization } from './organization.model'; import { Organization } from './organization.model';
import { Participant } from './participant.model'; import { PaginationMeta } from './pagination-meta.model';
import { Service } from './service.model'; import { Service } from './service.model';
import { User, UserApiResponse } from './user.model';
export interface Employee extends User { export interface Employee {
languages: string[]; id: string;
participants: Participant[]; firstName: string;
lastName: string;
fullName: string;
ssn: string;
organizations: Organization[];
authorizations: Authorization[];
services: Service[]; services: Service[];
} }
export interface EmployeeApiResponse extends UserApiResponse { export interface EmployeesApiResponse {
languages: string[]; data: Employee[];
participants: Participant[]; meta: PaginationMeta;
}
export interface EmployeesData {
data: Employee[];
meta: PaginationMeta;
}
export interface EmployeeApiResponse {
data: Employee;
}
export interface EmployeeApiResponseData {
id: string;
firstName: string;
lastName: string;
ssn: string;
organizations: Organization[];
authorizations: Authorization[];
services: Service[]; services: Service[];
} }
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface EmployeeApiRequestData { export interface EmployeeApiRequestData {
firstName: string; firstName: string;
lastName: string; lastName: string;
@@ -38,8 +59,8 @@ export function mapEmployeeToEmployeeApiRequestData(data: Employee): EmployeeApi
}; };
} }
export function mapEmployeeReponseToEmployee(data: EmployeeApiResponse): Employee { export function mapEmployeeReponseToEmployee(data: EmployeeApiResponseData): Employee {
const { id, firstName, lastName, ssn, services, languages, organizations, authorizations, participants } = data; const { id, firstName, lastName, ssn, services, organizations, authorizations } = data;
return { return {
id, id,
firstName, firstName,
@@ -48,8 +69,6 @@ export function mapEmployeeReponseToEmployee(data: EmployeeApiResponse): Employe
organizations, organizations,
authorizations, authorizations,
services, services,
languages,
ssn, ssn,
participants,
}; };
} }

View File

@@ -0,0 +1,6 @@
export interface PaginationMeta {
count: number;
limit: number;
page: number;
totalPages: number;
}

View File

@@ -1,7 +1,31 @@
import { ParticipantStatus } from '@dafa-enums/participant-status.enum'; import { ParticipantStatus } from '@dafa-enums/participant-status.enum';
import { Service } from '@dafa-enums/service.enum'; import { Service } from '@dafa-enums/service.enum';
import { PaginationMeta } from './pagination-meta.model';
export interface Participant { export interface Participant {
id: string;
firstName: string;
lastName: string;
fullName: string;
status: ParticipantStatus;
nextStep: string;
service: Service;
errandNumber: number;
startDate: Date;
endDate: Date;
handleBefore: Date;
}
export interface ParticipantsApiResponse {
data: ParticipantApiResponseData[];
meta?: PaginationMeta;
}
export interface ParticipantApiResponse {
data: ParticipantApiResponseData;
}
export interface ParticipantApiResponseData {
id: string; id: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
@@ -12,5 +36,21 @@ export interface Participant {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
handleBefore: Date; handleBefore: Date;
fullName?: string; }
export function mapParticipantApiResponseToParticipant(data: ParticipantApiResponseData): Participant {
const { id, firstName, lastName, status, nextStep, service, errandNumber, startDate, endDate, handleBefore } = data;
return {
id,
firstName,
lastName,
fullName: `${firstName} ${lastName}`,
status,
nextStep,
service,
errandNumber,
startDate,
endDate,
handleBefore,
};
} }

View File

@@ -6,11 +6,15 @@ export interface Service {
} }
export interface ServiceApiResponse { export interface ServiceApiResponse {
data: ServiceApiResponseData[];
}
export interface ServiceApiResponseData {
id: string; id: string;
name: ServiceEnum; name: ServiceEnum;
} }
export function mapServiceApiResponseToService(data: ServiceApiResponse): Service { export function mapServiceApiResponseToService(data: ServiceApiResponseData): Service {
const { id, name } = data; const { id, name } = data;
return { return {
id, id,

View File

@@ -1,7 +0,0 @@
import { Employee } from './employee.model';
import { Participant } from './participant.model';
export interface SortBy {
key: keyof Participant | keyof Employee;
reverse: boolean;
}

View File

@@ -0,0 +1,6 @@
import { SortOrder } from '@dafa-enums/sort-order.enum';
export interface Sort<Key> {
key: Key;
order: SortOrder;
}

View File

@@ -12,6 +12,10 @@ export interface User {
} }
export interface UserApiResponse { export interface UserApiResponse {
data: UserApiResponseData;
}
export interface UserApiResponseData {
id: string; id: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
@@ -20,7 +24,7 @@ export interface UserApiResponse {
authorizations: Authorization[]; authorizations: Authorization[];
} }
export function mapUserApiResponseToUser(data: UserApiResponse): User { export function mapUserApiResponseToUser(data: UserApiResponseData): User {
const { id, firstName, lastName, ssn, organizations, authorizations } = data; const { id, firstName, lastName, ssn, organizations, authorizations } = data;
return { return {
id, id,

View File

@@ -8,6 +8,7 @@ export class CustomErrorHandler implements ErrorHandler {
handleError(error: any): void { handleError(error: any): void {
const customError: CustomError = errorToCustomError(error); const customError: CustomError = errorToCustomError(error);
console.error(error);
this.errorService.add(customError); this.errorService.add(customError);
} }
} }

View File

@@ -14,7 +14,7 @@ const routes: Routes = [
loadChildren: () => import('./pages/employees/employees.module').then(m => m.EmployeesModule), loadChildren: () => import('./pages/employees/employees.module').then(m => m.EmployeesModule),
}, },
{ {
path: 'personal/:id', path: 'personal/:employeeId',
loadChildren: () => import('./pages/employee-card/employee-card.module').then(m => m.EmployeeCardModule), loadChildren: () => import('./pages/employee-card/employee-card.module').then(m => m.EmployeeCardModule),
}, },
{ {
@@ -22,7 +22,7 @@ const routes: Routes = [
loadChildren: () => import('./pages/employee-form/employee-form.module').then(m => m.EmployeeFormModule), loadChildren: () => import('./pages/employee-form/employee-form.module').then(m => m.EmployeeFormModule),
}, },
{ {
path: 'redigera-konto/:id', path: 'redigera-konto/:employeeId',
loadChildren: () => import('./pages/employee-form/employee-form.module').then(m => m.EmployeeFormModule), loadChildren: () => import('./pages/employee-form/employee-form.module').then(m => m.EmployeeFormModule),
}, },
]; ];

View File

@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { UnsubscribeDirective } from '@dafa-directives/unsubscribe.directive';
import { Employee } from '@dafa-models/employee.model'; import { Employee } from '@dafa-models/employee.model';
import { Participant } from '@dafa-models/participant.model'; import { Participant } from '@dafa-models/participant.model';
import { EmployeeService } from '@dafa-services/api/employee.service'; import { EmployeeService } from '@dafa-services/api/employee.service';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
@Component({ @Component({
selector: 'dafa-employee-card', selector: 'dafa-employee-card',
@@ -12,20 +12,15 @@ import { BehaviorSubject, Observable } from 'rxjs';
styleUrls: ['./employee-card.component.scss'], styleUrls: ['./employee-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class EmployeeCardComponent extends UnsubscribeDirective { export class EmployeeCardComponent {
detailedEmployeeData$: Observable<Employee>;
authorizationsAsString$: Observable<string>;
private _pendingSelectedParticipants$ = new BehaviorSubject<string[]>([]); private _pendingSelectedParticipants$ = new BehaviorSubject<string[]>([]);
private _employeeId$: Observable<string> = this.activatedRoute.params.pipe(map(({ employeeId }) => employeeId));
authorizationsAsString$: Observable<string>;
detailedEmployeeData$: Observable<Employee> = this._employeeId$.pipe(
switchMap(employeeId => this.employeeService.fetchDetailedEmployeeData$(employeeId))
);
constructor(private activatedRoute: ActivatedRoute, private employeeService: EmployeeService) { constructor(private activatedRoute: ActivatedRoute, private employeeService: EmployeeService) {}
super();
super.unsubscribeOnDestroy(
this.activatedRoute.params.subscribe(({ id }) => {
this.detailedEmployeeData$ = this.employeeService.getDetailedEmployeeData(id);
})
);
}
get pendingSelectedParticipants(): string[] { get pendingSelectedParticipants(): string[] {
return this._pendingSelectedParticipants$.getValue(); return this._pendingSelectedParticipants$.getValue();

View File

@@ -6,27 +6,45 @@
<th scope="col" class="employees-list__column-head"> <th scope="col" class="employees-list__column-head">
<button class="employees-list__sort-button" (click)="handleSort('fullName')"> <button class="employees-list__sort-button" (click)="handleSort('fullName')">
Namn Namn
<ng-container *ngIf="sortBy?.key === 'fullName'"> <ng-container *ngIf="sort.key === 'fullName'">
<digi-icon-caret-up class="employees-list__sort-icon" *ngIf="!sortBy.reverse"></digi-icon-caret-up> <digi-icon-caret-up
<digi-icon-caret-down class="employees-list__sort-icon" *ngIf="sortBy.reverse"></digi-icon-caret-down> class="employees-list__sort-icon"
*ngIf="sort.order === orderType.ASC"
></digi-icon-caret-up>
<digi-icon-caret-down
class="employees-list__sort-icon"
*ngIf="sort.order === orderType.DESC"
></digi-icon-caret-down>
</ng-container> </ng-container>
</button> </button>
</th> </th>
<th scope="col" class="employees-list__column-head"> <th scope="col" class="employees-list__column-head">
<button class="employees-list__sort-button" (click)="handleSort('services')"> <button class="employees-list__sort-button" (click)="handleSort('services')">
Tjänst Tjänst
<ng-container *ngIf="sortBy?.key === 'services'"> <ng-container *ngIf="sort.key === 'services'">
<digi-icon-caret-up class="employees-list__sort-icon" *ngIf="!sortBy.reverse"></digi-icon-caret-up> <digi-icon-caret-up
<digi-icon-caret-down class="employees-list__sort-icon" *ngIf="sortBy.reverse"></digi-icon-caret-down> class="employees-list__sort-icon"
*ngIf="sort.order === orderType.ASC"
></digi-icon-caret-up>
<digi-icon-caret-down
class="employees-list__sort-icon"
*ngIf="sort.order === orderType.DESC"
></digi-icon-caret-down>
</ng-container> </ng-container>
</button> </button>
</th> </th>
<th scope="col" class="employees-list__column-head"> <th scope="col" class="employees-list__column-head">
<button class="employees-list__sort-button" (click)="handleSort('organizations')"> <button class="employees-list__sort-button" (click)="handleSort('organizations')">
Utförandeverksamheter Utförandeverksamheter
<ng-container *ngIf="sortBy?.key === 'organization'"> <ng-container *ngIf="sort.key === 'organizations'">
<digi-icon-caret-up class="employees-list__sort-icon" *ngIf="!sortBy.reverse"></digi-icon-caret-up> <digi-icon-caret-up
<digi-icon-caret-down class="employees-list__sort-icon" *ngIf="sortBy.reverse"></digi-icon-caret-down> class="employees-list__sort-icon"
*ngIf="sort.order === orderType.ASC"
></digi-icon-caret-up>
<digi-icon-caret-down
class="employees-list__sort-icon"
*ngIf="sort.order === orderType.DESC"
></digi-icon-caret-down>
</ng-container> </ng-container>
</button> </button>
</th> </th>
@@ -34,7 +52,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let employee of pagedEmployees"> <tr *ngFor="let employee of employees">
<th scope="row"> <th scope="row">
<a [routerLink]="employee.id" class="employees-list__link">{{ employee.fullName }}</a> <a [routerLink]="employee.id" class="employees-list__link">{{ employee.fullName }}</a>
</th> </th>
@@ -64,8 +82,8 @@
[afTotalPages]="totalPages" [afTotalPages]="totalPages"
[afCurrentResultStart]="currentResultStart" [afCurrentResultStart]="currentResultStart"
[afCurrentResultEnd]="currentResultEnd" [afCurrentResultEnd]="currentResultEnd"
[afTotalResults]="employees.length" [afTotalResults]="count"
(afOnPageChange)="handlePagination($event.detail)" (afOnPageChange)="setNewPage($event.detail)"
af-result-name="medarbetare" af-result-name="medarbetare"
> >
</digi-navigation-pagination> </digi-navigation-pagination>

View File

@@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { SortOrder } from '@dafa-enums/sort-order.enum';
import { Employee } from '@dafa-models/employee.model'; import { Employee } from '@dafa-models/employee.model';
import { SortBy } from '@dafa-models/sort-by.model'; import { PaginationMeta } from '@dafa-models/pagination-meta.model';
import { BehaviorSubject } from 'rxjs'; import { Sort } from '@dafa-models/sort.model';
@Component({ @Component({
selector: 'dafa-employees-list', selector: 'dafa-employees-list',
@@ -11,37 +12,36 @@ import { BehaviorSubject } from 'rxjs';
}) })
export class EmployeesListComponent { export class EmployeesListComponent {
@Input() employees: Employee[]; @Input() employees: Employee[];
@Input() sortBy: SortBy | null; @Input() meta: PaginationMeta;
@Input() sort: Sort<keyof Employee>;
@Input() order: SortOrder;
@Output() sorted = new EventEmitter<keyof Employee>(); @Output() sorted = new EventEmitter<keyof Employee>();
@Output() paginated = new EventEmitter<number>();
private _currentPage$ = new BehaviorSubject<number>(1); orderType = SortOrder;
private _employeesPerPage = 10;
get currentPage(): number { get currentPage(): number {
return this._currentPage$.getValue(); return this.meta.page;
} }
get totalPages(): number { get totalPages(): number {
return Math.ceil(this.employees.length / this._employeesPerPage); return this.meta?.totalPages;
} }
get count(): number {
get pagedEmployees(): Employee[] { return this.meta.count;
return [...this.employees].slice(this.currentResultStart - 1, this.currentResultEnd);
} }
get currentResultStart(): number { get currentResultStart(): number {
return (this.currentPage - 1) * this._employeesPerPage + 1; return (this.currentPage - 1) * this.meta.limit + 1;
} }
get currentResultEnd(): number { get currentResultEnd(): number {
return this.currentResultStart + this._employeesPerPage - 1; const end = this.currentResultStart + this.meta.limit - 1;
return end < this.count ? end : this.count;
} }
handleSort(key: keyof Employee): void { handleSort(key: keyof Employee): void {
this.sorted.emit(key); this.sorted.emit(key);
} }
handlePagination(page: number): void { setNewPage(page: number): void {
this._currentPage$.next(page); this.paginated.emit(page);
} }
} }

View File

@@ -22,10 +22,13 @@
</form> </form>
<dafa-employees-list <dafa-employees-list
*ngIf="filteredEmployees$ | async as employees; else loadingRef" *ngIf="employeesData$ | async as employeesData; else loadingRef"
[employees]="employees" [employees]="employeesData.data"
[sortBy]="employeesSortBy$ | async" [meta]="employeesData.meta"
[sort]="sort$ | async"
[order]="order$ | async"
(sorted)="handleEmployeesSort($event)" (sorted)="handleEmployeesSort($event)"
(paginated)="setNewPage($event)"
></dafa-employees-list> ></dafa-employees-list>
</digi-typography> </digi-typography>

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IconType } from '@dafa-enums/icon-type.enum'; import { IconType } from '@dafa-enums/icon-type.enum';
import { Employee } from '@dafa-models/employee.model'; import { Employee, EmployeesData } from '@dafa-models/employee.model';
import { SortBy } from '@dafa-models/sort-by.model'; import { Sort } from '@dafa-models/sort.model';
import { EmployeeService } from '@dafa-services/api/employee.service'; import { EmployeeService } from '@dafa-services/api/employee.service';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
@@ -13,8 +13,8 @@ import { BehaviorSubject, Observable } from 'rxjs';
}) })
export class EmployeesComponent { export class EmployeesComponent {
private _searchValue$ = new BehaviorSubject<string>(''); private _searchValue$ = new BehaviorSubject<string>('');
filteredEmployees$: Observable<Employee[]> = this.employeeService.filteredEmployees$; employeesData$: Observable<EmployeesData> = this.employeeService.employeesData$;
employeesSortBy$: Observable<SortBy | null> = this.employeeService.employeesSortBy$; sort$: Observable<Sort<keyof Employee>> = this.employeeService.sort$;
iconType = IconType; iconType = IconType;
constructor(private employeeService: EmployeeService) {} constructor(private employeeService: EmployeeService) {}
@@ -32,6 +32,10 @@ export class EmployeesComponent {
} }
handleEmployeesSort(key: keyof Employee): void { handleEmployeesSort(key: keyof Employee): void {
this.employeeService.setEmployeesSortKey(key); this.employeeService.setSort(key);
}
setNewPage(page: number): void {
this.employeeService.setPage(page);
} }
} }

View File

@@ -6,8 +6,14 @@
<button class="participants-list__sort-button" (click)="handleSort('fullName')"> <button class="participants-list__sort-button" (click)="handleSort('fullName')">
Namn Namn
<ng-container *ngIf="sortBy?.key === 'fullName'"> <ng-container *ngIf="sortBy?.key === 'fullName'">
<digi-icon-caret-up class="participants-list__sort-icon" *ngIf="!sortBy.reverse"></digi-icon-caret-up> <digi-icon-caret-up
<digi-icon-caret-down class="participants-list__sort-icon" *ngIf="sortBy.reverse"></digi-icon-caret-down> class="participants-list__sort-icon"
*ngIf="sortBy.order === 'asc'"
></digi-icon-caret-up>
<digi-icon-caret-down
class="participants-list__sort-icon"
*ngIf="sortBy.order === 'desc'"
></digi-icon-caret-down>
</ng-container> </ng-container>
</button> </button>
</th> </th>
@@ -15,8 +21,14 @@
<button class="participants-list__sort-button" (click)="handleSort('errandNumber')"> <button class="participants-list__sort-button" (click)="handleSort('errandNumber')">
Ärendenummer Ärendenummer
<ng-container *ngIf="sortBy?.key === 'errandNumber'"> <ng-container *ngIf="sortBy?.key === 'errandNumber'">
<digi-icon-caret-up class="participants-list__sort-icon" *ngIf="!sortBy.reverse"></digi-icon-caret-up> <digi-icon-caret-up
<digi-icon-caret-down class="participants-list__sort-icon" *ngIf="sortBy.reverse"></digi-icon-caret-down> class="participants-list__sort-icon"
*ngIf="sortBy.order === 'asc'"
></digi-icon-caret-up>
<digi-icon-caret-down
class="participants-list__sort-icon"
*ngIf="sortBy.order === 'desc'"
></digi-icon-caret-down>
</ng-container> </ng-container>
</button> </button>
</th> </th>
@@ -24,8 +36,14 @@
<button class="participants-list__sort-button" (click)="handleSort('service')"> <button class="participants-list__sort-button" (click)="handleSort('service')">
Tjänst Tjänst
<ng-container *ngIf="sortBy?.key === 'service'"> <ng-container *ngIf="sortBy?.key === 'service'">
<digi-icon-caret-up class="participants-list__sort-icon" *ngIf="!sortBy.reverse"></digi-icon-caret-up> <digi-icon-caret-up
<digi-icon-caret-down class="participants-list__sort-icon" *ngIf="sortBy.reverse"></digi-icon-caret-down> class="participants-list__sort-icon"
*ngIf="sortBy.order === 'asc'"
></digi-icon-caret-up>
<digi-icon-caret-down
class="participants-list__sort-icon"
*ngIf="sortBy.order === 'desc'"
></digi-icon-caret-down>
</ng-container> </ng-container>
</button> </button>
</th> </th>
@@ -33,8 +51,14 @@
<button class="participants-list__sort-button" (click)="handleSort('startDate')"> <button class="participants-list__sort-button" (click)="handleSort('startDate')">
Startdatum Startdatum
<ng-container *ngIf="sortBy?.key === 'startDate'"> <ng-container *ngIf="sortBy?.key === 'startDate'">
<digi-icon-caret-up class="participants-list__sort-icon" *ngIf="!sortBy.reverse"></digi-icon-caret-up> <digi-icon-caret-up
<digi-icon-caret-down class="participants-list__sort-icon" *ngIf="sortBy.reverse"></digi-icon-caret-down> class="participants-list__sort-icon"
*ngIf="sortBy.order === 'asc'"
></digi-icon-caret-up>
<digi-icon-caret-down
class="participants-list__sort-icon"
*ngIf="sortBy.order === 'desc'"
></digi-icon-caret-down>
</ng-container> </ng-container>
</button> </button>
</th> </th>
@@ -42,8 +66,14 @@
<button class="participants-list__sort-button" (click)="handleSort('endDate')"> <button class="participants-list__sort-button" (click)="handleSort('endDate')">
Slutdatum Slutdatum
<ng-container *ngIf="sortBy?.key === 'endDate'"> <ng-container *ngIf="sortBy?.key === 'endDate'">
<digi-icon-caret-up class="participants-list__sort-icon" *ngIf="!sortBy.reverse"></digi-icon-caret-up> <digi-icon-caret-up
<digi-icon-caret-down class="participants-list__sort-icon" *ngIf="sortBy.reverse"></digi-icon-caret-down> class="participants-list__sort-icon"
*ngIf="sortBy.order === 'asc'"
></digi-icon-caret-up>
<digi-icon-caret-down
class="participants-list__sort-icon"
*ngIf="sortBy.order === 'desc'"
></digi-icon-caret-down>
</ng-container> </ng-container>
</button> </button>
</th> </th>
@@ -51,8 +81,14 @@
<button class="participants-list__sort-button" (click)="handleSort('handleBefore')"> <button class="participants-list__sort-button" (click)="handleSort('handleBefore')">
Hantera innan Hantera innan
<ng-container *ngIf="sortBy?.key === 'handleBefore'"> <ng-container *ngIf="sortBy?.key === 'handleBefore'">
<digi-icon-caret-up class="participants-list__sort-icon" *ngIf="!sortBy.reverse"></digi-icon-caret-up> <digi-icon-caret-up
<digi-icon-caret-down class="participants-list__sort-icon" *ngIf="sortBy.reverse"></digi-icon-caret-down> class="participants-list__sort-icon"
*ngIf="sortBy.order === 'asc'"
></digi-icon-caret-up>
<digi-icon-caret-down
class="participants-list__sort-icon"
*ngIf="sortBy.order === 'desc'"
></digi-icon-caret-down>
</ng-container> </ng-container>
</button> </button>
</th> </th>

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 { Participant } from '@dafa-models/participant.model'; import { Participant } from '@dafa-models/participant.model';
import { SortBy } from '@dafa-models/sort-by.model'; import { Sort } from '@dafa-models/sort.model';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
@Component({ @Component({
@@ -11,7 +11,7 @@ import { BehaviorSubject } from 'rxjs';
}) })
export class ParticipantsListComponent { export class ParticipantsListComponent {
@Input() participants: Participant[]; @Input() participants: Participant[];
@Input() sortBy: SortBy | null; @Input() sortBy: Sort<keyof Participant> | null;
@Output() sorted = new EventEmitter<keyof Participant>(); @Output() sorted = new EventEmitter<keyof Participant>();
private _currentPage$ = new BehaviorSubject<number>(1); private _currentPage$ = new BehaviorSubject<number>(1);

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Participant } from '@dafa-models/participant.model'; import { Participant } from '@dafa-models/participant.model';
import { SortBy } from '@dafa-models/sort-by.model'; import { Sort } from '@dafa-models/sort.model';
import { ParticipantsService } from '@dafa-services/api/participants.service'; import { ParticipantsService } from '@dafa-services/api/participants.service';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
@@ -14,8 +14,10 @@ export class ParticipantsComponent {
private _searchValue$ = new BehaviorSubject<string>(''); private _searchValue$ = new BehaviorSubject<string>('');
activeParticipants$: Observable<Participant[]> = this.participantsService.activeParticipants$; activeParticipants$: Observable<Participant[]> = this.participantsService.activeParticipants$;
followUpParticipants$: Observable<Participant[]> = this.participantsService.followUpParticipants$; followUpParticipants$: Observable<Participant[]> = this.participantsService.followUpParticipants$;
activeParticipantsSortBy$: Observable<SortBy | null> = this.participantsService.activeParticipantsSortBy$; activeParticipantsSortBy$: Observable<Sort<keyof Participant> | null> = this.participantsService
followUpParticipantsSortBy$: Observable<SortBy | null> = this.participantsService.followUpParticipantsSortBy$; .activeParticipantsSortBy$;
followUpParticipantsSortBy$: Observable<Sort<keyof Participant> | null> = this.participantsService
.followUpParticipantsSortBy$;
constructor(private participantsService: ParticipantsService) {} constructor(private participantsService: ParticipantsService) {}

View File

@@ -17,8 +17,10 @@ const API_HEADERS = { headers: environment.api.headers };
export class AuthorizationService { export class AuthorizationService {
private _authorizationsApiUrl = `${environment.api.url}/authorizations`; private _authorizationsApiUrl = `${environment.api.url}/authorizations`;
public authorizations$: Observable<Authorization[]> = this.httpClient public authorizations$: Observable<Authorization[]> = this.httpClient
.get<AuthorizationApiResponse[]>(this._authorizationsApiUrl, API_HEADERS) .get<AuthorizationApiResponse>(this._authorizationsApiUrl, API_HEADERS)
.pipe(map(response => response.map(authorization => mapAuthorizationApiResponseToAuthorization(authorization)))); .pipe(
map(response => response.data.map(authorization => mapAuthorizationApiResponseToAuthorization(authorization)))
);
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}
} }

View File

@@ -1,79 +1,83 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ErrorType } from '@dafa-enums/error-type.enum'; import { ErrorType } from '@dafa-enums/error-type.enum';
import { SortOrder } from '@dafa-enums/sort-order.enum';
import { environment } from '@dafa-environment'; import { environment } from '@dafa-environment';
import { import {
Employee, Employee,
EmployeeApiResponse, EmployeeApiResponse,
EmployeesApiResponse,
EmployeesData,
mapEmployeeReponseToEmployee, mapEmployeeReponseToEmployee,
mapEmployeeToEmployeeApiRequestData, mapEmployeeToEmployeeApiRequestData,
} from '@dafa-models/employee.model'; } from '@dafa-models/employee.model';
import { SortBy } from '@dafa-models/sort-by.model'; import { Sort } from '@dafa-models/sort.model';
import { sort } from '@dafa-utils/sort.util';
import { BehaviorSubject, combineLatest, Observable, throwError } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map, switchMap } from 'rxjs/operators';
function filterEmployees(employees: Employee[], searchFilter: string): Employee[] {
return employees.filter(person => person.fullName.toLowerCase().includes(searchFilter.toLowerCase()));
}
const API_HEADERS = { headers: environment.api.headers }; const API_HEADERS = { headers: environment.api.headers };
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class EmployeeService { export class EmployeeService {
private _employeeApiUrl = `${environment.api.url}/employee`; private _apiUrl = `${environment.api.url}/employee`;
private _employeesRawData: Observable<EmployeeApiResponse[]> = this.httpClient.get<EmployeeApiResponse[]>( private _limit$ = new BehaviorSubject<number>(20);
this._employeeApiUrl, private _page$ = new BehaviorSubject<number>(1);
API_HEADERS private _sort$ = new BehaviorSubject<Sort<keyof Employee>>({ key: 'fullName', order: SortOrder.ASC });
); public sort$: Observable<Sort<keyof Employee>> = this._sort$.asObservable();
private _allEmployees$: Observable<Employee[]> = this._employeesRawData.pipe(
map(results => results.map(result => mapEmployeeReponseToEmployee(result)))
);
private _employeesSortBy$ = new BehaviorSubject<SortBy | null>({ key: 'fullName', reverse: false });
public employeesSortBy$: Observable<SortBy> = this._employeesSortBy$.asObservable();
private _searchFilter$ = new BehaviorSubject<string>(''); private _searchFilter$ = new BehaviorSubject<string>('');
public searchFilter$: Observable<string> = this._searchFilter$.asObservable(); public searchFilter$: Observable<string> = this._searchFilter$.asObservable();
private _filteredEmployees$: Observable<Employee[]> = combineLatest([this._allEmployees$, this._searchFilter$]).pipe( private _fetchEmployees$(limit: number, page: number, sort: Sort<keyof Employee>): Observable<EmployeesData> {
map(([employees, searchFilter]) => filterEmployees(employees, searchFilter)) return this.httpClient
.get<EmployeesApiResponse>(this._apiUrl, {
...API_HEADERS,
params: {
sort: sort.key,
order: sort.order,
limit: limit.toString(),
page: page.toString(),
},
})
.pipe(
map(({ data, meta }) => {
return { data: data.map(employee => mapEmployeeReponseToEmployee(employee)), meta };
})
);
}
public employeesData$: Observable<EmployeesData> = combineLatest([this._limit$, this._page$, this._sort$]).pipe(
switchMap(([limit, page, sort]) => this._fetchEmployees$(limit, page, sort))
); );
public resultCount$: Observable<number> = this._employeesRawData.pipe(map(results => results.length)); // TODO: need META public fetchDetailedEmployeeData$(id: string): Observable<Employee> {
public filteredEmployees$: Observable<Employee[]> = combineLatest([ return this.httpClient
this._filteredEmployees$, .get<EmployeeApiResponse>(`${this._apiUrl}/${id}`, { ...API_HEADERS })
this._employeesSortBy$, .pipe(map(result => mapEmployeeReponseToEmployee(result.data)));
]).pipe( }
map(([employees, sortBy]) => {
return sortBy ? sort(employees, sortBy) : employees;
})
);
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}
public getDetailedEmployeeData(id: string): Observable<Employee> {
return this.httpClient
.get<EmployeeApiResponse>(`${this._employeeApiUrl}/${id}`, { ...API_HEADERS })
.pipe(map(result => mapEmployeeReponseToEmployee(result)));
}
public setSearchFilter(value: string) { public setSearchFilter(value: string) {
this._searchFilter$.next(value); this._searchFilter$.next(value);
} }
public setEmployeesSortKey(key: keyof Employee) { public setSort(newSortKey: keyof Employee) {
const currentSortBy = this._employeesSortBy$.getValue(); const currentSort = this._sort$.getValue();
const reverse = currentSortBy?.key === key ? !currentSortBy.reverse : false; let order = currentSort.key === newSortKey && currentSort.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
this._employeesSortBy$.next({ key, reverse });
this._sort$.next({ key: newSortKey, order });
}
public setPage(page: number) {
this._page$.next(page);
} }
public postNewEmployee(employeeData: Employee): Observable<string> { public postNewEmployee(employeeData: Employee): Observable<string> {
return this.httpClient return this.httpClient.post<any>(this._apiUrl, mapEmployeeToEmployeeApiRequestData(employeeData), API_HEADERS).pipe(
.post<any>(this._employeeApiUrl, mapEmployeeToEmployeeApiRequestData(employeeData), API_HEADERS) map(({ id }) => id),
.pipe( catchError(error => throwError({ message: error, type: ErrorType.API }))
map(({ id }) => id), );
catchError(error => throwError({ message: error, type: ErrorType.API }))
);
} }
} }

View File

@@ -1,9 +1,14 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ParticipantStatus } from '@dafa-enums/participant-status.enum'; import { ParticipantStatus } from '@dafa-enums/participant-status.enum';
import { SortOrder } from '@dafa-enums/sort-order.enum';
import { environment } from '@dafa-environment'; import { environment } from '@dafa-environment';
import { Participant } from '@dafa-models/participant.model'; import {
import { SortBy } from '@dafa-models/sort-by.model'; mapParticipantApiResponseToParticipant,
Participant,
ParticipantsApiResponse,
} from '@dafa-models/participant.model';
import { Sort } from '@dafa-models/sort.model';
import { sort } from '@dafa-utils/sort.util'; import { sort } from '@dafa-utils/sort.util';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@@ -21,13 +26,23 @@ function filterParticipants(participants: Participant[], searchFilter: string):
providedIn: 'root', providedIn: 'root',
}) })
export class ParticipantsService { export class ParticipantsService {
private _allParticipants$: Observable<Participant[]> = this.httpClient.get<Participant[]>( private _allParticipants$: Observable<Participant[]> = this.httpClient
`${environment.api.url}/participants` .get<ParticipantsApiResponse>(`${environment.api.url}/participants`)
); .pipe(map(response => response.data.map(participant => mapParticipantApiResponseToParticipant(participant))));
private _activeParticipantsSortBy$ = new BehaviorSubject<SortBy | null>({ key: 'handleBefore', reverse: false }); private _activeParticipantsSortBy$ = new BehaviorSubject<Sort<keyof Participant> | null>({
public activeParticipantsSortBy$: Observable<SortBy> = this._activeParticipantsSortBy$.asObservable(); key: 'handleBefore',
private _followUpParticipantsSortBy$ = new BehaviorSubject<SortBy | null>({ key: 'handleBefore', reverse: false }); order: SortOrder.ASC,
public followUpParticipantsSortBy$: Observable<SortBy> = this._followUpParticipantsSortBy$.asObservable(); });
public activeParticipantsSortBy$: Observable<
Sort<keyof Participant>
> = this._activeParticipantsSortBy$.asObservable();
private _followUpParticipantsSortBy$ = new BehaviorSubject<Sort<keyof Participant> | null>({
key: 'handleBefore',
order: SortOrder.ASC,
});
public followUpParticipantsSortBy$: Observable<
Sort<keyof Participant>
> = this._followUpParticipantsSortBy$.asObservable();
private _searchFilter$ = new BehaviorSubject<string>(''); private _searchFilter$ = new BehaviorSubject<string>('');
public searchFilter$: Observable<string> = this._searchFilter$.asObservable(); public searchFilter$: Observable<string> = this._searchFilter$.asObservable();
@@ -64,14 +79,20 @@ export class ParticipantsService {
public setActiveParticipantsSortKey(key: keyof Participant) { public setActiveParticipantsSortKey(key: keyof Participant) {
const currentSortBy = this._activeParticipantsSortBy$.getValue(); const currentSortBy = this._activeParticipantsSortBy$.getValue();
const reverse = currentSortBy?.key === key ? !currentSortBy.reverse : false; let order = currentSortBy.order;
this._activeParticipantsSortBy$.next({ key, reverse }); if (currentSortBy?.key === key) {
order = order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
}
this._activeParticipantsSortBy$.next({ key, order });
} }
public setFollowUpParticipantsSortKey(key: keyof Participant) { public setFollowUpParticipantsSortKey(key: keyof Participant) {
const currentSortBy = this._followUpParticipantsSortBy$.getValue(); const currentSortBy = this._followUpParticipantsSortBy$.getValue();
const reverse = currentSortBy?.key === key ? !currentSortBy.reverse : false; let order = currentSortBy.order;
this._followUpParticipantsSortBy$.next({ key, reverse }); if (currentSortBy?.key === key) {
order = order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
}
this._followUpParticipantsSortBy$.next({ key, order });
} }
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}

View File

@@ -13,8 +13,8 @@ const API_HEADERS = { headers: environment.api.headers };
export class ServiceService { export class ServiceService {
private _servicesApiUrl = `${environment.api.url}/services`; private _servicesApiUrl = `${environment.api.url}/services`;
public services$: Observable<Service[]> = this.httpClient public services$: Observable<Service[]> = this.httpClient
.get<ServiceApiResponse[]>(this._servicesApiUrl, API_HEADERS) .get<ServiceApiResponse>(this._servicesApiUrl, API_HEADERS)
.pipe(map(response => response.map(service => mapServiceApiResponseToService(service)))); .pipe(map(response => response.data.map(service => mapServiceApiResponseToService(service))));
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}
} }

View File

@@ -14,7 +14,7 @@ export class UserService {
private _userApiUrl = `${environment.api.url}/currentUser`; private _userApiUrl = `${environment.api.url}/currentUser`;
public currentUser$: Observable<User> = this.httpClient public currentUser$: Observable<User> = this.httpClient
.get<UserApiResponse>(this._userApiUrl, API_HEADERS) .get<UserApiResponse>(this._userApiUrl, API_HEADERS)
.pipe(map(response => mapUserApiResponseToUser(response))); .pipe(map(response => mapUserApiResponseToUser(response.data)));
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}
} }

View File

@@ -21,7 +21,6 @@ export class ErrorService {
} }
public add(error: CustomError) { public add(error: CustomError) {
console.error(error);
this.errorQueue$.next([...this.errorQueue$.value, error]); this.errorQueue$.next([...this.errorQueue$.value, error]);
this.appRef.tick(); this.appRef.tick();
} }

View File

@@ -1,11 +1,10 @@
import { SortBy } from '@dafa-models/sort-by.model'; import { Sort } from '@dafa-models/sort.model';
// eslint-disable-next-line @typescript-eslint/no-explicit-any export function sort<T>(data: T[], sort: Sort<keyof T>): T[] {
export function sort(data: any[], sortBy: SortBy): any[] { const reverse = sort.order === 'desc' ? -1 : 1;
const reverse = sortBy.reverse ? -1 : 1;
return [...data].sort((a, b) => { return [...data].sort((a, b) => {
const first = a[sortBy.key]; const first = a[sort.key];
const second = b[sortBy.key]; const second = b[sort.key];
return reverse * (+(first > second) - +(second > first)); return reverse * (+(first > second) - +(second > first));
}); });

View File

@@ -4,7 +4,7 @@
"description": "A mock api implementing all needed endpoints for dafa-web", "description": "A mock api implementing all needed endpoints for dafa-web",
"scripts": { "scripts": {
"generate-api": "node ./scripts/generate-api.js", "generate-api": "node ./scripts/generate-api.js",
"start": "npm run generate-api && json-server --watch api.json --port 8000 --routes routes.json", "start": "npm run generate-api && node server.js",
"start:delay": "npm start -- --delay 500" "start:delay": "npm start -- --delay 500"
}, },
"author": "Erik Tiekstra (erik.tiekstra@arbetsformedlingen.se)", "author": "Erik Tiekstra (erik.tiekstra@arbetsformedlingen.se)",

View File

@@ -1,7 +0,0 @@
{
"/api/*": "/$1",
"/participants": "/participants?_embed=employees",
"/participant/:id": "/participants/:id?_embed=employees",
"/employee": "/employees",
"/employee/:id": "/employees/:id"
}

View File

@@ -25,6 +25,7 @@ function generateEmployees(amount = 10) {
organizations: [ORGANIZATIONS[Math.floor(Math.random() * ORGANIZATIONS.length)]], organizations: [ORGANIZATIONS[Math.floor(Math.random() * ORGANIZATIONS.length)]],
services: [SERVICES[Math.floor(Math.random() * SERVICES.length)]], services: [SERVICES[Math.floor(Math.random() * SERVICES.length)]],
authorizations: authorizations.generate(), authorizations: authorizations.generate(),
createdAt: Date.now(),
}; };
employees.push(person); employees.push(person);

View File

@@ -8,7 +8,7 @@ import organizations from './organizations.js';
import participants from './participants.js'; import participants from './participants.js';
import services from './services.js'; import services from './services.js';
const generatedEmployees = employees.generate(10); const generatedEmployees = employees.generate(50);
const apiData = { const apiData = {
services: services.generate(), services: services.generate(),

View File

@@ -0,0 +1,61 @@
import jsonServer from 'json-server';
const server = jsonServer.create();
const router = jsonServer.router('api.json');
const middlewares = jsonServer.defaults();
server.use(middlewares);
server.use(
jsonServer.rewriter({
'/api/*': '/$1',
'*sort=services*': '$1sort=services[0].name$2',
'*sort=organizations*': '$1sort=organizations[0].address.city$2',
'*sort=fullName*': '$1sort=firstName,lastName$2',
'/employee*': '/employees$1',
'/participants': '/participants?_embed=employees',
'/participant/:id': '/participants/:id?_embed=employees',
'*page=*': '$1_page=$2',
'*limit=*': '$1_limit=$2',
'*sort=*': '$1_sort=$2',
'*order=*': '$1_order=$2',
})
);
router.render = (req, res) => {
const params = new URLSearchParams(req._parsedUrl.query);
// Add createdAt to the body
if (req.originalMethod === 'POST') {
req.body.createdAt = Date.now();
}
res.jsonp({
data: res.locals.data,
...appendMetaData(params, res),
});
};
server.use(router);
server.listen(8000, () => {
console.info('JSON Server is running');
});
function appendMetaData(params, res) {
if (params.has('_page')) {
const limit = +params.get('_limit');
const page = +params.get('_page');
const count = res.get('X-Total-Count');
const totalPages = Math.ceil(count / limit);
return {
meta: {
count,
limit,
page,
totalPages,
},
};
}
return null;
}