refactor: improve library account views

This commit is contained in:
Jovan Krunić
2022-05-19 15:41:07 +00:00
parent 9efc41a8f8
commit 5edec7154e
36 changed files with 694 additions and 303 deletions

View File

@@ -125,7 +125,7 @@ executable:
review:
stage: deploy
script:
- npm run build:prod
- npm run build
- .gitlab/ci/enableGitlabReviewToolbar.sh www/index.html "$CI_PROJECT_ID" "$CI_OPEN_MERGE_REQUESTS"
- cp www/index.html www/200.html
- ./node_modules/.bin/surge -p ./www -d https://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.surge.sh/

View File

@@ -0,0 +1,36 @@
import {Injectable} from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse,
} from '@angular/common/http';
import {Observable, throwError} from 'rxjs';
import {NGXLogger} from 'ngx-logger';
import {catchError} from 'rxjs/operators';
@Injectable()
export class ServiceHandlerInterceptor implements HttpInterceptor {
constructor(private readonly logger: NGXLogger) {}
intercept(
request: HttpRequest<unknown>,
next: HttpHandler,
): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
// Fixes the issue of errors dropping into "toPromise()"
// and being not able to catch it in the "caller methods"
catchError((error: HttpErrorResponse) => {
const errorMessage =
error.error instanceof ErrorEvent
? `Error: ${error.error.message}`
: `Error Code: ${error.status}, Message: ${error.message}`;
this.logger.error(errorMessage);
return throwError(errorMessage);
}),
);
}
}

View File

@@ -18,7 +18,11 @@ import {
PathLocationStrategy,
registerLocaleData,
} from '@angular/common';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {
HTTP_INTERCEPTORS,
HttpClient,
HttpClientModule,
} from '@angular/common/http';
import localeDe from '@angular/common/locales/de';
import {APP_INITIALIZER, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
@@ -64,6 +68,7 @@ import {BackgroundModule} from './modules/background/background.module';
import {LibraryModule} from './modules/library/library.module';
import {StorageProvider} from './modules/storage/storage.provider';
import {AssessmentsModule} from './modules/assessments/assessments.module';
import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor';
import {RoutingStackService} from './util/routing-stack.service';
import {SCSettingValue} from '@openstapps/core';
import {DefaultAuthService} from './modules/auth/default-auth.service';
@@ -79,7 +84,6 @@ registerLocaleData(localeDe);
* @param settingsProvider provider of settings (e.g. language that has been set)
* @param configProvider TODO
* @param translateService TODO
* @param _routingStackService Just for init and to track the stack from the get go
*/
export function initializerFactory(
storageProvider: StorageProvider,
@@ -207,6 +211,11 @@ export function createTranslateLoader(http: HttpClient) {
],
useFactory: initializerFactory,
},
{
provide: HTTP_INTERCEPTORS,
useClass: ServiceHandlerInterceptor,
multi: true,
},
],
})
export class AppModule {

View File

@@ -2,6 +2,7 @@ import {Injectable} from '@angular/core';
import {Requestor} from '@openid/appauth';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {XhrSettings} from 'ionic-appauth/lib/cordova';
import {Observable} from 'rxjs';
@Injectable({
providedIn: 'root',
@@ -14,28 +15,32 @@ export class NgHttpService implements Requestor {
settings.method = 'GET';
}
let observable: Observable<T>;
switch (settings.method) {
case 'GET':
return this.http
.get<T>(settings.url, {headers: this.getHeaders(settings.headers)})
.toPromise();
observable = this.http.get<T>(settings.url, {
headers: this.getHeaders(settings.headers),
});
break;
case 'POST':
return this.http
.post<T>(settings.url, settings.data, {
observable = this.http.post<T>(settings.url, settings.data, {
headers: this.getHeaders(settings.headers),
})
.toPromise();
});
break;
case 'PUT':
return this.http
.put<T>(settings.url, settings.data, {
observable = this.http.put<T>(settings.url, settings.data, {
headers: this.getHeaders(settings.headers),
})
.toPromise();
});
break;
case 'DELETE':
return this.http
.delete<T>(settings.url, {headers: this.getHeaders(settings.headers)})
.toPromise();
observable = this.http.delete<T>(settings.url, {
headers: this.getHeaders(settings.headers),
});
break;
}
return observable.toPromise();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component} from '@angular/core';
import {Component, Input} from '@angular/core';
/**
* A placeholder to show when a simple card is being loaded
@@ -21,4 +21,16 @@ import {Component} from '@angular/core';
selector: 'stapps-skeleton-simple-card',
templateUrl: 'skeleton-simple-card.html',
})
export class SkeletonSimpleCardComponent {}
export class SkeletonSimpleCardComponent {
/**
* Show title
*/
@Input()
title = true;
/**
* The number of lines after the title
*/
@Input()
lines = 1;
}

View File

@@ -1,8 +1,14 @@
<ion-card>
<ion-card-header>
<ion-card-header *ngIf="title">
<ion-skeleton-text animated style="width: 15%"></ion-skeleton-text>
</ion-card-header>
<ion-card-content>
<p><ion-skeleton-text animated style="width: 85%"></ion-skeleton-text></p>
<p>
<ion-skeleton-text
*ngFor="let line of [].constructor(lines)"
animated
style="width: 85%"
></ion-skeleton-text>
</p>
</ion-card-content>
</ion-card>

View File

@@ -9,11 +9,14 @@
</ion-header>
<ion-content>
<p *ngIf="name">
{{ 'library.account.greeting' | translate | titlecase }}
<p *ngIf="name; else loading">
{{ 'library.account.greeting' | translate }}
{{ name | firstLastName }}!
{{ 'library.account.login.success' | translate }}
</p>
<ng-template #loading>
<p><ion-skeleton-text animated style="width: 80%"></ion-skeleton-text></p>
</ng-template>
<ion-item [routerLink]="['profile']">
<ion-icon name="person" slot="start"></ion-icon
>{{ 'library.account.pages.profile.title' | translate | titlecase }}

View File

@@ -1,22 +1,17 @@
import {Component} from '@angular/core';
import {LibraryAccountService} from './library-account.service';
// import {Router} from '@angular/router';
@Component({
templateUrl: './account.page.html',
styleUrls: ['./account.page.scss'],
})
export class LibraryAccountPageComponent {
name: string;
name?: string;
constructor(private readonly libraryAccountService: LibraryAccountService) {}
async ionViewWillEnter(): Promise<void> {
try {
const patron = await this.libraryAccountService.getProfile();
this.name = patron.name;
} catch {
// this.router.navigate(['profile']);
}
this.name = patron?.name;
}
}

View File

@@ -1,22 +0,0 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{
'library.account.pages.checked_out.title' | translate | titlecase
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ng-container *ngIf="checkedOutItems">
<stapps-paia-item
*ngFor="let checkedOutItem of checkedOutItems"
[item]="checkedOutItem"
pageName="checked_out"
>
</stapps-paia-item>
</ng-container>
</ion-content>

View File

@@ -1,23 +1,37 @@
import {Component} from '@angular/core';
import {PAIADocument} from '../../types';
import {DocumentAction, PAIADocument} from '../../types';
import {LibraryAccountService} from '../library-account.service';
@Component({
selector: 'app-checked-out',
templateUrl: './checked-out-page.component.html',
styleUrls: ['./checked-out-page.component.scss'],
templateUrl: './checked-out-page.html',
styleUrls: ['./checked-out-page.scss'],
})
export class CheckedOutPageComponent {
checkedOutItems: PAIADocument[];
checkedOutItems?: PAIADocument[];
constructor(private readonly libraryAccountService: LibraryAccountService) {}
async ionViewWillEnter(): Promise<void> {
await this.fetchItems();
}
async onDocumentAction(documentAction: DocumentAction) {
const answer = await this.libraryAccountService.handleDocumentAction(
documentAction,
);
if (answer) await this.fetchItems();
}
async fetchItems() {
try {
this.checkedOutItems = undefined;
this.checkedOutItems =
await this.libraryAccountService.getCheckedOutItems();
} catch {
// TODO: error handling
await this.libraryAccountService.handleError();
this.checkedOutItems = [];
}
}
}

View File

@@ -0,0 +1,38 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{
'library.account.pages.checked_out.title' | translate | titlecase
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ng-container *ngIf="checkedOutItems; else fallback">
<stapps-paia-item
*ngFor="let checkedOutItem of checkedOutItems"
[item]="checkedOutItem"
[propertiesToShow]="['endtime', 'label']"
(documentAction)="onDocumentAction($event)"
listName="checked_out"
>
</stapps-paia-item>
</ng-container>
<ng-template #fallback>
<stapps-skeleton-list-item
hideThumbnail="true"
*ngIf="!checkedOutItems; else nothingFound"
></stapps-skeleton-list-item>
<ng-template #nothingFound>
<ion-label
*ngIf="checkedOutItems && checkedOutItems.length === 0"
class="centeredMessageContainer"
>
{{ 'search.nothing_found' | translate | titlecase }}
</ion-label>
</ng-template>
</ng-template>
</ion-content>

View File

@@ -0,0 +1,33 @@
import {Component, Input} from '@angular/core';
import {PAIAFee} from '../../../types';
import {SCArticle, SCBook, SCPeriodical} from '@openstapps/core';
import {LibraryAccountService} from '../../library-account.service';
@Component({
selector: 'stapps-fee-item',
templateUrl: './fee-item.html',
styleUrls: ['./fee-item.scss'],
})
export class FeeItemComponent {
_fee: PAIAFee;
book?: SCBook | SCPeriodical | SCArticle;
@Input()
set fee(fee: PAIAFee) {
this._fee = fee;
this.libraryAccountService
.getDocumentFromHDS(fee.edition as string)
.then(book => {
this.book = book;
});
}
get fee() {
return this._fee;
}
propertiesToShow: (keyof PAIAFee)[] = ['about', 'date', 'amount'];
constructor(private readonly libraryAccountService: LibraryAccountService) {}
}

View File

@@ -0,0 +1,23 @@
<ion-item>
<ion-label class="ion-text-wrap">
<h2 *ngIf="book; else loading" class="name">
{{ 'library.account.pages.fines.labels.edition' | translate }}:
{{ book.name }}
</h2>
<ng-template #loading
><h2>
<ion-skeleton-text animated style="width: 80%"></ion-skeleton-text></h2
></ng-template>
<ng-container *ngFor="let property of propertiesToShow">
<p *ngIf="fee[property]">
{{ 'library.account.pages.fines.labels' + '.' + property | translate }}:
<ng-container *ngIf="!['date'].includes(property); else date">
{{ fee[property] }}
</ng-container>
<ng-template #date>
{{ fee[property] | amDateFormat: 'll' }}
</ng-template>
</p></ng-container
>
</ion-label>
</ion-item>

View File

@@ -1,38 +0,0 @@
<!--
~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-card class="bold-header">
<ion-card-header *ngIf="book">{{ book.name }}</ion-card-header>
<ion-card-content>
<ion-grid *ngFor="let property of additionalPropertiesToShow">
<ion-row *ngIf="item[property]">
<ion-col>
{{
'library.account.pages' +
'.' +
pageName +
'.' +
'labels' +
'.' +
property
| translate
| titlecase
}}:</ion-col
>
<ion-col width-60 text-right>{{ item[property] }}</ion-col>
</ion-row>
</ion-grid>
</ion-card-content>
</ion-card>

View File

@@ -13,43 +13,27 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {PAIADocument} from '../../../types';
import {SCArticle, SCBook, SCPeriodical} from '@openstapps/core';
import {LibraryAccountService} from '../../library-account.service';
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {DocumentAction, PAIADocument} from '../../../types';
@Component({
selector: 'stapps-paia-item',
templateUrl: './paiaitem.component.html',
styleUrls: ['./paiaitem.component.scss'],
templateUrl: './paiaitem.html',
styleUrls: ['./paiaitem.scss'],
})
export class PAIAItemComponent {
book?: SCBook | SCPeriodical | SCArticle;
private _item: PAIADocument;
additionalPropertiesToShow: (keyof PAIADocument)[] = [
'about',
'endtime',
'label',
];
@Input() item: PAIADocument;
@Input()
set item(item: PAIADocument) {
this._item = item;
this.libraryAccountService
.getDocumentFromHDS(item.edition as string)
.then(book => {
this.book = book;
});
}
get item() {
return this._item;
}
propertiesToShow: (keyof PAIADocument)[];
@Input()
pageName: string;
listName: string;
constructor(private readonly libraryAccountService: LibraryAccountService) {}
@Output()
documentAction: EventEmitter<DocumentAction> = new EventEmitter<DocumentAction>();
async onClick(action: DocumentAction['action']) {
this.documentAction.emit({doc: this.item, action});
}
}

View File

@@ -0,0 +1,58 @@
<!--
~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-item>
<!-- TODO: text not selectable in Chrome, bugfix needed https://github.com/ionic-team/ionic-framework/issues/24956 -->
<ion-label class="ion-text-wrap"
><h2 *ngIf="item.about" class="name">{{ item.about }}</h2>
<ng-container *ngFor="let property of propertiesToShow">
<p *ngIf="item[property]">
{{
'library.account.pages' +
'.' +
listName +
'.' +
'labels' +
'.' +
property | translate
}}:
<ng-container
*ngIf="!['endtime', 'duedate'].includes(property); else date"
>
{{ item[property] }}
</ng-container>
<ng-template #date>
{{ item[property] | amDateFormat: 'll' }}
</ng-template>
</p>
</ng-container>
<span class="ion-float-right">
<ion-button
*ngIf="item.cancancel"
color="primary"
(click)="onClick('cancel')"
>
{{ 'library.account.actions.cancel.header' | translate }}</ion-button
>
<ion-button
*ngIf="item.canrenew"
color="primary"
(click)="onClick('renew')"
>
{{ 'library.account.actions.renew.header' | translate }}</ion-button
>
</span>
</ion-label>
</ion-item>

View File

@@ -1,42 +0,0 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{
'library.account.pages.fines.title' | translate | titlecase
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-card *ngFor="let fine of fines" class="bold-header">
<ion-card-header *ngIf="fine.about">{{ fine.about }}</ion-card-header>
<ion-card-content>
<ion-grid *ngFor="let property of additionalPropertiesToShow">
<ion-row *ngIf="fine[property]">
<ion-col>
{{
'library.account.pages.fines.labels' + '.' + property
| translate
| titlecase
}}:</ion-col
>
<ion-col width-60 text-right>{{ fine[property] }}</ion-col>
</ion-row>
</ion-grid>
</ion-card-content>
</ion-card>
<ion-grid *ngIf="amount">
<ion-row *ngIf="amount">
<ion-col>
{{
'library.account.pages.fines.labels.amount' | translate | titlecase
}}:
</ion-col>
<ion-col>
{{ amount }}
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -1,38 +1,24 @@
import {Component} from '@angular/core';
import {LibraryAccountService} from '../library-account.service';
import {PAIAFee, PAIAFees} from '../../types';
import {PAIAFee} from '../../types';
@Component({
selector: 'app-fines',
templateUrl: './fines-page.component.html',
styleUrls: ['./fines-page.component.scss'],
templateUrl: './fines-page.html',
styleUrls: ['./fines-page.scss'],
})
export class FinesPageComponent {
amount: string | undefined;
additionalPropertiesToShow: (keyof PAIAFee)[] = [
'item',
'date',
'feetype',
'feeid',
];
amount?: string;
fines: PAIAFee[];
constructor(private readonly libraryAccountService: LibraryAccountService) {}
async ionViewWillEnter(): Promise<void> {
let fees: PAIAFees;
try {
fees = await this.libraryAccountService.getFees();
this.amount = fees.amount;
this.fines = this.cleanUp(fees.fee);
} catch {
// TODO: error handling
}
await this.fetchItems();
}
private cleanUp(fines: PAIAFee[]): PAIAFee[] {
private async cleanUp(fines: PAIAFee[]): Promise<PAIAFee[]> {
for (const fine of fines) {
for (const property in fine) {
// remove properties with "null" included
@@ -43,4 +29,16 @@ export class FinesPageComponent {
}
return fines;
}
async fetchItems() {
try {
const fees = await this.libraryAccountService.getFees();
if (fees) {
this.amount = fees.amount;
this.fines = await this.cleanUp(fees.fee);
}
} catch {
await this.libraryAccountService.handleError();
}
}
}

View File

@@ -0,0 +1,40 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{
'library.account.pages.fines.title' | translate | titlecase
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list *ngIf="fines; else loading">
<stapps-fee-item *ngFor="let fine of fines" [fee]="fine"></stapps-fee-item>
</ion-list>
<ng-template #loading>
<stapps-skeleton-list-item
*ngFor="let _ of [].constructor(2)"
hideThumbnail="true"
>
</stapps-skeleton-list-item>
</ng-template>
<ion-grid>
<ion-row *ngIf="amount; else amount_loading" class="ion-float-right">
<ion-col size="auto">
{{ 'library.account.pages.fines.labels.total_amount' | translate }}:
</ion-col>
<ion-col size="auto">
{{ amount }}
</ion-col>
</ion-row>
<ng-template #amount_loading>
<ion-row class="ion-float-right">
<ion-col size="auto">
<ion-skeleton-text animated style="width: 100px"></ion-skeleton-text>
</ion-col>
</ion-row>
</ng-template>
</ion-grid>
</ion-content>

View File

@@ -1,17 +0,0 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{
'library.account.pages.holds.title' | translate | titlecase
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ng-container *ngIf="holds">
<stapps-paia-item *ngFor="let hold of holds" [item]="hold" pageName="holds">
</stapps-paia-item>
</ng-container>
</ion-content>

View File

@@ -1,22 +1,45 @@
import {Component} from '@angular/core';
import {DocumentAction, PAIADocument, PAIADocumentStatus} from '../../types';
import {LibraryAccountService} from '../library-account.service';
import {PAIADocument} from '../../types';
@Component({
selector: 'app-holds-and-reservations',
templateUrl: './holds-and-reservations-page.component.html',
styleUrls: ['./holds-and-reservations-page.component.scss'],
templateUrl: './holds-and-reservations-page.html',
styleUrls: ['./holds-and-reservations-page.scss'],
})
export class HoldsAndReservationsPageComponent {
holds: PAIADocument[];
paiaDocuments?: PAIADocument[];
paiaDocumentStatus = PAIADocumentStatus;
constructor(private readonly libraryAccountService: LibraryAccountService) {}
async ionViewWillEnter(): Promise<void> {
await this.fetchItems();
}
async fetchItems(status: PAIADocumentStatus = PAIADocumentStatus.Ordered) {
this.paiaDocuments = undefined;
try {
this.holds = await this.libraryAccountService.getHoldsAndReservations();
this.paiaDocuments = await (Number(status) === PAIADocumentStatus.Ordered
? this.libraryAccountService.getHolds()
: this.libraryAccountService.getReservations());
} catch {
// TODO: error handling
await this.libraryAccountService.handleError();
this.paiaDocuments = [];
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async segmentChanged(event: any) {
await this.fetchItems(event.detail.value);
}
async onDocumentAction(documentAction: DocumentAction) {
const answer = await this.libraryAccountService.handleDocumentAction(
documentAction,
);
if (answer) await this.fetchItems();
}
}

View File

@@ -0,0 +1,53 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{
'library.account.pages.holds.title' | translate | titlecase
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-segment
(ionChange)="segmentChanged($event)"
[value]="paiaDocumentStatus.Ordered"
mode="md"
>
<ion-segment-button [value]="paiaDocumentStatus.Ordered">
<ion-label>{{
'library.account.pages.holds.holds' | translate
}}</ion-label>
</ion-segment-button>
<ion-segment-button [value]="paiaDocumentStatus.Reserved">
<ion-label>{{
'library.account.pages.holds.reservations' | translate
}}</ion-label>
</ion-segment-button>
</ion-segment>
<ion-list *ngIf="paiaDocuments && paiaDocuments.length > 0; else fallback">
<stapps-paia-item
*ngFor="let hold of paiaDocuments"
[item]="hold"
[propertiesToShow]="['label']"
(documentAction)="onDocumentAction($event)"
listName="holds"
>
</stapps-paia-item>
</ion-list>
<ng-template #fallback>
<stapps-skeleton-list-item
hideThumbnail="true"
*ngIf="!paiaDocuments; else nothingFound"
></stapps-skeleton-list-item>
<ng-template #nothingFound>
<ion-label
*ngIf="paiaDocuments && paiaDocuments.length === 0"
class="centeredMessageContainer"
>
{{ 'search.nothing_found' | translate | titlecase }}
</ion-label>
</ng-template>
</ng-template>
</ion-content>

View File

@@ -20,11 +20,21 @@ import {
SCFeatureConfiguration,
SCFeatureConfigurationExtern,
} from '@openstapps/core';
import {PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
import {
DocumentAction,
PAIADocument,
PAIADocumentStatus,
PAIAFees,
PAIAItems,
PAIAPatron,
} from '../types';
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
import {ConfigProvider} from '../../config/config.provider';
import {AuthHelperService} from '../../auth/auth-helper.service';
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
import {AuthHelperService} from '../../auth/auth-helper.service';
import {ConfigProvider} from '../../config/config.provider';
import {TranslateService} from '@ngx-translate/core';
import {AlertController} from '@ionic/angular';
import {SCHebisSearchResponse} from '../../hebis/protocol/response';
@Injectable({
providedIn: 'root',
@@ -45,6 +55,8 @@ export class LibraryAccountService {
private readonly hebisDataProvider: HebisDataProvider,
private readonly authHelper: AuthHelperService,
readonly configProvider: ConfigProvider,
private readonly translateService: TranslateService,
private readonly alertController: AlertController,
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const config: SCFeatureConfigurationExtern = (
@@ -66,7 +78,11 @@ export class LibraryAccountService {
return this.performRequest<PAIAFees>(`${this.baseUrl}/{patron}/fees`);
}
private async performRequest<T>(urlTemplate: string): Promise<T> {
private async performRequest<T>(
urlTemplate: string,
method = 'GET',
data?: JQuery.PlainObject,
): Promise<T | undefined> {
const token = await this.authHelper
.getProvider(this.authType)
.getValidToken();
@@ -77,30 +93,45 @@ export class LibraryAccountService {
const settings: JQueryAjaxSettings = {
url: url,
dataType: 'json',
method: 'GET',
method: method,
headers: {
'Authorization': `Bearer: ${token.accessToken}`,
'Content-Type': 'application/json',
},
};
return this.requestor.xhr(settings);
if (method === 'POST') settings.data = data;
let result: T;
try {
result = await this.requestor.xhr(settings);
return result;
} catch {
void this.handleError();
}
return;
}
getRawId(input: string) {
return input.split(':').pop();
}
async getHoldsAndReservations() {
return (await this.getItems()).doc.filter(document => {
return [PAIADocumentStatus.Reserved, PAIADocumentStatus.Ordered].includes(
Number(document.status),
);
async getHolds() {
return (await this.getItems())?.doc.filter(document => {
return [PAIADocumentStatus.Ordered].includes(Number(document.status));
});
}
async getReservations() {
return (await this.getItems())?.doc.filter(document => {
return [PAIADocumentStatus.Reserved].includes(Number(document.status));
});
}
async getCheckedOutItems() {
return (await this.getItems()).doc.filter(document => {
return (await this.getItems())?.doc.filter(document => {
return PAIADocumentStatus.Held === Number(document.status);
});
}
@@ -109,14 +140,92 @@ export class LibraryAccountService {
if (typeof edition === 'undefined') {
return;
}
const response = await this.hebisDataProvider.hebisSearch(
let response: SCHebisSearchResponse;
try {
response = await this.hebisDataProvider.hebisSearch(
{
query: this.getRawId(edition) as string,
page: 0,
},
{addPrefix: true},
);
return response.data[0];
} catch {
await this.handleError();
}
return;
}
cancelReservation(document: PAIADocument) {
return this.performRequest<void>(
`${this.baseUrl}/{patron}/cancel`,
'POST',
{doc: [document]},
);
}
renewLanding(document: PAIADocument) {
return this.performRequest<void>(`${this.baseUrl}/{patron}/renew`, 'POST', {
doc: [document],
});
}
async handleDocumentAction(documentAction: DocumentAction): Promise<boolean> {
return new Promise(async resolve => {
const handleDocument = () => {
switch (documentAction.action) {
case 'cancel':
return this.cancelReservation(documentAction.doc);
break;
case 'renew':
return this.renewLanding(documentAction.doc);
}
};
const alert = await this.alertController.create({
buttons: [
{
role: 'cancel',
text: this.translateService.instant('abort'),
handler: () => {
resolve(false);
},
},
{
handler: async () => {
await handleDocument();
resolve(true);
},
text: this.translateService.instant('OK'),
},
],
header: this.translateService.instant(
`library.account.actions.${documentAction.action}.header`,
),
message: this.translateService.instant(
`library.account.actions.${documentAction.action}.text`,
{
value:
documentAction.doc.about ??
this.translateService.instant(
`library.account.actions.${documentAction.doc.about}.unknown_book`,
),
},
),
});
await alert.present();
});
}
async handleError() {
const alert = await this.alertController.create({
header: this.translateService.instant('app.ui.ERROR'),
message: this.translateService.instant('app.errors.SERVICE'),
buttons: ['OK'],
});
await alert.present();
}
}

View File

@@ -1,29 +0,0 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{
'library.account.pages.profile.title' | translate | titlecase
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-grid *ngIf="patron">
<ng-container *ngFor="let property of propertiesToShow">
<ion-row *ngIf="patron[property]">
<ion-col>
{{
'library.account.pages.profile.labels' + '.' + property
| translate
| titlecase
}}:
</ion-col>
<ion-col>
{{ patron[property] }}
</ion-col>
</ion-row>
</ng-container>
</ion-grid>
</ion-content>

View File

@@ -4,11 +4,11 @@ import {PAIAPatron} from '../../types';
@Component({
selector: 'app-profile',
templateUrl: './profile-page.component.html',
styleUrls: ['./profile-page.component.scss'],
templateUrl: './profile-page.html',
styleUrls: ['./profile-page.scss'],
})
export class ProfilePageComponent {
patron: PAIAPatron;
patron?: PAIAPatron;
propertiesToShow: (keyof PAIAPatron)[] = [
'name',
@@ -23,9 +23,8 @@ export class ProfilePageComponent {
async ionViewWillEnter(): Promise<void> {
try {
this.patron = await this.libraryAccountService.getProfile();
console.log(this.patron);
} catch {
// TODO: error handling
await this.libraryAccountService.handleError();
}
}
}

View File

@@ -0,0 +1,41 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{
'library.account.pages.profile.title' | translate | titlecase
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-card *ngIf="patron; else loading">
<ion-card-content>
<ion-grid>
<ng-container *ngFor="let property of propertiesToShow">
<ion-row *ngIf="patron[property]">
<ion-col>
{{
'library.account.pages.profile.labels' + '.' + property
| translate
}}:
</ion-col>
<ion-col>
<ng-container *ngIf="!['expires'].includes(property); else date">
{{ patron[property] }}
</ng-container>
<ng-template #date>
{{ patron[property] | amDateFormat: 'll' }}
</ng-template>
</ion-col>
</ion-row>
</ng-container>
</ion-grid>
</ion-card-content>
</ion-card>
<ng-template #loading>
<stapps-skeleton-simple-card [title]="false" [lines]="4">
</stapps-skeleton-simple-card>
</ng-template>
</ion-content>

View File

@@ -13,6 +13,9 @@ import {PAIAItemComponent} from './account/elements/paia-item/paiaitem.component
import {FirstLastNamePipe} from './account/first-last-name.pipe';
import {AuthGuardService} from '../auth/auth-guard.service';
import {ProtectedRoutes} from '../auth/protected.routes';
import {MomentModule} from 'ngx-moment';
import {FeeItemComponent} from './account/elements/fee-item/fee-item.component';
import {DataModule} from '../data/data.module';
const routes: ProtectedRoutes | Routes = [
{
@@ -21,13 +24,30 @@ const routes: ProtectedRoutes | Routes = [
data: {authProvider: 'paia'},
canActivate: [AuthGuardService],
},
{path: 'library-account/profile', component: ProfilePageComponent},
{path: 'library-account/checked-out', component: CheckedOutPageComponent},
{
path: 'library-account/profile',
component: ProfilePageComponent,
data: {authProvider: 'paia'},
canActivate: [AuthGuardService],
},
{
path: 'library-account/checked-out',
component: CheckedOutPageComponent,
data: {authProvider: 'paia'},
canActivate: [AuthGuardService],
},
{
path: 'library-account/holds-and-reservations',
component: HoldsAndReservationsPageComponent,
data: {authProvider: 'paia'},
canActivate: [AuthGuardService],
},
{
path: 'library-account/fines',
component: FinesPageComponent,
data: {authProvider: 'paia'},
canActivate: [AuthGuardService],
},
{path: 'library-account/fines', component: FinesPageComponent},
];
@NgModule({
@@ -37,6 +57,8 @@ const routes: ProtectedRoutes | Routes = [
IonicModule,
RouterModule.forChild(routes),
TranslateModule,
MomentModule,
DataModule,
],
declarations: [
LibraryAccountPageComponent,
@@ -46,6 +68,7 @@ const routes: ProtectedRoutes | Routes = [
FinesPageComponent,
PAIAItemComponent,
FirstLastNamePipe,
FeeItemComponent,
],
})
export class LibraryModule {}

View File

@@ -67,3 +67,8 @@ export enum PAIADocumentStatus {
Provided = 4,
Rejected = 5,
}
export interface DocumentAction {
action: 'cancel' | 'renew';
doc: PAIADocument;
}

View File

@@ -12,6 +12,7 @@
"ERROR": "Fehler"
},
"errors": {
"SERVICE": "Fehler bei Dienstausführung.",
"UNKNOWN": "Unbekannter Fehler."
}
},
@@ -181,7 +182,7 @@
"library": {
"account": {
"title": "Bibliothekskonto",
"greeting": "hallo",
"greeting": "Hallo",
"login": {
"success": "Du bist eingeloggt und kannst Dein Konto nutzen.",
"error": "Du bist nicht eingeloggt oder deine Sitzung ist abgelaufen."
@@ -204,18 +205,20 @@
"title": "Bestellungen und Vormerkungen",
"labels": {
"title": "Titel",
"about": "mehr Informationen",
"label": "Label",
"endtime": "voraussichtliche Verfügbarkeit"
}
"about": "Mehr Informationen",
"label": "Signatur",
"endtime": "Zum Abholen bis"
},
"holds": "Bestellungen",
"reservations": "Vormerkungen"
},
"checked_out": {
"title": "Deine Ausleihen",
"labels": {
"title": "Titel",
"about": "mehr Informationen",
"label": "Label",
"endtime": "Leihfrist"
"about": "Mehr Informationen",
"label": "Signatur",
"endtime": "Rückgabedatum"
}
},
"fines": {
@@ -223,13 +226,26 @@
"labels": {
"amount": "Betrag",
"about": "Information",
"date": "Datum",
"date": "Rückgabedatum",
"item": "Artikel",
"edition": "Ausgabe",
"feetype": "Gebührenart",
"feeid": "Gebühren ID"
"feeid": "Gebühren ID",
"total_amount": "Gesamtbetrag"
}
}
},
"actions": {
"cancel": {
"header": "Vormerkung löschen",
"text": "Bist Du dir sicher, die Vormerkung von \"{{value}}\" zu löschen?",
"unknown_book": "unbekanntem Titel"
},
"renew": {
"header": "Ausleihfrist verlängern",
"text": "Bist Du dir sicher, die Ausleihfrist von \"{{value}}\" zu verlängern?",
"unknown_book": "unbekanntem Titel"
}
}
}
},

View File

@@ -12,6 +12,7 @@
"ERROR": "Error"
},
"errors": {
"SERVICE": "Service error.",
"UNKNOWN": "Unknown problem."
}
},
@@ -181,7 +182,7 @@
"library": {
"account": {
"title": "library account",
"greeting": "hello",
"greeting": "Hello",
"login": {
"success": "You are logged-in and ready to access your account.",
"error": "Not logged in or login expired."
@@ -190,46 +191,61 @@
"profile": {
"title": "library profile",
"labels": {
"id": "user ID",
"name": "name",
"email": "email",
"id": "User ID",
"name": "Name",
"email": "Email",
"address": "Address",
"expires": "membership expires",
"status": "status",
"type": "type",
"note": "note"
"expires": "Membership expires",
"status": "Status",
"type": "Type",
"note": "Note"
}
},
"holds": {
"title": "holds and reservations",
"labels": {
"title": "title",
"about": "more information",
"label": "label",
"endtime": "Expected to be available"
}
"title": "Title",
"about": "More information",
"label": "Label",
"endtime": "Available for pickup until"
},
"holds": "holds",
"reservations": "reservations"
},
"checked_out": {
"title": "checked out items",
"labels": {
"title": "title",
"about": "more information",
"label": "label",
"endtime": "Loan period ends"
"title": "Title",
"about": "More information",
"label": "Label",
"endtime": "Return date"
}
},
"fines": {
"title": "fines",
"labels": {
"amount": "amount",
"about": "about",
"date": "date",
"item": "item",
"edition": "edition",
"feetype": "fee type",
"feeid": "fee ID"
"amount": "Amount",
"about": "About",
"date": "Return date",
"item": "Item",
"edition": "Edition",
"feetype": "Fee type",
"feeid": "Fee ID",
"total_amount": "Total amount"
}
}
},
"actions": {
"cancel": {
"header": "Cancel reservation",
"text": "Are you sure you want to extend the landing period of \"{{value}}\"?",
"unknown_book": "unknown title"
},
"renew": {
"header": "Extend landing period",
"text": "Are you sure you want to extend the landing period of \"{{value}}\"?",
"unknown_book": "unknown title"
}
}
}
},